標籤:code 虛擬碼 變數聲明 儲存 name 對象 scope 奮鬥 exe
目錄
- 閉包的概念
- 談談函數執行環境,範圍鏈以及變數對象
- 閉包和函數柯裡化
- 閉包造成的額外的記憶體佔用 (注意我說的不是“記憶體流失”!)
- 閉包只能取得包含函數的最後一個值
本文
前言: 在這篇文章裡,我將對那些在各種有關閉包的資料中頻繁出現,但卻又千篇一律,且曖昧模糊得讓人難以理解的表述,做一次自己的解讀。或者說是對“紅寶書”的《函數運算式/閉包》的那一章節所寫的簡潔短小的描述,做一些自己的註解,僅供拋磚引玉 好,看到文章標題,你就應該知道我下文的畫風是怎樣的了,嘿嘿嘿... 回到頂部閉包的概念首先要搞懂的就是閉包的概念:
閉包是能夠訪問另一個函數範圍中變數的函數(這個“另外一個函數”,通常指的是包含閉包函數的外部函數), 例如:
function outerFunction () {var a = 1return function () { console.log(a); }} var innerFunction = outerFunction();innerFunction();
在這個例子裡:負責列印a的匿名函數被包裹在外部函數outerFunction裡面,且訪問了外部函數outerFunction範圍裡的變數a,所以從定義上說,它是一個閉包。 我在標題上說過我要講故事的對吧,
但... 在聽故事前,你需要先看以完下兩個方面的知識:
1. 談談函數執行環境,範圍鏈以及變數對象
2. 閉包和函數柯裡化 回到頂部談談函數執行環境,範圍鏈以及變數對象
(範圍和執行環境其實是同一個概念,我下面的介紹主要會以後者為名) 首先我想讓大家理解的是: 函數執行環境,範圍鏈以及變數對象的相互關係以及各自作用 先引用一下《javaScript進階語言程式》中的兩段原話: 1. "當某個函數被調用時,會建立一個執行環境 (execution context)及相應的範圍鏈(scope Chain)"
— —第178頁 7.2 閉包2. "每個執行環境都有一個與之關聯的變數對象(variable object),環境中定義的所有變數和函數都儲存在這個對象中"
— — 第73頁 4.2 執行環境及其範圍
這是我在“紅寶書”上所能找到的最關鍵的一句話,但看完後,我。。。。一臉懵逼!!!! 現在我知道了函數被調用的時候就會連帶產生和這個函數息息相關的三個東東:
執行環境(execution context),範圍鏈(scope Chain)以及變數對象(variable object),但這三者們具體是什麼關係呢? 後來我看了湯姆大叔的文章,頓時豁然開朗: (文末有相關連結) 下面貼出他寫的虛擬碼:
ExecutionContext = { variableObject: { .... }, this: thisValue, Scope: [ // Scope chain // 所有變數對象的列表 ]};
所以說,
關於三者,更準確的描述或許是這樣的: 在函數調用的時候,會建立一個函數的執行環境,這個執行環境有一個與之對應的變數對象和範圍鏈。 嗯,這下三者的關係應該就比較明朗了吧(雖然好像也並沒有什麼卵用。。)所以說,下面我要介紹的是變數對象和範圍鏈的作用。 變數對象的作用:
每個函數的變數對象儲存了它所擁有的資料,以供函數內部訪問和調用,這些資料包括:(位於執行環境內部的)
1.聲明變數
2.聲明函數
3.接收參數 ”
雖然我們編寫的代碼無法訪問到這個對象,但解析器還處理資料的時候會在後台使用它“ 例如:
function foo (arg) { var variable = ’我是變數‘; function innerFoo () { alert("我是彭湖灣") }}foo(‘我是參數‘);
這個時候執行環境對應的變數對象就變成了這樣:
ExecutionContext = { variableObject: { variable:’我是變數‘ innerFoo: [對函式宣告innerFoo的引用] arg: ‘我是參數‘ }, this: thisValue, Scope: [ // Scope chain // 所有變數對象的列表 ]};
範圍鏈的作用
通過範圍鏈,函數能夠訪問來自它上層範圍(執行環境)中的變數 先看一個例子
function foo () { var a = 1; function innerFoo () { console.log(a) } innerFoo();}foo(); // 列印 1
在這裡,變數a並不是innerFoo範圍(執行環境)內聲明的變數呀,為什麼能夠取到它外部函數foo範圍內的變數呢? 這就是範圍鏈的作用啦,現在的執行環境用湯姆大叔的虛擬碼描述是這樣的:
InnerFoo函數的執行環境:
InnerFooExecutionContext = { variableObject: { }, this: thisValue, Scope: [ // Scope chain innerFooExecutionContext. variableObject, // innerFoo的變數對象 FooExecutionContext.variableObject, // Foo的變數對象 globalContext.variableObject // 全域執行環境window的變數對象 ]};
Foo函數的執行環境:
FooExecutionContext = { variableObject: { a: 1 }, this: thisValue, Scope: [ // Scope chain FooExecutionContext.variableObject, // Foo的變數對象 globalContext.variableObject // 全域執行環境window的變數對象 ]};
你可以看到,
範圍鏈其實就是個從當前函數的變數對象開始,從裡到外取出所有變數對象,組成的一個列表。通過這個範圍鏈列表,就可以實現對上層範圍的訪問。 innerFoo在自己的執行環境的變數對象中沒有找到 a 的變數聲明, 它感到很苦惱,但轉念一想: 誒! 我可以向上層函數執行環境的變數對象(variableObject)中找嘛! 於是乎沿著範圍鏈( Scope chain)攀爬,往上找變數a,幸運的是,在父函數Foo的變數對象,它找到了自己需要的變數a“啊! 找到a了! 它的值是1” 如果今天innerFoo恰逢水逆,沒有在Foo的變數對象中找到a呢? 那麼它會沿著範圍鏈繼續向上“攀爬‘,直到它到達全域執行環境window(global)
回到頂部閉包和函數柯裡化閉包和函數柯裡化在定義一個函數的時候,可能會使用到多層嵌套的閉包,這種用法,叫做“柯裡化”。
而閉包柯裡化有兩大作用:參數累加和延遲調用例子:
function foo (a) { return function (b) { return function (c) { console.log(a + b + c); } }}
foo(‘我‘)(‘叫‘)(‘彭湖灣‘); // 列印 我叫彭湖灣
從這裡,我們可以很直觀地看出閉包柯裡化的時候參數累加的作用我們把上面那個例子改變一下:
function foo (a) { return function (b) { return function (c) { console.log(a + b + c); } }} var foo1 = foo(‘我‘);var foo2 = foo1(‘叫‘);foo2(‘彭湖灣‘); // 列印 我叫彭湖灣
可以看到,最內層的閉包在外層函數foo和foo1調用的時候都沒有調用,
直到最後得到foo2並調用foo2()的時候,這個最內層的閉包才得到執行, 這也是閉包的一大特性——順延強制
好,如果你看完了以上兩個方面的內容,那接下來就可以聽我將故事啦。 回到頂部閉包造成的額外的記憶體佔用 (注意我說的不是“記憶體流失”!)
函數的變數對象一般在函數調用結束後被銷毀(它的“任務”已經完成了,可以被記憶體回收了)
但閉包的情況卻不同
function foo (a) { return function () { console.log(a) }} var foo1 = foo(1);var foo2 = foo(2);var foo3 = foo(3);foo1(); // 輸出1foo2(); // 輸出2foo3(); // 輸出3
實際上,foo函數調用結束後,
foo函數的變數對象並不會被立即銷毀,而是只有當取得foo函數閉包的值的foo1, foo2, foo3調用結束, 這三個函數的變數對象和範圍鏈被銷毀後, foo函數才算“完成任務”,這時,它才能被銷毀。
所以說,閉包會造成額外的記憶體佔用(注意這種記憶體佔用是有必要的,和記憶體流失不同!!) 如果你不是很明白。看看我下面這個故事:
故事: 有這麼一個差異化明顯的班級,
班級成員由一個學霸和一堆學渣組成,在某次監管很寬鬆的測驗中(老師不在) ,
為了其他人能夠不去教導處喝茶,非常老好人的學霸用10分鐘做完了試卷後,把卷子給全班同學抄, 弘揚了中華民族一貫以來的團結和諧,共同奮鬥的精神。。。。
這個外層函數,就是那個學霸;
裡面的閉包,就是那些學渣;
閉包所引用的外層函數的變數,就是學霸遞給學渣們的試卷!!!!!
問: 學霸10分鐘就做完了試卷,那為什麼他一整節課都忙的滿頭大汗
???(為什麼外層函數的變數對象在外層函數調用完畢之後沒有立即銷毀???)
答案: 因為他要忙著給其他同學們傳遞他做好的試卷,又因為他是個老好人,所以只有最後一個同學做完試卷後,這位善良“負責”的學霸才能休息 呀!!!!!!!
(因為閉包通過範圍鏈還保留著對這個外部函數的變數對象的引用,所以外部函數並不能立即得到銷毀)
回到頂部閉包只能取得包含函數的最後一個值讓我們來看看《紅寶書》閉包那一章節中的一個典型例子:
function createArray() { var arr = new Array(); for (var i = 0; i < 10; i++) { arr[i] = function () { return i; } } return arr;}var funcs = createArray();for (var i = 0; i < funcs.length; i++) { document.write(funcs[i]() + "<br />");}
實際上,最後輸出的不是1,2,3,4,5,6,7 。。10,而是全部都是10,
為什嗎? 因為:
1. 這幾個函數都保留著對同一個外部函數的變數對象的引用
2. 因為閉包函數“延遲調用”的特性,而關鍵變數值i的擷取是在閉包函數調用(f也即uncs[i]())的時候才從外部函數的變數對象中擷取,而這個時候,外部函數早就完成for迴圈使 i =10了 !!!
自己對javascript閉包的瞭解