面向對象的JavaScript-009-閉包

引自: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);

 

size12size14 和 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 };

 

在前面的兩個示例中,繼承的原型能夠爲全部對象共享,且沒必要在每一次建立對象時定義方法。參見 對象模型的細節 一章能夠了解更爲詳細的信息。

相關文章
相關標籤/搜索