词法作用域
上回书说到,我们将作用域定义为一套规则,这套规则用来管理引擎如何查找变量,而作用域的工作模型主要有两种: 词法作用域和动态作用域,词法作用域是最普遍的,被大多数编程语言使用,JS也是使用的这种模型.作用域如何工作就是由其采用的工作模型确定的
词法阶段
在第一章中说过,源代码执行前会经历三个阶段: 分词/词法分析,解析/语法分析,生成机器指令.第一个阶段就是词法阶段.
静态作用域
词法作用域就是定义在词法阶段的作用域,也可以说: 词法作用域是由写代码时变量写在哪里决定的,因此作用域在写代码时就确定了.(大部分情况是这样,当使用eval,with时,词法作用域是可以动态修改的)
在没有深入js之前就听过一句话: ''js的作用域是静态的'',现在知道了这句话的依据,那就是js的作用域使用的工作模型是词法作用域,词法作用域在词法阶段就已经确定了,
遮蔽效应
作用域查找会在找到第一个匹配的标识符时停止,因此内层作用域和外层作用域的同名变量,由内向外查找时优先使用内层的变量
欺骗词法
上文中说,作用域可以通过eval和with动态修改,来欺骗词法作用域,然而这种做法是非常不推荐的: 欺骗词法作用域只会导致性能下降
eval
js中eval()可以接收一个字符串作为参数,并将其中内容视为好像书写时就存在该位置的代码进行运行.
因此根据传递的字符串,可以动态的修改当前作用域:
function foo(str, a) {
eval(str);
console.log(a, b);
}
var b = 2;
foo('var b = 3', 2); //输出2 3而非2 2
如果作用域没有改变,当执行console.log(a,b)时,会对b进行RHS,发现当前作用域没有,便向外部作用域查找,并返回2
然而由于由eval的存在,将var b = 3当作本来就在这个位置进行处理,结果就变成: 当执行console.log(a,b)时,对b进行RHS,由于使用eval在当前作用域动态声明了一个b变量,那么由于遮蔽效应,直接使用当前作用域的变量b
另: 严格模式下,eval()有自己的词法作用域,此时无法修改所在作用域的
与eval类似setTimeout,setInterval的第一个参数可以是字符串,也会被当作动态生成的代码函数,new Function的最后一个参数也可以接收字符串并将其转换成动态函数
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;
}
with单独形成一个obj的作用域,在这个作用域下访问变量都是obj上的属性
与eval修改作用域不同,with是创建一个全新的词法作用域,我们传递obj给whit时,with所声明的作用域就是obj
再看一个泄漏的例子:
function foo(obj){
with(obj){
a=2
}
}
var o1 = {
a:1
}
var o2 = {
b:2
}
foo(o1)
console.log(o1.a) //2
foo(o2)
console.log(o1.b) //undefined
console.log(a) // 2,a直接泄漏到全局作用域中
上述例子中,执行foo(o1)时,由于o1中包有a属性,所以一切正常,将o1.a赋值为2,当执行foo(o2)时,由于o2没有a属性,所以没有赋值o2.a为undefined,那么为什么在全局作用域打印a也能成功呢?
其实foo(o2)的整个过程是这样的: 由于whit创建了一个新的作用域,那么整个作用域嵌套式这样的: o2作用域 => 函数作用域 => 全局作用域,当执行a=2时,对a进行RHS,发现o2作用域没有该变量,于是就去函数作用域查找,直到全局作用域都没有,于是在全局作用域隐式声明了a
性能问题
为什么说eval和with会对性能有很大的影响呢?其实真正对性能产生影响的是不确定的作用域的产生
在JavaScript中引擎会在编译阶段做很多性能优化,其中有些优化依赖于能够根据代码的词法做静态分析,并且预先确定所有变量和函数定义的位置,以便需要的时候能快速找到.
但是如果代码中使用了eval和with,就不能在词法阶段确定作用域是否进行了修改,如何修改,进而预先确定所有变量和函数的定义位置的优化就无法进行,进行了也是无意义的,最坏的情况就是不做任何优化.因此如果代码大量使用了eval,with,那么运行起来一定会很慢.