js中閉包
閉包是函式和宣告該函式的詞法環境的組合。
詞法作用域
考慮如下情況:
function init() {
var name = "Mozilla"; // name 是一個被 init 建立的區域性變數
function displayName() { // displayName() 是內部函式,一個閉包
alert(name); // 使用了父函式中宣告的變數
}
displayName();
}
init();
init()
建立了一個區域性變數 name
和一個名為 displayName()
的函式。displayName()
是定義在 init()
裡的內部函式,僅在該函式體內可用。displayName()
displayName()
可以使用父函式 init()
中宣告的變數 name
。但是,如果有同名變數 name
在 displayName()
中被定義,則會使用的 displayName()
中定義的 name
。執行程式碼可以發現 displayName()
內的 alert()
語句成功的顯示了在其父函式中宣告的 name
變數的值。這個詞法作用域的例子介紹了引擎是如何解析函式巢狀中的變數的。詞法作用域中使用的域,是變數在程式碼中宣告的位置所決定的。巢狀的函式可以訪問在其外部宣告的變數。
閉包
現在來考慮如下例子 :
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
執行這段程式碼和之前的 init()
示例的效果完全一樣。其中的不同 — 也是有意思的地方 — 在於內部函式 displayName()
在執行前,被外部函式返回。
第一眼看上去,也許不能直觀的看出這段程式碼能夠正常執行。在一些程式語言中,函式中的區域性變數僅在函式的執行期間可用。一旦 makeFunc()
name
變數將不能被訪問。然而,因為程式碼執行的沒問題,所以很顯然在 JavaScript 中並不是這樣的。這個謎題的答案是,JavaScript中的函式會形成閉包。 閉包是由函式以及建立該函式的詞法環境組合而成。這個環境包含了這個閉包建立時所能訪問的所有區域性變數。在我們的例子中,myFunc
是執行 makeFunc
時建立的 displayName
函式例項的引用,而 displayName
例項仍可訪問其詞法作用域中的變數,即可以訪問到 name
。由此,當 myFunc
被呼叫時,name
仍可被訪問,其值 Mozilla
就被傳遞到alert
中。
下面是一個更有意思的示例 — makeAdder
函式:
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 求和。
add5
和 add10
都是閉包。它們共享相同的函式定義,但是儲存了不同的詞法環境。在 add5
的環境中,x
為 5。而在 add10
中,x
則為 10。
實用的閉包
閉包很有用,因為他允許將函式與其所操作的某些資料(環境)關聯起來。這顯然類似於面向物件程式設計。在面向物件程式設計中,物件允許我們將某些資料(物件的屬性)與一個或者多個方法相關聯。
因此,通常你使用只有一個方法的物件的地方,都可以使用閉包。
在 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;
<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.