簡介
閉包closure是javascript中一個非常強大的功能。所謂閉包就是函式中的函式,內部函式可以訪問外部函式的作用域範圍,從而可以使用閉包來做一些比較強大的工作。
今天將會給大家詳細介紹一下閉包。
函式中的函式
我們提到了函式中的函式可以訪問父函式作用域範圍的變數,我們看一個例子:
function parentFunction() {
var address = 'flydean.com';
function alertAddress() {
alert(address);
}
alertAddress();
}
parentFunction();
上面的例子中,我們在parentFunction中定義了一個變數address,在parentFunction內部定義了一個alertAddress方法,在該方法內部訪問外部函式中定義的address變數。
上面程式碼執行是沒問題的,可以正確的訪問到資料。
Closure閉包
函式中的函式有了,那麼什麼是閉包呢?
我們看下面的例子:
function parentFunction() {
var address = 'flydean.com';
function alertAddress() {
alert(address);
}
return alertAddress;
}
var myFunc = parentFunction();
myFunc();
這個例子和第一個例子很類似,不同之處就是我們將內部函式返回了,並且賦值給了myFunc。
接下來我們直接呼叫了myFunc。
myFunc中訪問了parentFunction中的address變數,雖然parentFunction已經執行完畢返回。
但是我們在呼叫myFunc的時候,任然可以訪問到address變數。這就是閉包。
閉包的這個特性非常擁有,我們可以使用閉包來生成function factory,如下所示:
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
其中add5和add10都是閉包,他們是由makeAdder這個function factory建立出來的。通過傳遞不同的x引數,我們得到了不同的基數的add方法。
最終生成了兩個不同的add方法。
使用function factory的概念,我們可以考慮一個閉包的實際應用,比如我們在頁面上有三個button,通過點選這些button可實現修改字型的功能。
我們可以先通過function factory來生成三個方法:
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
有了這三個方法,我們把DOM元素和callback方法繫結起來:
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
使用閉包實現private方法
對比java來說,java中有private訪問描述符,通過private,我們可以指定方法只在class內部訪問。
當然,在JS中並沒有這個東西,但是我們可以使用閉包來達到同樣的效果。
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()); // 0.
counter.increment();
counter.increment();
console.log(counter.value()); // 2.
counter.decrement();
console.log(counter.value()); // 1.
我們在父function中定義了privateCounter屬性和changeBy方法,但是這些方法只能夠在內部function中訪問。
我們通過閉包的概念,將這些屬性和方法封裝起來,暴露給外部使用,最終達到了私有變數和方法封裝的效果。
閉包的Scope Chain
對於每個閉包來說,都有一個作用域範圍,包括函式本身的作用域,父函式的作用域和全域性的作用域。
如果我們在函式內部嵌入了新的函式,那麼就會形成一個作用域鏈,我們叫做scope chain。
看下面的一個例子:
// global scope
var e = 10;
function sum(a){
return function(b){
return function(c){
// outer functions scope
return function(d){
// local scope
return a + b + c + d + e;
}
}
}
}
console.log(sum(1)(2)(3)(4)); // log 20
閉包常見的問題
第一個常見的問題就是在迴圈遍歷中使用閉包,我們看一個例子:
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();
上面的例子中,我們建立了一個setupHelp函式,setupHelp中,onfocus方法被賦予了一個閉包,所以閉包中的item可以訪問到外部function中定義的item變數。
因為在迴圈裡面賦值,所以我們實際上建立了3個閉包,但是這3個閉包共享的是同一個外部函式的作用域範圍。
我們的本意是,不同的id觸發不同的help訊息。但是如果我們真正執行就會發現,不管是哪一個id,最終的訊息都是最後一個。
因為onfocus是在閉包建立完畢之後才會觸發,這個時候item的值實際上是變化的,在迴圈結束之後,item的值已經指向了最後一個元素,所以全部顯示的是最後一條資料的help訊息。
怎麼解決這個問題呢?
最簡單的辦法使用ES6中引入的let描述符,從而將item定義為block的作用域範圍,每次迴圈都會建立一個新的item,從而保持閉包中的item的值不變。
for (let i = 0; i < helpText.length; i++) {
let item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
還有一種方法,就是再建立一個閉包:
function makeHelpCallback(help) {
return function() {
showHelp(help);
};
}
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
}
這裡用到了之前我們提到的function factory的概念,我們為不同的閉包建立了不同的作用域環境。
還有一種方法就是將item包含在一個新的function作用域範圍之內,從而每次建立都是新的item,這個和let的原理是相似的:
for (var i = 0; i < helpText.length; i++) {
(function() {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
})();
}
第二個常見的問題就是記憶體洩露。
function parentFunction(paramA)
{
var a = paramA;
function childFunction()
{
return a + 2;
}
return childFunction();
}
上面的例子中,childFunction引用了parentFunction的變數a。只要childFunction還在被使用,a就無法被釋放,從而導致parentFunction無法被垃圾回收。
閉包效能的問題
我們定義了一個物件,並且通過閉包來訪問其私有屬性:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
上面的物件會有什麼問題呢?
上面物件的問題就在於,對於每一個new出來的物件,getName和getMessage方法都會被複制一份,一方面是內容的冗餘,另一方面是效能的影響。
通常來說,我們將物件的方法定義在prototype上面:
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;
};
注意,我們不要直接重寫整個prototype,這樣會導致未知的錯誤,我們只需要根據需要重寫特定的方法即可。
總結
閉包是JS中非常強大和有用的概念,希望大家能夠喜歡。
本文作者:flydean程式那些事
本文連結:http://www.flydean.com/js-closure/
本文來源:flydean的部落格
歡迎關注我的公眾號:「程式那些事」最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!