之前写到的 JavaScript - 作用域 中有提到作用域以及在 JavaScript - 提升 中提到了变量声明和函数声明的提升,在这里做一些深入的探究。

先来了解两个概念:

变量对象

每一个执行环境(Execution Context)都有一个与之关联的变量对象(Variable Object),在全局的执行环境中,window 就是其变量对象。

在函数执行环境中,其变量对象就是所有的局部变量以及局部函数的集合,在最开始时只有 arguments 对象。

函数的 [[Scope]]

函数的生命周期会经历几个阶段:

1、函数的创建
2、函数的调用
3、函数的销毁

在函数创建阶段,会创建内部属性 [[Scope]],[[Scope]] 是所有父变量对象的层级链。[[Scope]] 属性是静态的,并且会永远存在,直到函数被销毁。

思考如下代码:

1
2
3
4
5
6
7
8
var b = 2

function foo() {
var a = 1
b = 3
}

foo()

以上代码的执行过程如下:

1、初始化阶段,执行环境栈中只包含全局执行环境:

1
2
3
4
5
6
ECStack = [
globalContext = {
b: 2,
foo: <reference to FunctionDeclaration foo>
}
]

全局环境执行代码完毕之后,foo 函数也就创建完毕,[[Scope]] 属性也已经定义好:

1
2
3
foo.[[Scope]] = [
globalContext.VO
]

2、准备执行 foo 函数,此时进入了 foo 的执行环境,其执行环境被推入栈中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ECStack = [
fooContext = {
VO: {
arguments: [
length: 0
],
// 此时还没有执行 foo 的代码,所以 a 还是 undefined
a: undefined,
}
// 作用域链
scope: fooContext.AO.concat(foo.[[Scpoe]])
},
globalContext = {
VO: {
b: 3,
},
foo: <reference to FunctionDeclaration foo>
}
]

此时可以发现 foo 中的 a 变量已经被赋值为 undefined,这就是「提升」。

foo 函数的执行环境已经准备好,作用域链也创建完毕,可以执行 foo 的代码。

3、执行 foo 函数的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ECStack = [
fooContext = {
VO: {
arguments: [
length: 0
],
a: 1,
}
// 作用域链
scope: fooContext.AO.concat(foo.[[Scpoe]])
},
globalContext = {
VO: {
b: 2,
},
foo: <reference to FunctionDeclaration foo>
}
]

执行 foo 函数的代码时,需要对 b 变量赋值,于是顺着 foo 函数执行环境的作用域链一层层往上搜索,当搜索 fooContext.AO 时,没有搜索到 b 标识符,继续往上搜索 foo.[[Scpoe]] 也就是 globalContext.VO,搜索到了 b 标识符,随即对其进行赋值。

这个搜索过程只能够从作用域链的最前端往后开始搜索,不能从后端往前端方向搜索。这就是作用域链的作用。

总结

函数在创建时会创建 [[Scope]] 内部属性,定义为父变量对象的层级链。静态的,不可变,可以不调用函数,但是 [[Scope]] 内部属性会一直存在,直到函数被销毁。

在执行函数中的代码时,会把函数执行环境关联的变量对象与函数的 [[Scope]] 内部属性组成作用域链,执行代码时通过这个作用域链查找变量。

参考

深入理解 JavaScript 系列(14):作用域链(Scope Chain)

深入理解 JavaScript 系列(12):变量对象(Variable Object)

Javascript 中你必须理解的执行上下文和调用栈

JavaScript 深入之变量对象

《JavaScript 高级程序设计(第三版)》