细读《你不知道的JavaScript·上卷》1-2 词法做用域

墨言妹带你细读《你不知道的 JavaScript 》系列的世界,深刻 JavaScript 语言内部,弄清楚 JavaScript 每个零部件的用途,知其然更要知其因此然。html

导读

在第 1 章中,学习了 做用域,它是一套规则,用来管理引擎如何在当前做用域以及嵌套的子做用域中根据标识符名称进行变量查找。git

做用域共有两种主要的工做模型,一是 词法做用域JavaScript 等);二是 动态做用域bash 脚本等)。github

词法做用域

  • 什么是词法做用域
  • 为何废弃欺骗词法做用域的两种机制

2.1 词法阶段

在第1章学习了,大部分标准语言编译器的 第一个工做阶段 是词法化( token 化 )。性能优化

词法做用域 就是在词法分析时定义的做用域,即在写代码时,由变量和块做用域的位置决定的。所以,在词法分析时也是固定不变的了(不考虑欺骗词法做用域状况)。bash

下面这段示例代码有三个嵌套做用域:函数

  • 圈1 包含了全局做用域,只有一个标识符号 foo
  • 圈2 包含 foo 做用域,有三个标识符 abarb
  • 圈3 包含 bar 做用域 ,有一个标识符 c

做用域的范围 是根据做用域代码块定义的位置决定的,在这里每一个函数建立了一个做用域。post

这里做用域嵌套是严格的,一个函数不能同时存在于两个外部函数中。 性能

2.1.1 查找

  • 做用域查找会在找到第一个匹配的标识符时中止。学习

  • 遮蔽效应:在多层嵌套做用域中能够定义同名的标识符,内部的标识符会 遮蔽 外部的标识符。优化

  • 全局变量是全局对象的属性,被覆盖的非全局对象则没法被访问到。

    window.a
    复制代码
  • 词法做用域查找只会查找一级标识符, 好比 abc 。若是代码中引用了 foo.bar.baz ,词法做用域只会查找 foo 标识符,找到后,对象属性访问规则 会分别接管对 barbaz 属性的访问。

2.2 欺骗词法

欺骗词法做用域 会致使性能降低,如下两种方法都 不推荐使用

2.2.1 eval

  • eval(...) 函数能够接受一个字符串做为参数,并把字符串的内容看成代码运行,从而实现对词法做用域环境的修改。
  • 在执行 eval() 以后的那些代码,引擎不知道、也不去关心前面的代码是 动态编译 的,且 修改 了词法做用域环境。引擎只会一如既往地进行词法做用域查找。
非严格模式下:
function foo(a, str){
        console.log(str);           //2       // var b = 3;
	eval(str); // 欺骗!
	console.log(eval(str));     //2      //undefined
	console.log(a, b);          //0   2  // 1  3
	console.log(a, window.b);   //0   2  //1  2
}
var b = 2;
foo(0, b);
foo(1, " var b = 3 ;"); 
复制代码
  • eval() 被调用时,字符串参数 “ var b = 3; ” 被看成真正的代码声明了变量 b ,并修改了 foo() 的词法做用域。在 foo() 内部建立了一个变量 b , 遮蔽了外部全局做用域中的同名变量 b
  • console.log() 被执行时,会在 foo() 的内部同时找到 ab , 可是永远也没法找到外部的 b 。所以会输出 1 , 3 ,而不是正常状况下会输出的 1 ,2

拓展 eval() 函数,理解值为 undefiend 的知识,请点击以下:

MDN 解析 eval() 案例

js中的eval方法详解(一)

eval()函数的使用

严格模式下:
function foo(a, str){
    "use strict";
     console.log(str);           //2       // var b = 3;
     eval(str); 
     console.log(eval(str));    //2       //undefined
     console.log(a, b);         //0  2    // 1  2
     console.log(a, window.b);  //0  2    //1  2
}
var b = 2;
foo(0, b);
foo(1, " var b = 3 ;");
复制代码
  • eval() 在严格模式下,有本身的词法做用域,其中的声明没法修改做用域。
  • setTimeout(...)setInterval(...) 的第一个参数能够是字符串,字符串的内容会被解释为一段动态生成的函数代码。已废弃使用
  • 构造函数new Function()的最后一个参数能够接受代码字符串(前面的参数是新生成的函数的形参), 避免使用

2.2.2 with

with 一般被看成重复引用同一个对象中的多个属性快捷方式,可不用重复引用对象自己

var obj = {
	a:1,
	b:2,
	c:3
};

//单调乏味的重复“ obj ”
obj.a = 2;
obj.b = 3;
obj.c = 4;

//简单的快捷方式
with(obj){
	a = 3;
	b = 4;
	c = 5;
}
复制代码

不单单是一个属性访问的 快捷方式 。以下:

function foo(obj){
	with(obj){
		a = 2;
	}
}

var o1 = {
	a : 3
};

var o2 = {
	b : 4
};

console.log(o1.a);//3
foo(o1);
console.log(o1.a);//2

foo(o2);
console.log(02.a);//undefined
console.log(a);//2 -> 很差,a 被泄露到全局做用域上了!
复制代码
  • o1 传进后,with 声明的做用域是 o1a = 2 赋值操做找到 o1.a 并将 2 赋值给它。
  • o2 传进后,做用域 o2 中没有 a 属性,则进行 LHS 标识符查找,o2 的做用域、 foo() 的做用域 和全局做用域都没找到标识符 a ,所以当 a = 2 执行时,产生反作用,自动建立了一个全局变量(非严格模式)a ,并将 2 赋值给 a ,因此 o2.a 保持 undefined

在严格模式下,with 语句被彻底禁用,eval() 则只保留核心功能,都不推荐使用。

2.2.3 性能

JavaScript 引擎在 编译阶段 进行各类性能优化,一些优化在词法分析阶段,静态分析了代码,预先肯定了变量和函数声明的位置,因此在执行期间就能够快速解析标识符。

2.3小结

词法做用域只由函数被声明时所处的位置决定。

如下两个机制能够 欺骗 词法做用域:

  • eval(...) : 对一段包含一个或多个声明的 代码 字符串进行演算,借此来修改已经存在的词法做用域(运行时)。
  • with : 将一个对象的引用 看成 做用域,将对象的属性看成做用域的标识符,建立一个新的词法做用域(运行时)。

反作用 是引擎没法在编译时对做用域查找进行优化。由于引擎只能谨慎地认为这样的优化是无效的,使用任何一个机制都将致使代码运行变慢。废弃它们。

最后, 读书是由厚到薄,又由薄到厚的双向过程,注重领悟、实践,不断踩坑、提高,如有帮助,请点个赞,谢谢您的支持与指教。

参考文献:

木易杨博客

隙游尘博客

taopoppy 博客

历史文章:

细读《你不知道的JavaScript·上卷》1-1 做用域是什么

【译】30 Seconds of ES6 (一)

相关文章
相关标签/搜索