閉包
函數和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起構成閉包(closure)。也就是說,閉包可以讓你從內部函數訪問外部函數作用域。在 JavaScript 中,每當函數被創建,就會在函數生成時生成閉包。
由于閉包的概念比較抽象,所以本篇幅會有較多的主觀理解。
在作用域相關的內容中可以知道,全局下的作用域想訪問一個函數內部的作用域是辦不到的,但是 閉包
的特性可以突破這一限制。
每個函數都會形成一個閉包。
1. 什么是閉包
閉包可以理解成,保留了函數作用域鏈的一個環境。
var fn = function() {
var number = 0;
};
fn();
console.log(number);
這個例子是訪問不到 number
的,想訪問到就可以借助閉包的特性。
var fn = function() {
var number = 0;
return function() {
number++;
console.log(number);
};
};
var increment = fn();
increment();
increment();
increment();
這里的 fn
函數返回了一個函數,在這個返回的函數所形成的閉包環境就擁有訪問上一層作用域的能力,所以每次在調用 fn
返回的函數時,就可以累加 number
。
借助一個函數形成的閉包環境作為跳板,來訪問另一個函數的作用域,就是閉包最常見的使用場景。
網絡上有許多文獻把閉包稱為能訪問其他函數內部變量的函數
,這樣理解可能更容易一些。
函數用到了上層作用域的變量,所以這些變量會在內存中被保留,不會被釋放,一些舊的瀏覽器在內存管理上沒有現代瀏覽器完善,大量的閉包可能會導致頁面卡頓,不過通常業務開發,會先考慮效果,再考慮性能。
2. 閉包的應用
2.1 模擬私有屬性
在 JavaScript
中是沒有私有屬性特性的,利用閉包來隱藏變量,就可以模擬出私有屬性的效果。
var counter = (function() {
var count = 0;
return {
increment: function() {
count++;
return count;
},
zero: function() {
count = 0;
return count;
},
get value() {
return count;
},
};
})();
counter.increment();
console.log(counter.value); // 輸出:1
counter.increment();
console.log(counter.value); // 輸出:2
console.log(counter.count); // 輸出:undefined
這里的自執行匿名函數返回一個對象,對象中的方法就具有訪問上層函數中的變量的能力,所以他們都能訪問 count。
因為 count 不會被釋放,所以可以當作一個屬性來使用。
2.2 回調函數幾乎都用到了閉包的特性
回調函數通常會用到上層作用域的變量,然后在某一情況下進行調用。
var fn = function(cb) {
console.log('異步操作開始');
setTimeout(function() {
console.log('異步操作結束');
cb();
}, 1000);
};
var obj = {
flag: false,
};
fn(function() {
obj.flag = true;
console.log(obj);
});
很明顯,這里的回調函數就是用到了閉包的特性。
所以閉包其實很常用,結合日常的這些場景能更好的理解閉包。
3. 小結
每個函數都有閉包,閉包可以訪問到這個函數所在的上層作用域,利用這一特性,就能訪問到一個函數作用域下的變量。
大量的閉包可能會造成性能問題,不過現在的計算機處理器、內存已經讓開發者不太需要關注這方面的問題,但在設計一個會被大量應用的庫和框架時,應當做這方面的考慮,因為用戶的環境千變萬化。