閉包
閉包是指那些能夠訪問獨立(自由)變量的函數(shù) (變量在本地使用,但定義在一個封閉的作用域中)芬骄。換句話說猾愿,這些函數(shù)可以“記憶”它被創(chuàng)建時候的環(huán)境。
詞法作用域**EDIT
考慮如下情況:
function init() { var name = "Mozilla"; // name是被init創(chuàng)建的局部變量 function displayName() { // displayName()是一個內(nèi)部函數(shù), alert(name); // 它是一個使用在父函數(shù)中聲明的變量的閉包 } displayName();}init();
函數(shù) init()
創(chuàng)建了一個局部變量 name和一個
名為 displayName()
的函數(shù)账阻。 displayName()
是一個內(nèi)部函數(shù)——定義于 init()
之內(nèi)且僅在該函數(shù)體內(nèi)可用蒂秘。displayName()
沒有任何自己的局部變量,然而它可以訪問到外部函數(shù)的變量淘太,即可以使用父函數(shù) init()
中聲明的 name
變量姻僧。
運行代碼可以發(fā)現(xiàn) displayName()
內(nèi)的 alert()
語句成功的顯示了在其父函數(shù)中聲明的 name
變量的值。這是詞法作用域的一個例子:在 JavaScript 中琴儿,變量的作用域是由它在源代碼中所處位置決定的(顯然如此)段化,并且嵌套的函數(shù)可以訪問到其外層作用域中聲明的變量。
閉包**EDIT
現(xiàn)在來考慮如下的例子:
function makeFunc() { var name = "Mozilla"; function displayName() { alert(name); } return displayName;}var myFunc = makeFunc();myFunc();
運行這段代碼的效果和之前的 init()
示例完全一樣:字符串 "Mozilla" 將被顯示在一個 JavaScript 警告框中造成。其中的不同 — 也是有意思的地方 — 在于 displayName()
內(nèi)部函數(shù)在執(zhí)行前被從其外圍函數(shù)中返回了显熏。
這段代碼看起來別扭卻能正常運行。在一些編程語言中晒屎,函數(shù)中的局部變量僅在函數(shù)的執(zhí)行期間可用喘蟆。一旦 makeFunc()
執(zhí)行過后缓升,我們會很合理的認(rèn)為 name 變量將不再可用。然而蕴轨,因為代碼運行的沒問題港谊,所以很顯然在 JavaScript 中并不是這樣的。
這個謎題的答案是 myFunc
變成一個 閉包 了橙弱。 閉包是一種特殊的對象歧寺。它由兩部分構(gòu)成:函數(shù),以及創(chuàng)建該函數(shù)的環(huán)境棘脐。環(huán)境由閉包創(chuàng)建時在作用域中的任何局部變量組成斜筐。在我們的例子中,myFunc
是一個閉包蛀缝,由 displayName
函數(shù)和閉包創(chuàng)建時存在的 "Mozilla" 字符串形成顷链。
下面是一個更有意思的示例 — makeAdder
函數(shù):
function makeAdder(x) { return function(y) { return x + y; };}var add5 = makeAdder(5);var add10 = makeAdder(10);console.log(add5(2)); // 7console.log(add10(2)); // 12
在這個示例中,我們定義了 makeAdder(x)
函數(shù):帶有一個參數(shù) x
并返回一個新的函數(shù)屈梁。返回的函數(shù)帶有一個參數(shù) y
嗤练,并返回 x
和 y
的和。
從本質(zhì)上講在讶,makeAdder
是一個函數(shù)工廠 — 創(chuàng)建將指定的值和它的參數(shù)求和的函數(shù)煞抬,在上面的示例中,我們使用函數(shù)工廠創(chuàng)建了兩個新函數(shù) — 一個將其參數(shù)和 5 求和真朗,另一個和 10 求和此疹。
add5
和 add10
都是閉包。它們共享相同的函數(shù)定義遮婶,但是保存了不同的環(huán)境蝗碎。在 add5
的環(huán)境中,x
為 5旗扑。而在 add10
中蹦骑,x
則為 10。
實用的閉包**EDIT
理論就是這些了 — 可是閉包確實有用嗎臀防?讓我們看看閉包的實踐意義眠菇。閉包允許將函數(shù)與其所操作的某些數(shù)據(jù)(環(huán)境)關(guān)連起來。這顯然類似于面向?qū)ο缶幊谈ぶ浴T诿鎸ο缶幊讨猩臃希瑢ο笤试S我們將某些數(shù)據(jù)(對象的屬性)與一個或者多個方法相關(guān)聯(lián)。
因而致燥,一般說來登疗,可以使用只有一個方法的對象的地方,都可以使用閉包。
在 Web 中辐益,您可能想這樣做的情形非常普遍断傲。大部分我們所寫的 Web JavaScript 代碼都是事件驅(qū)動的 — 定義某種行為,然后將其添加到用戶觸發(fā)的事件之上(比如點擊或者按鍵)智政。我們的代碼通常添加為回調(diào):響應(yīng)事件而執(zhí)行的函數(shù)认罩。
以下是一個實際的示例:假設(shè)我們想在頁面上添加一些可以調(diào)整字號的按鈕。一種方法是以像素為單位指定 body
元素的 font-size
续捂,然后通過相對的 em 單位設(shè)置頁面中其它元素(例如頁眉)的字號:
body { font-family: Helvetica, Arial, sans-serif; font-size: 12px;}h1 { font-size: 1.5em;}h2 { font-size: 1.2em;}
我們的交互式的文本尺寸按鈕可以修改 body
元素的 font-size
屬性垦垂,而由于我們使用相對的單位,頁面中的其它元素也會相應(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>
用閉包模擬私有方法**EDIT
諸如 Java 在內(nèi)的一些語言支持將方法聲明為私有的聂渊,即它們只能被同一個類中的其它方法所調(diào)用。
對此四瘫,JavaScript 并不提供原生的支持汉嗽,但是可以使用閉包模擬私有方法。私有方法不僅僅有利于限制對代碼的訪問:還提供了管理全局命名空間的強(qiáng)大能力找蜜,避免非核心的方法弄亂了代碼的公共接口部分饼暑。
下面的示例展現(xiàn)了如何使用閉包來定義公共函數(shù),且其可以訪問私有函數(shù)和變量洗做。這個方式也稱為 模塊模式(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 */
這里有很多細(xì)節(jié)弓叛。在以往的示例中,每個閉包都有它自己的環(huán)境诚纸;而這次我們只創(chuàng)建了一個環(huán)境撰筷,為三個函數(shù)所共享:Counter.increment,
Counter.decrement
和 Counter.value
畦徘。
該共享環(huán)境創(chuàng)建于一個匿名函數(shù)體內(nèi)毕籽,該函數(shù)一經(jīng)定義立刻執(zhí)行。環(huán)境中包含兩個私有項:名為 privateCounter
的變量和名為 changeBy
的函數(shù)井辆。 這兩項都無法在匿名函數(shù)外部直接訪問关筒。必須通過匿名包裝器返回的三個公共函數(shù)訪問。
這三個公共函數(shù)是共享同一個環(huán)境的閉包杯缺。多虧 JavaScript 的詞法范圍的作用域蒸播,它們都可以訪問 privateCounter
變量和 changeBy
函數(shù)。
您應(yīng)該注意到了夺谁,我們定義了一個匿名函數(shù)用于創(chuàng)建計數(shù)器廉赔,然后直接調(diào)用該函數(shù)肉微,并將返回值賦給 Counter
變量。也可以將這個函數(shù)保存到另一個變量中蜡塌,以便創(chuàng)建多個計數(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 */
請注意兩個計數(shù)器是如何維護(hù)它們各自的獨立性的。每次調(diào)用 makeCounter()
函數(shù)期間馏艾,其環(huán)境是不同的劳曹。每次調(diào)用中, privateCounter 中含有不同的實例琅摩。
這種形式的閉包提供了許多通常由面向?qū)ο缶幊蘒所享有的益處铁孵,尤其是數(shù)據(jù)隱藏和封裝。
在循環(huán)中創(chuàng)建閉包:一個常見錯誤**EDIT
在 JavaScript 1.7 引入 let
關(guān)鍵字 之前房资,閉包的一個常見的問題發(fā)生于在循環(huán)中創(chuàng)建閉包蜕劝。參考下面的示例:
<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
中定義了三個有用的提示信息,每一個都關(guān)聯(lián)于對應(yīng)的文檔中的輸入域的 ID轰异。通過循環(huán)這三項定義岖沛,依次為每一個輸入域添加了一個 onfocus
事件處理函數(shù),以便顯示幫助信息搭独。
運行這段代碼后婴削,您會發(fā)現(xiàn)它沒有達(dá)到想要的效果。無論焦點在哪個輸入域上牙肝,顯示的都是關(guān)于年齡的消息唉俗。
該問題的原因在于賦給 onfocus
是閉包(setupHelp)中的匿名函數(shù)而不是閉包對象;在閉包(setupHelp)中一共創(chuàng)建了三個匿名函數(shù)配椭,但是它們都共享同一個環(huán)境(item)虫溜。在 onfocus
的回調(diào)被執(zhí)行時,循環(huán)早已經(jīng)完成颂郎,且此時 item
變量(由所有三個閉包所共享)已經(jīng)指向了 helpText
列表中的最后一項吼渡。
解決這個問題的一種方案是使onfocus指向一個新的閉包對象。
function showHelp(help) { document.getElementById('help').innerHTML = help;}function makeHelpCallback(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)不再共享同一個環(huán)境寺酪, makeHelpCallback
函數(shù)為每一個回調(diào)創(chuàng)建一個新的環(huán)境。在這些環(huán)境中替劈,help
指向 helpText
數(shù)組中對應(yīng)的字符串寄雀。
性能考量**EDIT
如果不是因為某些特殊任務(wù)而需要閉包,在沒有必要的情況下陨献,在其它函數(shù)中創(chuàng)建函數(shù)是不明智的盒犹,因為閉包對腳本性能具有負(fù)面影響,包括處理速度和內(nèi)存消耗。
例如急膀,在創(chuàng)建新的對象或者類時沮协,方法通常應(yīng)該關(guān)聯(lián)于對象的原型,而不是定義到對象的構(gòu)造器中卓嫂。原因是這將導(dǎo)致每次構(gòu)造器被調(diào)用慷暂,方法都會被重新賦值一次(也就是說,為每一個對象的創(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; };}
上面的代碼并未利用到閉包的益處行瑞,因此,應(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; }};
或者改成:
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;};
下面的代碼可以更簡潔的實現(xiàn)同樣效果
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString();}(function() { this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; };}).call(MyObject.prototype);
在前面的三個示例中餐禁,繼承的原型可以為所有對象共享血久,且不必在每一次創(chuàng)建對象時定義方法。參見 對象模型的細(xì)節(jié) 一章可以了解更為詳細(xì)的信息帮非。
文檔標(biāo)簽和貢獻(xiàn)者
** 標(biāo)簽: Java