首頁 >web前端 >js教程 >理解並運用JavaScript的閉包機制_基礎知識

理解並運用JavaScript的閉包機制_基礎知識

WBOY
WBOY原創
2016-05-16 15:45:12914瀏覽

偉大的愛因斯坦同志說過:「如果你無法向一個 6 歲小孩解釋清楚某問題,那就表示你自己都沒整明白」。然而,當我向一個 27 歲的朋友解釋什麼是閉包時,卻徹底失敗了。

這原本是國外某哥們在 Stack Overflow 上對 JavaScript 閉包所提出的問題。不過既然此問題是在 Stack Overflow 提出的,當然也會有很多高手出來解答,其中有些回答確實是經典,如下面這個:

如果在一個外部函數中再定義一個內部函數,即函數巢狀函數,那麼內部函數也可以存取外部函數中的變數:

function foo(x) {
 var tmp = 3;
 function bar(y) {
 alert(x + y + (++tmp));
 }
 bar(10);
}

foo(2); // alert 16
foo(2); // alert 16
foo(2); // alert 16

此段程式碼可以正確執行,並傳回結果:16,因為 bar 能存取外部函數的變數 tmp, 同時也能存取外部函數 foo 的參數 x。但以上範例不是閉包!

要實現閉包的話,需要將內部函數作為外部函數的返回值返回,內部函數在返回前,會將所有已訪問過的外部函數中的變量在內存中鎖定,也就是說,這些變量將常駐bar 的記憶體中,不會被垃圾回收器回收,如下:

function foo(x) {
 var tmp = 3;
 return function (y) {
 alert(x + y + (++tmp));
 }
}
var bar = foo(2); // bar 现在是个闭包了
bar(10); // alert 16
bar(10); // alert 17
bar(10); // alert 18

上述程式碼中,第一次執行 bar 時,仍會傳回結果:16,因為 bar 仍可存取 x 及 tmp,儘管它已經不直接存在於 foo 的作用域內。那麼既然 tmp 被鎖在 bar 的閉包裡,那麼每次執行 bar 的時候,tmp 都會自增一次,所以第二次和第三次執行 bar 時,分別回傳 17 和 18。

此範例中,x 只是一個純粹的數值,當 foo 被呼叫時,數值 x 就會作為參數被拷貝至 foo 內。

但是JavaScript 處理物件的時候,使用的總是引用,如果用一個物件作為參數來呼叫foo,那麼foo 中傳入的其實就是原始物件的引用,所以這個原始物件也相當於被閉包了,如下:

function foo(x) {
 var tmp = 3;
 return function (y) {
 alert(x + y + tmp++);
 x.memb = x.memb ? x.memb + 1 : 1;
 alert(x.memb);
 }
}
var age = new Number(2);
var bar = foo(age); // bar 现在是个闭包了
bar(10); // alert 15 1
bar(10); // alert 16 2
bar(10); // alert 17 3

和期望的一樣,每次執行 bar(10) 時,不但 tmp 自增了,x.memb 也自增了,因為函數體內的 x 和函數體外的 age 引用的是同一個物件。

via http://stackoverflow.com/questions/111102/how-do-javascript-closures-work

補充:透過以上範例,應該能比較清楚的理解閉包了。如果覺得自己理解了,可以試著猜猜看下面這段程式碼的執行結果:

function foo(x) {
 var tmp = 3;
 return function (y) {
 alert(x + y + tmp++);
 x.memb = x.memb ? x.memb + 1 : 1;
 alert(x.memb);
 }
}
var age = new Number(2);
var bar1 = foo(age); // bar1 现在是个闭包了
bar1(10); // alert 15 1
bar1(10); // alert 16 2
bar1(10); // alert 17 3

var bar2 = foo(age); // bar2 现在也是个闭包了
bar2(10); // alert ? ?
bar2(10); // alert ? ?
bar2(10); // alert ? ?

bar1(10); // alert ? ?
bar1(10); // alert ? ?
bar1(10); // alert ? ?

實際使用的時候,閉包可以創造出非常優雅的設計,允許對funarg上定義的多種計算方式進行自訂。如下就是數組排序的例子,它接受一個排序條件函數作為參數:

[1, 2, 3].sort(function (a, b) {
 ... // 排序条件
});

同樣的例子還有,數組的map方法是根據函數中定義的條件將原始數組映射到一個新的數組:

[1, 2, 3].map(function (element) {
 return element * 2;
}); // [2, 4, 6]

使用函數式參數,可以很方便的實作一個搜尋方法,並且可以支援無限制的搜尋條件:

someCollection.find(function (element) {
 return element.someProperty == 'searchCondition';
});

還有應用函數,例如常見的forEach方法,將函數應用到每個陣列元素:

[1, 2, 3].forEach(function (element) {
 if (element % 2 != 0) {
  alert(element);
 }
}); // 1, 3

順便提下,函數物件的 apply 和 call方法,在函數式程式設計中也可以用作應用函數。 這裡,我們將它們視為應用函數 —— 應用到參數中的函數(在apply中是參數列表,在call中是獨立的參數):

(function () {
 alert([].join.call(arguments, ';')); // 1;2;3
}).apply(this, [1, 2, 3]);

閉包還有另外一個很重要的應用 —— 延遲呼叫:

var a = 10;
setTimeout(function () {
 alert(a); // 10, after one second
}, 1000);
还有回调函数:

//...
var x = 10;
// only for example
xmlHttpRequestObject.onreadystatechange = function () {
 // 当数据就绪的时候,才会调用;
 // 这里,不论是在哪个上下文中创建
 // 此时变量“x”的值已经存在了
 alert(x); // 10
};
//...

也可以建立封裝的作用域來隱藏輔助物件:

var foo = {};

// 初始化
(function (object) {

 var x = 10;

 object.getX = function _getX() {
  return x;
 };

})(foo);

alert(foo.getX()); // 获得闭包 "x" – 10

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn