前言
如果你耐心地閱讀完這篇文章舷蒲,你將會(huì)了解到閉包的定義、用法盏求、優(yōu)點(diǎn)以及缺點(diǎn)噪漾!
簡單來說:Closures(閉包)是一個(gè)
函數(shù)的作用域
通俗來說:Closures(閉包)是使用被作用域封閉
的變量
充活,函數(shù)
蜂莉,閉包
等執(zhí)行的一個(gè)函數(shù)的作用域
閉包是一個(gè)函數(shù)和聲明該函數(shù)的詞法環(huán)境的組合,從理論角度來說混卵,所有有函數(shù)都是閉包
閉包的好處:提供了面向?qū)ο缶幊痰脑S多優(yōu)點(diǎn)
尤其是數(shù)據(jù)隱藏和數(shù)據(jù)封裝
閉包的缺點(diǎn): 內(nèi)存泄漏映穗,影響處理速度和內(nèi)存消耗
本文摘要
- 詞法作用域
- 閉包
- 實(shí)用的閉包
- 用閉包模式模擬私有方法(模塊化)
- 在循環(huán)中創(chuàng)建閉包:一個(gè)常見錯(cuò)誤
- 性能考量
正文
詞法作用域
function init(){
//name是一個(gè)被init創(chuàng)建的局部變量
let name = "js";
//displayName()是一個(gè)內(nèi)部函數(shù)
function displayName(){
//一個(gè)閉包使用父函數(shù)中聲明的變量
console.log(`name=${name}`);
}
displayName();
}
init()
函數(shù)init()
創(chuàng)建了一個(gè)局部變量name
和一個(gè)名為displayName()
的函數(shù)
displayName()
是一個(gè)內(nèi)部函數(shù)--定義于init()之內(nèi)且僅在該函數(shù)體內(nèi)可用
displayName
沒有任何自己的局部變量,然而它可以訪問外部函數(shù)的變量幕随,即可以使用父函數(shù)init()
中聲明的name
變量
閉包
現(xiàn)在來考慮如下的例子
function makeFunc() {
let name = "js";
function displayName() {
console.log(`name = ${name}`);
}
return displayName;
}
let myFunc = makeFunc();
myFunc();
運(yùn)行這段代碼的效果和之前的init()
實(shí)例完全一樣蚁滋,字符串'js'將會(huì)打印在控制臺(tái),其中的不同-也是有意思的地方-在于displayName()
內(nèi)部函數(shù)在執(zhí)行前從其外圍函數(shù)返回了
這段代碼看起來別扭卻能正常運(yùn)行赘淮。在一些編程語言中辕录,函數(shù)中的局部變量僅在函數(shù)的執(zhí)行周期可用。一旦makeFunc()
執(zhí)行過后梢卸,我們會(huì)很合理地認(rèn)為name變量將不再可用走诞,然后,js中并不是這樣的蛤高?
這個(gè)謎底的答案是myFunc 變成了一個(gè)閉包蚣旱。閉包是一種特殊的對(duì)象碑幅。它由兩部分構(gòu)成:函數(shù),以及創(chuàng)建該函數(shù)的環(huán)境姻锁。環(huán)境由閉包創(chuàng)建時(shí)在作用域中的任何局部變量組成枕赵。在我們的例子中,myFunc
是一個(gè)閉包位隶,由displayName
函數(shù)和閉包創(chuàng)建時(shí)存在的“js” 字符串形成
簡單來說:
閉包的構(gòu)成:函數(shù),以及創(chuàng)建該函數(shù)的環(huán)境(如圖所示)
環(huán)境:由閉包創(chuàng)建時(shí)在作用域中的任何局部變量組成
實(shí)用的閉包
理論就是這些了 — 可是閉包確實(shí)有用嗎开皿?讓我們看看閉包的實(shí)踐意義涧黄。閉包允許將函數(shù)與其所操作的某些數(shù)據(jù)(環(huán)境)關(guān)連起來。這顯然類似于面向?qū)ο缶幊谈尘!T诿嫦驅(qū)ο缶幊讨兴裢祝瑢?duì)象允許我們將某些數(shù)據(jù)(對(duì)象的屬性)與一個(gè)或者多個(gè)方法相關(guān)聯(lián)。
因而窄潭,一般說來春宣,可以使用只有一個(gè)方法的對(duì)象的地方,都可以使用閉包
大部分我們所寫的 Web JavaScript 代碼都是事件驅(qū)動(dòng)的 — 定義某種行為嫉你,然后將其添加到用戶觸發(fā)的事件之上(比如點(diǎn)擊或者按鍵)月帝。我們的代碼通常添加為回調(diào):響應(yīng)事件而執(zhí)行的函數(shù)
以下是一個(gè)實(shí)際的示例:假設(shè)我們想在頁面上添加一些可以調(diào)整字號(hào)的按鈕。一種方法是以像素為單位指定 body 元素的 font-size幽污,然后通過相對(duì)的 em 單位設(shè)置頁面中其它元素(例如頁眉)的字號(hào):
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
我們的交互式的文本尺寸按鈕可以修改 body 元素的 font-size 屬性嚷辅,而由于我們使用相對(duì)的單位,頁面中的其它元素也會(huì)相應(yīng)地調(diào)整距误。
以下是 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
文本相應(yīng)地調(diào)整為 12,14准潭,16 像素的函數(shù)趁俊。我們可以將它們分別添加到按鈕上(這里是鏈接)。如下所示:
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在內(nèi)的一些語言支持將方法聲明為私有的刑然,即他們只能被用一個(gè)類中的其他方法所調(diào)用
對(duì)此寺擂,javascript并不提供原生的支持,但是可以使用閉包模擬私有方法闰集。私有方法不僅僅有利于限制對(duì)代碼的訪問沽讹;還提供了管理全局變量命名空間的強(qiáng)大能力,避免非核心的方法弄亂了代碼的公共接口部分
下面的實(shí)例展示了如果使用閉包來定義公共函數(shù)武鲁,且其可以訪問私有函數(shù)和變量爽雄。這個(gè)方式稱為模塊模式
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 */
這里有很多細(xì)節(jié)。在以往的示例中沐鼠,每個(gè)閉包都有它自己的環(huán)境挚瘟;而這次我們只創(chuàng)建了一個(gè)環(huán)境叹谁,為三個(gè)函數(shù)所共享:Counter.increment
, Counter.decrement
, Counter.value
該共享環(huán)境創(chuàng)建于一個(gè)匿名函數(shù)體內(nèi),該函數(shù)一經(jīng)定義立即執(zhí)行乘盖。環(huán)境中包含兩個(gè)私有項(xiàng):名為 privateCounter
的變量和名為changeBy
的函數(shù)焰檩。這兩項(xiàng)都無法再匿名函數(shù)外部直接訪問。必須通過匿名包裝器返回的三個(gè)公共函數(shù)訪問
你應(yīng)該注意到了订框,我們定義了一個(gè)匿名函數(shù)用于創(chuàng)建計(jì)數(shù)器析苫,然后直接調(diào)用該函數(shù),并將返回值賦值給Counter變量穿扳。也可以將這個(gè)函數(shù)保存到另一個(gè)變量中衩侥,以便創(chuàng)建多個(gè)計(jì)數(shù)器。
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 */
請(qǐng)注意兩個(gè)計(jì)數(shù)器是如何維護(hù)他們各自的獨(dú)立性的矛物。每次調(diào)用makeCounter()
函數(shù)期間茫死,其環(huán)境是不同的。每次調(diào)用中履羞,privateCounter 中含有不同的實(shí)例
簡單來說:
閉包可以提供面向?qū)ο缶幊痰脑S多優(yōu)點(diǎn)
1.數(shù)據(jù)隱藏
2.數(shù)據(jù)封裝
在循環(huán)中創(chuàng)建閉包:一個(gè)常見錯(cuò)誤
在ECMAScript2015引入let
關(guān)鍵詞之前峦萎,閉包的一個(gè)常見的問題發(fā)生于循環(huán)中創(chuàng)建閉包,參考下面的實(shí)例
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
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();
數(shù)組 helpText 中定義了三個(gè)有用的提示信息忆首,每一個(gè)都關(guān)聯(lián)于對(duì)應(yīng)的文檔中的輸入域的 ID爱榔。通過循環(huán)這三項(xiàng)定義,依次為每一個(gè)輸入域添加了一個(gè) onfocus 事件處理函數(shù)雄卷,以便顯示幫助信息
運(yùn)行這段代碼后搓蚪,您會(huì)發(fā)現(xiàn)它沒有達(dá)到想要的效果。無論焦點(diǎn)在哪個(gè)輸入域上丁鹉,顯示的都是關(guān)于年齡的消息妒潭。
該問題的原因在于賦給focus
是閉包(setupHelp)中的匿名函數(shù)而不是閉包對(duì)象;在閉包(setupHelp)中一共創(chuàng)建了三個(gè)匿名函數(shù)揣钦,但是他們都共享與用一個(gè)環(huán)境(item)雳灾。在onfus
的回調(diào)被執(zhí)行時(shí),循環(huán)早已經(jīng)完成,且此時(shí)item
變量(由所有三個(gè)閉包所共享)已經(jīng)指向了helpText
列表的最后一項(xiàng)
解決方案一
解決這個(gè)問題的一種方案是使onfocus指向一個(gè)新的閉包對(duì)象冯凹。
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function makeHelpCallback(help) {
//相當(dāng)于var help = help
return function() {
showHelp(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 = makeHelpCallback(item.help);
}
}
setupHelp();
這段代碼可以如我們所期望的那樣工作谎亩。所有的回調(diào)不再共享同一個(gè)環(huán)江,makeHelpCallback
函數(shù)為每一個(gè)回調(diào)創(chuàng)建一個(gè)新的環(huán)境宇姚、在這些環(huán)境中匈庭,help
指向helpText
數(shù)組中對(duì)應(yīng)的字符串
解決方案二 使用匿名閉包
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);
}
})(); // Immediate event listener attachment with the current value of item (preserved until iteration).
}
}
setupHelp();
解決方案三(推薦)避免使用過多的閉包,可以用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();
用let而不用var,使得每個(gè)閉包綁定塊內(nèi)變量浑劳,不需要額外的閉包
性能考量
如果不是因?yàn)槟承┨厥馊蝿?wù)而需要閉包阱持,在沒有必要的請(qǐng)款項(xiàng)給I啊,在其他函數(shù)中創(chuàng)建函數(shù)是不明智的魔熏,因?yàn)殚]包對(duì)腳本性能具有負(fù)面影響衷咽,影響處理速度和內(nèi)存消耗
例如鸽扁,在創(chuàng)建新的對(duì)象或者類時(shí),方法通常應(yīng)該關(guān)聯(lián)于對(duì)象的原型镶骗,而不是定義到對(duì)象的構(gòu)造器中桶现。原因是這將導(dǎo)致每次構(gòu)造器被調(diào)用,方法都會(huì)被重新賦值一次(也就是說鼎姊,為每一個(gè)對(duì)象的創(chuàng)建)骡和。
考慮以下雖然不切實(shí)際但卻說明問題的示例:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
上面的代碼并未利用到閉包的益處,因此此蜈,應(yīng)該修改為如下常規(guī)形式
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype = {
getName: function() {
return this.name;
},
getMessage: function() {
return this.message;
}
};