javascript
面向对象的JavaScript-009-闭包
引自:https://developer.mozilla.org/cn/docs/Web/JavaScript/Closures
閉包是指能夠訪問自由變量的函數 (變量在本地使用,但在閉包中定義)。換句話說,定義在閉包中的函數可以“記憶”它被創建時候的環境。
詞法作用域
考慮如下的函數:
function init() {var name = "Mozilla";function displayName() {alert(name);}displayName(); } init();函數?init()?創建了一個局部變量?name,然后定義了名為?displayName()?的函數。displayName()?是一個內部函數——定義于?init()?之內且僅在該函數體內可用。displayName()?沒有任何自己的局部變量,然而它可以訪問到外部函數的變量,即可以使用父函數中聲明的?name?變量。
??
?
運行代碼可以發現這可以正常工作。這是詞法作用域的一個例子:在 JavaScript 中,變量的作用域是由它在源代碼中所處位置決定的(顯然如此),并且嵌套的函數可以訪問到其外層作用域中聲明的變量。
閉包
現在來考慮如下的例子:
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()?內部函數在執行前被從其外圍函數中返回了。
這段代碼看起來別扭卻能正常運行。通常,函數中的局部變量僅在函數的執行期間可用。一旦makeFunc()?執行過后,我們會很合理的認為 name 變量將不再可用。雖然代碼運行的沒問題,但實際并不是這樣的。
這個謎題的答案是?myFunc?變成一個?閉包?了。 閉包是一種特殊的對象。它由兩部分構成:函數,以及創建該函數的環境。環境由閉包創建時在作用域中的任何局部變量組成。在我們的例子中,myFunc?是一個閉包,由?displayName?函數和閉包創建時存在的 "Mozilla" 字符串形成。
下面是一個更有意思的示例 —?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 };?
在前面的兩個示例中,繼承的原型可以為所有對象共享,且不必在每一次創建對象時定義方法。參見?對象模型的細節?一章可以了解更為詳細的信息。
轉載于:https://www.cnblogs.com/shamgod/p/5644533.html
總結
以上是生活随笔為你收集整理的面向对象的JavaScript-009-闭包的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux下查看当前用户的 三个命令
- 下一篇: 复合非聚集索引里列的顺序的重要性