var|「建议收藏」词法作用域( 二 )


    eval(str) //欺骗
    console.log(a b)


var b = 2
foo('var b = 3'1) // 1 3
eval 调用中的 'var b = 3' , 这段代码会被当作它们本来就在那里一样处理 。 由于那段代码声明了一个新的变量 , 因此它对已经存在的 foo 的词法作用域进行了修改 。 事实上 , 和前面提到的原理一样 , 这段代码实际上在 foo 内部创建了一个变量 b , 并遮蔽了外部作用域中的同名变量 。
当 console.log 被执行的时候 , 会在 foo 的内部同时找到 a 和 b , 但是永远也无法找到外部的 b , 因此会输出 1 3 , 而不是正常情况下的 1 2 。
默认情况下 , 如果 eval 中所执行的代码包含有一个或多个声明 , 就会对 eval 所处的词法作用域进行修改 。 技术上 , 通过一些技巧可以间接调用 eval 来使其运行在全局作用域中 ,并对全局作用域进行修改 。
JavaScript 中还有一些功能效果和 eval 很相似 , setTimeout 和 setInterval 中第一个参数可以是字符串 , 字符串的内容可以被解释为一段动态生成的函数代码 。 这些功能已经过时并不被提倡 。
new Function 函数的行为也很类似 , 最后一个参数可以接受代码字符串 , 并将其转化为动态的函数 , 这种构建函数的语法比 eval 略微安全一些 , 但也要尽量避免使用 。
在程序中动态生成代码的使用场景非常罕见 , 因为它所带来的好处无法抵消性能上的损耗 。
withJavaScript 中另一个难以掌握的用来欺骗词法作用域的功能是 with 关键字 。 可以有很多的方法来解释 with , 在这里我选择从这个角度来解释它:它如何同被它所影响的词法作用域进行交互 。
with 通常被当作重复引用同一个对象中的的多个属性的快捷方式 , 可以不需要重复引用对象本身 。
比如:
var obj = {
    a:1
    b:2
    c:3

// 单调乏味的重复 obj
obj.a = 2;
obj.b = 3
obj.c = 3

// 简单快捷的方式
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) // Object { a: 2 
foo(o2)
console.log(o2) // Object { b: 3 

console.log(a)// a 被泄漏到全局作用域上了!

在这个例子中创建了 o1 和 o2 两个对象 , 其中一个具有 a 属性 , 另外一个没有 。 foo 函数接受了一个 obj 参数 , 该参数是一个对象的引用 , 并对这个对象执行了 with 方法 。
在 with 内部 , 我们写的代码看起来只是对变量 a 进行简单的词法引用 , 实际上就是一个 LHS 引用 , 并将 2 赋值给它 。


LHS 查找如果最终没有找到 , 会在全局作用域里面创建一个 。
当我们将 o1 传递进去 , a = 2 赋值操作找到了 o1.a 并将 2 赋值给它 , 这在后面的 console.log 中可以得到体现 。 而当 o2 传递进去 ,o2 并没有 a 属性 , 因此不会创建这个属性 , o2.a 保持 undefined 。
但是注意到一个奇怪的副作用 , 实际上 a = 2 赋值操作创建了一个全局的变量 a , 这是怎么回事?
with 可以将一个没有属性的对象处理为一个完全隔离的词法作用域 , 因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符 。
eval 函数如果接受了含一个声明的代码 , 就会修改其所处的词法作用域 , 而 with 声明实际上是根据你传递给它的对象 , 凭空创建了一个全新的词法作用域 。
可以这样理解 , 当我们传递 o1 给 with 时 , with 所声明的作用域是 o1 , 而这个作用域中包含有一个同 o1.a 属性的标识符 。 但当我们将 o2 作为作用域时 , 其中并没有 a 标识符 , 因此进行了正常 LHS 标识符查找 。
o2 的作用域、foo 的作用域和全局作用域都没有找到标识符 a , 因此当 a = 2 执行时 , 自动地创建了一个全局变量 。
with 这种将对象及其属性放进一个作用域并同时分配标识符的行为很令人费解 。