深刻學習js系列是本身階段性成長的見證,但願經過文章的形式更加嚴謹、客觀地梳理js的相關知識,也但願可以幫助更多的前端開發的朋友解決問題,期待咱們的共同進步。javascript
若是以爲本系列不錯,歡迎點贊、評論、轉發,您的支持就是我堅持的最大動力。html
閉包(closure)是 Javascript 語言的一個難點,也是它的特點,不少高級應用都要依靠閉包實現。前端
要理解閉包,首先必須理解 Javascript 特殊的變量做用域。java
變量的做用域無非就是兩種:全局變量和局部變量。git
MDN 對於閉包的定義爲:github
閉包是指那些可以訪問自由變量的函數。面試
那麼什麼是自由變量呢?數組
自由變量是指在函數中使用的,但既不是函數的參數也不是函數的局部變量的變量。微信
由此,咱們能夠看出來閉包有兩部分組成:閉包
閉包 = 函數+函數可以訪問的自由變量。
舉一個例子:
var a = 1;
function foo() {
console.log(a);
}
foo();
複製代碼
foo 函數能夠訪問變量 a 可是 a 既不是 foo 函數的局部變量,也不是 foo 函數的參數, 因此 a 就是自由變量。
那麼,函數 foo + foo 函數訪問的自由變量 a 就已經構成了一個閉包……
還真的是這樣!
因此在《JavaScript 權威指南》中就講到:從技術的角度講,全部的 JavaScript 函數都是閉包。
上面所說的是理論角度的閉包,其實還有一種實踐角度的閉包,讓咱們看看湯姆大叔翻譯的關於閉包的文章中的定義:
ECMAScript 中閉包指的是:
一、從理論角度:全部的函數,由於它們都在建立的時候就將上層上下文的數據保存下來了,哪怕是簡單的全局變量也是如此,由於函數中訪問全局變量就至關因而在訪問自由變量,這個時候使用最外層的做用域。 二、從實踐角度:如下函數纔算是閉包:
- 一、即便建立它的上下文已經銷燬,它仍然存在(好比,內部函數從父函數中返回)。
- 二、在代碼中引用自由變量。
接下來就來聊聊實踐上閉包。
讓咱們來先看一個例子:
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() {
return scope;
}
return f;
}
var foo = checkscope();
foo();
複製代碼
首先咱們要分析一下這段代碼中執行上下文棧和執行上下文的變化。
這裏給出簡要的執行過程:
一、進入全局代碼,建立全局執行上下文,全局上下文壓入執行上下文棧。
二、全局執行上下文初始化。
三、執行 checkscope 函數,建立 checkscope函數執行上下文,checkscope 執行上下文被壓入執行上下文棧。
四、checkscope 執行上下文初始化,建立變量對象、做用域鏈、this 等。
五、checkscope 函數執行完畢,checkscope 執行上下文從執行上下文棧彈出。
六、執行 f 函數,建立 f 函數的執行上下文,f 執行上下文被壓入執行上下文棧
七、f 執行上下文初始化,建立變量對象、做用域鏈、this 等
八、f 函數執行完畢,f 函數上下文從執行上下文棧中彈出
複製代碼
瞭解到這個過程,咱們應該思考一個問題,那就是:
當 f 函數執行的時候,checkscope 函數上下文已經被銷燬了啊(即已經從執行上下文棧中被彈出),怎麼還會讀取到 checkscope 做用域下的 scope 值呢?
咱們瞭解了具體的執行過程後,咱們知道 f 執行上下文維護了一個做用域鏈:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO]
};
複製代碼
對的!,就是由於這個做用域鏈,f 函數依然能夠讀取到checkscopeContext.AO
的值,說明當 f 函數引用了 checkscopeContext.AO
中的值的時候,即便 checkscopeContext 被銷燬了,可是 JavaScript 依然會讓 checkscopeContext.AO 活在內存中,f 函數依然能夠經過 f 函數的做用域鏈找到它,正是由於 JavaScript 作到了這一點,從而實現了閉包這個概念。
因此,讓咱們再看一遍實踐角度上閉包的定義:
一、即便建立它的上下文已經銷燬,它仍然存在(好比,內部函數從父函數中返回) 二、在代碼中引用了自由變量
接下來,看這道刷題必刷。面試必考的閉包題目:
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function() {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
複製代碼
答案都是 3,讓咱們分析一下緣由:
當執行到 data[0] 函數以前,此時全局上下文 VO 爲:
globalContext = {
VO:{
data:[...],
i:3
}
}
複製代碼
當執行 data[0] 函數的時候,data[0] 函數的做用域鏈爲:
data[0]Context = {
Scope:[AO.globalContext.VO]
}
複製代碼
data[0]Context 的 AO 中並無 i 的值,因此會從 globalConetxt.VO 中查找 i 爲 3, 因此打印的結果就是 3,data[1]和 data[2]是同樣的道理。
因此讓咱們改爲閉包看一下
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function(i) {
return function() {
console.log(i);
};
})(i);
}
data[0]();
data[1]();
data[2]();
複製代碼
當指向到 data[0]函數以前,此時全局執行上下文的 VO 爲:
globalContext:{
VO:{
dataL[...],
i:3
}
}
複製代碼
跟沒改變以前同樣,
當執行 data[0]函數的時候,data[0]函數的做用域鏈發生了改變:
data[0]Context = {
Scope:[AO,匿名函數Context.VO globalContext.VO]
}
複製代碼
匿名函數執行上下文的 AO 爲:
匿名函數Context:{
AO:{
arguments:{
0:0,
length:1
},
i = 0
}
}
複製代碼
data[0]Context 的 AO 並無 i 的值,因此會沿着做用域鏈 從 匿名函數 Context.AO中查找,這個時候就會找 i 爲 0,找到了就不會往 globalContext.VO 中查找了,即便 globalContext.VO也有 i 的值(值爲 3),因此打印的結果就是 0,
data[1] 和 data[2] 是同樣的道理。
歡迎添加個人我的微信討論技術和個體成長。