理論上的定義
MDN 對閉包的定義為:
閉包是指那些能夠訪問自由變量的函數(shù)塔猾。
那什么是自由變量呢莺琳?
自由變量是指在函數(shù)中使用的茂契,但既不是函數(shù)參數(shù)也不是函數(shù)的局部變量的變量饮亏。
由此,我們可以看出閉包共有兩部分組成:
閉包 = 函數(shù) + 函數(shù)能夠訪問的自由變量
舉個例子:
var a = 1;
function foo() {
console.log(a);
}
foo();
foo 函數(shù)可以訪問變量 a忠藤,但是 a 既不是 foo 函數(shù)的局部變量挟伙,也不是 foo 函數(shù)的參數(shù),所以 a 就是自由變量模孩。
那么尖阔,函數(shù) foo + foo 函數(shù)訪問的自由變量 a 不就是構成了一個閉包嘛……
所以在《JavaScript權威指南》中就講到:從技術的角度講贮缅,所有的JavaScript函數(shù)都是閉包。
別急這是理論上的閉包介却,我們還有一個實踐角度上的閉包谴供,讓我們看看湯姆大叔翻譯的關于閉包的文章中的定義:
實踐角度上的閉包
ECMAScript中,閉包指的是:
1.從理論角度:所有的函數(shù)齿坷。因為它們都在創(chuàng)建的時候就將上層上下文的數(shù)據(jù)保存起來了桂肌。哪怕是簡單的全局變量也是如此,因為函數(shù)中訪問全局變量就相當于是在訪問自由變量胃夏,這個時候使用最外層的作用域轴或。
2.從實踐角度:以下函數(shù)才算是閉包:
即使創(chuàng)建它的上下文已經(jīng)銷毀,它仍然存在(比如仰禀,內(nèi)部函數(shù)從父函數(shù)中返回)
在代碼中引用了自由變量
分析
舉個例子:
var scope = "global scope";
function checkscope () {
var scope = "local scope";
function f() {
return scope;
}
return f;
}
var foo = checkscope();
foo();
首先我們要分析一下這段代碼中執(zhí)行上下文棧和執(zhí)行上下文的變化情況蚕愤。
- 進入全局代碼答恶,創(chuàng)建全局執(zhí)行上下文,全局執(zhí)行上下文壓入執(zhí)行上下文棧
- 全局執(zhí)行上下文初始化
- 執(zhí)行 checkscope 函數(shù)萍诱,創(chuàng)建 checkscope 函數(shù)執(zhí)行上下文悬嗓,checkscope 執(zhí)行上下文被壓入執(zhí)行上下文棧
- checkscope 執(zhí)行上下文初始化,創(chuàng)建變量對象裕坊、作用域鏈包竹、this等
- checkscope 函數(shù)執(zhí)行完畢,checkscope 執(zhí)行上下文從執(zhí)行上下文棧中彈出
- 執(zhí)行 f 函數(shù)籍凝,創(chuàng)建 f 函數(shù)執(zhí)行上下文周瞎,f 執(zhí)行上下文被壓入執(zhí)行上下文棧
- f 執(zhí)行上下文初始化,創(chuàng)建變量對象饵蒂、作用域鏈声诸、this等
- f 函數(shù)執(zhí)行完畢,f 函數(shù)上下文從執(zhí)行上下文棧中彈出
了解到這個過程退盯,我們應該思考一個問題彼乌,那就是:
當 f 函數(shù)執(zhí)行的時候,checkscope 函數(shù)上下文已經(jīng)被銷毀了啊(即從執(zhí)行上下文棧中被彈出)渊迁,怎么還會讀取到 checkscope 作用域下的 scope 值呢慰照?
當我們了解了具體的執(zhí)行過程后,我們知道 f 執(zhí)行上下文維護了一個作用域鏈:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
對的琉朽,就是因為這個作用域鏈毒租,f 函數(shù)依然可以讀取到 checkscopeContext.AO 的值,說明當 f 函數(shù)引用了 checkscopeContext.AO 中的值的時候漓骚,即使 checkscopeContext 被銷毀了蝌衔,但是 JavaScript 依然會讓 checkscopeContext.AO 活在內(nèi)存中榛泛,f 函數(shù)依然可以通過 f 函數(shù)的作用域鏈找到它,正是因為 JavaScript 做到了這一點噩斟,從而實現(xiàn)了閉包這個概念曹锨。
所以,讓我們再看一遍實踐角度上閉包的定義:
1.即使創(chuàng)建它的上下文已經(jīng)銷毀剃允,它仍然存在(比如沛简,內(nèi)部函數(shù)從父函數(shù)中返回)
2.在代碼中引用了自由變量
必刷題
接下來,看這道刷題必刷斥废,面試必考的閉包題:
var data = [];
for(var i=0; i<3; i++) {
data[i] = function() {
console.log(i);
}
}
data[0]();
data[1]();
data[2]();
答案是都是 3椒楣,讓我們分析一下原因:
當執(zhí)到data[0]函數(shù)之前,此時全局上下VO為:
globalContext = {
vo:{
data:[...],
i:3
}
}
當執(zhí)行 data[0] 函數(shù)的時候牡肉,data[0] 函數(shù)的作用域鏈為:
data[0]Context = {
Scope: [AO, globalContext.VO]
}
data[0]Context 的 AO 并沒有 i 值捧灰,所以會從 globalContext.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]();
當執(zhí)行到 data[0] 函數(shù)之前饲窿,此時全局上下文的 VO 為:
globalContext = {
VO: {
data: [...],
i: 3
}
}
跟沒改之前一模一樣煌寇。
當執(zhí)行 data[0] 函數(shù)的時候,data[0] 函數(shù)的作用域鏈發(fā)生了改變:
data[0]Context = {
Scope: [AO, 匿名函數(shù)Context.AO globalContext.VO]
}
( AO 表示活動對象逾雄,儲存了函數(shù)的參數(shù)阀溶、函數(shù)內(nèi)聲明的變量等)
匿名函數(shù)執(zhí)行上下文的AO為:
匿名函數(shù)Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}
ata[0]Context 的 AO 并沒有 i 值,所以會沿著作用域鏈從匿名函數(shù) Context.AO 中查找鸦泳,這時候就會找 i 為 0银锻,找到了就不會往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值為3)辽故,所以打印的結果就是0徒仓。
data[1] 和 data[2] 是一樣的道理。
彩蛋
問題1:
為什么
globalContext = {
VO: {
data: [...],
i: 3
}
}
i為3 不是0誊垢,1掉弛,2
答:
當執(zhí)行到data[0]函數(shù)的時候,for循環(huán)已經(jīng)執(zhí)行完了喂走,i是全局變量殃饿,此時的值為3,舉個例子:
for (var i = 0; i < 3; i++) {}
console.log(i) // 3
for 循環(huán)結束后
data[0] = function(){console.log(i)}
data[1] = function(){console.log(i)}
data[2] = function(){console.log(i)}
執(zhí)行data[0] ()芋肠,data[1] ()乎芳,data[2] ()時,i=3,所以都打印3
繼續(xù)努力 加油。
原文詳見:
JavaScript深入之閉包