在理解閉包之前,需要先來了解幾個概念,上下文、作用域鏈、活動對象、變量對象:
- 上下文:函數的上下文決定了他們可以訪問哪些數據,以及他們的行為。全局上下文是最外層的上下文,當代碼執行流進入到函數時,函數的上下文被推到上下文棧上,當函數執行完之後,上下文棧會彈出該函數上下文。
- 作用域鏈:上下文中代碼執行的時候會創建作用域鏈,它決定了各級上下文中代碼訪問變量或函數的順序。代碼正在執行的上下文變量對象總是位於作用域鏈最頂端,然後是包含上下文對象,然後是下一個包含上下文對象,直至到全局上下文對象為止。
- 活動對象:函數上下文中,包含其中變量的對象。
- 變量對象:全局上下文中,包含其中變量的對象。
我們來總結一下,在調用一個函數時,它的作用域鏈都保存了哪些對象?
- 在函數被調用時,先創建一個執行上下文,並創建一個作用域鏈。
- argument和其他命名參數初始化該函數的活動對象。
- 外部函數的活動對象是該函數作用域鏈的第二個對象,該作用域鏈一直向外部串起所有的包含該函數的活動對象,知道全局上下文才終止。
下面聲明並調用了一個方法compare(),現在來梳理一下該方法從創建到執行的過程:
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
let result = compare(5, 6);
1.執行全局代碼,創建全局執行上下文,全局上下文被壓入執行上下文棧
ECStack = [//執行上下文棧
globalContext
];
2.全局上下文變量即變量對象初始化,初始化的同時,compare 函數被創建,創建作用域鏈,並在內部屬性[[scope]]中預裝載全局變量對象。
globalContext = {//變量對象初始化
VO: [global],
Scope: [globalContext.VO],
this: globalContext.VO
}
compare.[[scope]] = [//compare在作用域鏈預裝載全局變量對象
globalContext.VO
];
3.執行 compare 函數,創建 compare 函數執行上下文,compare 函數執行上下文被壓入執行上下文棧
ECStack = [//執行上下文棧
comapreContext,
globalContext
];
4.comapre 函數執行上下文初始化併為變量賦值:
1)複製函數 [[scope]] 屬性創建作用域鏈,
2)用 arguments 創建活動對象,
3)初始化活動對象,即加入形參、函數聲明、變量聲明,
4)將活動對象壓入 comapre 作用域鏈頂端。
comapreContext = {
AO: {
arguments: {
0:5
1:6
length: 2
},
scope: undefined,
},
Scope: [AO, globalContext.VO],
}
5.執行代碼,函數執行完畢後返回,並將函數comapre的執行上下文從執行上下文棧中彈出。
ECStack = [
globalContext
];
compare()方法的作用域鏈如下圖:
作用域鏈實際上是一個包含指針的列表,每個指針分別指向一個變量對象,但是物理上不會包含相應的對象。
閉包
《JavaScript高級編程》: 閉包是指那些引用了另一個函數作用域中變量的函數,通常是在嵌套函數中實現的。
MDN--閉包:一個函數和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起(或者説函數被引用包圍),這樣的組合就是閉包(closure)。
定義略顯抽象,借用阮一峯老師的理解:閉包就是能夠讀取其他函數內部變量的函數。 由於在Javascript語言中,只有函數內部的子函數才能讀取局部變量,因此可以把閉包簡單理解成"定義在一個函數內部的函數"。所以,在本質上,閉包就是將函數內部和函數外部連接起來的一座橋樑。
var winVar = 'window-小白';
function fun() {
var funVar = 'fun-小白';
console.log(winVar)//window-小白
console.log(funVar)//fun-小白
}
fun();
console.log(funVar)//Uncaught ReferenceError: funVar is not defined
函數作用域鏈如下圖:當函數在作用域鏈頂端找不到winVar對象,就會去全局變量對象中尋找,而在winodow中,無法訪問到fun()函數的變量,因此需要使用閉包。
閉包可以用在許多地方。它的最大用處有兩個,一個是可以讀取函數內部的變量,另一個就是讓這些變量的值始終保持在內存中。
function fun() {
var funVar = 'fun-小白';
return function () {
return funVar;
}
}
let variable = fun();
let result = variable();
console.log(result)//fun-小白
函數的作用域鏈如下圖,在fun方法返回匿名函數後,匿名函數的作用域鏈被初始化為包含fun的活動對象和全局變量對象,雖然fun()函數執行結束後,其執行上下文的作用域鏈會銷燬,但是在匿名函數的作用域鏈中,仍然有對他的引用。這樣應該就不難理解為什麼閉包可以讀取函數內部的變量,也可以讓變量的值始終保持在內存中了。
返回的匿名函數被保存在variable方法中,把variable()方法設置為null可以解除對函數的引用,從而讓垃圾回收程序將內存釋放掉。 variable = null;
同時因為閉包會保留包含它們的函數作用域,所以比其他函數更佔內存,過度使用閉包會導致內存過度佔用,所以不能濫用閉包,否則會造成網頁的性能問題。
參考資料:
- MDN--閉包
- 阮一峯的網絡日誌
- JavaScript深入之作用域鏈
- JavaScript深入之閉包
- 《JavaScript高級編程(第四版)》