天天看點

JavaScript 指南 - 閉包閉包

閉包通常被視作 JavaScript 的進階特性,但是,了解閉包對于掌握這門語言至關重要。

考慮如下的函數:

函數 <code>init()</code> 建立了一個局部變量 <code>name</code>,然後定義了名為 <code>displayName()</code> 的函數。 <code>displayName()</code> 是一個内部函數(inner function) — 定義于<code>init()</code> 之内且僅在該函數體内可用。<code>displayName()</code> 沒有任何自己的局部變量,而是重用了聲明在外圍函數中的 <code>name</code> 變量。

這種方式沒有什麼問題 — 可以試試運作這段代碼看看會發生什麼。這就是所謂 函數作用域(functional scoping) 的一個例子:在 JavaScript 中, 變量的作用域是由它在源代碼中所處的位置定義的,且嵌套的函數可以通路在其外層作用域中聲明的變量。

現在來考慮如下的例子:

運作這段代碼的效果和之前的 <code>init()</code> 示例完全一樣:字元串 "Mozilla" 将被顯示在一個 JavaScript 警告框中。其中的不同 — 也是有意思的地方 — 在于<code>displayName()</code> 内部函數在執行前被從其外圍函數中傳回了。

這段代碼看起來别扭卻能正常運作。通常,函數中的局部變量僅在函數的執行期間可用。一旦 <code>makeFunc()</code> 執行過後,我們會很合理的認為 name 變量将不再可用。不過,既然代碼運作的沒問題,顯然不是我們想象的那樣。

這個謎題的答案是 <code>myFunc</code> 變成一個 閉包 了。 閉包是一種特殊的對象。它由兩部分構成:函數,以及建立該函數的環境。環境由閉包建立時在作用域中的任何局部變量組成。在我們的例子中,<code>myFunc</code> 是一個閉包,由 <code>displayName</code> 函數和閉包建立時存在的 "Mozilla" 字元串形成。

下面是一個更有意思的示例 — <code>makeAdder</code> 函數:

在這個示例中,我們定義了 <code>makeAdder(x)</code> 函數:帶有一個參數 <code>x</code> 并傳回一個新的函數。傳回的函數帶有一個參數 <code>y</code>,并傳回 <code>x</code> 和 <code>y</code> 的和。

從本質上講,<code>makeAdder</code> 是一個函數工廠 — 建立将指定的值和它的參數求和的函數,在上面的示例中,我們使用函數工廠建立了兩個新函數 — 一個将其參數和 5 求和,另一個和 10 求和。

<code>add5</code> 和 <code>add10</code> 都是閉包。它們共享相同的函數定義,但是儲存了不同的環境。在 <code>add5</code> 的環境中,<code>x</code> 為 5。而在 <code>add10</code> 中,<code>x</code> 則為 10。

理論就是這些了 — 可是閉包确實有用嗎?讓我們看看閉包的實踐意義。閉包允許将函數與其所操作的某些資料(環境)關連起來。這顯然類似于面向對象程式設計。在面對象程式設計中,對象允許我們将某些資料(對象的屬性)與一個或者多個方法相關聯。

因而,一般說來,可以使用隻有一個方法的對象的地方,都可以使用閉包。

在 Web 中,您可能想這樣做的情形非常普遍。大部分我們所寫的 Web JavaScript 代碼都是事件驅動的 — 定義某種行為,然後将其添加到使用者觸發的事件之上(比如點選或者按鍵)。我們的代碼通常添加為回調:響應事件而執行的函數。

以下是一個實際的示例:假設我們想在頁面上添加一些可以調整字号的按鈕。一種方法是以像素為機關指定 <code>body</code> 元素的 <code>font-size</code>,然後通過相對的 em 機關設定頁面中其它元素(例如頁眉)的字号:

我們的互動式的文本尺寸按鈕可以修改 <code>body</code> 元素的 <code>font-size</code> 屬性,而由于我們使用相對的機關,頁面中的其它元素也會相應地調整。

以下是 JavaScript:

<code>size12</code>,<code>size14</code> 和 <code>size16</code> 為将 <code>body</code> 文本相應地調整為 12,14,16 像素的函數。我們可以将它們分别添加到按鈕上(這裡是連結)。如下所示:

