你不知道的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,那么运行起来一定会非常慢。无论引擎多聪明,试图将这些悲观情况的副作用限制在最小范围内,也无法避免如果没有这些优化,代码会运行的更慢这个事实

 

posted @ 2020-02-17 11:24  wzndkj  阅读(184)  评论(0编辑  收藏  举报