JavaScript深入系列第八篇荠锭,介紹理論上的閉包和實踐上的閉包配乱,以及從作用域鏈的角度解析經(jīng)典的閉包題排抬。
定義
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 不就是構(gòu)成了一個閉包嘛……
還真是這樣的爬泥!
所以在《JavaScript權(quán)威指南》中就講到:從技術(shù)的角度講柬讨,所有的JavaScript函數(shù)都是閉包。
咦袍啡,這怎么跟我們平時看到的講到的閉包不一樣呢2裙佟?
別著急境输,這是理論上的閉包蔗牡,其實還有一個實踐角度上的閉包,讓我們看看湯姆大叔翻譯的關(guān)于閉包的文章中的定義:
ECMAScript中嗅剖,閉包指的是:
- 從理論角度:所有的函數(shù)辩越。因為它們都在創(chuàng)建的時候就將上層上下文的數(shù)據(jù)保存起來了。哪怕是簡單的全局變量也是如此窗悯,因為函數(shù)中訪問全局變量就相當于是在訪問自由變量区匣,這個時候使用最外層的作用域。
- 從實踐角度:以下函數(shù)才算是閉包:
- 即使創(chuàng)建它的上下文已經(jīng)銷毀,它仍然存在(比如亏钩,內(nèi)部函數(shù)從父函數(shù)中返回)
- 在代碼中引用了自由變量
接下來就來講講實踐上的閉包莲绰。
分析
讓我們先寫個例子,例子依然是來自《JavaScript權(quán)威指南》姑丑,稍微做點改動:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();
首先我們要分析一下這段代碼中執(zhí)行上下文棧和執(zhí)行上下文的變化情況蛤签。
另一個與這段代碼相似的例子,在《JavaScript深入之執(zhí)行上下文》中有著非常詳細的分析栅哀。如果看不懂以下的執(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 值呢?
以上的代碼荒揣,要是轉(zhuǎn)換成 PHP篷角,就會報錯,因為在 PHP 中系任,f 函數(shù)只能讀取到自己作用域和全局作用域里的值恳蹲,所以讀不到 checkscope 下的 scope 值。(這段我問的PHP同事……)
然而 JavaScript 卻是可以的俩滥!
當我們了解了具體的執(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)了閉包這個概念掷倔。
所以眉孩,讓我們再看一遍實踐角度上閉包的定義:
- 即使創(chuàng)建它的上下文已經(jīng)銷毀,它仍然存在(比如勒葱,內(nèi)部函數(shù)從父函數(shù)中返回)
- 在代碼中引用了自由變量
在這里再補充一個《JavaScript權(quán)威指南》英文原版對閉包的定義:
This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.
閉包在計算機科學中也只是一個普通的概念浪汪,大家不要去想得太復雜。
必刷題
接下來凛虽,看這道刷題必刷死遭,面試必考的閉包題:
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,所以打印的結(jié)果就是 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]
}
匿名函數(shù)執(zhí)行上下文的 AO 為:
匿名函數(shù)Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}
data[0]Context 的 AO 并沒有 i 值,所以會沿著作用域鏈從匿名函數(shù) Context.AO 中查找夭苗,這時候就會找 i 為 0信卡,找到了就不會往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值為3)题造,所以打印的結(jié)果就是 0傍菇。
data[1] 和 data[2] 是一樣的道理。
作者:冴羽
github:https://github.com/mqyqingfeng/Blog
掘金主頁:https://juejin.im/user/58e4b9b261ff4b006b3227f4
segmentfault主頁:https://segmentfault.com/u/yayu/articles
Vicky丶Amor 經(jīng)授權(quán)轉(zhuǎn)載界赔,版權(quán)歸原作者所有丢习。
求關(guān)注,求點贊