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