閉包真的是一個談爛掉的內(nèi)容。說到閉包,自然就涉及到執(zhí)行環(huán)境、變量對象以及作用域鏈。湯姆大叔翻譯的《深入理解JavaScript系列》很好,幫我解決了一直以來似懂非懂的很多問題,包括閉包。下面就給自己總結(jié)一下。包括參考大叔的譯文以及《JavaScript高級程序設(shè)計(第3版)》,一些例子引用自它們。
附上大叔的鏈接:《深入理解JavaScript系列》
一、執(zhí)行環(huán)境(或“執(zhí)行上下文”,意義一樣)
首先說下ECMAScript可執(zhí)行代碼的類型包括:全局代碼、函數(shù)代碼、eval_r()代碼。
每當執(zhí)行流轉(zhuǎn)到可執(zhí)行代碼時,即會進入一個執(zhí)行環(huán)境?;顒拥膱?zhí)行環(huán)境構(gòu)成一個棧:棧的底部始終是全局環(huán)境,頂部是當前活動的執(zhí)行環(huán)境。
全局執(zhí)行環(huán)境是最外圍的一個執(zhí)行環(huán)境。在瀏覽器中,全局環(huán)境就是window對象,因此所有全局變量和函數(shù)都是作為window對象的屬性和方法創(chuàng)建的。
每個函數(shù)都有自己的執(zhí)行環(huán)境。當執(zhí)行流進入一個函數(shù)時,函數(shù)的環(huán)境被推入棧中。而在函數(shù)執(zhí)行之后,棧將其環(huán)境彈出,把控制權(quán)返回給之前的執(zhí)行環(huán)境。某個執(zhí)行環(huán)境中的代碼執(zhí)行完后,該環(huán)境銷毀,保存在其中的所有變量和函數(shù)定義也隨之銷毀。而全局執(zhí)行環(huán)境直到應(yīng)用程序退出才會被銷毀。
eval的執(zhí)行環(huán)境與調(diào)用環(huán)境的執(zhí)行環(huán)境相同。
二、變量對象
我們知道變量和執(zhí)行環(huán)境有著密切的關(guān)系:
var a = 10; // 全局上下文中的變量 (function () { var b = 20; // function上下文中的局部變量})(); alert(a); // 10alert(b); // 全局變量 "b" 沒有聲明
而且我們也知道在JS里沒有塊級作用域這一說法,ES規(guī)范指出獨立作用域只能通過函數(shù)(function)代碼類型的執(zhí)行環(huán)境創(chuàng)建。也就是說,像for循環(huán)并不能創(chuàng)建一個局部環(huán)境:
for (var k in {a: 1, b: 2}) { alert(k);} alert(k); // 盡管循環(huán)已經(jīng)結(jié)束但變量k依然在當前作用域
既然變量與執(zhí)行環(huán)境相關(guān),那變量自己應(yīng)該知道它的數(shù)據(jù)存放在哪里,并知道如何訪問。這就引出了“變量對象”這個概念。
每個執(zhí)行環(huán)境都有一個與之關(guān)聯(lián)的變量對象,這個對象存儲著在環(huán)境中定義的以下內(nèi)容:
1. 函數(shù)的形參
2. var聲明的變量
3. 函數(shù)聲明(不包括函數(shù)表達式)
舉例來說,用一個普通對象來表示變量對象,它是執(zhí)行環(huán)境的一個屬性:
執(zhí)行環(huán)境 = { 變量對象:{ //環(huán)境中的數(shù)據(jù) }};
例如:
對應(yīng)的變量對象為:
// 全局執(zhí)行環(huán)境的變量對象全局環(huán)境的變量對象= { a: 10, test: 指向test()函數(shù)}; // test函數(shù)執(zhí)行環(huán)境的變量對象test函數(shù)環(huán)境的變量對象 = { x: 30, b: 20};
全局環(huán)境中的變量對象
先看下全局對象的明確定義:
全局對象 是在進入任何執(zhí)行環(huán)境之前就已經(jīng)創(chuàng)建了的對象。
這個對象只存在一份,它的屬性在程序中的任何地方都可以訪問,全局對象的生命周期終止于程序退出那一刻。
全局對象初始創(chuàng)建階段,將Math、String等作為自身屬性,初始化如下:
在這里,變量對象就是全局對象自己。
函數(shù)環(huán)境中的變量對象
在函數(shù)執(zhí)行環(huán)境中,“活動對象”扮演著變量對象這個角色?;顒訉ο笫窃谶M入函數(shù)執(zhí)行環(huán)境時創(chuàng)建的,它通過函數(shù)的arguments屬性初始化:
活動對象 = { arguments: //是個對象,包括callee、length等屬性 };
理解了變量對象的初始化之后,下面就是關(guān)于變量對象的核心了。
環(huán)境中的代碼,被分為兩個階段來處理:進入執(zhí)行環(huán)境 、執(zhí)行代碼。變量對象的修改變化與這兩個階段緊密相關(guān)。
這2個階段的處理是一般行為,和環(huán)境的類型無關(guān)(即,在全局環(huán)境和函數(shù)環(huán)境中的表現(xiàn)是一樣的)。
①進入環(huán)境
當進入執(zhí)行環(huán)境時(代碼執(zhí)行之前),變量對象已包含下列屬性(上面有提到):
①函數(shù)的所有形參(如果是在函數(shù)執(zhí)行環(huán)境中。因為全局環(huán)境沒有形參。)
————由 形參名稱 和 對應(yīng)值 組成,作為變量對象的屬性。如果沒有傳遞對應(yīng)的參數(shù),將undefined作為對應(yīng)值。
②所有函數(shù)聲明(注意是聲明,函數(shù)表達式不算。)
————由 函數(shù)名 和 對應(yīng)值(函數(shù)對象)組成,作為變量對象的屬性。如果變量對象已經(jīng)存在同名的屬性,則覆蓋這個屬性。
③所有變量聲明(由var聲明的變量)
————由 變量名 和 對應(yīng)值(undefined) 組成,作為變量對象的屬性。如果變量名與已經(jīng)聲明的形參或函數(shù)相同,則變量聲明不會干擾已經(jīng)存在的這類屬性。
————注意:此時的對應(yīng)值是undefined。
讓我們來看一個例子:
function test(a, b) { alert(c); //undefined alert(d); //function d() {} alert(e); //undefined alert(x); //出錯 var c = 10; function d() {} var e = function _e() {}; (function x() {});} test(10); //
注意,活動對象里不包含函數(shù)x。這是因為x是一個函數(shù)表達式而不是函數(shù)聲明,函數(shù)表達式不會影響變量對象(在這里是活動對象)。函數(shù)_e同樣是函數(shù)表達式,但是我們注意到它分配給了變量e,所以可以通過名稱e來訪問。
在這之后,將進入處理代碼的第二個階段:執(zhí)行代碼。
②執(zhí)行代碼
這個階段內(nèi),變量/活動對象已經(jīng)擁有了屬性(不過,并不是所有屬性都有值,就像上面那個例子,大部分屬性的值還是系統(tǒng)默認的undefined)。
繼續(xù)上面那個例子,活動對象在“執(zhí)行代碼”這個階段被修改如下():
AO(test) = { a: 10, b: undefined, //沒有相應(yīng)該參數(shù)傳入,undefined c: 10, //之前是undefined d: 指向函數(shù)d, e: 指向函數(shù)表達式_e //之前是undefined};
注意此時,函數(shù)表達式_e保存到了已聲明的變量e上,但函數(shù)表達式"x"本身不存在于活動對象中,也就是說,如果嘗試調(diào)用函數(shù)"x",無論在函數(shù)定義之前或之后,都會出現(xiàn)
理解了以上內(nèi)容之后,再來看一個例子:
為什么第一個alert(x)的值是function,而且它還是在x聲明之前訪問的x?為什么不是10或20呢?
現(xiàn)在我們知道,函數(shù)聲明是在進入環(huán)境時填入活動對象的,同一時間,還有一個變量聲明'x',但是正如前面所說,變量聲明在順序上跟在函數(shù)聲明和形參聲明之后。即,在進入環(huán)境階段,變量聲明不會干擾變量對象中已經(jīng)存在的同名函數(shù)或形參聲明。所以,就這個例子來說,在進入環(huán)境時,變量對象的結(jié)構(gòu)如下:
變量對象 = { x:指向函數(shù)x //如果function x沒有已經(jīng)聲明的話,這時的x應(yīng)該是undefined};
緊接著,在代碼執(zhí)行階段,變量對象作如下修改:
變量對象['x'] = 10;變量對象['x'] = 20;//可以在第二、三個alert看到這個結(jié)果
再看一個例子:
if (true) { var a = 1; } else { var b = 2;}//變量是在進入環(huán)境階段放入變量對象的,雖然else部分永遠不會執(zhí)行,//但是不管怎樣,變量b仍然存在于變量對象中。alert(a); //1alert(b); //undefined,不是b未聲明,而是b的值是undefined
另外,關(guān)于var聲明變量和不用var聲明:
大叔的譯文中指出:任何時候,變量只能通過var關(guān)鍵字才能聲明。
像a =10;這僅僅是給全局對象創(chuàng)建了一個新屬性(但它不是變量)。它之所以能成為全局對象的屬性,完全是因為全局對象===全局變量對象??蠢樱?/p>
alert(a); // undefinedalert(b); // "b" 沒有聲明,出錯 b = 10;var a = 20;
進入環(huán)境階段:
變量對象 = { a: undefined};
可以看到,因為b不是一個變量,所以在這個階段根本就沒有b,b將只在代碼執(zhí)行階段才會出現(xiàn),但在這里,還未執(zhí)行到那就出錯了。
還有一個要注意的:var聲明的變量,相對于屬性(如a = 10;或window.a =10;),變量的[[Configurable]]特性值為false,即不能通過delete刪除,而屬性則可以。
三、作用域鏈
現(xiàn)在我們已經(jīng)知道,一個執(zhí)行環(huán)境的數(shù)據(jù)(變量、函數(shù)聲明和函數(shù)形參)作為屬性存儲在變量對象中。
同時也知道,變量對象在每次進入環(huán)境時創(chuàng)建,并填入初始值,值的更新出現(xiàn)在代碼執(zhí)行階段。
下面的內(nèi)容討論作用域鏈。
如果要簡要地描述并展示其重點,那么作用域鏈大多數(shù)與內(nèi)部函數(shù)相關(guān)。
我們可以創(chuàng)建內(nèi)部函數(shù),甚至能從父函數(shù)中返回這些函數(shù)。
var x = 10; function foo() { var y = 20; function bar() { alert(x + y); } return bar; } foo()(); // 30
很明顯每個環(huán)境擁有自己的變量對象:對于全局環(huán)境,它是全局對象自身;對于函數(shù),它是活動對象。
作用域鏈正是內(nèi)部環(huán)境所有變量對象(包括父變量對象)的列表。此鏈用來在標識符解析中變量查找。
作用域鏈本質(zhì)上,是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。
對于上面這個例子,bar執(zhí)行環(huán)境中的作用域鏈包括:bar變量對象、foo變量對象、全局變量對象。
函數(shù)執(zhí)行環(huán)境中的作用域鏈在函數(shù)調(diào)用時創(chuàng)建,包含這個函數(shù)的活動對象和函數(shù)的[[scope]]屬性。示例如下:
活動的執(zhí)行環(huán)境 = { 變量對象: {...}, // or 活動對象 this: thisValue, Scope: [ // 作用域鏈 // 它是所有變量對象的列表。 ]};
其中的Scope定義為:Scope = 被調(diào)用函數(shù)的活動對象 + [[scope]]。
這種標識符的解析過程,與函數(shù)的生命周期相關(guān),下面詳細討論。
(1)函數(shù)的生命周期
函數(shù)的生命周期分為創(chuàng)建和激活(調(diào)用時)兩個階段。
函數(shù)創(chuàng)建
讓我們先看看在全局環(huán)境中的變量和函數(shù)聲明(這里的變量對象就是全局對象自身,我們懂的。)
函數(shù)激活時,得到了正確的也是預(yù)期中的結(jié)果。但我們注意到,變量y在函數(shù)foo中定義(意味著它在foo的活動對象中),但是x并未在foo環(huán)境中定義,相應(yīng)地,它不會添加到foo的活動對象中。那么,foo是如何訪問到變量x的?其實我們大都知道函數(shù)能訪問更高一層的環(huán)境中的變量對象,事實也是如此,而這種機制正是通過函數(shù)內(nèi)部的[[scope]]屬性實現(xiàn)的。
[[scope]]是所有父變量對象的層級鏈,處于當前函數(shù)環(huán)境,在函數(shù)創(chuàng)建時存在于其中。
注意重要的一點:[[scope]]屬性在函數(shù)創(chuàng)建時被存儲,永遠不變,直到函數(shù)銷毀。函數(shù)可以不被調(diào)用,但這個屬性一直存在。
且,與作用域鏈相比,作用域鏈是執(zhí)行環(huán)境的一個屬性,而[[scope]]是函數(shù)的屬性。
上面的例子,函數(shù)foo的[[scope]]如下:
foo.[[Scope]] = [ 全局執(zhí)行環(huán)境.變量對象 // === Global];
繼續(xù),我們知道在函數(shù)調(diào)用時進入執(zhí)行環(huán)境,這時活動對象被創(chuàng)建,this、作用域鏈被確定。下面詳細考慮這個時刻。
函數(shù)激活
正如上面提到的,進入環(huán)境創(chuàng)建變量/活動對象之后,環(huán)境的Scope屬性(即作用域鏈)定義為:Scope = 變量/活動對象 +[[scope]]。
這個定義意思是:活動對象是被添加到[[scope]]前端,在作用域鏈中處理第一位。這很重要,對于標識符的查找,是從自身變量對象開始的,逐漸往父變量對象查找。
(2)通過構(gòu)造函數(shù)創(chuàng)建的函數(shù)的[[scope]]
在上面的例子中,我們看到,在函數(shù)創(chuàng)建時,函數(shù)獲得[[scope]]屬性,該屬性存儲著所有父環(huán)境的變量/活動對象。但有一個例外,那就是通過構(gòu)造函數(shù)創(chuàng)建的函數(shù)。
var x = 10; function foo() { var y = 20; function barFD() { // 函數(shù)聲明 alert(x); alert(y); } var barFE = function () { // 函數(shù)表達式 alert(x); alert(y); }; var barFn = Function('alert(x); alert(y);'); barFD(); // 10, 20 barFE(); // 10, 20 barFn(); // 10, "y" is not defined } foo();
從以上例子中,我們看出問題所在:通過構(gòu)造函數(shù)創(chuàng)建的函數(shù),它的[[scope]]僅包含全局對象。
另外關(guān)于eval,實踐中很少用到eval,但有一點提示,eval代碼的環(huán)境與當前的調(diào)用環(huán)境擁有相同的作用域鏈。
(3)延長作用域鏈
有兩個能延長作用域鏈的方法:with聲明和catch語句。它們添加到作用域鏈的最前端(比被調(diào)用函數(shù)的活動對象還要靠前)。
如果發(fā)生其中一個,作用域鏈作如下修改:
Scope = withObject|catchObject +活動/變量對象 + [[Scope]]
看個例子:
var x = 10, y = 10; with ({x: 20}) { var x = 30, y = 30; alert(x); // 30 alert(y); // 30} alert(x); // 10alert(y); // 30
//1. x = 10,y = 10;
//2. 進入環(huán)境,對象{x:20}添加到作用域鏈的前端。
//3. 執(zhí)行代碼,x為20,變?yōu)?0,y為10,變?yōu)?0。
//4.with聲明完成后,對象被移除,那個因with對象而改變的x=30也被移除。
//最后兩個alert,x保持最初不變,y在with里已發(fā)生改變。
四、閉包
到了這里,其實如果對前面的[[scope]]和作用域鏈完全理解的話,閉包也就懂了。
大叔的譯文對閉包給出的2個定義是:
從理論角度:所有函數(shù)都是閉包。因為它們在創(chuàng)建的時候就將所有父環(huán)境的數(shù)據(jù)保存起來了。哪怕是簡單的全局變量也是如此,因為在函數(shù)中訪問全局變量就相當于在訪問自由變量(指不在參數(shù)聲明,也不在局部聲明的變量),這個時候使用最外層的作用域。
從實踐角度:以下函數(shù)才算是閉包:
下面我們再來具體看一下。
var x = 10;function foo() { alert(x);}(function (funArg) { var x = 20; // 變量"x"在foo中靜態(tài)保存的,在該函數(shù)創(chuàng)建的時候就保存了 funArg(); // 10, 而不是20})(foo);
我們已經(jīng)知道,創(chuàng)建foo函數(shù)的父級環(huán)境(在這里是全局環(huán)境)的數(shù)據(jù)是保存在foo函數(shù)的內(nèi)部屬性[[scope]]中的。
這里還要注意的是:同一個父環(huán)境創(chuàng)建的閉包是共用一個[[scope]]屬性的。也就是說,某個閉包對其中[[scope]]的變量的修改會影響到其他閉包對其變量的讀取。
var firstClosure;var secondClosure;function foo() { var x = 1; firstClosure = function () { return ++x; }; secondClosure = function () { return --x; }; x = 2; // 影響"x", 在2個閉包公有的[[Scope]]中 alert(firstClosure()); // 3, 通過第一個閉包的[[Scope]]}foo();alert(firstClosure()); // 4alert(secondClosure()); // 3
關(guān)于這個問題,大叔的譯文和《JS高級》里都有一個例子:
var data = [];for (var k = 0; k < 3; k++) { data[k] = function () { alert(k); };}data[0](); // 3, 而不是0data[1](); // 3, 而不是1data[2](); // 3, 而不是2
這就是閉包共用一個[[scope]]的問題。可以按下面的方法解決:
var data = [];for (var k = 0; k < 3; k++) { data[k] = (function _helper(x) { return function () { alert(x); }; })(k); // 傳入"k"值}// 現(xiàn)在結(jié)果是正確的了data[0](); // 0data[1](); // 1data[2](); // 2
在上例中,每次_helper都會創(chuàng)建一個新的變量對象,其中含有參數(shù)x,其值就是傳遞進來的k值。此時,返回的函數(shù)的[[scope]]如下:
data[0].[[Scope]] === [ ... // 其它變量對象 父級環(huán)境中的活動對象: {data: [...], k: 3}, _helper環(huán)境中的活動對象: {x: 0}];data[1].[[Scope]] === [ ... // 其它變量對象 父級環(huán)境中的活動對象: {data: [...], k: 3}, _helper環(huán)境中的活動對象: {x: 1}];data[2].[[Scope]] === [ ... // 其它變量對象 父級環(huán)境中的活動對象: {data: [...], k: 3}, _helper環(huán)境中的活動對象: {x: 2}];
要注意的是,如果在返回的函數(shù)中,要獲取k值,那么該值還會是3。