词法作用域

作用域模式有两种,一种是词法作用域,另一种是动态作用域,JavaScript采用的是词法作用域。

大部分的编译器会在编译阶段把程序进行词法化,也就是会对源代码中的字符进行解析,并且赋予词语语义。简单来讲,词法作用域就是词法阶段的作用域,词法作用域是由你在写代码时讲变量和块写在哪里决定的,当词法分析器在处理代码时会保持作用域不变。

考虑下面的代码:

1
2
3
4
5
6
7
8
9
10
11
var name = 'limoer';
function showNameAPI(name){
var city = 'Chongqing';
function showCity(){
var cid = 'CN';
console.log(city + cid);
}
showCity();
console.log(name);
}
showNameAPI(name)

这个代码一共包含三个逐级嵌套的作用域,全局作用域中声明了变量name,全局函数showNameAPI,函数作用域中showNameAPI所创建的作用域,包含标识符city以及showCity,最后是showCity创建的作用域,包含了标识符cid。

作用域查找会在找到第一个匹配的标识符时停止。这里的查找是由内而外的,并且在多级嵌套的作用域内可以定义同名的标识符,但是会产生覆盖。因为作用域查找的规则就是找到第一个匹配的标识符后停止

无论函数在哪里被调用,也无论其是怎么被调用的,其词法作用域只与其被声明的位置有关。

欺骗词法

上面说到词法作用域是在是完全在书写代码是就已经决定,但是也可以通过下面的两种方式在运行时来改变词法作用域。

当然,不出意外的,这两种方式会是不那么讨人喜欢的eval()和with。

我们首先来回顾一下eval(),这个函数接收一个字符串作为参数,这个字符串好像是运行时写在这里的代码一样。这明显是一种词法欺骗,其假装是在书写期间就在那里,而在运行时修改词法作用域。但是引擎对此并不知情,所以其依旧照常按照词法作用域进行查找。
看一个例子:

1
2
3
4
5
6
var name = 'limoer';
function showName(str){
eval(str);
console.log(name);
}
showName("var name = 'lindo'") // lindo

eval(‘name=”lindo”‘)会被引擎误认为在书写时就在那里,由于执行了上面的语句,此时name的值已经被修改了,并且产生了覆盖,遮蔽了外部同名的变量name。

在默认的情况下,如果eval中所执行代码中存在一个或者多个申明,其就会对eval()所处的作用域进行修改。无论何情况,eval(..) 都可以在运行期修改书写期的词法作用域。

再来谈with关键字,我们都知道with关键字用于重复引用一个对象的多个属性的快捷方式,而不需要重复引用对象本身。
看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {
name: 'limoer',
age: 20,
city: 'Chongqing'
}
console.log(obj.name)
console.log(obj.age)
console.log(obj.city)
// 重复
with(obj){
console.log(name);
console.log(age);
console.log(city);
}

上面的这段代码突出了with关键字优点,它可以简化我们的代码,但是我们这里谈的是with关键字的词法欺骗,看下面一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function info(obj){
with(obj){
name = 'lindo'
}
}
var p1 = {
name: 'limoer'
}
var p2 = {
city: 'Jinan'
}
info(p1)
console.log(p1.name) // 'lindo'
info(p2)
console.log(p2.name) // undefined
console.log(name) //lindo!

上面的这个例子很好的展示了with关键字的词法欺骗,这里创建了两个对象p1和p2,并通过info函数执行with(obj){…},这里进行了简单的LHS查找,并将新值赋给name属性。但是请注意,这里p2对象并不存在name属性,也不会创建name属性,所以p2.name为undefined;这里很好理解,但是为什么神奇的是竟然多出了一个全局变量name呢!?

这里执行with(obj){…}的时候,执行的LHS查找,所以当查找不成功时自动隐式创建一个全局变量,如果这样考虑,那么出乎意料的name属性就不难理解了。

总结

JavaScript拥有的是词法作用域,所谓的词法作用域就是在进行词法分析时的作用域,也就是说,JS的作用域在代码一旦书写完成就能确定(靠书写位置来确定)。词法作用域的理解很简单,但是我们还是需要注意使用eval()和with语句带来的词法欺骗的原因。也许有人会说,在运行时修改词法作用域有利于实现复杂的功能,又利于扩展,何乐而不为呢?可我们在前面提到,在进行编译的时候,JS引擎会对代码进行优化,而这个优化则是根据代码的词法作用域,预先确定变量和函数的位置,才能在执行过程中快速找到标识符。而eval()和with的出现则有可能打破这样的格局,因为引擎在词法分析阶段并不能知道传入的代码到底是什么,会对词法作用域造成怎样的影响。所以,一切优化都是徒劳的,因为在运行时谁都不能确定此时此法作用域到底是怎么样的,所以JS引擎并不会进行优化,导致代码运行缓慢,性能并不好。所以,尽量不要使用它们。

分享到 评论