閉包的定義
function init() {
var name = "Mozilla"; // name 是一個被 init 創建的局部變量
function displayName() { // displayName() 是內部函數,一個閉包
alert(name); // 使用了父函數中聲明的變量
}
displayName();
}
init();
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
上面兩段代碼運行結果是完全一樣的。不同的是:makeFunc
函數中,內部函數 displayName()
在執行前,被外部函數返回。在一些編程語言中,函數中的局部變量僅在函數的執行期間可用。一旦 makeFunc()
執行完畢,我們會認為 name
變量將不能被訪問。然而,因為代碼運行得沒問題,所以很顯然在 JavaScript 中並不是這樣的。
JavaScript這樣的原因是:JavaScript中的函數會形成閉包。 閉包是由函數以及創建該函數的詞法環境組合而成,這個環境包括了這個閉包創建時所能訪問的所有局部變量。在上面的例子中,myFunc
是執行 makeFunc
時創建的 displayName
函數實例的引用,而 displayName
實例仍可訪問其詞法作用域中的變量,即可以訪問到 name
。由此,當 myFunc
被調用時,name
仍可被訪問,其值 Mozilla
就被傳遞到alert
中。
下面看一個更加有趣的例子:
function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
在這個示例中,我們定義了 makeAdder(x)
函數,它接受一個參數 x
,並返回一個新的函數。返回的函數接受一個參數 y
,並返回x+y
的值。從本質上講,makeAdder
是一個函數工廠,他創建了將指定的值和它的參數相加求和的函數。在上面的示例中,我們使用函數工廠創建了兩個新函數—— 一個將其參數和 5 求和,另一個和 10 求和。
閉包的應用
應用於面向對象編程
閉包很有用,因為它允許將函數與其所操作的某些數據(環境)關聯起來。這顯然類似於面向對象編程。在面向對象編程中,對象允許我們將某些數據(對象的屬性)與一個或者多個方法相關聯。因此,通常你使用只有一個方法的對象的地方,都可以使用閉包。
應用於Web開發
在 Web 中,大部分我們所寫的 JavaScript 代碼都是基於事件定義某種行為,然后將其添加到用戶觸發的事件之上(比如點擊或者按鍵)。我們的代碼通常作為回調:為響應事件而執行的函數。
假如,我們想在頁面上添加一些可以調整字號的按鈕。一種方法是以像素為單位指定 body
元素的 font-size
,然后通過相對的 em
單位設置頁面中其它元素(例如header
)的字號:
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
我們的文本尺寸調整按鈕可以修改 body
元素的 font-size
屬性,由於我們使用相對單位,頁面中的其它元素也會相應地調整。下面是JavaScript:
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
size12
,size14
和 size16
三個函數將分別把 body
文本調整為 12,14,16 像素。我們可以將它們分別添加到按鈕的點擊事件上。如下所示:
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
<p>Some paragraph text</p>
<h1>some heading 1 text</h1>
<h2>some heading 2 text</h2>
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>
用閉包模擬私有方法
編程語言中,比如 Java,是支持將方法聲明為私有的,即它們只能被同一個類中的其它方法所調用。而 JavaScript 沒有這種原生支持,但我們可以使用閉包來模擬私有方法。私有方法不僅僅有利於限制對代碼的訪問:還提供了管理全局命名空間的強大能力,避免非核心的方法弄亂了代碼的公共接口部分。
下面的示例展現了如何使用閉包來定義公共函數,並令其可以訪問私有函數和變量。這個方式也稱為 模塊模式(module pattern),下面創建一個計數器可以實現數字加減和查看數字:
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
上面計數器代碼只創建了一個詞法環境,為三個函數所共享Counter.increment,Counter.decrement
和 Counter.value
。該共享環境創建於一個立即執行的匿名函數體內。這個環境中包含兩個私有項:名為privateCounter
的變量和名為 changeBy
的函數。這兩項都無法在這個匿名函數外部直接訪問。必須通過匿名函數返回的三個公共函數訪問。這三個公共函數是共享同一個環境的閉包。因為JavaScript 的詞法作用域,它們都可以訪問 privateCounter
變量和 changeBy
函數。
下面改進一下,定義一個不立即執行的非匿名函數,用於創建計數器。這樣可以創建多個計數器並且互相不影響:
var makeCounter = function () {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
上面兩個計數器,counter1
和 counter2
是相互獨立的,每個閉包都是引用自己詞法作用域內的變量 privateCounter
。每次調用其中一個計數器時,通過改變這個變量的值,會改變這個閉包的詞法環境。然而在一個閉包內對變量的修改,不會影響到另外一個閉包中的變量。
閉包的問題
問題來源
問題例子引用自廖雪峰JavaScript教程之閉包,在應用閉包時,需要注意一個問題,返回的函數並沒有立刻執行,而是直到調用了f()
才執行。我們來看一個例子:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
在上面的例子中,每次循環,都創建了一個新的函數,然后,把創建的3個函數都添加到一個Array
中返回了。你可能認為調用f1()
,f2()
和f3()
結果應該是1
,4
,9
,但實際結果是:
f1(); // 16
f2(); // 16
f3(); // 16
全部都是16
!原因就在於返回的函數引用了變量i
,但它並非立刻執行。等到3個函數都返回時,它們所引用的變量i
已經變成了4
,因此最終結果為16
。
問題解析
首先我們弄懂上面代碼的運行流程:首先var results = count();
之后,函數count
已經被調用了,所以一次執行函數內的各段代碼:var arr = [];
,for (var i=1; i<=3; i++)
,這個for循環尤其值得注意。因為此時循環體執行了push方法,將一個個函數function () { return i * i;}
添加到數組內,但是這個函數並沒有被調用,還只是一個變量,所以for循環依次執行,直到i = 4
。因為閉包,內部函數function () { return i * i;}
引用的i
就是外部變量,for循環中的i = 4
。所以,之后數組arr
內的函數的i
都是4。
調用函數count
后,變量results
已經是數組arr
了。數組里面元素依次是function f1() { return i * i;} function f2() { return i * i;} function f3() { return i * i;}
。但是三個函數都沒有被調用,直到var f1 = results[0];
,此時function f1() { return i * i;}
開始執行,如上段所寫,此時的i = 4
,所以,返回值就是16了。后面兩個調用也是類似情況。
問題啟示
返回閉包時牢記的一點就是:返回函數不要引用任何循環變量,或者后續會發生變化的變量。如果一定要引用循環變量,方法是再創建一個函數,用該函數的參數綁定循環變量當前的值,無論該循環變量后續如何更改,已綁定到函數參數的值不變:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push((function (n) {
return function () {
return n * n;
}
})(i));
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
f1(); // 1
f2(); // 4
f3(); // 9
上述代碼中,避免在arr.push
方法中,實現了每次循環都立即將當前的參數i
傳送給函數,然后立即塞進數組。這樣就避免了i
最后才傳給函數。注意,這里用了一個“創建一個匿名函數並立刻執行”的語法,才能及時綁定參數i
。
(function (x) {
return x * x;
})(3); // 9