你不知道的JS系列 ( 7 ) - 欺骗词法作用域
如果词法作用域完全由写代码期间函数所声明的位置来定义,怎样才能在运行时来“修改”词法作用域呢?有些人喜欢特殊的办法来解决遇到的问题。我们规定词法作用域是代码写在哪里决定的,一旦决定了无法更改,因为一些问题,我们不得不更改作用域,尽管这是不被推荐的,那是什么办法,JavaScript 中有两种机制来实现这个目的
eval
function foo(str, a) { eval(str); console.log(a, b) } var b = 2; foo("var b = 3", 1); // 1, 3
eval() 函数可以接受一个字符串为参数,然后可以在写的代码中用程序生成代码并运行,就好像代码是写在那个位置一样
eval() 调用中的 "var b - 3;",这段代码声明了一个新的变量 b,因此它对已经存在的 foo 的词法作用域进行了修改。事实上,和前面提到的原理一样,这段代码实际上在 foo 作用域内部创建了一个变量 b,并遮蔽了外部作用域的同名变量。
当 console.log() 被执行时,在 foo 作用域内部同时找到 a 和 b,因为作用域查找会在找到第一个匹配当标识符时停止,所以永远也无法找到外部的 b。
默认情况下,如果 eval() 中所执行的代码包含有一个或多个声明的变量还是函数,就会对 eval() 所处的词法作用域进行修改。严格模式下不会,严格模式下, eval 有自己的词法作用域,意味着其中的声明无法修改所在的作用域。
在程序中动态生成代码的使用场景非常罕见,因为它所带来的好处无法抵消性能上的损失
with
JavaScript 中的另一个难以掌握的用来欺骗词法作用域的功能可能是 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 被泄漏到全局作用域了
o2 的作用域,foo 的作用域和全局作用域都没有找到标识符 a,因此当 a = 2 执行时,自动创建了一个全局变量(非严格模式下)。
另外一个不推荐使用 eval() 和 with 的原因是会被严格模式所影响。with 完全禁止,而在保留核心功能的前提下,间接或非安全地使用 eval() 也被禁止了
如果它们能实现更复杂的功能,并且代码更具有扩展行,难道不是非常好的功能吗?答案是否定的。
JavaScript 引擎在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。但如果引擎在代码中发现 eval() 或 with,它只能简单地假设关于标识符位置的判断都是无效的。
如果代码中大量使用 eval() 或 with,那么运行起来一定会非常慢。无论引擎多聪明,试图将这些悲观情况的副作用限制在最小范围内,也无法避免如果没有这些优化,代码会运行的更慢这个事实