你不知道的JS-上(二)

29 阅读5分钟

你不知道的 JS-上

作用域和闭包

词法作用域

作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域,另一种叫做动态作用域。

程序中的代码在执行前一般都要经历三个步骤 分词/词法分析、解析/语法分析、代码生成

词法阶段

大部分标准语言编译器的第一个工作阶段叫作词法化(也叫单词化)。词法化的过程会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义。

简单来说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。

function foo(a) {
    var b = a * 2;

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

    bar(b * 3);
}
foo(2)

该代码片段中,引擎执行 console.log(..)声明,并查找 a、b 和 c 三个变量的引用。会先在最内部的 bar(..)函数的作用域开始查找,如果无法查找就会去到上一级作用域 foo(..)中继续查找。在这里找到 a,因此引擎就使用了这个引用。对 b 来说也一样。而 c 在 bar(..)中就找到了。

作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫做“遮蔽效应”(内部的标识符“遮蔽了”外部的标识符)。抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外进行,知道遇见第一个匹配的标识符为止。

欺骗词法

通过欺骗词法可以“修改”(或者说是欺骗)词法作用域,但在 JS 中并不推荐使用,这会导致性能下降。

JS 中有两种机制实现词法欺骗:eval、with

eval

eval(..)函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中的这个位置的代码。

在执行 eval(..)之后的代码时,引擎并不“知道”或“在意”前面的代码是以动态的形式插入进来的,并对词法作用域的环境进行修改的。引擎只会如往常地进行词法作用域查找。

function foo(str,a){
    eval(str); //欺骗!
    console.log(a,b)
}

var b = 2;

foo("var b = 3;",1)  //1,3

eval(..)调用中的“var b = 3;”,就像本来就处于 foo 中一样,由于原本的代码声明了一个变量 b,因此 eval 对已经存在的词法作用域进行了修改。所以在执行时,这段代码在 foo(..)内部创建了一个变量 b,并遮蔽了外部打同名变量。

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

with

with 关键字是 JS 中另一个难以掌握(也不推荐使用)的用来欺骗词法作用域的功能。with 语句会将给定的对象添加到作用域链的头部,在语句块内可以直接访问该对象的属性。

我们将从它如何同被它影响的词法作用域进行交互来解释

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

var obj = {
    a:1,
    b:2,
    c:3
};

//单调乏味的重复"obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;

//简单的快捷方式
with(obj){
    a=3;
    b=4;
    c=5
}

但实际上这不仅仅是为了方便地访问对象属性。考虑如下代码:

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被泄露全局作用域上了!

将o1传递进foo中,a=2的LHS操作在o1.a中找到并将2赋予它,所以输出2。而将o2传递进foo中,o2没有a属性,因此不会创建这个属性o2.a保存undefined,但却在全局作用域下创建了变量a,且赋值为2。这是怎么回事?

with可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符

即上述例子中o2.a没有定义,a=2操作会一直向上寻找,直到全局也没找到,则隐式创建a。

尽管with块可以将一个对象处理为词法作用域,但是这个块内部正常的var声明并不会被限制在这个块的作用域中,而是被添加到with所处的函数作用域中。

  • eval(..)函数修改的是参数所处的作用域

  • with声明是根据传递的对象凭空创建一个全新的词法作用域。

另一个不推荐使用eval(..)和with的原因是会被严格模式所影响(限制)。with被完全禁止,而在保留核心功能的前提下,间接或非安全地使用eval(..)也被禁止了。

性能

eval(..)和with会在运行时修改或创建新的作用域,以此来欺骗其他在书写时定义的词法作用域。

在JS引擎编译阶段会进行数项的性能优化,其中有些优化依赖于能够根据代码的词法进行静态分析,预先确定所有函数和变量的定义位置,以便在执行过程中快速找到。

但eval(..)和with会对词法作用域进行动态修改,使得引擎难以判断标识符的位置,使得优化难以进行。