1.什么是词法作用域
第一节讲到作用域是一套规则,那么词法作用域又是什么呢?简单来说词法作用域是由编写代码时将变量和块级作用域写在哪里决定的。
考虑以下代码
function foo(a){
var b = a * 2;
function bar(c) {
console.log(a, b, c)
}
bar(b * 3);
}
foo(3);
分析一下各自的作用域:foo
是在顶层作用域的,引擎可以在任意位置访问到它。a、bar、b
是嵌套在foo
作用域下的变量,而c
则是嵌套在bar
下的变量。
再来分析一下代码的执行过程:当引擎执行到foo(2)
时会在当前作用域找到foo
并执行,同时会找到foo
作用域下的a
并赋值为3
。接下来引擎会在foo
作用域下找到b
并将其赋值为6
。代码继续执行到bar(b * 3)
此时foo
作用域下的b = 6
那么引擎通过计算将bar
作用域下的c
赋值为18
。这时来到了打印函数,在bar
作用域下引擎只能找到c
变量,关于a和b
引擎只能向上一级foo
的作用域查找并且最终找到a = 2, b = 6
,最终输出 3 6 18
。这时就会发现变量的查询逻辑跟当初编写代码的位置是一样的。这就是词法作用域,它跟代码编写的位置有关,也就是说在编写完代码的时候词法作用域就已经确定了,而不是在程序运行到某处时作用域才确定。
2.欺骗词法
经过上面的分析已经了解到词法作用域
是什么了,如果词法作用域
完全由写代码期间函数或块所声明的位置来确定的话,那么怎样在运行时来修改(欺骗)词法作用域
呢?
在JavaScript中有两种方式来达到这个目的(非常不推荐,因为它会产生预期之外的结果,这里只是介绍,在实际使用中应避免)
2.1 eval
eval
函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码,换句话说就是:可以在写代码的过程中使用程序生成代码片段并运行,就像它原本就在那里一样。
在引擎执行的过程中它并不知道这段代码是以动态的形式插入进来的,并对词法作用域
环境进行了修改。
思考以下代码
function foo(str, a){
eval(str);
console.log(a, b);
}
var b = 2;
foo('var b = 3;', 1);
分析一下这段代码:引擎执行到var b = 2;
时会认为此时b = 2
,执行到eval(str)
时这段代码var b = 3;
会被引擎当做它本来就在这个位置而不是动态插入的。此时在foo
作用域下会有变量b = 3
,因此这段代码对已经存在foo
的词法作用域进行了动态的修改并遮蔽了外部作用域中的同名变量。这时导致的输出就会变成1, 3
而不是期望中的1, 2
,这样引擎就永远找不到外部的变量b = 2
了。
上面的小栗子为了简洁展示传入的代码片段式固定的,但在实际情况中可以非常容易地根据程序逻辑动态地将字符拼接在一起之后传递。eval
通常被用来执行动态创建的代码,技术上通过一些技巧可以间接调用eval
来使其运行在全局作用域中,并对全局作用域进行修改。无论什么情况eval
都可以在运行时修改书写期间的词法作用域。使用不当可能会导致预期之外的结果,这里还是建议慎用。
JavaScript中还有一些与eval
类似的函数如setTimeout(), setInterval
这些功能已经不提倡使用。
2.2 with
with
是JavaScript中另一个难以掌握的用来欺骗词法作用域的关键字,with
通常被当做重复引用同一个对象中的多个属性的快捷方式,不需要重复引用对象本身。
with
使用小栗子
var obj = {
a: 1,
b: 2,
c: 3
}
// 单独调用需要重复写对象本身
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 使用with快捷方式
with(obj) {
a = 3;
b = 4;
c = 5;
}
这样看起来with
还挺好用的!
但是思考如下代码
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 不对劲了!!!
来简单分析一下,当foo
函数接收o1
的时候,a
是o1
的一个属性自然会把o1.a
的值改为2
,但是在执行foo(o2)
的时候因为o2
没有a
这个属性,因此不会在o2
上创建a
这个属性,所以o2.a
是undefined
。但是这里有一个副作用,实际上a = 2
赋值操作创建了一个全局的变量a
。
with
可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符(变量),但是这个块内部a = 2
在当前作用域下并没有找到声明,那么引擎就会像上一级寻找,直到全局作用域也没有找到,此时则会在全局创建一个变量a
并将2
赋值给它。
到这里可能会有疑问:第一节不是说引擎会首先询问当前作用域是否有这个变量,没有的话直接创建吗?
这里要提醒一点在JavaScript非严格模式中var a = 2;
和a = 2;
是有区别的:如果a
在当前作用域中已经被声明过,那么它会对已声明的变量重新赋值。如果a
在当前作用域中未被声明,在非严格模式下,它会隐式地在全局作用域创建一个新的变量a
并赋值为 2
;在严格模式下,直接对未声明的变量赋值会抛出引用错误。
所以对于with
来说也是不推荐使用,在严格模式下with
会被完全禁止。
3.小结
词法作用域
意味着作用域在编写代码的时候就已经决定了,通过编写的代码就可以分析出变量来自哪里,从而可以预测自己程序的走向,但在JavaScript中的两个机制:eval
和with
,eval
可以对一段包含一个或多个声明的代码字符串进行演算,并动态的修改已经存在的词法作用域(代码运行时),with
的本质上是通过将一个对象的引用当做作用域来处理,将对象的属性当做作用域的标识符(变量)从而创建一个新的词法作用域(代码运行时),这两个机制的副作用
是引擎在编译时无法进行优化的,使用任意一个机制都将导致代码运行变慢。
最终的结论就是不要在代码中使用它们。