DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> 關於JavaScript >> JavaScript閉包解析
JavaScript閉包解析
編輯:關於JavaScript     
閉包是 JavaScript 的重要特性,非常強大,可用於執行復雜的計算,可並不容易理解,尤其是對之前從事面向對象編程的人來說,對 JavaScript 認識和編程顯得更難。特別是在看一些開源的 JavaScript 代碼時感覺尤其如此,跟看天書沒什麼區別。

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

首先看兩段代碼。

示例 1:

var sMessage = "hello world";

function sayHelloWorld() {
alert(sMessage);
}

sayHelloWorld();
示例 2:

var iBaseNum = 10;

function addNum(iNum1, iNum2) {
function doAdd() {
return iNum1 + iNum2 + iBaseNum;
}
return doAdd();
}
示例 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。這就形成了一個閉包。如下圖所示:


圖 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)
}
XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved