天天看點

Javascript 閉包全面解析

        今天有人問了下有關javascript的閉包的問題,自己也沒有看相關的文檔,隻是模糊的回答了下。回答完之後感覺那樣對自己不好,一定要弄清javascript的閉包。正好在火狐開發者社群看到一篇有關閉包的文章https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures 

一、什麼是閉包?

     閉包是指函數有自主獨立的變量,也就是說定義閉包中的函數可以記憶它建立時候的“環境”。

二、 文法的作用域

看看下面的函數

<script type="text/javascript">


function init() {
	
	var name ="dongtian";
	function displayName(){
		alert(name);
	}
}

init();
</script>      

在函數init()裡面定義一個變量名稱為name的臨時變量,然後定義一個名稱為displayName 的内部函數。此内部函數僅僅隻有在本init方法内可用,其他都沒有辦法調用,你看displayName函數内并沒有自己的局部變量,然而它可以通路到外包函數的變量-可以使用父函數使用的變量。

我們更改上面的代碼

<script type="text/javascript">


function init() {
	
	var name ="dongtian";
	function displayName(){
		alert(name);
	}
	//僅僅在init方法體内調用
	displayName();
}

init();
</script>      

 這是作用域的一個例子,在javascript 中,變量的作用域是嵌套函數可以通路外部的變量。

三、閉包

我們先看下面的一個例子

<script type="text/javascript">


function init() {
	
	var name ="dongtian";
	function displayName(){
		alert(name);
	}
	//僅僅在init方法體内調用
	return displayName;
}

var display = new init();

display();
</script>      

        運作代碼與上面的結果一樣,這段代碼看起來比較别扭,但是運作正常,我們都知道一個函數的作用域僅僅在運作期可用,當init方法運作完之後我們會認為name變量已經失效不可用了,但是雖然運作沒有問題,實際上是對應name變量不是那樣不可用已經失效了。

   因為這個init已經變成閉包了,是閉包的一個特殊對象,它有兩部分構成:函數以及建立函數的環境,環境由閉包建立時候在作用域的任何局部變量組成,-上面我們可以說 init 是個閉包,由displayName函數和閉包建立時候的name="dongtian"組成。

下面看一個加法器的一個閉包的寫法:

<script type="text/javascript">

function  subAdd(x) {
	
	return function(y) {
		return x +y;
	}
}


var add1 = subAdd(2);
var add2 = subAdd(5);

alert(add1(2));
alert(add2(3));


</script>      

   這個例子我們定義了subAdd()函數,帶有一個參數x,并且傳回一個新的函數,傳回的函數中帶有一個參數y并傳回 x和y的和。從本質上講 subAdd()函數是個工廠函數,建立它并且要指定值并且包含一個求和的函數。在上面的例子中我們通過此函數工廠建立了兩個函數一個将參數2求和一個将參數5求和,從這我們可以看出此兩個函數都是閉包,它們共享函數的定義并且他們處在不同的環境中。

運作結果是 彈出 4 和 8

四、為何使用閉包

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

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

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

         以下是一個實際的示例:假設我們想在頁面上添加一些可以調整字号的按鈕。一種方法是以像素為機關指定

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="#" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  id="size-12">12</a>
<a href="#" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  id="size-14">14</a>
<a href="#" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  id="size-16">16</a>
           

五、用閉包模拟私有方法

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

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

下面的示例展現了如何使用閉包來定義公共函數,且其可以通路私有函數和變量。這個方式也稱為 

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 */      

         這裡有很多細節。在以往的示例中,每個閉包都有它自己的環境;而這次我們隻建立了一個環境,為三個函數所共享:

Counter.increment,

Counter.decrement

 和 

Counter.value

         該共享環境建立于一個匿名函數體内,該函數一經定義立刻執行。環境中包含兩個私有項:名為

privateCounter

 的變量和名為 

changeBy

 的函數。 這兩項都無法在匿名函數外部直接通路。必須通過匿名包裝器傳回的三個公共函數通路。

        這三個公共函數是共享同一個環境的閉包。多虧 JavaScript 的詞法範圍的作用域,它們都可以通路

privateCounter

 變量和 

changeBy

 函數。

         您應該注意到了,我們定義了一個匿名函數用于建立計數器,然後直接調用該函數,并将傳回值賦給

Counter

 變量。也可以将這個函數儲存到另一個變量中,以便建立多個計數器。

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()

 函數期間,其環境是不同的。每次調用中, 

privateCounter 中含有不同的執行個體。

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

六、在循環中建立閉包---- 一種常見的錯誤

    在 JavaScript 1.7 引入 

let

 關鍵字 之前,閉包的一個常見的問題發生于在循環中建立閉包。參考下面的示例:

<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。通過循環這三項定義,依次為每一個輸入域添加了一個 

onfocus

 事件處理函數,以便顯示幫助資訊。

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

          該問題的原因在于賦給 

onfocus

 是閉包(setupHelp)中的匿名函數而不是閉包對象;在閉包(setupHelp)中一共建立了三個匿名函數,但是它們都共享同一個環境(item)。在 

onfocus

 的回調被執行時,循環早已經完成,且此時 

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();      

           這段代碼可以如我們所期望的那樣工作。所有的回調不再共享同一個環境, 

makeHelpCallback

 函數為每一個回調建立一個新的環境。在這些環境中,

help

 指向 

helpText

 數組中對應的字元串。

七、性能

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

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

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

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}      

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

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

   在前面的兩個示例中,繼承的原型可以為所有對象共享,且不必在每一次建立對象時定義方法。

八、總結

記住閉包的作用一般有兩種,所有局部變量永駐記憶體和外包無法通路到局部變量。

繼續閱讀