做爲一個 JavaScript 語言的開發者,提起閉包確定不會感到陌生,那麼到底什麼纔是閉包哪?javascript
閉包不是什麼新奇的概念,它早在高級語言開始發展的年代就產生了。閉包(Closure)是詞法閉包的簡稱。對閉包的具體定義有不少種說法,這些說法大致能夠分爲兩類:java
這兩種定義在某種意義上是對立的,一個認爲閉包是函數,另外一個認爲閉包是函數和引用環境組成的總體。很明顯第二種說法更確切一些,閉包只是在形式和表現上像函數,但實際上不是函數。函數是一些可執行的代碼,這些代碼在函數被定義後就肯定了,不會在執行時發生變化,因此一個函數只有一個實例。閉包在運行時能夠有多個實例,不一樣的引用環境和相同的函數組合能夠產生不一樣的實例。所謂引用環境是指在程序執行中的某個點全部處於活躍狀態的約束組成的集合。其中的約束是指一個變量的名字和其所表明的對象之間的聯繫。linux
在支持嵌套做用域的語言中,有時不能簡單直接的肯定函數的引用環境。這樣的語言通常具備這樣的特性:設計模式
JavaScript 閉包的源自兩點,詞法做用域和函數當作值傳遞。數組
做用域是查找變量時的一些規則。詞法做用域就是定義在詞法階段的做用域。或者換句話說,詞法做用域是由你書寫代碼時將變量和塊做用域寫在哪裏來決定的。按照代碼書寫時的樣子,內部函數能夠順着做用域鏈一層一層地查找、訪問函數外的變量,或者咱們叫它自由變量。瀏覽器
函數當作值傳遞,也就是上面所說的函數是一等公民。函數內部的自由變量是在外層函數執行時建立的,外層函數執行完之後,這些變量理應被銷燬,可是若是將內層函數做爲返回值返回,這些自由變量就被保存了下來。並且沒法訪問,必須經過內層函數來訪問。原本執行過程和詞法做用域是封閉的,將內層函數做爲返回值返回就提供了一種訪問自由變量的方式。閉包
一個函數如何能封閉外部狀態哪?當外部狀態的scope失效的時候,還有一份留在內部狀態裏面。在執行過程當中,返回函數,或者將函數得以保留下來,而且函數中有自由變量就會造成閉包。一個函數中沒有自由變量時,引用環境不會發生變化。函數
知道了什麼是閉包,也理解了閉包的本質,下面能夠了解下閉包的幾種應用,或許你在平常的開發中已經用到很多了。ui
// 將計算的結果保存在 sum 中
function add(init) {
var sum = init;
return function getSum(number) {
sum += number;
return sum;
}
}
複製代碼
// 延遲計算
function add(init) {
var sum = init;
var args = [];
return function getSum() {
// 當參數到達必定的數量時再進行運算
args = args.concat(Array.from(arguments));
if(args.length > 5) {
for(let i = 0; i < args.length; i++) {
sum += args[i];
}
return sum;
}
}
}
複製代碼
img 對象常常用於進行數據上報,可是經過查詢後臺的記錄能夠得知,由於一些低版本的瀏覽器的實現可能存在 bug,在這些瀏覽器下使用 report 函數進行數據上報會丟失 30% 左右的數據,也就是說,report 函數並非每一次都發起了 HTTP 請求。丟失數據的緣由是 img 是 report 函數中的局部變量,當 report 函數調用結束後, img 局部變量隨即被銷燬,而此時或許還沒來得及發出 HTTP 請求,因此這次請求就會丟失掉。spa
// 這種方法會丟失 30% 左右的數據
var report = function (src) {
var img = new Image();
img.src = src;
};
// 把 img 變量封裝起來,就能夠解決請求丟失的問題
var report = (function(){
var imgs = [];
return function(src) {
var img = new Image();
imgs.push(img);
img.src = src;
}
})();
複製代碼
有時,你想強制程序與數據的交互方式,以便保護其完整性。經過是使用閉包,徹底能夠作到這一點。建立此類接口的一種常見方法就是從函數返回對象。這時,定義在原函數中的數據只能由返回對象上定義的方法訪問,下面是一個例子:
function makeCalendar(name) {
var calendar = {
owner: name,
events: [],
};
return {
addEvent: function(event, dateString) {
var eventInfo = {
event: event,
date: new Date(dateString),
};
calendar.events.push(eventInfo);
calendar.events.sort(function(a, b) {
return a.date - b.date;
});
},
listEvents: function() {
if (calendar.events.length > 0) {
console.log(calendar.owner + "'s events are: ");
calendar.events.forEach(function(eventInfo) {
var dateStr = eventInfo.date.toLocaleDateString();
var description = dateStr + ": " + eventInfo.event;
console.log(description);
});
} else {
console.log(calendar.owner + " has no events.");
}
},
};
}
複製代碼
局部變量原本應該在函數退出的時候被解除引用,但若是局部變量被封閉在閉包造成的環境中,那麼這個局部變量就會一直存在。在這個意義上看,閉包的確會使一些數據沒法被及時銷燬。使用閉包的一部分緣由是咱們選擇主動把一些變量封閉在閉包中,由於可能在之後還須要使用這些變量,把這些變量放在閉包中和放在全局做用域,對內存方面的影響是一致的。若是在未來須要回收這些變量的時候,能夠手動把這些變量設置爲 null。