1. 程式人生 > >【javascript】詳解javascript閉包 — 大家準備好瓜子,我要開始講故事啦~~

【javascript】詳解javascript閉包 — 大家準備好瓜子,我要開始講故事啦~~

重要 參數 銷毀 弘揚 它的 bject 標題 多層嵌套 早就

前言: 在這篇文章裏,我將對那些在各種有關閉包的資料中頻繁出現,但卻又千篇一律,且曖昧模糊得讓人難以理解的表述,做一次自己的解讀。或者說是對“紅寶書”的《函數表達式/閉包》的那一章節所寫的簡潔短小的描述,做一些自己的註解,僅供拋磚引玉 好,看到文章標題,你就應該知道我下文的畫風是怎樣的了,嘿嘿嘿...

閉包的概念

首先要搞懂的就是閉包的概念: 閉包是能夠訪問另一個函數作用域中變量的函數(這個“另外一個函數”,通常指的是包含閉包函數的外部函數), 例如:
function outerFunction () {
var a = 1
return 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();  // 輸出1
foo2();  // 輸出2
foo3();  // 輸出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了 !!! 技術分享

還不太理解的話看我接下來的這個故事: 改完卷子後, 老師把除了學霸以外的所有同學叫到辦公室:為什麽你們的答案TM都是一樣的??? 在這之前我再附加一個現實場景: 學霸雖然學力無窮,但對一些比較難的題目,也不是一下子就能答對的,比如下面這道選擇題: 請問中國最富盛名的博客社區是以下哪個? A: 博客園 B: CSDN C:51CTO D: JB之家 但學霸不知道哪根筋斷了選了B, 後來變本加厲改為C, 最後無可救藥的改為D,但最後學霸發現做這道題的時候自己犯了萬分之一的腦子進水的概率,於是把前面的答案都塗掉了,重新選為A !! 問題: 學霸做這道題的時候,他先後選了A—>B—>C—>D—>A; 那麽!!!為什麽全班人都不是:“有的選A有的選B有的選C有的選D”, 而是全部選了A呢?  答案: 因為!! 學霸把試卷全班傳閱的時候 1.其他人參考的只有學霸那唯一一張試卷(唯一一章是重點,劃起來呀!!) 2.其他人抄的時候,學霸已經做完了!做完了!做完了!(重要的事情說三遍)所以那道選擇題其他人只能看到他最後選的A,而不是B,C,D!! 技術分享

參考書籍或文章: 1.《javaScrpt高級語言程序設計》 2. 深入理解JavaScript系列(12):變量對象(Variable Object) ——湯姆大叔 http://www.cnblogs.com/TomXu/archive/2012/01/16/2309728.html 3.深入理解JavaScript系列(14):作用域鏈(Scope Chain) http://www.cnblogs.com/TomXu/archive/2012/01/18/2312463.html

【javascript】詳解javascript閉包 — 大家準備好瓜子,我要開始講故事啦~~