之前说过,执行上下文有三个重点:
- 变量对象
VO
。 - 作用域链
Scope Chain
。 this
。
这篇文章主要记录的是 作用域链
。
什么是作用域链
- 在一个执行上下文中,有属于自己的变量对象,可很多时候我们的执行上下文并不是那么简单的,会出现执行上下文嵌套的情况。如果在当前的执行上下文没有找到变量,就会向外层执行上下文中的变量对象中查找,一直找到全局上下文。
- 这种变量对象嵌套的变量对象链表,我们可以称之为 作用域链。
作用域链有什么用
保证对执行上下文中的有权访问的所有变量和函数的有序访问。这句话有点绕口,大家可以简单的认为作用域链保证了上下文嵌套中,变量访问的顺序。
从一个函数开始分析作用域链
这个是《高程》中的一个例子。这个过程会涉及之前提到的词法分析
、作用域
、执行上下文栈
、变量对象
。
var color = 'blue';
function changeColor() {
var anotherColor = 'red';
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColors();
}
changeColor();
-
changeColor
函数声明。因为它的声明是在全局上下文中,所以它所继承的父级作用域链是全局上下文的变量对象,也就是VO
。 之前我们也说过,在JS的作用域是静态作用域,因此它的作用域是在声明的时候被决定的。这个时候作用域就会被保存在[[scope]]
属性中。changeColor.[[scope]] = [globalContext.VO];
-
接下来遇到一个函数被执行,也就是
changeColor();
执行上下文是在函数被执行的时候创建的,并且需要进入到执行上下文栈中(ECStack)。ECStack.push("changeColorContext");
-
在
changeColor
正式被JS引擎执行前,为了方便理解,我认为他还有一个预编译阶段。在这个阶段中,我们会把之前保存在changeColor.[[scope]]
中的作用域放进changeColorContext
中。changeColorContext = { scope: changeColor.[[scope]] }
-
进入
changeColor
的执行上下文,创建活动变量AO
。我们在变量对象中说到,AO 大体上会分为两个阶段:声明阶段、执行阶段。因此先进行声明阶段的工作。changeColorContext = { scope: changeColor.[[scope]], AO: { arguments:{ length: 0 }, // 因为这个函数中没有形参 anotherColor: undefined, swapColors: reference to function swapColor() } }
-
在
AO
进行执行阶段前,先会合并作用域,让之后的改值有变量可依。changeColorContext = { AO: { arguments:{ length: 0 }, // 因为这个函数中没有形参 anotherColor: undefined, swapColors: reference to function swapColor() }, scope: [AO, changeColor.[[scope]]], }
-
AO
进入执行阶段。修改各个变量的值。changeColorContext = { AO: { arguments:{ length: 0 }, // 因为这个函数中没有形参 anotherColor: 'red', swapColors: reference to function swapColor() }, scope: [AO, changeColor.[[scope]]], }
-
进入
changeColor
的执行上下文之后,有一个函数声明swapColors
同理,进行作用域的创建。swapColors.[[scope]] = [changeColorContext.scope];
-
执行
swapColors
前先 push 到ECStack
中。ECStack.push("swapColorsContext"); // 此时 ECStack 中已经有两个执行上下文。
-
初始化
swapColorsContext
的作用域链。swapColorsContext = { scope: swapColors.[[scope]] }
-
初始化
swapColorsContext
的AO
。swapColorsContext = { scope: swapColors.[[scope]], AO: { arguments:{ length: 0 }, // 因为这个函数中没有形参 tempColor: undefined } }
-
合并
swapColorsContext
的scope
。swapColorsContext = { scope: [AO, swapColors.[[scope]]], AO: { arguments:{ length: 0 }, // 因为这个函数中没有形参 tempColor: undefined } }
-
AO
进入执行阶段。修改各个变量的值。swapColorsContext = { scope: [AO, swapColors.[[scope]]], AO: { arguments:{ length: 0 }, // 因为这个函数中没有形参 tempColor: 'red' } }
-
swapColors
执行结束,出栈。ECStack.pop();
-
changeColor
执行结束,出栈。ECStack.pop();
-
程序终于结束了!!!写的我好累。
总结
- 作用域是在函数声明阶段创建的。
- 执行上下文每次创建都会进入执行上下文栈。
- 执行上下文中会先初始化活动变量
AO
,再合并作用域链,最后才修改AO
中的值。