第2章:词法作用域
作用域共有两种主要的工作模型:
- 第一种是最为普遍的,被大多数编程语言所采用的
词法作用域
。 - 另外一种叫作
动态作用域
,仍有一些编程语言在使用(比如 Bash 脚本、Perl 中的一些模式等)。
词法作用域 和 动态作用域的区别:
function foo() {
console.log( a ); // 2
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
复制代码
以上代码:词法作用域让 foo() 中的 a 通过 RHS 引用到了全局作用域中的 a,因此会输出 2。 JavaScript只有词法作用域
。
如果是动态作用域
:因为当 foo() 无法找到 a 的变量引用时,会顺着调用栈在调用 foo() 的地方查找 a,而不是在嵌套的词法作用域链中向上查找。由于 foo() 是在 bar() 中调用的,引擎会检查 bar() 的作用域,并在其中找到值为 3 的变量 a。
需要明确的是,事实上 JavaScript 并不具有动态作用域。它只有词法作用域,简单明了。但是 this 机制某种程度上很像动态作用域。
主要区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。(this 也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。
1 词法阶段
- 包含着整个全局作用域,其中只有一个标识符:foo。
- 包含着 foo 所创建的作用域,其中有三个标识符:a、bar 和 b。 . 包含着 bar 所创建的作用域,其中只有一个标识符:c。
1.1 查找
以上代码的查找过程:
在上一个代码片段中,引擎执行 console.log(..) 声明,并查找 a、b 和 c 三个变量的引用。它首先从最内部的作用域,也就是 bar(..) 函数的作用域气泡开始查找。引擎无法在这里找到 a,因此会去上一级到所嵌套的 foo(..) 的作用域中继续查找。在这里找到了 a,因此引擎使用了这个引用。对 b 来讲也是一样的。而对 c 来说,引擎在 bar(..) 中就找到了它。
如果 a、c 都存在于 bar(..) 和 foo(..) 的内部,console.log(..) 就可以直接使用 bar(..)中的变量,而无需到外面的 foo(..) 中查找。
- 作用域查找会在找到第一个匹配的标识符时停止。(外部被遮蔽)
- 无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处 的位置决定。
- 词法作用域查找只会查找一级标识符
2 欺骗词法作用域
在运行时“修改”词法作用域 JavaScript 有两种这样的机制:eval 和 with。(正常应用场景很少,而且会影响性能在代码中应当被避免。)
2.1 eval
JavaScript 中的 eval(..)
函数接收一个字符串作为参数值,并将这个字符串的内容看作是好像它已经被实际编写在程序的那个位置上。换句话说,你可以用编程的方式在你编写好的代码内部生成代码,而且你可以运行这个生成的代码,就好像它在编写时就已经在那里了一样。
在 eval(..)
被执行的后续代码行中,引擎 将不会“知道”或“关心”前面的代码是被动态翻译的,而且因此修改了词法作用域环境。引擎 将会像它一直做的那样,简单地进行词法作用域查询。
考虑如下代码:(非 strict 模式)
function foo(str, a) {
eval( str ); // 作弊!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1 3
// 1,执行 foo();
// 2,执行 eval(str);
// 3,在 eval(..) 调用的位置上 生成var b = 3,修改了现存的 foo(..) 的词法作用域
// 4,执行console.log(..) 在foo(..) 的作用域中找到 a 和 b (并不会在全局作用域中查找)
复制代码
strict 模式下会报错:
function foo(str) {
"use strict";
eval(str);
console.log(a); // ReferenceError: a is not defined
}
foo("var a = 2");
复制代码
2.2 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 -- 哦,全局作用域被泄漏了!
// 1,执行foo(o1) 赋值 a = 2 , 找到属性 o1.a, o1.a = 2
// 2,执行foo(o2) 赋值 a = 2 , o2 没有 a 属性 o2.a = undefined,
//(这里赋值 a = 2 创建了一个全局变量 a,如果 a = 2 加 var 则a属于foo函数的作用域)
复制代码
“作用域” o2
中没有,foo(..)
的作用域中也没有,甚至连全局作用域中都没有找到标识符 a
,所以当 a = 2
被执行时,其结果就是自动全局被创建(因为我们没有在 strict 模式下)。
3 性能
JavaScript引擎在编译阶段期行许多性能优化工作。其中的一些优化原理都归结为实质上在进行词法分析时可以静态地分析代码,并提前决定所有的变量和函数声明都在什么位置,这样在执行期间就可以少花些力气来解析标识符。
但如果引擎在代码中发现一个 eval(..)
或 with
,它实质上就不得不假定自己知道的所有的标识符的位置可能是无效的,因为它不可能在词法分析时就知道你将会向eval(..)
传递什么样的代码来修改词法作用域,或者你可能会向with
传递的对象有什么样的内容来创建一个新的将被查询的词法作用域。
换句话说,悲观地看,如果 eval(..)
或 with
出现,那么它将做的几乎所有的优化都会变得没有意义,所以它就会简单地根本不做任何优化。
在旧的浏览器中如果你使用了eval,性能会下降10倍。在现代浏览器中有两种编译模式:fast path
和slow path
。fast path
是编译那些稳定和可预测(stable and predictable)的代码。而明显的,eval不可预测,所以将会使用slow path
,所以会慢。
使用with
关键字或者eval(..)
对性能的影响还有一点就是js压缩工具,它无法对代码进行压缩,这也是影响性能的一个因素。
复习
词法作用域意味着作用域是由编写时函数被声明的位置的决策定义的。编译器的词法分析阶段实质上可以知道所有的标识符是在哪里和如何声明的,并如此在执行期间预测它们将如何被查询。
在 JavaScript 中有两种机制可以“欺骗”词法作用域:eval(..)
和 with
。前者可以通过对一个拥有一个或多个声明的“代码”字符串进行求值,来(在运行时)修改现存的词法作用域。后者实质上是通过将一个对象引用看作一个“作用域”,并将这个对象的属性看作作用域中的标识符,(同样,也是在运行时)创建一个全新的词法作用域。
这些机制的缺点是,它压制了引擎在作用域查询上进行编译期优化的能力,因为引擎不得不悲观地假定这样的优化是无效的。这两种特性的结果就是代码将会运行的更慢。不要使用它们。
附录:关于eval的一些问题
原文:www.nczonline.net/blog/2013/0…
eval()
这个简单的函数被设计用来执行一个字符串作为JavaScript代码,有几点需要了解:
滥用
滥用与性能或安全无关,而是与不理解如何构建和使用JavaScript中的引用有关。假设您有多个表单输入,其名称包含一个数字,例如“option1”和“option2”,通常会看到:
function isChecked(optionNumber) {
return eval("forms[0].option" + optionNumber + ".checked");
}
var result = isChecked(1);
复制代码
在这种情况下,开发人员正在尝试编写,forms[0].option1.checked
但没有意识到如何在不使用的情况下做到这一点eval()
。你会看到这种类型的模式在大约十岁以上的代码中很多,因为当时的开发人员不明白如何正确使用该语言。在eval()
这里使用不合适,因为它不是不必要的,不是因为它不好。您可以轻松地将此功能重写为:
function isChecked(optionNumber) {
return forms[0]["option" + optionNumber].checked;
}
var result = isChecked(1);
复制代码
可调试
eval()
不容易调试。用 chromeDev
等调试工具无法打断点调试,这意味着你将代码运行到一个黑盒子中,然后从中取出。Chrome开发者工具现在可以调试 eval()
编码,但仍然很痛苦。您必须等待代码执行一次后,才会显示在“来源”面板中。避免 eval()
编辑代码使调试变得更加容易,使您可以轻松查看和逐步浏览代码。
性能
上面有提到,使用时性能确实是一个大问题。
安全
如果你正在接受用户输入并eval()
以某种方式传递它,那么你是在寻求麻烦。永远不要这样做。但是,如果您使用的eval()
输入只有您自己控制并且不能被用户修改,那么就没有安全风险。
所以,只要你的信息源不安全,你的代码就不安全。不单单是因为eval引起的。