1、什么是词法作用域?

大部分标准语言编译器的第一个工作阶段叫作词法化(也叫单词化),那么词法作用域,就是定义在词法阶段的作用域,是由写代码期间函数所声明的位置来定义。

function foo (a) {
 var b = a * 2
 function bar (c) {
   console.log(a, b, c)
 }
 bar(b * 3)
}

foo(2)

在上一个代码片段中,作用域气泡的结构和相互之间位置关系给引擎提供了足够的位置信息,引擎用这些信息来查找标识符的位置。 全局变量会自动成为全局对象的属性,因此可以不通过全局对象的词法名称,而是间接通过对全局对象属性的引用对其进行访问。

window.a

无论函数在哪里被调用,无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。

2、eval

欺骗词法,eval(...) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在与程序中这个位置的代码。在执行 eval(...) 之后的代码时,引擎并不 “知道” 或 “在意”前面的代码以动态形式插入进来,并对词法作用域的环境进行修改。引擎只会如往常地进行词法作用域查找。

function foo(str, a) {
    // 欺骗!
    eval( str ) 
    console.log( a, b )
}
var b = 2
 // 1, 3
foo('var b = 3', 1 )

在严格模式中, eval(...) 在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域。

function foo (str) {
    'use strict'
    eval(str)
    // ReferenceError: a is not defined
    console.log(a)
} 
foo('var a = 2')

setTimeout(...) setInterval(...) new Function(...) 函数行为也很类似,最后一个参数可以接受代码字符串,并将其转化为动态生成的函数

3、with

JavaScript 中 with 通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

var obj = {
   a: 1,
   b: 2,
   c: 3
}
// 简单的快捷方式
with (obj) {
   a = 0
   b = 1
   c = 2  
}

尽管 with 块可以将一个对象赤利伟词法作用域,但是这个块内部正常的 var 声明并不会被限制在这个块作用域中,而是被添加到 with 所属的函数作用域中。

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 被泄漏到全局作用域上了!