閉包

閉包

在本文章中**


閉包是指那些能夠訪問獨立(自由)變量的函數 (變量在本地使用,但定義在一個封閉的作用域中)。換句話說,這些函數可以“記憶”它被創(chuàng)建時候的環(huán)境。
詞法作用域**EDIT
考慮如下情況:
function init() { var name = "Mozilla"; // name是被init創(chuàng)建的局部變量 function displayName() { // displayName()是一個內部函數, alert(name); // 它是一個使用在父函數中聲明的變量的閉包 } displayName();}init();

函數 init()
創(chuàng)建了一個局部變量 name和一個
名為 displayName()
的函數。 displayName()
是一個內部函數——定義于 init()
之內且僅在該函數體內可用。displayName()
沒有任何自己的局部變量,然而它可以訪問到外部函數的變量,即可以使用父函數 init()
中聲明的 name
變量。

運行代碼可以發(fā)現 displayName()
內的 alert()
語句成功的顯示了在其父函數中聲明的 name
變量的值。這是詞法作用域的一個例子:在 JavaScript 中,變量的作用域是由它在源代碼中所處位置決定的(顯然如此),并且嵌套的函數可以訪問到其外層作用域中聲明的變量。
閉包**EDIT
現在來考慮如下的例子:
function makeFunc() { var name = "Mozilla"; function displayName() { alert(name); } return displayName;}var myFunc = makeFunc();myFunc();

運行這段代碼的效果和之前的 init()
示例完全一樣:字符串 "Mozilla" 將被顯示在一個 JavaScript 警告框中。其中的不同 — 也是有意思的地方 — 在于 displayName()
內部函數在執(zhí)行前被從其外圍函數中返回了。
這段代碼看起來別扭卻能正常運行。在一些編程語言中,函數中的局部變量僅在函數的執(zhí)行期間可用。一旦 makeFunc()
執(zhí)行過后,我們會很合理的認為 name 變量將不再可用。然而,因為代碼運行的沒問題,所以很顯然在 JavaScript 中并不是這樣的。
這個謎題的答案是 myFunc
變成一個 閉包 了。 閉包是一種特殊的對象。它由兩部分構成:函數,以及創(chuàng)建該函數的環(huán)境。環(huán)境由閉包創(chuàng)建時在作用域中的任何局部變量組成。在我們的例子中,myFunc
是一個閉包,由 displayName
函數和閉包創(chuàng)建時存在的 "Mozilla" 字符串形成。
下面是一個更有意思的示例 — makeAdder
函數:
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)
函數:帶有一個參數 x
并返回一個新的函數。返回的函數帶有一個參數 y
,并返回 x
和 y
的和。
從本質上講,makeAdder
是一個函數工廠 — 創(chuàng)建將指定的值和它的參數求和的函數,在上面的示例中,我們使用函數工廠創(chuàng)建了兩個新函數 — 一個將其參數和 5 求和,另一個和 10 求和。
add5
和 add10
都是閉包。它們共享相同的函數定義,但是保存了不同的環(huán)境。在 add5
的環(huán)境中,x
為 5。而在 add10
中,x
則為 10。
實用的閉包**EDIT
理論就是這些了 — 可是閉包確實有用嗎?讓我們看看閉包的實踐意義。閉包允許將函數與其所操作的某些數據(環(huán)境)關連起來。這顯然類似于面向對象編程。在面對象編程中,對象允許我們將某些數據(對象的屬性)與一個或者多個方法相關聯。
因而,一般說來,可以使用只有一個方法的對象的地方,都可以使用閉包。
在 Web 中,您可能想這樣做的情形非常普遍。大部分我們所寫的 Web JavaScript 代碼都是事件驅動的 — 定義某種行為,然后將其添加到用戶觸發(fā)的事件之上(比如點擊或者按鍵)。我們的代碼通常添加為回調:響應事件而執(zhí)行的函數。
以下是一個實際的示例:假設我們想在頁面上添加一些可以調整字號的按鈕。一種方法是以像素為單位指定 body
元素的 font-size
,然后通過相對的 em 單位設置頁面中其它元素(例如頁眉)的字號:
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>

用閉包模擬私有方法**EDIT
諸如 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 */

這里有很多細節(jié)。在以往的示例中,每個閉包都有它自己的環(huán)境;而這次我們只創(chuàng)建了一個環(huán)境,為三個函數所共享:Counter.increment,
Counter.decrement
和 Counter.value

該共享環(huán)境創(chuàng)建于一個匿名函數體內,該函數一經定義立刻執(zhí)行。環(huán)境中包含兩個私有項:名為 privateCounter
的變量和名為 changeBy
的函數。 這兩項都無法在匿名函數外部直接訪問。必須通過匿名包裝器返回的三個公共函數訪問。
這三個公共函數是共享同一個環(huán)境的閉包。多虧 JavaScript 的詞法范圍的作用域,它們都可以訪問 privateCounter
變量和 changeBy
函數。
您應該注意到了,我們定義了一個匿名函數用于創(chuàng)建計數器,然后直接調用該函數,并將返回值賦給 Counter
變量。也可以將這個函數保存到另一個變量中,以便創(chuàng)建多個計數器。
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 */

請注意兩個計數器是如何維護它們各自的獨立性的。每次調用 makeCounter()
函數期間,其環(huán)境是不同的。每次調用中, privateCounter 中含有不同的實例。

這種形式的閉包提供了許多通常由面向對象編程U所享有的益處,尤其是數據隱藏和封裝。
在循環(huán)中創(chuàng)建閉包:一個常見錯誤**EDIT
在 JavaScript 1.7 引入 let
關鍵字
之前,閉包的一個常見的問題發(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();

數組 helpText
中定義了三個有用的提示信息,每一個都關聯于對應的文檔中的輸入域的 ID。通過循環(huán)這三項定義,依次為每一個輸入域添加了一個 onfocus
事件處理函數,以便顯示幫助信息。
運行這段代碼后,您會發(fā)現它沒有達到想要的效果。無論焦點在哪個輸入域上,顯示的都是關于年齡的消息。
該問題的原因在于賦給 onfocus
是閉包(setupHelp)中的匿名函數而不是閉包對象;在閉包(setupHelp)中一共創(chuàng)建了三個匿名函數,但是它們都共享同一個環(huán)境(item)。在 onfocus
的回調被執(zhí)行時,循環(huán)早已經完成,且此時 item
變量(由所有三個閉包所共享)已經指向了 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();

這段代碼可以如我們所期望的那樣工作。所有的回調不再共享同一個環(huán)境, makeHelpCallback
函數為每一個回調創(chuàng)建一個新的環(huán)境。在這些環(huán)境中,help
指向 helpText
數組中對應的字符串。
性能考量**EDIT
如果不是因為某些特殊任務而需要閉包,在沒有必要的情況下,在其它函數中創(chuàng)建函數是不明智的,因為閉包對腳本性能具有負面影響,包括處理速度和內存消耗。
例如,在創(chuàng)建新的對象或者類時,方法通常應該關聯于對象的原型,而不是定義到對象的構造器中。原因是這將導致每次構造器被調用,方法都會被重新賦值一次(也就是說,為每一個對象的創(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; };}

上面的代碼并未利用到閉包的益處,因此,應該修改為如下常規(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;};

下面的代碼可以更簡潔的實現同樣效果
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)建對象時定義方法。參見 對象模型的細節(jié) 一章可以了解更為詳細的信息。

文檔標簽和貢獻者
** 標簽: Java

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容