a = 1;
function Outer(x){
function Inner(y){return x + y;}
return Inner
}
var inner = Outer(1);
inner(2);
執(zhí)行上面這段代碼的過程中,有哪些事情發(fā)生?Inner函數(shù)為什么可以引用Outer函數(shù)的參數(shù)x?closure是怎么實(shí)現(xiàn)的?本文試圖回答這些問題。
術(shù)語(yǔ)
本文雖然所講理論并不復(fù)雜,但用到不少名詞,初讀時(shí)相對(duì)比較晦澀,下面列出術(shù)語(yǔ)和簡(jiǎn)短解釋,便于閱讀時(shí)隨時(shí)查看。
- global:engine預(yù)先創(chuàng)建好的一個(gè)object,里面有所有built-in objects的屬性。
- globalContext:本文術(shù)語(yǔ),用作表示全局的execution context。
- globalScopeChain:本文術(shù)語(yǔ),用作表示全局的execution context所擁有的Scope Chain,里面只有一個(gè)對(duì)象為global,用代碼表示為 [global]
- functionContext:本文術(shù)語(yǔ),用作表示執(zhí)行函數(shù)代碼時(shí),進(jìn)入的新的execution context。
- VariableObject:ECMAScript術(shù)語(yǔ),在globalContext中即為global,在functionContext中是被創(chuàng)建的一個(gè)對(duì)象。在進(jìn)入context時(shí),被放到scope chain的最前方。
- outerVariable:本文術(shù)語(yǔ),表示進(jìn)入OuterFunctionContext時(shí)被創(chuàng)建的Variable Object。
- innerVariable:本文術(shù)語(yǔ),表示進(jìn)入InnerFunctionContext時(shí)被創(chuàng)建的Variable Object。
- outerFunctionContext:本文術(shù)語(yǔ),用作表示執(zhí)行Outer這個(gè)函數(shù)時(shí),進(jìn)入的execution context。
- outerScopeChain:本文術(shù)語(yǔ),用作表示outerFunctionContext所擁有的Scope Chain。可用[outerVariable, global]表示。
- innerFunctionContext:本文術(shù)語(yǔ),用作表示執(zhí)行Inner這個(gè)函數(shù)時(shí),進(jìn)入的execution context。
- innerScopeChain:本文術(shù)語(yǔ),用作表示innerFunctionContext所擁有的Scope Chain?捎肹innerVariable, outerVariable, global]表示。
JS代碼種類
JS代碼分三種:
- Global code,全局代碼
- Functioncode,函數(shù)內(nèi)的代碼。
- Eval code,為簡(jiǎn)單計(jì),不在本文說明。
Execution context
任何一句JS代碼,都是執(zhí)行在一個(gè)特定的“execution context”下面。
執(zhí)行Global code時(shí),JavaScript engine將會(huì)創(chuàng)建一個(gè)全局的context,為表述簡(jiǎn)單,我們把它叫做globalContext。
而每次進(jìn)入Functioncode時(shí),將會(huì)創(chuàng)建一個(gè)新的context,在函數(shù)返回(或有未捕獲的異常發(fā)生)時(shí),退出這個(gè)新的context,本文把它叫做functionContext。
a = 1; //進(jìn)入globalContext
function Outer(x){
function Inner(y){return x + y;}
return Inner
} //在globalContext中創(chuàng)建Outer這個(gè)Function
var inner = Outer(1); //執(zhí)行Outer函數(shù)時(shí)進(jìn)入新創(chuàng)建的outerFunctionContext上下文。
//然后退出,回到globalContext,把Outer(1)的返回值賦給inner這個(gè)變量。
inner(2); //進(jìn)入InnerContext,執(zhí)行Inner函數(shù)的return x + y,然后退出,回到globalContext
Scope Chain
每個(gè)execution context都有一個(gè)關(guān)聯(lián)的Scope Chain。所謂Scope Chain,其實(shí)就是一個(gè)List,里面有若干個(gè)object。
global
globalContext所關(guān)聯(lián)的Scope Chain,這里不妨稱之為globalScopeChain,這個(gè)chain里面只有一個(gè)object,就是global,global是一個(gè)engine事先創(chuàng)建好的對(duì)象,所有的built-in Object(比如Function()、Object()、Math)都會(huì)作為這個(gè)global對(duì)象的屬性。
Function型對(duì)象的[[Scope]]屬性
在第一篇?jiǎng)?chuàng)建Function型對(duì)象的步驟里,第5步說了,會(huì)為這個(gè)Function型對(duì)象創(chuàng)建一個(gè)[[Scope]]屬性,不過當(dāng)初沒有提到,這個(gè)屬性的值是當(dāng)前context的Scope Chain。
Outer函數(shù)是在globalContext下創(chuàng)建起來的,因此Outer.[[Scope]] = globalScopeChain,也就是[global]。而Inner函數(shù)是在執(zhí)行Outer函數(shù)時(shí),也就是在outerFunctionContext下創(chuàng)建起來的,因此Inner.[[Scope]] = OuterContext的ScopeChain,是什么呢,往下看。
Entering execution context
每次進(jìn)入一個(gè)context(不管是globaContext還是functionContext)時(shí),都會(huì)有一系列的事情發(fā)生。
- 上面說到,每個(gè)context都有一個(gè)關(guān)聯(lián)的Scope Chain,這個(gè)Scope Chain就是在此時(shí)會(huì)被創(chuàng)建起來的。
- 確定或創(chuàng)建一個(gè)Variable Object(ECMAScript術(shù)語(yǔ)),并把它放到Scope Chain的最前面。
對(duì)于globalContext,這個(gè)Variable Object就是global,被放到globalScopeChain里(也是globalScopeChain里唯一的一個(gè)對(duì)象);
而如果進(jìn)入到一個(gè)functionContext,則會(huì)創(chuàng)建一個(gè)Variable Object起來,也放到Scope Chain的最前面,并且還會(huì)額外再做一件事——就是把當(dāng)前Function的[[Scope]]里所有object,放到Scope Chain里面。因此執(zhí)行Outer函數(shù)時(shí),Scope Chain是這樣的:[outerVariable, global];上面知道,創(chuàng)建Inner函數(shù)時(shí),這個(gè)Chain將作為Inner函數(shù)的[[Scope]]屬性,因此進(jìn)入Inner函數(shù)的執(zhí)行時(shí),它的Scope Chain就是[innerVariable, outerScopeChain],也就是[innerVariable, outerVariable, global]。 - 實(shí)例化Variable Object,就是為Variable Object創(chuàng)建一些屬性。
首先,如果是functionContext,則把函數(shù)的參數(shù)作為Variable Object的屬性;
其次,把聲明的函數(shù)作為Variable Object的屬性;這里的屬性將覆蓋上面的同名屬性。
再次,把聲明的變量作為Variable Object的屬性,屬性的初始值均為undefined,只有在執(zhí)行賦值語(yǔ)句后,才會(huì)有值。這邊的屬性不會(huì)覆蓋上面的同名屬性。 - 為當(dāng)前context確定this,this在context中是不變的。
詳細(xì)見下面的注解。
//在執(zhí)行一切代碼之前,進(jìn)入globalContext,global對(duì)象也已經(jīng)創(chuàng)建好。
//1.然后創(chuàng)建Scope Chain
globalContext.ScopeChain = [];
//2.確定variable object為global,并加入到scope chain中
variable = global;
globalContext.ScopeChain.push(global);
//3.實(shí)例化variable object,創(chuàng)建a、Outer和inner三個(gè)屬性,初始值為null。
variable.a = null;
variable.Outer = null;
variable.inner = null;
//4.確定this,在globalContext中為global。
this = global;
//以上是進(jìn)入globalContext時(shí)所做的事情
//以下開始執(zhí)行代碼。
a = 1; function Outer(x){
function Inner(y){return x + y;}
return Inner
} //對(duì)于以上這段代碼,發(fā)生的事用偽代碼表示如下:
//創(chuàng)建Outer函數(shù),傳入當(dāng)前的scope chain,即[global]
Outer = new Function('', '' [global])
//為Outer.[[Scope]]賦值
Outer.[[Scope]] = [];
Outer.[[Scope]].push(global);
//這時(shí)variable的屬性O(shè)uter就指向這個(gè)函數(shù)了,不再是null。
variable.Outer = Outer
var inner = Outer(1); //這段代碼用偽代碼表示如下:
//執(zhí)行Outer函數(shù),進(jìn)入新創(chuàng)建的outerFunctionContext上下文
//1.創(chuàng)建ouerFunctionContext的Scope Chain,并放入Outer函數(shù)的[[Scope]]力所有的object
outerFunctionContext.ScopeChain = [];
outerFunctionContext.ScopeChain.push(global) //global是[[Scope]]里唯一的對(duì)象。
//2.創(chuàng)建Variable Object屬性,并放到Scope Chain的最前方。
outerVariable= {arguments: xxx} //創(chuàng)建的variable有arguments等屬性
outerFunctionContext.ScopeChain.push(variable)
//3.實(shí)例化variable object
outerVariable.x = 1
outerVariable.Inner = new Function('y', 'return x + y', [outerVariable, global]) //注意上句創(chuàng)建Inner函數(shù)時(shí),會(huì)傳入當(dāng)前的Scope Chain,即[outerVariable, global] //4.確定Outer函數(shù)體內(nèi)的this參數(shù),就是新創(chuàng)建的函數(shù)對(duì)象。
//最后回到globalContext中,把新建的Inner函數(shù)對(duì)象,返回給inner變量。
inner(2); //最后執(zhí)行的這句代碼,將創(chuàng)建并進(jìn)入InnerContext。
初步結(jié)論
現(xiàn)在已經(jīng)知道,執(zhí)行Outer函數(shù)時(shí),對(duì)應(yīng)的outerScopeChain的圖如下,注意global對(duì)象忽略了指向所有built-in object的屬性:
執(zhí)行Inner函數(shù)時(shí),對(duì)應(yīng)的innerScopeChain的圖如下:
Scope Chain的作用
Scope chain的圖出來了,那么它用來干嘛呢?執(zhí)行inner函數(shù)的return x + y,會(huì)發(fā)現(xiàn),我們需要兩個(gè)變量,x和y。那么JavaScript將循著Scope Chain來查找,與__proto__鏈配合,也就是首先在innerVariable(以及其__proto__鏈)找x,沒找到,則到outerVariable中找x,找到為1。 找y時(shí)類似。這就是Inner函數(shù)體中,可以訪問得到Outer函數(shù)中定義的參數(shù)x的原理所在,不難想象,如果Outer函數(shù)中定義了局部變量z,那么z也會(huì)出現(xiàn)在outerVariable對(duì)象中,因此同樣可以被Inner函數(shù)訪問。內(nèi)部函數(shù)可以引用外部函數(shù)的參數(shù)以及變量,這就是JavaScript傳說中的閉包(Closure)。