對於 JavaScript 的初學者來講,閉包的概念和應用均可以算的上是難點。 MDN 的 JavaScript 文檔對閉包的概念給出了準確的定義,也提供了簡單直觀的的實例,是一個很是好的學習材料。 這篇文章將從文檔出發,對閉包的知識點進行一個簡單的梳理。html
首先,咱們須要對閉包提供一個準確的定義。 在文檔中,閉包的定義是 'A closure is the combination of a function and the lexical environment within which that function was declared'。這個定義是很拗口的一句話。 詞法環境(lexical environment)這個描述對於初學者來講過於學術和抽象,咱們只須要記住就好。真正理解定義最好方式就是經過實際的代碼。 假設:閉包
function init() { var name = "Hello"; // name 是一個被 init 建立的局部變量 function displayName() { // displayName() 是內部函數,一個閉包 alert(name); // 使用了父函數中聲明的變量 } displayName(); } init();
經過以上的代碼塊來看,咱們能夠看到閉包實際上指的就是一個’擁有外部環境變量的函數‘。 在上面的例子中函數 displayName 調用了不屬於自己的外部變量 name,無論此 displayName 函數最終是否被返回,實際上由 name 和 displayName 組成的閉包已經造成。app
function init() { var name = "Hello"; // name 是一個被 init 建立的局部變量 function displayName() { // displayName() 是內部函數,一個閉包 alert(name); // 使用了父函數中聲明的變量 } return displayName(); // 閉包被返回 } var fun = init(); fun();
咱們再來看一下這一塊的新的代碼,惟一的區別在於這個代碼中 函數init 返回了一個函數 displayName()。也就是返回了一個閉包。經過這個返回的閉包,咱們就能夠訪問這個函數所相關聯的詞法環境或者說數據。原本應該被銷燬的 name 變量保留了下來,並且只能經過調用閉包的方式來訪問,這也就是私有性。函數
實際上在上一個例子中,咱們已經看到了閉包的做用,閉包能夠用來模擬私有變量和方法。 它讓函數和函數所操做的某些數據(環境)關聯了起來。學習
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 */
在上面的例子中 咱們能夠看到變量 privateCounter 和 函數 changeBy 做爲下面三個函數共同的詞法環境造成了閉包。 在 makeCounter()執行以後, 本該消失的詞法環境被保留下來,只能經過返回的三個函數進行更改和訪問。這種行爲模擬出了相似 JAVA 類中的私有變量和私有方法。code
在 ECMAScript 2015 引入 let 這個關鍵字以前,在循環中有一個常見的閉包建立問題。htm
<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();
這段代碼的結果就是,不管你選擇哪個輸入框,helper 信息永遠都會顯示 Your age (you must be over 16)'。 緣由就在於在返回的三個閉包實際上共享了 item 這一個詞法環境,因此 helper 永遠只顯示爲最後 age 的 helper。 這裏就是閉包裏另外一個很重要的知識點,閉包只會捕獲自由變量的引用,因此當 item 指向的helpText值最後變爲 age 時,三個閉包的中的 item 也都變成了 age。 根據這一點咱們能夠將代碼修改以下ip
function setupHelpAnonymous(){ 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]; (function() { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } })(); } } setupHelpAnonymous()
在上面的代碼片斷中 咱們使用了一個 IIFE (當即執行函數表達式) 對 item 這個引用進行了馬上求值。這樣咱們就能獲得想要的結果。而在ES6中的 ’塊級做用域‘ 也能夠解決這個問題。作用域
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++) { let item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } } setupHelp();
每一次循環,都有一個新的 item 被建立,三個閉包再也不共享同一個詞法環境;相比匿名閉包的方式,也沒有建立多餘的閉包。rem