词法阶段
简单地说, 词法作用域就是定义在词法阶段的作用域。
换句话说, 词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的, 因此当词法分析器处理代码时会保持作用域不变。
window.a
通过这种技术可以访问那些被同名变量所遮蔽的全局变量。
但非全局的变量如果被遮蔽了, 无论如何都无法被访问到。
欺骗词法
function foo(str, a) { eval(str); console.log(a, b); } var b = 2; foo('var b = 3;', 1); // 1, 3
JavaScript 中的 eval(..) 函数可以接受一个字符串为参数, 并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。
eval(..) 通常被用来执行动态创建的代码, 因为像例子中这样动态地执行一段固定字符所组成的代码, 并没有比直接将代码写在那里更有好处。
function foo(str) { 'use strict'; eval(str); console.log(a); // ReferenceError: a is not defined } foo('var a = 2');
在严格模式的程序中, eval(..) 在运行时有其自己的词法作用域, 意味着其中的声明无法修改所在的作用域。
setTimeout(..) 和setInterval(..) 的第一个参数可以是字符串, 字符串的内容可以被解释为一段动态生成的函数代码。 这些功能已经过时且并不被提倡。 不要使用它们!
new Function(..) 函数的行为也很类似, 最后一个参数可以接受代码字符串, 并将其转化为动态生成的函数(前面的参数是这个新生成的函数的形参)。
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; }
with 通常被当作重复引用同一个对象中的多个属性的快捷方式, 可以不需要重复引用对象本身。
尽管 with 块可以将一个对象处理为词法作用域, 但是这个块内部正常的 var 声明并不会被限制在这个块的作用域中, 而是被添加到 with 所处的函数作用域中。
o2 的作用域、 foo(..) 的作用域和全局作用域中都没有找到标识符 a, 因此当 a=2 执行时, 自动创建了一个全局变量(因为是非严格模式)。
性能
1.eval(..) 和 with 会在运行时修改或创建新的作用域, 以此来欺骗其他在书写时定义的词法作用域。
2.JavaScript 引擎会在编译阶段进行数项的性能优化。
其中有些优化依赖于能够根据代码的词法进行静态分析, 并预先确定所有变量和函数的定义位置, 才能在执行过程中快速找到标识符。
3.但如果引擎在代码中发现了 eval(..) 或 with, 它只能简单地假设关于标识符位置的判断都是无效的, 因为无法在词法分析阶段明确知道 eval(..) 会接收到什么代码, 这些代码会如何对作用域进行修改, 也无法知道传递给 with 用来创建新词法作用域的对象的内容到底是什么。
4.如果代码中大量使用 eval(..) 或 with, 那么运行起来一定会变得非常慢。
小结
1.词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。
编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的, 从而能够预测在执行过程中如何对它们进行查找。
2.JavaScript 中有两个机制可以“欺骗” 词法作用域: eval(..) 和 with。
前者可以对一段包含一个或多个声明的“代码” 字符串进行演算, 并借此来修改已经存在的词法作用域(在运行时)。
后者本质上是通过将一个对象的引用当作作用域来处理, 将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。
3.这两个机制的副作用是引擎无法在编译时对作用域查找进行优化, 因为引擎只能谨慎地认为这样的优化是无效的,使用这其中任何一个机制都将导致代码运行变慢,不要使用它们。