上面的1.1-1.3可以看做作用域的类型。而这一小节,其实跟上面三小节还是有差别的,并不属于作用域的类型,只是关于作用域的一个补充说明吧。
2. 什么是作用域链(scope chain)在JS引擎中,通过标识符查找标识符的值,会从当前作用域向上查找,直到作用域找到第一个匹配的标识符位置。就是JS的作用域链。
var a = 1; function fn1 () { var a = 2; function fn2 () { var a = 3; console.log(a); } fn2 (); } fn1(); // 3console.log(a) 语句中,JS在查找 a变量标识符的值的时候,会从 fn2 内部向外部函数查找变量声明,它发现fn2内部就已经有了a变量,那么它就不会继续查找了。那么最终结果也就会打印3了。
3. 作用域链与执行上下文在此前的博客:js基础梳理-究竟什么是执行上下文栈(执行栈),执行上下文(可执行代码)?中讲到执行上下文的生命周期:
3.执行上下文的生命周期 3.1 创建阶段生成变量对象(Variable object, VO)
建立作用域链(Scope chain)
确定this指向
3.2 执行阶段变量赋值
函数引用
执行其他代码
上面做了那么多铺垫,其实重点是想梳理这一小节。
下面,以一个函数的创建和激活两个时期来讲解作用域链是如何创建及变化的。
上文中讲到,函数的作用域在函数定义的时候就决定了。
这是因为函数有一个内部属性[[scope]],当函数创建的时候,就会保存所有父变量对象到其中,但是注意:此时[[scope]]并不代表完整的作用域链,因为在创建阶段,它还没有包括自己的作用域。
举个栗子:
function foo () { function bar () { ... } }函数创建时,各自的[[scope]]为:
foo.[[scope]] = [ globalContext.VO ]; bar.[[scope]] = [ fooContext.AO, globalContext.AO ]; 3.2 函数激活阶段当函数激活时,进入函数上下文,创建VO/AO后,就会将活动对象添加到作用域链的前端。
这时候执行上下文的作用域链,命名为 Scope:
Scope = [AO].concat([[scope]]);
至此,作用域链创建完毕。
3.3 举个栗子以下面的例子为例,结合之前的变量对象,活动对象和执行上下文栈,总结一下函数执行上下文中作用域链和变量对象的创建过程:
var x = 10; function foo() { var y = 20; function bar() { var z = 30; console.log(x + y + z); } bar(); } foo(); // 60大家肯定都知道打印结果会是60。但是从第一行代码开始到最后一行代码结束,整个代码的执行上下文栈以及作用域链是怎样变化的呢?
// 第一步:进入全局上下文,此时的执行上下文栈是这样: ECStack = [ globalContext: { VO: { foo: <reference to function foo() {}>, x: 10 } } ]; // 第二步:foo函数被创建,此时的执行上下文栈没有变化,但是创建了foo函数的作用域,保存作用域链到内部属性[[scope]]。 ECStack = [ globalContext: { VO: { foo: <reference to function foo() {}>, x: 10 } } ]; foo.[[scope]] = [ globalContext.VO ]; // 第三步:foo函数执行,进入foo函数上下文的创建阶段 // 这个阶段它做了三件事: // 1.复制之前的foo.[[scope]]属性到foo函数上下文下,创建foo函数的作用域链; // 2. 创建foo函数上下文的变量对象,并初始化变量对象,依次加入形参,函数声明,变量声明 // 3. 把foo函数上下文的变量对象加入到第一步创建的foo函数作用域链的最前面。 // 最终,经过这三个步骤之后,整个执行上下文栈是这样 ECStack = [ globalContext: { VO: { foo: <reference to function foo() {}>, x: 10 } }, <foo>functionContext: { VO: { arguments: { length: 0 }, bar: <reference to function bar() {}>, y: undefined }, Scope: [foo.VO, globalContext.VO] } ]; foo.[[scope]] = [ foo.VO, globalContext.VO ]; // 第四步:foo函数执行,进入foo函数上下文的执行阶段。 // 这个阶段又做了以下2件事: // 1. 把foo执行上下文的变量对象VO改成了活动对象AO,并且修改AO中变量的值 // 2. 发现创建了一个 bar函数,就保存了bar函数的所有父变量对象到bar函数的[[scope]]属性上。 ECStack = [ globalContext: { VO: { foo: <reference to function foo() {}>, x: 10 } }, <foo>functionContext: { AO: { arguments: { length: 0 }, bar: <reference to function bar() {}>, y: 20 }, Scope: [foo.AO, globalContext.VO] } ]; foo.[[scope]] = [ foo.AO, globalContext.VO ]; bar.[[scope]] = [ foo.AO, globalContext.VO ]; // 第五步,bar函数执行,进入bar函数上下文的创建阶段 // 与第三步类似,也做了三件事,只不过主体变成了bar // 1.复制之前的bar.[[scope]]属性到bar函数上下文下,创建foo函数的作用域链; // 2. 创建bar函数上下文的变量对象,并初始化变量对象,依次加入形参,函数声明,变量声明 // 3. 把bar函数上下文的变量对象加入到第一步创建的bar函数作用域链的最前面。 // 最终,经过这三个步骤之后,整个执行上下文栈是这样 ECStack = [ globalContext: { VO: { foo: <reference to function foo() {}>, x: 10 } }, <foo>functionContext: { AO: { arguments: { length: 0 }, bar: <reference to function bar() {}>, y: 20 }, Scope: [foo.AO, globalContext.VO] }, <bar>functionContext: { VO: { arguments: { length: 0 }, z: undefined }, Scope: [bar.VO, foo.AO, globalContext.VO] } ]; foo.[[scope]] = [ foo.AO, globalContext.VO ]; bar.[[scope]] = [ bar.VO, foo.AO, globalContext.VO ]; // 第六步:bar函数执行,进入bar函数上下文的执行阶段 // 与第四步类似。不过此时bar函数里面不会再创建新的函数上下文了 // 1. 把bar执行上下文的变量对象VO改成了活动对象AO,并且修改AO中变量的值 ECStack = [ globalContext: { VO: { foo: <reference to function foo() {}>, x: 10 } }, <foo>functionContext: { AO: { arguments: { length: 0 }, bar: <reference to function bar() {}>, y: 20 }, Scope: [foo.AO, globalContext.VO] }, <bar>functionContext: { AO: { arguments: { length: 0 }, z: 30 }, Scope: [bar.AO, foo.AO, globalContext.VO] } ]; foo.[[scope]] = [ foo.AO, globalContext.VO ]; bar.[[scope]] = [ bar.AO, foo.AO, globalContext.VO ]; // 第七步:执行bar函数中的console.log(x + y +z),查找x,y,z三个标识符 - "x" -- <bar>functionContext.AO // 没找到,继续到foo.AO中找 -- <foo>functionContext.AO // 还没找到,再往globalContext.VO中找 -- globalContext.VO // 找到了,值为 10 - "y" -- <bar>functionContext.AO // 没找到,继续到foo.AO中找 -- <foo>functionContext.AO // 找到了,值为20 -- "z" -- <bar>functionContext.AO // 找到了,值为 30 打印结果: 60。 // 第八步:bar函数执行完毕,将其从执行上下文栈中弹出,foo函数执行完毕,将其从执行上下文栈中弹出。最终,执行上下文栈,只剩下globalContext ECStack = [ globalContext: { VO: { foo: <reference to function foo() {}>, x: 10 } } ]感觉其实可以简化理解一下,把第三四步,第五六步分别分成一个步骤。