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