五、JavaScript之作用域链

在上一篇的文章中,我们了解到执行上下文数据(变量、函数声明和函数的所有形参)是由这个变量对象的属性存储的,另外,我们知道每次进入上下文时都会创建变量对象并填充初始化值,并且在执行上下文代码时会发生它的修改。

下面我们叫了解执行上下文相关的另一个概念:作用域链。

定义

作用域链(Scope,Scope chain,缩写 SC)是与执行上下文相关联的变量对象链,在解析标识符名称时在其中搜索变量。

简而言之,作用域链更多的是关于嵌套函数。

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

1
2
3
4
5
6
7
8
9
var x = 10;
function foo() {
var y = 20;
function bar() {
alert(x + y);
}
return bar;
}
foo()(); // 30

同时,我们知道每个上下文都有自己的变量对象:对于全局上下文,这是全局对象本身,对于函数,这是活动对象。
作用域链是嵌套上下文的这些(父)变量对象的列表。该链用于搜索变量。那些。从上面的例子来看,“bar”上下文的作用域链将包括 AO (bar)、AO (foo) 和 VO (global)。

函数的作用域链是在执行时创建的,由一个活动对象和一个内部[[Scope]]函数属性组成。我们将在下面讨论 [[Scope]] 属性。

前面我们写了执行上下文的组称代码示例:

1
2
3
4
5
const ExecutionContextObj = {
VO: window, // 变量对象
Scope: {}, // 作用域链
this: window,
};

其中 Scope 的定义是:

1
Scope = AO + [[Scope]];

如果我们将 Scope 和 [[Scope]] 以普通 JavaScript 数组的形式表示——这样会更清晰。

1
var Scope = [VO1, VO2, ..., VOn]; // scope chain

换种说法[[scope]] 就是所有父变量对象的层级链。

函数声明周期

函数的生命周期分为创建阶段和激活(调用)阶段。现在我们从这两个方面来详细地分析它们。

创建函数

我们知道,函数声明在进入上下文阶段就属于变量对象(VO)/激活对象(AO)。思考一个在全局上下文中声明变量和函数的示例(其中变量对象是全局对象本身):

1
2
3
4
5
6
7
8
var x = 10;

function foo() {
var y = 20;
console.log(x + y);
}

foo(); // 30

执行之后我们看到了符合预期的结果:30,但是这里有一个很重要特性。
到目前为止,我们只讨论了同一上下文中的变量对象。这里我们看到变量“y”是在函数“foo”中定义的(也就是说它在函数“foo”的上下文的 AO 中),但是变量“x”在进入上下文的时候是没有在任何地方定义(因此,在 AO 中没有添加;“在表面上”,对于“foo”函数来说,“x”变量根本不存在,但是,正如我们将在下面看到的,只有“在表面上” )。函数上下文活动对象“foo”只包含一个属性——“y”属性:

1
2
3
fooContext.AO = {
y: undefined, // undefined - 进入执行上下文时, 20 - 执行时
};

那么函数“foo”是如何取到变量“x”的值?实际上它是通过函数内部的[[scope]]属性来实现的。
(流程图)
我们声明了一个函数 foo,需要查看 foo 的原型对象才能看到[[scopes]]属性,因为 foo.prototype.constructor===foo,所以展开 constructor 选项。
可以看到[[scopes]]属性是一个数组,里面只有一个元素 Global,也就是全局对象。
需要注意的一点--[[scope]]在函数创建时被存储--静态(不变的),直至函数销毁。即:函数可以永不调用,但[[scope]]属性已经写入,并存储在函数对象中,同时[[scope]] 并不代表完整的作用域链。
所以上面列子函数“foo”的[[scope]]如下:

1
2
3
foo[[Scope]] = [
globalContext.VO, // === Global
];

函数激活

当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端,作为作用域数组的第一个对象。

1
Scope = [AO].concat([[Scope]]);

至此,作用域链创建完毕。

下面我们用一个例子,结合着之前讲的变量对象和执行上下文栈,我们来总结一下函数执行上下文中作用域链和变量对象的创建过程:

1
2
3
4
5
6
7
8
9
10
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
console.log(x + y + z);
}
bar();
}
foo(); // 60

执行过程如下:

  1. 全局上下文的变量对象是:
1
2
3
4
globalContext.VO === Global = {
x: 10
foo: <reference to function>
};
  1. foo 函数被创建,保存作用域链到内部属性[[scope]]:
1
foo.[[scope]] = [globalContext.VO];
  1. 执行 foo 函数,创建 foo 函数执行上下文,foo 函数执行上下文被压入执行上下文栈:
1
ECStack = [fooContext, globalContext];
  1. foo 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链:
1
2
3
fooContext = {
Scope: foo.[[scope]],
};
  1. 第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
1
2
3
4
5
6
7
8
9
10
fooContext = {
AO: {
arguments: {
length: 0
},
y: undefined,
bar: reference to function bar(){}
},
Scope: foo.[[scope]],
}
  1. 第三步:将活动对象压入 foo 作用域链顶端
1
2
3
4
5
6
7
8
9
10
fooContext = {
AO: {
arguments: {
length: 0
},
y: undefined,
bar: reference to function bar(){}
},
Scope: [fooContext.AO, foo.[[Scope]]] // 等同于 [fooContext.AO, globalContext.VO]
}
  1. 执行函数 foo ,修改 AO 的属性值
1
2
3
4
5
6
7
8
9
10
fooContext = {
AO: {
arguments: {
length: 0
},
y: 20,
bar: reference to function bar(){}
},
Scope: [fooContext.AO, foo.[[Scope]]] // 等同于 [fooContext.AO, globalContext.VO]
}
  1. 修改y值后,内部函数 bar 开始创建:
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
37
38
39
40
41
42
43
44
45
46
// 其[[scope]]为:
bar.[[Scope]] = [
fooContext.AO,
globalContext.VO
];

// 执行上下文栈
ECStack = [barContext, fooContext, globalContext];

// 复制函数[[scope]]属性创建作用域链
barContext = {
Scope: bar.[[Scope]],
}

// 创建并初始化 bar 上下文活动对象
barContext = {
AO: {
arguments: {
length: 0
},
z: undefined,
},
Scope: bar.[[scope]],
}

// 将活动对象压入 bar 作用域链顶端
barContext = {
AO: {
arguments: {
length: 0
},
z: undefined,
},
Scope: [barContext.AO, bar.[[Scope]]] // 等同于 [barContext.AO, fooContext.AO, globalContext.VO]
}

//执行函数 bar ,修改bar.AO 的属性值
barContext = {
AO: {
arguments: {
length: 0
},
z: 30,
},
Scope: [barContext.AO, bar.[[Scope]]] // 等同于 [barContext.AO, fooContext.AO, globalContext.VO]
}
  1. 执行console.log语句,对“x”、“y”、“z”的标识符解析如下:
1
2
3
4
5
6
7
8
9
10
11
- "x"
-- barContext.AO // not found
-- fooContext.AO // not found
-- globalContext.VO // found - 10

- "y"
-- barContext.AO // not found
-- fooContext.AO // found - 20

- "z"
-- barContext.AO // found - 30
  1. 打印x,y,z的之后,函数执行完毕,函数上下文从执行上下文栈中依次弹出
1
2
3
4
5
// bar函数出栈
ECStack = [fooContext, globalContext];

// foo函数出栈
ECStack = [globalContext];

参考文献

http://dmitrysoshnikov.com/ecmascript/ru-chapter-4-scope-chain/