谈JavaScript学习之旅——从Scope Chain到Closure
a = 1;
function Outer(x){
function Inner(y){return x + y;}
return Inner
}
var inner = Outer(1);
inner(2);
执行上面这段代码的过程中,有哪些事情发生?Inner函数为什么可以引用Outer函数的参数x?closure是怎么实现的?本文试图回答这些问题。
术语
本文虽然所讲理论并不复杂,但用到不少名词,初读时相对比较晦涩,下面列出术语和简短解释,便于阅读时随时查看。
- global:engine预先创建好的一个object,里面有所有built-in objects的属性。
- globalContext:本文术语,用作表示全局的execution context。
- globalScopeChain:本文术语,用作表示全局的execution context所拥有的Scope Chain,里面只有一个对象为global,用代码表示为 [global]
- functionContext:本文术语,用作表示执行函数代码时,进入的新的execution context。
- VariableObject:ECMAScript术语,在globalContext中即为global,在functionContext中是被创建的一个对象。在进入context时,被放到scope chain的最前方。
- outerVariable:本文术语,表示进入OuterFunctionContext时被创建的Variable Object。
- innerVariable:本文术语,表示进入InnerFunctionContext时被创建的Variable Object。
- outerFunctionContext:本文术语,用作表示执行Outer这个函数时,进入的execution context。
- outerScopeChain:本文术语,用作表示outerFunctionContext所拥有的Scope Chain。可用[outerVariable, global]表示。
- innerFunctionContext:本文术语,用作表示执行Inner这个函数时,进入的execution context。
- innerScopeChain:本文术语,用作表示innerFunctionContext所拥有的Scope Chain。可用[innerVariable, outerVariable, global]表示。
JS代码种类
JS代码分三种:
- Global code,全局代码
- Functioncode,函数内的代码。
- Eval code,为简单计,不在本文说明。
Execution context
任何一句JS代码,都是执行在一个特定的“execution context”下面。
执行Global code时,JavaScript engine将会创建一个全局的context,为表述简单,我们把它叫做globalContext。
而每次进入Functioncode时,将会创建一个新的context,在函数返回(或有未捕获的异常发生)时,退出这个新的context,本文把它叫做functionContext。
a = 1; //进入globalContext
function Outer(x){
function Inner(y){return x + y;}
return Inner
} //在globalContext中创建Outer这个Function
var inner = Outer(1); //执行Outer函数时进入新创建的outerFunctionContext上下文。
//然后退出,回到globalContext,把Outer(1)的返回值赋给inner这个变量。
inner(2); //进入InnerContext,执行Inner函数的return x + y,然后退出,回到globalContext
Scope Chain
每个execution context都有一个关联的Scope Chain。所谓Scope Chain,其实就是一个List,里面有若干个object。
global
globalContext所关联的Scope Chain,这里不妨称之为globalScopeChain,这个chain里面只有一个object,就是global,global是一个engine事先创建好的对象,所有的built-in Object(比如Function()、Object()、Math)都会作为这个global对象的属性。
Function型对象的[[Scope]]属性
在第一篇创建Function型对象的步骤里,第5步说了,会为这个Function型对象创建一个[[Scope]]属性,不过当初没有提到,这个属性的值是当前context的Scope Chain。
Outer函数是在globalContext下创建起来的,因此Outer.[[Scope]] = globalScopeChain,也就是[global]。而Inner函数是在执行Outer函数时,也就是在outerFunctionContext下创建起来的,因此Inner.[[Scope]] = OuterContext的ScopeChain,是什么呢,往下看。
Entering execution context
每次进入一个context(不管是globaContext还是functionContext)时,都会有一系列的事情发生。
- 上面说到,每个context都有一个关联的Scope Chain,这个Scope Chain就是在此时会被创建起来的。
- 确定或创建一个Variable Object(ECMAScript术语),并把它放到Scope Chain的最前面。
对于globalContext,这个Variable Object就是global,被放到globalScopeChain里(也是globalScopeChain里唯一的一个对象);
而如果进入到一个functionContext,则会创建一个Variable Object起来,也放到Scope Chain的最前面,并且还会额外再做一件事——就是把当前Function的[[Scope]]里所有object,放到Scope Chain里面。因此执行Outer函数时,Scope Chain是这样的:[outerVariable, global];上面知道,创建Inner函数时,这个Chain将作为Inner函数的[[Scope]]属性,因此进入Inner函数的执行时,它的Scope Chain就是[innerVariable, outerScopeChain],也就是[innerVariable, outerVariable, global]。 - 实例化Variable Object,就是为Variable Object创建一些属性。
首先,如果是functionContext,则把函数的参数作为Variable Object的属性;
其次,把声明的函数作为Variable Object的属性;这里的属性将覆盖上面的同名属性。
再次,把声明的变量作为Variable Object的属性,属性的初始值均为undefined,只有在执行赋值语句后,才会有值。这边的属性不会覆盖上面的同名属性。 - 为当前context确定this,this在context中是不变的。
详细见下面的注解。
//在执行一切代码之前,进入globalContext,global对象也已经创建好。
//1.然后创建Scope Chain
globalContext.ScopeChain = [];
//2.确定variable object为global,并加入到scope chain中
variable = global;
globalContext.ScopeChain.push(global);
//3.实例化variable object,创建a、Outer和inner三个属性,初始值为null。
variable.a = null;
variable.Outer = null;
variable.inner = null;
//4.确定this,在globalContext中为global。
this = global;
//以上是进入globalContext时所做的事情
//以下开始执行代码。
a = 1; function Outer(x){
function Inner(y){return x + y;}
return Inner
} //对于以上这段代码,发生的事用伪代码表示如下:
//创建Outer函数,传入当前的scope chain,即[global]
Outer = new Function('', '' [global])
//为Outer.[[Scope]]赋值
Outer.[[Scope]] = [];
Outer.[[Scope]].push(global);
//这时variable的属性Outer就指向这个函数了,不再是null。
variable.Outer = Outer
var inner = Outer(1); //这段代码用伪代码表示如下:
//执行Outer函数,进入新创建的outerFunctionContext上下文
//1.创建ouerFunctionContext的Scope Chain,并放入Outer函数的[[Scope]]力所有的object
outerFunctionContext.ScopeChain = [];
outerFunctionContext.ScopeChain.push(global) //global是[[Scope]]里唯一的对象。
//2.创建Variable Object属性,并放到Scope Chain的最前方。
outerVariable= {arguments: xxx} //创建的variable有arguments等属性
outerFunctionContext.ScopeChain.push(variable)
//3.实例化variable object
outerVariable.x = 1
outerVariable.Inner = new Function('y', 'return x + y', [outerVariable, global]) //注意上句创建Inner函数时,会传入当前的Scope Chain,即[outerVariable, global] //4.确定Outer函数体内的this参数,就是新创建的函数对象。
//最后回到globalContext中,把新建的Inner函数对象,返回给inner变量。
inner(2); //最后执行的这句代码,将创建并进入InnerContext。
初步结论
现在已经知道,执行Outer函数时,对应的outerScopeChain的图如下,注意global对象忽略了指向所有built-in object的属性:
执行Inner函数时,对应的innerScopeChain的图如下:
Scope Chain的作用
Scope chain的图出来了,那么它用来干嘛呢?执行inner函数的return x + y,会发现,我们需要两个变量,x和y。那么JavaScript将循着Scope Chain来查找,与__proto__链配合,也就是首先在innerVariable(以及其__proto__链)找x,没找到,则到outerVariable中找x,找到为1。 找y时类似。这就是Inner函数体中,可以访问得到Outer函数中定义的参数x的原理所在,不难想象,如果Outer函数中定义了局部变量z,那么z也会出现在outerVariable对象中,因此同样可以被Inner函数访问。内部函数可以引用外部函数的参数以及变量,这就是JavaScript传说中的闭包(Closure)。