<a target="_blank" href="http://jsfiddle.net/vnkuZ">在JSFIDDLE中檢視</a>

諸如 Java 在内的一些語言支援将方法聲明為私有的,既它們隻能被同一個類中的其它方法所調用。

對此,JavaScript 并不提供原生的支援,但是可以使用閉包模拟私有方法。私有方法不僅僅有利于限制對代碼的通路:還提供了管理全局命名空間的強大能力,避免非核心的方法弄亂了代碼的公共接口部分。

這裡有好多細節。在以往的示例中,每個閉包都有它自己的環境;而這次我們隻建立了一個環境,為三個函數所共享:<code>Counter.increment,</code><code>Counter.decrement</code> 和 <code>Counter.value</code>。

該共享環境建立于一個匿名函數體内,該函數一經定義立刻執行。環境中包含兩個私有項:名為 <code>privateCounter</code> 的變量和名為 <code>changeBy</code> 的函數。 這兩項都無法在匿名函數外部直接通路。必須通過匿名包裝器傳回的三個公共函數通路。

這三個公共函數是共享同一個環境的閉包。多虧 JavaScript 的詞法範圍的作用域,它們都可以通路 <code>privateCounter</code> 變量和 <code>changeBy</code> 函數。

您應該注意到了,我們定義了一個匿名函數用于建立計數器,然後直接調用該函數,并将傳回值賦給 <code>Counter</code> 變量。也可以将這個函數儲存到另一個變量中,以便建立多個計數器。

請注意兩個計數器是如何維護它們各自的獨立性的。每次調用 <code>makeCounter()</code> 函數期間,其環境是不同的。每次調用中, <code>privateCounter 中含有不同的執行個體。</code>

這種形式的閉包提供了許多通常由面向對象程式設計U所享有的益處,尤其是資料隐藏和封裝。

<a target="_blank" href="http://jsfiddle.net/v7gjv">在JSFIDDLE中檢視</a>

數組 <code>helpText</code> 中定義了三個有用的提示資訊,每一個都關聯于對應的文檔中的輸入域的 ID。通過循環這三項定義,依次為每一個輸入域添加了一個<code>onfocus</code> 事件處理函數,以便顯示幫助資訊。

運作這段代碼後,您會發現它沒有達到想要的效果。無論焦點在哪個輸入域上,顯示的都是關于年齡的消息。

該問題的原因在于賦給 <code>onfocus</code> 的函數是閉包;它們由函數定義和記錄自 <code>setupHelp</code> 函數作用域的環境構成。一共建立了三個閉包,但是它們都共享同一個環境。在 <code>onfocus</code> 的回調被執行時,循環早已經完成,且此時 <code>item</code> 變量(由所有三個閉包所共享)已經指向了 <code>helpText</code> 清單中的最後一項。

解決這個問題的一種方案是使用更多的閉包:特别是使用前文所述的函數工廠。

<a target="_blank" href="http://jsfiddle.net/v7gjv/1">在JSFIDDLE中檢視</a>

這段代碼可以如我們所期望的那樣工作。所有的回調不再共享同一個環境, <code>makeHelpCallback</code> 函數為每一個回調建立一個新的環境。在這些環境中,<code>help</code> 指向 <code>helpText</code> 數組中對應的字元串。

如果不是因為某些特殊任務而需要閉包,在沒有必要的情況下,在其它函數中建立函數是不明智的,因為閉包對腳本性能具有負面影響,包括處理速度和記憶體消耗。

例如,在建立新的對象或者類時,方法通常應該關聯于對象的原型,而不是定義到對象的構造器中。原因是這将導緻每次構造器被調用,方法都會被重新指派一次(也就是說,為每一個對象的建立)。

考慮以下雖然不切實際但卻說明問題的示例:

上面的代碼并未利用到閉包的益處,是以,應該修改為如下正常形式:

或者改成:

标簽: 

<a target="_blank" href="https://developer.mozilla.org/zh-CN/docs/tag/%E9%97%AD%E5%8C%85">閉包</a>

<a target="_blank" href="https://developer.mozilla.org/zh-CN/docs/tag/%E5%87%BD%E6%95%B0">函數</a>

<a target="_blank" href="https://developer.mozilla.org/zh-CN/docs/tag/JavaScript">JavaScript</a>

繼續閱讀