词法阶段
大部分语言的第一个工作阶段是词法化(也叫单词化),词法化的过程会对源代码中的字符进行检查。
而词法作用域就是定义在词法阶段的作用域。它只和写代码时将变量和块作用域写在哪里来决定的。
function foo ( a ) {
var b = a;
function bar( c ) {
console.log( a, b, c );
}
bar( b );
}
foo( 2 );
对于以上例子,有三个作用域。分别是:
- 全局作用域,只有变量foo
- foo所创建的作用域,变量有a,b,bar
- bar所创建的作用域,变量有 c
作用域的查找规则
在上一个代码片段中,引擎执行到console.log( a, b, c )
时,需要查找a,b,c三个变量的位置。它首先从最内部的作用域(bar所创建的作用域)查找,然后逐层向外。顺序依次是 “bar->foo->window(全局作用域)”。作用域查找过程中有个重要的规则是:
作用域查找到第一个匹配的标识符时就停止往外层作用域查找。
上述例子中在bar中找到了c,就停止了c的查找。然后就是a,b,随之在foo中找到了a,b。所以引擎不会在全局作用域中查找变量了。在不同层级的作用域定义相同的变量,内层的会遮蔽外层的。这叫做 “遮蔽效应”。所以在内层的话,是无法访问到外层相同的变量的。除非是window中的变量,比如a。使用window.a
访问。
欺骗词法
词法作用域在写代码期间函数声明的位置定义,但是有办法在运行时来“修改”(欺骗)词法作用域。它们就是eval和with。但是这会造成性能下降。因为js引擎不会对此进行优化。
eval
eval()函数可以接受一个字符串作为参数,并将其中的内容视为好像在书写时就存在那个位置(eval()的位置)的代码。
在执行eval()之后的代码时,引擎并不知道前面的代码是动态插入还是正常书写的,尽管动态插入会对作用域进行欺骗,但引擎只会按照作用域查找规则向上层查找。
function foo ( str, a ) {
eval( str );
console.log( a, b );
}
var b = 2;
foo( 'var b = 3;', 1 ); // 1, 3
在eval( str );
中,对var b = 3;
进行解析,好像这个声明本来就在那儿一样。所以它会屏蔽外层的b变量的声明。在实际中,更常用的是用eval来执行动态创建的代码。
在严格模式(”use strict”)中,eval()在运行时有其自己的此法作用域,意味着其中的声明无法修改其所在的作用域。
function foo( str ) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 3" );
在JS中,还有一些功能效果和eval()类似,setTimeout()和setInterval的第一个参数可以是字符串,字符串的内容可以被解释成一段动态生成的函数代码。这些功能已经过时并且不被提倡,不要使用它们。
new Function()这种方式声明函数的行为也类似,它的参数可以是字符串,并将其转换为动态生成的函数。不建议使用这种方式。
var foo = new Function( "a", "b", "return a+b" );
alert( foo( 1, 2) )
with
with也可以欺骗词法作用域,并且也不推荐使用。
with通常被当做引用同一个对象中多个属性的快捷方式,可以不需要重复引用对象本身。
var obj = {
a: 1,
b: 2,
c: 3
}
// 修改obj
obj.a = 2;
obj.b = 3;
obj.c = 4;
//使用with
with( obj ) {
a = 2;
b = 3;
c = 4;
}
这样看起来还不错,但问题的关键是:
function foo( obj ) {
with( obj ) {
a = 2;
}
}
var o1 = { a: 3 };
var o2 = { b: 3 };
foo( o1 );
alert( o1.a ); // 2
foo( o2 );
alert( o2.a ); // undefined
alert( a ); // 2
alert( o2.a ); // undefined
可能会让人费解,其原因是with根据你传给它的对象凭空创建了一个一个新的词法作用域。传递o1给with时,with声明的作用域是o1,找到a属性并修改。传递o2给with时,with声明的作用域是o2,其中并没有a标识符,因此进行了一次LHS(作用域中有降到)查询。o2的作用域,foo()的作用域和全局作用域都没有找到a变量,所以引擎创建了一个全局变量a,并赋值为2。
尽管with会创建一个新的作用域,但在这个作用域中用var声明的变量并不会被限制在这个作用域中,而是被添加到with所在的作用域(这里是foo())中。
性能
eval()在运行时修改作用域,with()在运行时会创建一个新的作用域,以此来欺骗书写时定义的词法作用域。
这样可能确实起到了一些方便。但是从性能上来说,这是得不偿失的。
JS引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的此法进行竟然分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标志符。
但如果引擎在代码中发现了eval()和with(),它只能简单地认为这些优化(标识符位置的判断)是无效的。最悲观的情况是出现了它们,所有的优化都可能是无意义的。因此最简单的做法就是不做任何优化。
如果代码中有大量eval()和with(),那么运行起来一定会很慢,无论引擎会多聪明,试图将悲观的副作用限制在最小范围内。但毫无疑问的是,如果没有这些优化,代码会运行得更慢。
小结
1,词法作用域由书写代码时函数的位置决定的。编译的词法分析阶段基本能够知道全部标识符在哪以及是如何生命的,从而在运行的时候精确地对它们进行查找。
2,eval()和with()可以欺骗词法作用域。eval()在运行时修改作用域,with()在运行时会创建一个新的作用域,以此来欺骗书写时定义的词法作用域。在严格模式下,with被完全禁止,而eval()在运行时有其自己的此法作用域,意味着其中的声明无法修改其所在的作用域。
3,这两个机制的副作用是引擎无法在编译阶段对作用域进行优化,因此引擎对此不做优化。
4,不要使用它们。尽量!
参考
你不知道的JavaScript上卷