五、JavaScript之作用域链
在上一篇的文章中,我们了解到执行上下文数据(变量、函数声明和函数的所有形参)是由这个变量对象的属性存储的,另外,我们知道每次进入上下文时都会创建变量对象并填充初始化值,并且在执行上下文代码时会发生它的修改。
下面我们叫了解执行上下文相关的另一个概念:作用域链。
定义
作用域链(Scope,Scope chain,缩写 SC)是与执行上下文相关联的变量对象链,在解析标识符名称时在其中搜索变量。
简而言之,作用域链更多的是关于嵌套函数。
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
1 | var x = 10; |
同时,我们知道每个上下文都有自己的变量对象:对于全局上下文,这是全局对象本身,对于函数,这是活动对象。
作用域链是嵌套上下文的这些(父)变量对象的列表。该链用于搜索变量。那些。从上面的例子来看,“bar”上下文的作用域链将包括 AO (bar)、AO (foo) 和 VO (global)。
函数的作用域链是在执行时创建的,由一个活动对象和一个内部[[Scope]]函数属性组成。我们将在下面讨论 [[Scope]] 属性。
前面我们写了执行上下文的组称代码示例:
1 | const ExecutionContextObj = { |
其中 Scope 的定义是:
1 | Scope = AO + [[Scope]]; |
如果我们将 Scope 和 [[Scope]] 以普通 JavaScript 数组的形式表示——这样会更清晰。
1 | var Scope = [VO1, VO2, ..., VOn]; // scope chain |
换种说法[[scope]] 就是所有父变量对象的层级链。
函数声明周期
函数的生命周期分为创建阶段和激活(调用)阶段。现在我们从这两个方面来详细地分析它们。
创建函数
我们知道,函数声明在进入上下文阶段就属于变量对象(VO)/激活对象(AO)。思考一个在全局上下文中声明变量和函数的示例(其中变量对象是全局对象本身):
1 | var x = 10; |
执行之后我们看到了符合预期的结果:30,但是这里有一个很重要特性。
到目前为止,我们只讨论了同一上下文中的变量对象。这里我们看到变量“y”是在函数“foo”中定义的(也就是说它在函数“foo”的上下文的 AO 中),但是变量“x”在进入上下文的时候是没有在任何地方定义(因此,在 AO 中没有添加;“在表面上”,对于“foo”函数来说,“x”变量根本不存在,但是,正如我们将在下面看到的,只有“在表面上” )。函数上下文活动对象“foo”只包含一个属性——“y”属性:
1 | fooContext.AO = { |
那么函数“foo”是如何取到变量“x”的值?实际上它是通过函数内部的[[scope]]属性来实现的。
我们声明了一个函数 foo,需要查看 foo 的原型对象才能看到[[scopes]]属性,因为 foo.prototype.constructor===foo,所以展开 constructor 选项。
可以看到[[scopes]]属性是一个数组,里面只有一个元素 Global,也就是全局对象。
需要注意的一点--[[scope]]在函数创建时被存储--静态(不变的),直至函数销毁。即:函数可以永不调用,但[[scope]]属性已经写入,并存储在函数对象中,同时[[scope]] 并不代表完整的作用域链。
所以上面列子函数“foo”的[[scope]]如下:
1 | foo[[Scope]] = [ |
函数激活
当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端,作为作用域数组的第一个对象。
1 | Scope = [AO].concat([[Scope]]); |
至此,作用域链创建完毕。
下面我们用一个例子,结合着之前讲的变量对象和执行上下文栈,我们来总结一下函数执行上下文中作用域链和变量对象的创建过程:
1 | var x = 10; |
执行过程如下:
- 全局上下文的变量对象是:
1 | globalContext.VO === Global = { |
- foo 函数被创建,保存作用域链到内部属性[[scope]]:
1 | foo.[[scope]] = [globalContext.VO]; |
- 执行 foo 函数,创建 foo 函数执行上下文,foo 函数执行上下文被压入执行上下文栈:
1 | ECStack = [fooContext, globalContext]; |
- foo 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链:
1 | fooContext = { |
- 第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
1 | fooContext = { |
- 第三步:将活动对象压入 foo 作用域链顶端
1 | fooContext = { |
- 执行函数 foo ,修改 AO 的属性值
1 | fooContext = { |
- 修改y值后,内部函数 bar 开始创建:
1 | // 其[[scope]]为: |
- 执行console.log语句,对“x”、“y”、“z”的标识符解析如下:
1 | - "x" |
- 打印x,y,z的之后,函数执行完毕,函数上下文从执行上下文栈中依次弹出
1 | // bar函数出栈 |
参考文献
http://dmitrysoshnikov.com/ecmascript/ru-chapter-4-scope-chain/