《You Dont Know JS上卷》(二) ---词法作用域

69 阅读4分钟

词法作用域

上回书说到,我们将作用域定义为一套规则,这套规则用来管理引擎如何查找变量,而作用域的工作模型主要有两种: 词法作用域动态作用域,词法作用域是最普遍的,被大多数编程语言使用,JS也是使用的这种模型.作用域如何工作就是由其采用的工作模型确定的

词法阶段

在第一章中说过,源代码执行前会经历三个阶段: 分词/词法分析,解析/语法分析,生成机器指令.第一个阶段就是词法阶段.

静态作用域

词法作用域就是定义在词法阶段的作用域,也可以说: 词法作用域是由写代码时变量写在哪里决定的,因此作用域在写代码时就确定了.(大部分情况是这样,当使用eval,with时,词法作用域是可以动态修改的)

在没有深入js之前就听过一句话: ''js的作用域是静态的'',现在知道了这句话的依据,那就是js的作用域使用的工作模型是词法作用域,词法作用域词法阶段就已经确定了,

遮蔽效应

作用域查找会在找到第一个匹配的标识符时停止,因此内层作用域和外层作用域的同名变量,由内向外查找时优先使用内层的变量

欺骗词法

上文中说,作用域可以通过evalwith动态修改,来欺骗词法作用域,然而这种做法是非常不推荐的: 欺骗词法作用域只会导致性能下降

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是创建一个全新的词法作用域,我们传递objwhit时,with所声明的作用域就是obj

再看一个泄漏的例子:

function foo(obj){
    with(obj){
        a=2
    }
}
​
var o1 = {
    a:1
}
var o2 = {
    b:2
}
​
foo(o1)
console.log(o1.a) //2foo(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

性能问题

为什么说evalwith会对性能有很大的影响呢?其实真正对性能产生影响的是不确定的作用域的产生

在JavaScript中引擎会在编译阶段做很多性能优化,其中有些优化依赖于能够根据代码的词法静态分析,并且预先确定所有变量和函数定义的位置,以便需要的时候能快速找到.

但是如果代码中使用了evalwith,就不能在词法阶段确定作用域是否进行了修改,如何修改,进而预先确定所有变量和函数的定义位置的优化就无法进行,进行了也是无意义的,最坏的情况就是不做任何优化.因此如果代码大量使用了eval,with,那么运行起来一定会很慢.