閉包
語法作用域
考慮如下情況:
function init() {
var name = "Mozlilla"; // name是一個被 init 創建的區域變數
function displayName() { // displayName()是內部函式,一個閉包
alert(name); // 使用了父函式中宣告的變數
}
displayName();
}
init();
init()創建了一個區域變數name和一個名為displayName()的函式,displayName()是定義在init()里的內部函式,僅在該函式體可用,displayName()內沒有自己的區域變數,然而它可以訪問到外部函式的變數,所以displayName()可以使用父函式init()中宣告的變數name,但是,如果有同名變數name在displayName()中被定義,則會使用displayName()中定義的name,
閉包
現在來考慮如下例子:
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
運行這段代碼和之前的init()實體的效果完全一樣,其中的不同 --- 也是有意思的地方 --- 在于內部函式displayName()在執行前,被外部函式回傳,
JavaScript中的函式會形成閉包,閉包是由函式以及創建該函式的詞法環境組合而成,這個環境包含了這個閉包創建時所能訪問的所有區域變數,在我們的例子中,myFunc是執行makeFunc時創建的displayName函式實體的參考,而displayName實體仍可訪問其詞法作用域中的變數,即可以訪問到name,由此,當myFunc被呼叫時,name仍可被訪問,其值Mozilla就被傳遞到alert中,
下面是一個更有意思的實體makeAdder函式
function makeAdder(x) {
return function(y) {
return x + y;
}
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
在這個實體中,我們定義了makeAdder(x)函式,它接受了一個引數x,并回傳一個新的函式,回傳的函式接受一個引數y,并回傳x+y的值
在本質上講,makeAdder是一個函式工廠 --- 他創建了將制定的值和它的引數相加求和的函式,在上面的實體中,我們使用函式工廠創建了兩個新函式 --- 一個將其引數和5求和,另外和10求和,
add5和add10都是閉包,它們共享相同的函式定義,但是保存了不同的詞法環境,在add5環境中,x為5,而在add10中,x則為10.
實用的閉包
閉包很有用,因為它允許將函式與其所操作的某些資料(環境)關聯起來,這顯然類似于面向物件編程,在面向物件編程中,物件允許我們將某些資料(物件的屬性)與一個或者多個方法相關聯,
因此,通常你使用只有一個方法的物件的地方,都可以使用閉包,
在Web中,你想要這樣做的情況特別常見,大部分我們所寫的JavaScript代碼都是基于事件的 --- 定義某種行為,然后將其添加到用戶觸發的事件之上(比如點擊或者按鍵),我們的代碼通常作為回呼:為相應事件而執行的函式,
假如,我們想在頁面上添加一些可以調整字號的按鈕,一種方法是以像素為單位指定body元素的font-size,然后通過相對的em單位設定頁面中其它元素(例如header)的字號:
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 soze12 = makeSizer(12);
var soze14 = makeSizer(14);
var soze16 = 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="https://www.cnblogs.com/hleisurely/p/#" id="size-12">12</a>
<a href="https://www.cnblogs.com/hleisurely/p/#" id="size-14">14</a>
<a href="https://www.cnblogs.com/hleisurely/p/#" id="size-16">16</a>
用閉包模擬私有方法
編程語言中,比如Java,是支持將方法宣告為私有的,即它們只能被同一個類中的其它方法所呼叫,
而 JavaScript 沒有這種原生支持,但我們可以使用閉包來模擬私有方法,私有方法不僅僅有利于限制對代碼的訪問:還提供了管理全域命名空間的強大能力,避免非核心的方法弄亂了代碼的公共介面部分,
下面的示例展現了如何使用閉包來定義公共函式,并令其可以訪問私有函式和變數,這個方式也稱為 模塊模式(module pattern):
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,我們可以把這個函式儲存在另外一個變數makeCounter中,并用他來創建多個計數器,
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 */
請注意兩個計數器 counter1 和 counter2 是如何維護它們各自的獨立性的,每個閉包都是參考自己詞法作用域內的變數 privateCounter ,
每次呼叫其中一個計數器時,通過改變這個變數的值,會改變這個閉包的詞法環境,然而在一個閉包內對變數的修改,不會影響到另外一個閉包中的變數,
以這種方式使用閉包,提供了許多與面向物件編程相關的好處 —— 特別是資料隱藏和封裝,
在回圈中創建閉包:一個常見錯誤
在 ECMAScript 2015 引入 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();
注: onfocus:當元素獲取到焦點的時候觸發;
陣列 helpText 中定義了三個有用的提示資訊,每一個都關聯于對應的檔案中的input 的 ID,通過回圈這三項定義,依次為相應input添加了一個 onfocus 事件處理函式,以便顯示幫助資訊,
運行這段代碼后,您會發現它沒有達到想要的效果,無論焦點在哪個input上,顯示的都是關于年齡的資訊,
原因是賦值給 onfocus 的是閉包,這些閉包是由他們的函式定義和在 setupHelp 作用域中捕獲的環境所組成的,這三個閉包在回圈中被創建,但他們共享了同一個詞法作用域,在這個作用域中存在一個變數item,當onfocus的回呼執行時,item.help的值被決定,由于回圈在事件觸發之前早已執行完畢,變數物件item(被三個閉包所共享)已經指向了helpText的最后一項,
解決這個問題的一種方案是使用更多的閉包:特別是使用前面所述的函式工廠:
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 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++) {
(function() {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
})(); // 馬上把當前回圈項的item與事件回呼相關聯起來
}
}
setupHelp();
避免使用過多的閉包,可以用let關鍵詞:
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();
這個例子使用let而不是var,因此每個閉包都系結了塊作用域的變數,這意味著不再需要額外的閉包,
性能考量
如果不是某些特定任務需要使用閉包,在其它函式中創建函式是不明智的,因為閉包在處理速度和記憶體消耗方面對腳本性能具有負面影響,
例如,在創建新的物件或者類時,方法通常應該關聯于物件的原型,而不是定義到物件的構造器中,原因是這將導致每次構造器被呼叫時,方法都會被重新賦值一次(也就是,每個物件的創建),
考慮以下實體:
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;
};
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/98455.html
標籤:JavaScript
下一篇:匿名函式沒有自己的this
