javascript 作用域链
我在文章《javascript 执行上下文》中介绍了 javascript 代码在执行时,会相应地创建对应执行上下文并将其入栈出栈的过程。
每个执行上下文会包含三个重要属性,分别是变量对象(Variable Object,VO)、作用域链(Scope Chain)和 this 指向。
本篇主要详细介绍作用域链的建立过程。
作用域链是由当前上下文和上层上下文的一系列变量对象组成的层级链。它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
我们已经知道,执行上下文分为创建和执行两个阶段,而在执行上下文的执行阶段,当需要查找某个变量或函数时,会在当前上下文的变量对象(活动对象)中进行查找,若是没有找到,则会在上一层的上下文的变量对象中进行查找,一直找到全局上下文中的变量对象(全局对象)。
那么当前上下文是如何有序地去查找它所需要的变量或函数的呢?
答案就是依靠当前上下文中的作用域链,其包含了当前上下文和上层上下文中的变量对象,以便其一层一层地去查找其所需要的变量和函数。
那么我们接下来介绍一下作用域链的建立过程~
javascript 中主要包含了全局作用域和函数作用域,函数作用域是在函数被定义(声明)的时候确定的。
每一个函数都会包含一个 [[scope]] 内部属性,在函数被定义的时候,该函数的 [[scope]] 属性会保存其上层上下文的变量对象,形成包含上层上下文变量对象的层级链。
需要注意的是,该层级链并没有包含当前上下文的变量对象,所以该层级链并不是完整的作用域链。
所以上述想表达的重点就是,[[scope]] 属性的值是在函数被定义(声明)的时候确定的。这一点很重要。
当该函数被调用的时候,其执行上下文会被创建并入栈。在创建阶段,会生成其变量对象,并将该变量对象添加到作用域链(scope)的顶端并将原先的层级链([[scope]])添加进该作用域链。而在执行阶段,变量对象会变为活动对象,其相应属性会被赋值。
至此该执行上下文的作用域链被建立,在执行阶段代码执行时,会根据作用域链对符合访问权限的变脸和函数进行有序访问。
举个例子~
假设有一个 javascript 文件中包含如下代码
var a = 1;
function fn1(){
var b = 1;
function fn2(){
var c=1;
}
fn2();
}
fn1();
这里我们回顾一下其上述代码执行过程中执行上下文栈的行为
/*伪代码*/
// 代码执行时最先进入全局环境,全局上下文被创建并入栈
ECStack.push(globalContext);
// fn1 被调用,fn1 函数上下文被创建并入栈
ECStack.push(<fn1> functionContext);
// fn1 中调用 fn2,fn2 函数上下文被创建并入栈
ECStack.push(<fn2> functionContext);
// fn2 执行完毕,fn2 函数上下文出栈
ECStack.pop();
// fn1 执行完毕,fn1 函数上下文出栈
ECStack.pop();
// 代码执行完毕,全局上下文出栈
ECStack.pop();
这里我们主要关注的是 fn2 上下文被创建并入栈的过程。
上面说过,函数作用域是在函数被定义的时候确定的(即 [[scope]] 属性的值是在函数被定义的时候确定的)。所以我们可以知道,fn2 的上层上下文包括了 fn1 上下文和全局上下文
fn2.[[scope]]=[fn1Context.VO,globalContext.VO]
当 fn2 被调用的时候,其执行上下文被创建并入栈,此时会生成变量对象并将该变量对象添加进作用域链的顶端,并且将 [[scope]] 添加进作用域链
fn2Context.Scope=[fn2Context.VO].concat([[scope]])
=>
fn2Context.Scope=[fn2Context.VO,fn1Context.VO,globalContext.VO]
最后,在该上下文的执行阶段,变量对象会变为活动对象,并且其相应属性会被赋值。