0%

欺骗词法作用域的两种方式

说明
1. 部分参考作者姚屹晨在简书上的笔记https://www.jianshu.com/p/650e458bc9ce
2. 部分参考作者m4jing在Github上的译文https://github.com/getify/You-Dont-Know-JS/blob/1ed-zh-CN/scope%20%26%20closures/ch2.md

一、欺骗词法作用域

eval

  1. eval函数可以接收一个字符串作为参数,以动态形式插入程序的某个位置,并对其词法作用域的环境进行修改。

    1
    2
    3
    4
    5
    6
    7
    function foo(str,a){
    eval(str);
    console.log(a,b);
    }
    var b = 2;
    foo('var b = 5;',1);
    >>>1 5

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function foo(str,a){
    "use strict"
    eval(str);
    console.log(a,b);
    }
    var b = 2;
    foo('var b = 5;',1);
    >>>1 2

    function foo(str,a){
    "use strict"
    eval(str);
    console.log(a,b);
    }
    foo('var b = 5;',1);
    >>>Uncaught ReferenceError: b is not defined

with

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var obj = {
    a: 1,
    b: 2,
    c: 3
    };

    obj.a = 2;
    obj.b = 3;
    obj.c = 4;

    with(obj) {
    a = 3;
    b = 4;
    c = 5;
    }

  2. 实际上不仅仅是为了方便地访问对象属性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function foo(obj){
    with(obj){
    a = 2;
    }
    }
    var o1 = {
    a: 3
    };
    var o2 = {
    b: 3
    };
    foo(o1);
    o1.a;
    >>>2

    foo(o2);
    o2.a;
    >>>undefined

    a;
    >>>2
    //a被泄露到全局作用域中。
    因为,在非严格模式下,若a=2中的变量a未声明,也就是在任何作用域中都查找不到变量a,那么就会在全局作用域中创建一个变量a,并将2赋值于它。

    foo(o1)
    foo(o2)
  3. with语句接收一个对象,这个对象有0个或多个属性,并将这个对象视为好像它是一个完全隔离的词法作用域,因此这个对象的属性被视为在这个“作用域”中词法定义的标识符。如上图中的o1 Scopeo2 Scope

  4. 尽管一个with块将一个对象视为一个词法作用域,但在with块内部的一个普通var声明将不会归于这个with块的作用域,而是归于包含它的函数作用域。

  5. 当我们传递对象o1with时,with所声明的作用域是o1,而这个作用域中含有一个与o1.a属性同名的标识符。但当我们将o2对象作为作用域时,其中没有a标识符,因此进行了正常的LHS (Left Hand Side)查找,最后导致创建了一个全局变量a并赋值为2。

二、性能

JavaScript引擎在编译阶段期进行许多性能优化工作。其中的一些优化原理都归结为实质上在进行词法分析时可以静态地分析代码,并提前决定所有的变量和函数声明都在什么位置,这样在执行期间就可以少花些力气来解析标识符。

但如果引擎在代码中找到一个eval(..)with,它实质上就不得不假定自己知道的所有的标识符的位置可能是不合法的,因为它不可能在词法分析时就知道你将会向eval(..)传递什么样的代码来修改词法作用域,或者你可能会向with传递的对象有什么样的内容来创建一个新的将被查询的词法作用域。

换句话说,悲观地看,如果eval(..)with出现,那么它将做的几乎所有的优化都会变得没有意义,所以它就会简单地根本不做任何优化。没有优化,代码就运行的更慢。