简介

作用域共有两种主要的工作模型。

  1. 动态作用域,仍有一些编程语言在使用(比如Bash脚本、Perl中的一些模式等)。
  2. 词法作用域,是最为普遍的,被大多数编程语言所采用的词法作用域。词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。

但是JavaScript中存在两个机制可以“欺骗”词法作用域:eval(…)和with。

  1. eval(…)可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域。
  2. with本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域。

1.eval

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

function f(a,b){
    eval(a);//欺骗
    console.log(b,c);
}
var c=2;
f("var c=3",1);//输出结果为1 3复制代码

eval(..)调用中的"var c=3" 这段代码会被当作本来就在那里一样来处理。 由于声明了一个新的变量c,因此这段代码会在f(..)内部创建了一个变量c,并遮蔽了外部作用域中的同名变量。当console.log(..)被执行时,会在f(..)的内部同时找到b和c,但是永远也无法找到外部的c。因此会输出"1, 3"。

2.with

with通常被当作重复引用同一个对象中的多个属性的快捷方式,但其实还有其他用法。

function f(obj){
    with(obj){
        a=2;
    }
} 
var o1 ={ a:3 };
var o2 ={ b:3 };
f(o1);
console.log(o1.a);//输出结果为2
f(o2);
console.log(o2.a);//输出结果为undefined
console.log(a);//输出结果为2复制代码

我们将o1传递进去,a=2赋值操作找到了o1.a并将2赋值给它。而当o2传递进去,o2并没有a属性,因此不会创建这个属性o2.a保持undefined。

但是可以注意到一个奇怪的现象,为什么最后一行输出为2?实际上是因为a=2赋值操作创建了一个全局的变量。with可以将一个没有或者有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符。

当我们传递o1给with时,with所声明的作用域是o1,而这个作用域中含有一个同o1.a属性相符的标识符。但当我们将o2作为作用域时,其中并没有a标识符,因此进行了正常的LHS标识符查找。o2的作用域,f的作用域和全局作用域中都没有找到标识符a,因此当a=2执行时,自动创建了一个全局变量(因为是非严格模式)。

总结

eval(..)函数如果接受了含有一个或多个声明的代码,就会修改其所处的词法作用域,而with声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域。

但这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。所以尽量不要使用它们。