引自:https://developer.mozilla.org/cn/docs/Web/JavaScript/Closuresjavascript
閉包是指可以訪問自由變量的函數 (變量在本地使用,但在閉包中定義)。換句話說,定義在閉包中的函數能夠「記憶」它被建立時候的環境。java
考慮以下的函數:編程
function init() { var name = "Mozilla"; function displayName() { alert(name); } displayName(); } init();
函數 init()
建立了一個局部變量 name
,而後定義了名爲 displayName()
的函數。displayName()
是一個內部函數——定義於 init()
以內且僅在該函數體內可用。displayName()
沒有任何本身的局部變量,然而它能夠訪問到外部函數的變量,便可以使用父函數中聲明的 name
變量。數組
運行代碼能夠發現這能夠正常工做。這是詞法做用域的一個例子:在 JavaScript 中,變量的做用域是由它在源代碼中所處位置決定的(顯然如此),而且嵌套的函數能夠訪問到其外層做用域中聲明的變量。閉包
如今來考慮以下的例子:app
1 function makeFunc() { 2 var name = "Mozilla"; 3 function displayName() { 4 alert(name); 5 } 6 return displayName; 7 } 8 9 var myFunc = makeFunc(); 10 myFunc();
運行這段代碼的效果和以前的 init()
示例徹底同樣:字符串 "Mozilla" 將被顯示在一個 JavaScript 警告框中。其中的不一樣 — 也是有意思的地方 — 在於 displayName()
內部函數在執行前被從其外圍函數中返回了。ide
這段代碼看起來彆扭卻能正常運行。一般,函數中的局部變量僅在函數的執行期間可用。一旦makeFunc()
執行事後,咱們會很合理的認爲 name 變量將再也不可用。雖然代碼運行的沒問題,但實際並非這樣的。函數
這個謎題的答案是 myFunc
變成一個 閉包 了。 閉包是一種特殊的對象。它由兩部分構成:函數,以及建立該函數的環境。環境由閉包建立時在做用域中的任何局部變量組成。在咱們的例子中,myFunc
是一個閉包,由 displayName
函數和閉包建立時存在的 "Mozilla" 字符串造成。oop
下面是一個更有意思的示例 — makeAdder
函數:性能
1 function makeAdder(x) { 2 return function(y) { 3 return x + y; 4 }; 5 } 6 7 var add5 = makeAdder(5); 8 var add10 = makeAdder(10); 9 10 console.log(add5(2)); // 7 11 console.log(add10(2)); // 12
在這個示例中,咱們定義了 makeAdder(x)
函數:帶有一個參數 x
並返回一個新的函數。返回的函數帶有一個參數 y
,並返回 x
和 y
的和。
從本質上講,makeAdder
是一個函數工廠 — 建立將指定的值和它的參數求和的函數,在上面的示例中,咱們使用函數工廠建立了兩個新函數 — 一個將其參數和 5 求和,另外一個和 10 求和。
add5
和 add10
都是閉包。它們共享相同的函數定義,可是保存了不一樣的環境。在 add5
的環境中,x
爲 5。而在 add10
中,x
則爲 10。
理論就是這些了 — 但是閉包確實有用嗎?讓咱們看看閉包的實踐意義。閉包容許將函數與其所操做的某些數據(環境)關連起來。這顯然相似於面向對象編程。在面對象編程中,對象容許咱們將某些數據(對象的屬性)與一個或者多個方法相關聯。
於是,通常說來,可使用只有一個方法的對象的地方,均可以使用閉包。
在 Web 中,您可能想這樣作的情形很是廣泛。大部分咱們所寫的 Web JavaScript 代碼都是事件驅動的 — 定義某種行爲,而後將其添加到用戶觸發的事件之上(好比點擊或者按鍵)。咱們的代碼一般添加爲回調:響應事件而執行的函數。
如下是一個實際的示例:假設咱們想在頁面上添加一些能夠調整字號的按鈕。一種方法是以像素爲單位指定 body
元素的 font-size
,而後經過相對的 em 單位設置頁面中其它元素(例如頁眉)的字號:
1 body { 2 font-family: Helvetica, Arial, sans-serif; 3 font-size: 12px; 4 } 5 6 h1 { 7 font-size: 1.5em; 8 } 9 h2 { 10 font-size: 1.2em; 11 }
咱們的交互式的文本尺寸按鈕能夠修改 body
元素的 font-size
屬性,而因爲咱們使用相對的單位,頁面中的其它元素也會相應地調整。
如下是 JavaScript:
1 function makeSizer(size) { 2 return function() { 3 document.body.style.fontSize = size + 'px'; 4 }; 5 } 6 7 var size12 = makeSizer(12); 8 var size14 = makeSizer(14); 9 var size16 = makeSizer(16);
size12
,size14
和 size16
爲將 body
文本相應地調整爲 12,14,16 像素的函數。咱們能夠將它們分別添加到按鈕上(這裏是連接)。以下所示:
1 document.getElementById('size-12').onclick = size12; 2 document.getElementById('size-14').onclick = size14; 3 document.getElementById('size-16').onclick = size16; 4 <a href="#" id="size-12">12</a> 5 <a href="#" id="size-14">14</a> 6 <a href="#" id="size-16">16</a>
諸如 Java 在內的一些語言支持將方法聲明爲私有的,即它們只能被同一個類中的其它方法所調用。
對此,JavaScript 並不提供原生的支持,可是可使用閉包模擬私有方法。私有方法不只僅有利於限制對代碼的訪問:還提供了管理全局命名空間的強大能力,避免非核心的方法弄亂了代碼的公共接口部分。
下面的示例展示瞭如何使用閉包來定義公共函數,且其能夠訪問私有函數和變量。這個方式也稱爲 模塊模式(module pattern):
1 var Counter = (function() { 2 var privateCounter = 0; 3 function changeBy(val) { 4 privateCounter += val; 5 } 6 return { 7 increment: function() { 8 changeBy(1); 9 }, 10 decrement: function() { 11 changeBy(-1); 12 }, 13 value: function() { 14 return privateCounter; 15 } 16 } 17 })(); 18 19 console.log(Counter.value()); /* logs 0 */ 20 Counter.increment(); 21 Counter.increment(); 22 console.log(Counter.value()); /* logs 2 */ 23 Counter.decrement(); 24 console.log(Counter.value()); /* logs 1 */
這裏有不少細節。在以往的示例中,每一個閉包都有它本身的環境;而此次咱們只建立了一個環境,爲三個函數所共享:Counter.increment,
Counter.decrement
和 Counter.value
。
該共享環境建立於一個匿名函數體內,該函數一經定義馬上執行。環境中包含兩個私有項:名爲 privateCounter
的變量和名爲 changeBy
的函數。 這兩項都沒法在匿名函數外部直接訪問。必須經過匿名包裝器返回的三個公共函數訪問。
這三個公共函數是共享同一個環境的閉包。多虧 JavaScript 的詞法範圍的做用域,它們均可以訪問 privateCounter
變量和 changeBy
函數。
您應該注意到了,咱們定義了一個匿名函數用於建立計數器,而後直接調用該函數,並將返回值賦給 Counter
變量。也能夠將這個函數保存到另外一個變量中,以便建立多個計數器。
1 var makeCounter = function() { 2 var privateCounter = 0; 3 function changeBy(val) { 4 privateCounter += val; 5 } 6 return { 7 increment: function() { 8 changeBy(1); 9 }, 10 decrement: function() { 11 changeBy(-1); 12 }, 13 value: function() { 14 return privateCounter; 15 } 16 } 17 }; 18 19 var Counter1 = makeCounter(); 20 var Counter2 = makeCounter(); 21 console.log(Counter1.value()); /* logs 0 */ 22 Counter1.increment(); 23 Counter1.increment(); 24 console.log(Counter1.value()); /* logs 2 */ 25 Counter1.decrement(); 26 console.log(Counter1.value()); /* logs 1 */ 27 console.log(Counter2.value()); /* logs 0 */
請注意兩個計數器是如何維護它們各自的獨立性的。每次調用 makeCounter()
函數期間,其環境是不一樣的。每次調用中, privateCounter 中含有不一樣的實例。
這種形式的閉包提供了許多一般由面向對象編程U所享有的益處,尤爲是數據隱藏和封裝。
在 JavaScript 1.7 引入 let
關鍵字 以前,閉包的一個常見的問題發生於在循環中建立閉包。參考下面的示例:
1 <p id="help">Helpful notes will appear here</p> 2 <p>E-mail: <input type="text" id="email" name="email"></p> 3 <p>Name: <input type="text" id="name" name="name"></p> 4 <p>Age: <input type="text" id="age" name="age"></p> 5 function showHelp(help) { 6 document.getElementById('help').innerHTML = help; 7 } 8 9 function setupHelp() { 10 var helpText = [ 11 {'id': 'email', 'help': 'Your e-mail address'}, 12 {'id': 'name', 'help': 'Your full name'}, 13 {'id': 'age', 'help': 'Your age (you must be over 16)'} 14 ]; 15 16 for (var i = 0; i < helpText.length; i++) { 17 var item = helpText[i]; 18 document.getElementById(item.id).onfocus = function() { 19 showHelp(item.help); 20 } 21 } 22 } 23 24 setupHelp();
數組 helpText
中定義了三個有用的提示信息,每個都關聯於對應的文檔中的輸入域的 ID。經過循環這三項定義,依次爲每個輸入域添加了一個 onfocus
事件處理函數,以便顯示幫助信息。
運行這段代碼後,您會發現它沒有達到想要的效果。不管焦點在哪一個輸入域上,顯示的都是關於年齡的消息。
該問題的緣由在於賦給 onfocus
是閉包(setupHelp)中的匿名函數而不是閉包對象;在閉包(setupHelp)中一共建立了三個匿名函數,可是它們都共享同一個環境(item)。在 onfocus
的回調被執行時,循環早已經完成,且此時 item
變量(由全部三個閉包所共享)已經指向了helpText
列表中的最後一項。
解決這個問題的一種方案是使onfocus指向一個新的閉包對象。
1 function showHelp(help) { 2 document.getElementById('help').innerHTML = help; 3 } 4 5 function makeHelpCallback(help) { 6 return function() { 7 showHelp(help); 8 }; 9 } 10 11 function setupHelp() { 12 var helpText = [ 13 {'id': 'email', 'help': 'Your e-mail address'}, 14 {'id': 'name', 'help': 'Your full name'}, 15 {'id': 'age', 'help': 'Your age (you must be over 16)'} 16 ]; 17 18 for (var i = 0; i < helpText.length; i++) { 19 var item = helpText[i]; 20 document.getElementById(item.id).onfocus = makeHelpCallback(item.help); 21 } 22 } 23 24 setupHelp();
這段代碼能夠如咱們所指望的那樣工做。全部的回調再也不共享同一個環境,makeHelpCallback
函數爲每個回調建立一個新的環境。在這些環境中,help
指向helpText
數組中對應的字符串。
若是不是由於某些特殊任務而須要閉包,在沒有必要的狀況下,在其它函數中建立函數是不明智的,由於閉包對腳本性能具備負面影響,包括處理速度和內存消耗。
例如,在建立新的對象或者類時,方法一般應該關聯於對象的原型,而不是定義到對象的構造器中。緣由是這將致使每次構造器被調用,方法都會被從新賦值一次(也就是說,爲每個對象的建立)。
考慮如下雖然不切實際但卻說明問題的示例:
1 function MyObject(name, message) { 2 this.name = name.toString(); 3 this.message = message.toString(); 4 this.getName = function() { 5 return this.name; 6 }; 7 8 this.getMessage = function() { 9 return this.message; 10 }; 11 } 12 上面的代碼並未利用到閉包的益處,所以,應該修改成以下常規形式: 13 14 function MyObject(name, message) { 15 this.name = name.toString(); 16 this.message = message.toString(); 17 } 18 MyObject.prototype = { 19 getName: function() { 20 return this.name; 21 }, 22 getMessage: function() { 23 return this.message; 24 } 25 }; 26 或者改爲: 27 28 function MyObject(name, message) { 29 this.name = name.toString(); 30 this.message = message.toString(); 31 } 32 MyObject.prototype.getName = function() { 33 return this.name; 34 }; 35 MyObject.prototype.getMessage = function() { 36 return this.message; 37 };
在前面的兩個示例中,繼承的原型能夠爲全部對象共享,且沒必要在每一次建立對象時定義方法。參見 對象模型的細節 一章能夠了解更爲詳細的信息。