《你不知道的JavaScript-上》词法作用域

作用域有两种工作模式:词法作用域、动态作用域。

词法阶段

词法作用域是定义在词法阶段(编译的第一步)的作用域。也就是,词法作用域是由你在写代码时将变量和块级作用域写在哪里来决定的。

1
2
3
4
5
6
7
8
9
10
11
function foo(a){
var b=a*2;

function bar(c){
console.log(a,b,c);
}

bar(b*3);
}

foo(2);

将上列函数的作用域想象成几个逐级包含的气泡:

气泡1包含着整个全局作用域, 其中只有一个标识符: foo 。
气泡2包含着foo 所创建的作用域, 其中有三个标识符: a 、 bar 和b 。
气泡3包含着bar 所创建的作用域, 其中只有一个标识符: c 。

作用域气泡由对应的作用域块代码写在哪里决定的。

查找

作用域气泡的结构和互相之间的位置关系能够给引擎查找提供足够的位置信息。
遮蔽效应:内部的标识符会“遮蔽”外部的标识符。
全局变量自动成为全局对象的属性,可以通过这种属性window.a访问那些被同名变量所遮蔽的全局变量。

欺骗词法

两种欺骗词法的方法:eval()with
两种方法会导致性能下降。

eval

eval()函数接受一个字符串参数,将其中的内容视为好像在书写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并运行,就好象代码是写在那个位置一样。

1
2
3
4
5
6
7
function foo(str,a){
eval(str); //欺骗!
console.log(a,b);
}

var b=2;
foo('var b=3;',1); //1,3而不是1,2

严格模式下,eval()在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域。

1
2
3
4
5
6
7
function foo(str){
"use strict";
eval(str);
console.log(a); //ReferenceError:a is not defined
}

foo('var a=2');

JS中还有其他3个功能效果与eval()相似,setTimeout()setInterval()的第一个参数是字符串时以及new Function()最后一个参数是字符串时。

with

with通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function foo(obj){
with(obj){
a=2;
}
}

var o1={
a:3
}

var o2={
b:3
}

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

foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——不好,a被泄漏到全局作用域上了!

with在严格模式下完全禁用,不推荐使用。

性能

JS引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。
但如果引擎发现eval()with,它只能简单假设关于标识符位置的判断是无效的,进而不能进行优化,导致性能降低。

小结

词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。

JavaScript中有两个机制可以“欺骗”词法作用域:eval(..)with 。前者可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)。后者本质上是通过将一个对象的引用当作 当作 作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。

这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。

liborn wechat
欢迎您扫一扫上面的微信二维码,订阅我的公众号!