Javascript 閉包(Closures)

weixin_34054866發表於2013-10-01

本文內容

  • 閉包
  • 閉包和引用
  • 參考資料

閉包是 JavaScript 的重要特性,非常強大,可用於執行復雜的計算,可並不容易理解,尤其是對之前從事物件導向程式設計的人來說,對 JavaScript 認識和程式設計顯得更難。特別是在看一些開源的 JavaScript 程式碼時感覺尤其如此,跟看天書沒什麼區別。

一般情況下,人們認識一個事物,會根據之前的經驗,進行對比和總結,在腦中建立一個模型,從而理解掌握它,但是 JavaScript 與物件導向程式設計實在“沒有可比性”,最明顯的是某過於寫法,總覺得“怪怪的”,更不用說,其一些高階特性。如果說“物件”在物件導向程式設計時的出現相當有規律,但是在 JavaScript 中則毫無規律,無處不在,甚至在你意想不到的地方。

首先看兩段程式碼。

示例 1:








示例 2:









示例 1 和示例 2 都是閉包,只是 2 比 1 複雜,甚至還有更復雜的寫法,比如返回多個閉包。

示例 1,指令碼被載入記憶體後,並沒有為函式 sayHelloWorld() 計算變數 sMessage 的值。該函式捕獲 sMessage 的值只是為了以後的使用,也就是說,解釋程式知道在呼叫該函式時要檢查 sMessage 的值。sMessage 將在函式呼叫 sayHelloWorld() 時(最後一行)被賦值,顯示訊息 "hello world"。

示例 2,函式 addNum() 包括函式 doAdd() (閉包)。內部函式是一個閉包,因為它將獲取外部函式的引數 iNum1 和 iNum2 以及全域性變數 iBaseNum 的值。 addNum() 的最後一步呼叫了 doAdd(),把兩個引數和全域性變數相加,並返回它們的和。

這裡要掌握的重要概念是,doAdd() 函式根本不接受引數,它使用的值是從執行環境中獲取的。

閉包


閉包,根據 ECMAScript 描述,詞法(lexically)表示包括不被計算的變數的函式,函式可以使用函式之外定義的變數,它意味著當前作用域總能夠訪問外部作用域中的變數。函式是 JavaScript 中唯一擁有自身作用域的結構,因此閉包的建立依賴於函式。函式內部的函式訪問其所在函式的變數(區域性變數、形參),這些變數會受到內部函式的影響,當其外部函式外被呼叫時,就會形成閉包。內部的函式會在其外部函式返回後,被執行。

示例 3:

function foo() {
    var a = 10;
 
    function bar() {
        a *= 2;
        return a;
    }
    return bar; // 返回內部函式 bar
}
 
var baz = foo();
baz(); // 20

說明:

  • foo 是 bar 的外部函式,bar 是 foo 的內部函式;a 是 foo 的區域性變數;
  • bar 訪問 foo 的區域性變數 a;
  • foo 返回 bar。
  • bar 在 foo 的外部被呼叫。

當執行 baz() 後,閉包使 Javascript 垃圾回收機制不會回收 foo 所佔的資源。因為,baz 實際指向 foo 的內部函式 bar,bar 依賴 foo 的區域性變數 a。這樣,在執行 var baz=foo() 後,baz 實際指向了 bar,而不是 foo。bar 訪問了 foo 的區域性變數 a,當執行 baz() 後,a 為 20。這就形成了一個閉包。如下圖所示:

201111271751015074

圖 1

如果把 foo 看作是一個包,根據剪頭指示,形成了一個閉包。結果是區域性變數 a 的永續性(如示例 4 所示)。下面程式碼就不是閉包。無論執行多少次,都是顯示 20。

示例 4:

function foo() {
    var a = 10;
    function bar() {
        alert(a *= 2);
    }
    bar();
}
foo(); // 20
foo(); // 20
foo(); // 20

從以上兩個示例看,閉包有點類似於物件導向的介面和委託,——只是呼叫方法而無需知道具體細節。

示例 5:

function foo() {
    var a = 10;
    function bar() {
        a *= 2;
        return a;
    }
    return bar;
}
 
var baz = foo();
baz(); // 20
baz(); // 40
baz(); // 80
 
var blat = foo();
blat(); // 20

 

閉包和引用


模擬私有變數

程式碼 6:

function Counter(start) {
    var count = start;
    return {
        increment: function () {
            count++;
        },
 
        get: function () {
            return count;
        }
    }
}
 
var foo = Counter(4);
foo.increment();
foo.get(); // 5

這裡,Counter 函式返回兩個閉包,函式 increment 和函式 get。 這兩個函式都維持著 對外部作用域 Counter 的引用,因此總可以訪問此作用域內定義的變數 count.

為什麼不能在外部訪問私有變數

因為 JavaScript 中不可以對作用域進行引用或賦值,因此沒有辦法在外部訪問 count 變數,唯一的途徑就是通過上面那兩個閉包。

var foo = new Counter(4);
foo.hack = function() {
    count = 1337;
};
上面的程式碼不會改變定義在 Counter 作用域中的 count 變數的值,因為 foo.hack 沒有 定義在那個作用域內。它將會建立或者覆蓋全域性變數 count。

迴圈中的閉包

一個常見的錯誤出現在迴圈中使用閉包,假設我們需要在每次迴圈中呼叫迴圈序號,

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);  
    }, 1000);
}

上面的程式碼不會輸出數字 0 到 9,而是會輸出數字 10 十次。

當 console.log 被呼叫的時候,匿名函式保持對外部變數 i 的引用,此時for迴圈已經結束, i 的值被修改成了 10.

為了得到想要的結果,需要在每次迴圈中建立變數 i 的拷貝。

避免引用錯誤

為了正確的獲得迴圈序號,最好使用 匿名包裹器(自執行匿名函式)。

for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);  
        }, 1000);
    })(i);
}

外部的匿名函式會立即執行,並把 i 作為它的引數,此時函式內 e 變數就擁有了 i 的一個拷貝。

當傳遞給 setTimeout 的匿名函式執行時,它就擁有了對 e 的引用,而這個值是不會被迴圈改變的。

有另一個方法完成同樣的工作;那就是從匿名包裝器中返回一個函式。這和上面的程式碼效果一樣。

for(var i = 0; i < 10; i++) {
    setTimeout((function(e) {
        return function() {
            console.log(e);
        }
    })(i), 1000)
}

 

參考資料

 

下載 Demo

相關文章