在说声明提升之前, 首先应该了解三个概念性的东西, 执行上下文(Execution Context)/ 变量对象(Variable Object)和活动对象(Activation Object), 这三个概念是在 ES3 中出现的.

  • 执行上下文(Execution Context)
    执行上下文也可以说是 js 代码的运行环境, 代码一旦被加载 js 的解释器首先进入的就是这个环境. 他有全局的执行上下文和函数的执行上下文(eval 不说, 它内部也有), js 的解释器默认先进入的是全局上下文所以我们一开始就能直接使用一些的内置方法, 比如: Array/ Number… 每当一个函数被调用时, 就会创建他自己的执行上下文, 每个执行上下文都有一个与之关联的变量对象.

  • 作用域链(也称调用栈)
    知道了每当一个函数调用时会创建自己的执行上下文, 这个执行上下文就会被添加到作用域链的顶端. 而解释器总是先运行作用域链顶端的执行上下文, 一旦函数调用完毕, 它的执行上下文就会从作用域链的顶端移除并将控制权交由上一个执行上下文, 直到回到全局上下文. 如下图所示:
    执行环境

  • 变量对象(Variable Object)
    当执行上下文创建后, 进入建立阶段(函数调用, 具体代码执行之前), 在这个阶段变量对象/ 作用域链/ this的指向都会被确定. 变量对像会按顺序填充:
  1. 函数参数(没有就是 undefined)
  2. 函数声明(命名冲突, 会覆盖之前的)
  3. 变量声明(值为 undefined, 命名冲突, 会被忽略)
  • 活动对象(也称激活对象)
    函数调用活动对象就作为变量对象, 所以在函数调用时变量对象 === 活动对象, 活动对象会先包含arguments对象及方法.

还是看个代码来说吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function foo (i) {
var a = 'hello'
var b = function bar () {}
function c () {}
}
foo(11)
// 建立阶段
fooExecutionContext = {
variableObject: {
arguments: {
0: 11,
length: 1
},
i: 11, // 只有参数被赋值
c: pointer to function c()
a: undefined,
b: undefined,
},
scopeChain: {...},
this: {...}
}
// 执行阶段
fooExecutionContext = {
variableObject: {
arguments: {
0: 11,
length: 1
},
i: 11,
c: pointer to function c(),
a: 'hello', // 变量在这个时候才被赋值
b: pointer to function bar()
},
scopeChain: {...},
this: {...}
}

以上三个概念都无法用过代码访问, 是解释器在后台使用.