該文章是MDN閉包文檔學(xué)習(xí)筆記,方便日后查閱。
如要查閱源文檔,請(qǐng)移步閉包文檔傳送門
閉包
閉包是函數(shù)和聲明該函數(shù)的詞法環(huán)境的組合。
一、詞法作用域
嵌套的函數(shù)可以訪問(wèn)在其外部聲明的變量。
function init() {
var name = "Mozilla"; // name 是一個(gè)被 init 創(chuàng)建的局部變量
function displayName() { // displayName() 是內(nèi)部函數(shù),一個(gè)閉包
alert(name); // 使用了父函數(shù)中聲明的變量
}
displayName();
}
init();
二、閉包
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc()
- 在于內(nèi)部函數(shù) displayName() 在執(zhí)行前,被外部函數(shù)返回
- JavaScript中的函數(shù)會(huì)形成閉包,閉包是由函數(shù)以及創(chuàng)建該函數(shù)的詞法環(huán)境組合而成。這個(gè)環(huán)境包含了這個(gè)閉包創(chuàng)建時(shí)所能訪問(wèn)的所有局部變量
- myFunc 是執(zhí)行 makeFunc 時(shí)創(chuàng)建的 displayName 函數(shù)實(shí)例的引用,而 displayName 實(shí)例仍可訪問(wèn)其詞法作用域中的變量,即可以訪問(wèn)到 name
再看一個(gè)例子
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 是一個(gè)函數(shù)工廠 — 他創(chuàng)建了將指定的值和它的參數(shù)相加求和的函數(shù)
- add5 和 add10 都是閉包。它們共享相同的函數(shù)定義,但是保存了不同的詞法環(huán)境。
三、閉包的使用
閉包允許將函數(shù)與其所操作的某些數(shù)據(jù)(環(huán)境)關(guān)聯(lián)起來(lái),類似于面向?qū)ο缶幊痰膶?duì)象(有成員也有方法)
1. 作為回調(diào)函數(shù)工廠
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(12);
document.getElementById('size-12').onclick = size12 ;
2. 用閉包模擬私有方法
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})(); //立即執(zhí)行的匿名函數(shù)
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 */
這個(gè)環(huán)境中包含兩個(gè)私有項(xiàng):名為 privateCounter 的變量和名為 changeBy 的函數(shù)。這兩項(xiàng)都無(wú)法在這個(gè)匿名函數(shù)外部直接訪問(wèn)。必須通過(guò)匿名函數(shù)返回的三個(gè)公共函數(shù)訪問(wèn)。
注意:兩個(gè)計(jì)數(shù)器 counter1 和 counter2 是如何維護(hù)它們各自的獨(dú)立性的。每個(gè)閉包都是引用自己詞法作用域內(nèi)的變量 privateCounter 。每次調(diào)用其中一個(gè)計(jì)數(shù)器時(shí),通過(guò)改變這個(gè)變量的值,會(huì)改變這個(gè)閉包的詞法環(huán)境。然而在一個(gè)閉包內(nèi)對(duì)變量的修改,不會(huì)影響到另外一個(gè)閉包中的變量。
3. 一個(gè)常見錯(cuò)誤:在循環(huán)中創(chuàng)建閉包
可以再循環(huán)中創(chuàng)建閉包,但如果編碼過(guò)于想當(dāng)然,容易犯一個(gè)常見的錯(cuò)誤
在 ECMAScript 2015 引入 let 關(guān)鍵字 之前,在循環(huán)中有一個(gè)常見的閉包創(chuàng)建問(wèn)題。參考下面的示例:
code 1
function showHelp(help) {
return function() {
document.getElementById('help').innerHTML = help;
}
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = showHelp(item.help);
}
}
setupHelp();
比較如下代碼
code 2
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help); // ①
}
}
}
setupHelp();
- ①處在循環(huán)中創(chuàng)建閉包,三個(gè)共享了同一個(gè)詞法作用域,在這個(gè)作用域中存在一個(gè)變量item。當(dāng)onfocus的回調(diào)執(zhí)行時(shí),item.help的值被決定。由于循環(huán)在事件觸發(fā)之前早已執(zhí)行完畢,變量對(duì)象item(被三個(gè)閉包所共享)已經(jīng)指向了helpText的最后一項(xiàng)。
- 應(yīng)該使用第一種方式:函數(shù)工廠的方式創(chuàng)建閉包,三個(gè)共享不同的語(yǔ)法作用域
code 3 : 另一種方法使用了匿名閉包
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
(function() {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
})(); // 馬上把當(dāng)前循環(huán)項(xiàng)的item與事件回調(diào)相關(guān)聯(lián)起來(lái)
}
}
setupHelp();
code 4: 避免使用過(guò)多的閉包,可以用let關(guān)鍵詞
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
let item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
這個(gè)例子使用let而不是var,因此每個(gè)閉包都綁定了塊作用域的變量,這意味著不再需要額外的閉包。
四、性能考量
如果不是某些特定任務(wù)需要使用閉包,在其它函數(shù)中創(chuàng)建函數(shù)是不明智的,因?yàn)殚]包在處理速度和內(nèi)存消耗方面對(duì)腳本性能具有負(fù)面影響。
例如,在創(chuàng)建新的對(duì)象或者類時(shí),方法通常應(yīng)該關(guān)聯(lián)于對(duì)象的原型,而不是定義到對(duì)象的構(gòu)造器中。原因是這將導(dǎo)致每次構(gòu)造器被調(diào)用時(shí),方法都會(huì)被重新賦值一次(也就是,每個(gè)對(duì)象的創(chuàng)建)。
考慮以下示例:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
上面的代碼并未利用到閉包的好處,我們可以修改成如下:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype = {
getName: function() {
return this.name;
},
getMessage: function() {
return this.message;
}
};
但我們不建議重新定義原型??筛某扇缦吕樱?/p>
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};
在前面的兩個(gè)示例中,繼承的原型可以為所有對(duì)象共享,不必在每一次創(chuàng)建對(duì)象時(shí)定義方法。參見 對(duì)象模型的細(xì)節(jié) 一章可以了解更為詳細(xì)的信息。