javascript深入理解-從作用域鏈理解閉包
一、概要
紅寶書(P178)對於閉包的定義:閉包就是有權訪問另外一個函數作用域中變量的函數。
MDN,對於閉包的定義:閉包就是指能夠訪問自由變量的函數。
那麽什麽是自由變量?自由變量就是在函數中使用,但既不是函數參數arguments,也不是函數的局部變量的變量,就是說另外一個函數作用域中的變量。
文章首發地址:https://www.mwcxs.top/page/573.html
閉包組成?閉包 = 函數 + 函數能夠訪問的變量
二、分析
舉個栗子:
var a = 1;
function foo() {
console.log(a);
}
foo();
foo函數可以訪問到變量a,但是a並不是foo函數的局部變量,也不是foo函數的參數,所以a就是自由變量,那麽函數foo+foo函數可以訪問自由變量a不就是構成了一個閉包嘛。
我們再來看一個栗子:
function getOuter(){
var date = ‘1127‘;
function getDate(str){
console.log(str + date); //訪問外部的date
}
return getDate(‘今天是:‘); //"今天是:1127"
}
getOuter();
其中date不是函數getDate的參數,也不是局部變量,所以date是自由變量。
總結起來就是兩點:
1、是一個函數(比如:內部函數從父函數中返回)
2、能夠訪問上級函數作用域中的變量(哪怕上級函數的上下文已經銷毀)
然後我們再來看一個栗子(來自《JavaScript權威指南》)來分析:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope(); // foo指向函數f
foo();
這時候需要我們來分析一下這段代碼中執行上下文棧和執行上下文的變化情況。
簡要的分析一下執行過程:
1、進入全局代碼,創建全局執行上下文,全局執行上下文壓入執行上下文棧;
2、全局執行上下文初始化;
3、執行checkscope函數,創建sheckscope函數執行上下文,checkscope執行上下文被壓入執行上下文棧;
4、checkscope執行上下文初始化,創建變量對象,作用域鏈,this等;
5、checkscope函數執行完畢,checkscope執行上下文從執行上下文棧中彈出;
6、執行f函數,創建f函數執行上下文,f執行上下文壓入執行上下文棧;
7、f執行上下文初始化,創建變量對象,作用域鏈,this等;
8、f函數執行完畢,f函數上下文從執行上下文棧中彈出
那麽問題來了,函數f執行的時候,checkscope函數上下文已經被銷毀了,那函數f是如何取到scope變量的?
答:函數f執行上下文維護了一盒作用域鏈,作用域鏈會指向checkscope作用域,作用域鏈是一個數組,結構如下:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
所以指向關系:當前作用域-->checkscope作用域-->全局作用域,即使checkscopeContext被銷毀了,但是js依然會讓checkscopeContext.AO(活動對象)繼續存在內存中,f函數依然可以通過f函數的作用域鏈找到它,這就是閉包的關鍵。
註:AO 表示活動對象,儲存了函數的參數、函數內聲明的變量等
三、概念
上面分析介紹的是閉包的實踐角度,其實閉包有很多種介紹,說法不一。
湯姆大叔在關於閉包對的文章的定義。ECMAScript中,閉包指的是:
1、從理論角度:所有的函數,因為它們都是創建的時候就將上層上下文的數據保存起來了。哪怕是簡單的全局變量也是如此,因為函數中訪問全局變量就是相當於再訪問自由變量,這個時候使用最外層的作用域。
2、從實踐角度:以下函數才算閉包:
(1)即使創建它的上下文已經摧毀了,它依然存在(比如,內部函數中返回)
(2)在代碼中引用了自由變量
四、面試必刷的題
var data = []; for (var i = 0; i < 3; i++) { data[i] = function () { console.log(i); }; } data[0](); data[1](); data[2]();
如果知道是閉包,答案很明顯,都是3。
循環結束後,全局執行上下文的VO是
globalContext = {
VO: {
data: [...],
i: 3
}
}
執行data[0]函數的時候,data[0]函數的作用域鏈為:
data[0]Context = {
Scope: [AO, globalContext.VO]
}
由於其自身沒有i變量,就會向上查找,所有從全局上下文查找到i為3,data[1] 和 data[2] 是一樣的。
註:
1、for 循環不會創建一個執行上下文,所有不會有 AO, i 的值是在全局對象的 AO 中,代碼初始的時候為:
globalContext = {
VO: {
data: [...],
i: 0
}
}
2、data[0] 是一個函數名,data[0]() 表示執行這個函數,當執行函數的時候,循環已經走完了;
3、函數能夠讀取到的值跟函數定義的位置有關,跟執行的位置無關。
解決辦法:
改成閉包,方法就是data[i]返回一個函數,並且訪問變量i
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0](); // 0
data[1](); // 1
data[2](); // 2
循環結束後的全局執行上下文沒有變化。
執行 data[0] 函數的時候,data[0] 函數的作用域鏈發生了改變:
data[0]Context = {
Scope: [AO, 匿名函數Context.AO, 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
五、思考題
把for循環中的var i = 0
,改成let i = 0
。結果是什麽,為什麽?
var data = [];
for (let i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
或者這樣:
var data = [];
var _loop = function _loop(i) {
data[i] = function () {
console.log(i);
};
};
for (var i = 0; i < 3; i++) {
_loop(i);
}
data[0]();
data[1]();
data[2]();
六、參考
https://github.com/mqyqingfeng/Blog/issues/9
javascript深入理解-從作用域鏈理解閉包