本文一共 1300 字,讀完只需 5 分鐘
概述
閉包
, 可以說是每個前端工程師都聽說的一個詞,咋一看很難從字面上去理解,從而給人留下了閉包
是一個重要又難以理解的概念。
但是,閉包在 JS 程式碼可以說是隨處可見,閉包也只是計算機領域的一個概念而已,它的存在是因為 JS 的一些語言特性,比如:函式式語言
,執行上下文
,執行上下文棧
,作用域鏈
,詞法作用域
。
執行上下文:Execution Context
執行上下文棧:Execution Context Stack
作用域鏈:Scope Chain
作用域:Scope
本篇文章,將先給結論,到底什麼是閉包,再來分析產生閉包的過程和原因。
一、什麼是閉包
當函式記住並訪問所在詞法作用域的自由變數時,就產生了閉包,即使函式是在當前詞法作用域外執行。 --《你不知道的 JavaScript》
來段經典的閉包程式碼:
function outter() {
var a = 123;
function inner() {
console.log(a);
}
return inner;
}
var foo = outter();
foo(); // 123
複製程式碼
內部函式 inner 記住了它被定義時的詞法作用域,也就是 outter 的函式作用域,並訪問了該作用域裡的自由變數 a, 同時,inner 函式作用返回值,在外部作用域中被執行。
以上描述,全部符合閉包的描述,那這就是閉包
。
二、執行過程
之前的文章講了函式的執行上下文棧,變數物件,作用域鏈等內容,接下來通過閉包程式碼回顧程式碼是怎麼樣的執行過程。
function outter() {
var a = 123;
function inner() {
console.log(a);
}
return inner;
}
var foo = outter();
foo(); // 123
複製程式碼
- 進入全域性程式碼的執行上下文,全域性上下文被壓入執行上下文棧。
ECStack = [
globalContext
];
複製程式碼
- 全域性上下文建立全域性變數物件,建立 this 並指向全域性上下文。
globalContext = {
VO: global,
scope: [global.VO],
this: global
}
複製程式碼
- 全域性上下文初始化時,outter 函式被建立,建立作用域鏈,複製 Scope 屬性到 outter 函式的內部屬性[[scope]]
outter.[[scope]] = [
globalContext.VO
];
複製程式碼
- 執行 outter 函式,建立 outter 函式執行上下文,將 outter 上下文壓入執行上下文棧。
ECStack = [
globalContext,
outterContext
];
複製程式碼
- 初始化 outter 函式執行上下文,用 arguments 建立活動物件,加入形參、函式宣告、變數宣告。將活動物件壓入 outter 作用域鏈頂端。
outterContext = {
AO: {
arguments: {
a: undefined,
}
length: 1
},
scope: undefined,
inner: reference to function inner(){}
Scope: [AO, globalContext.VO],
this: undefined
}
複製程式碼
- outter 執行完畢,接著執行 outter 返回的被變數引用的函式 inner;
ECStack = [
globalContext,
innerContext
];
複製程式碼
- inner 函式初始化,過程和第4步一樣。
innerContext = {
AO: {
arguments: {
length: 0
}
},
Scope: [AO, outterContext.AO, globalContext.VO],
this: undefined
}
複製程式碼
- inner 執行,沿著作用域鏈查詢變數 a, 列印 a 值。
- inner 函式執行結束,彈出執行上下文棧。
ECStack = [
globalContext
];
複製程式碼
在這個過程中,第 5 步,outter 已經執行結束,執行上下文按理來說已經被銷燬,內部函式 inner 怎麼還能訪問 outter 作用域的變數呢。
正是由於閉包,inner 引用了它所在詞法作用域的自由變數 a,inner 的作用域鏈中仍然是完整的, 儘管 inner 在其他地方執行,還是返回了正確結果。
三、函式式語言
閉包中,一個很重要的特點就是,內部函式作為一個資料被返回。這是由於 JS 是函式式語言,函式可以作為引數傳遞進函式,也可以作為一個資料返回。函式的巢狀構成了作用域的巢狀,也就有了作用域鏈。
由於函式具有作用域,且變數的尋找具有 “遮蔽效應”(從內到外,找到第一個就停止),使得區域性作用域的變數對於外部作用域是不可見的,於是函式就有了封閉性,所以我們拿函式來包裹封裝私有變數,同時也有了閉包。
四、自由變數
自由變數是指在函式中使用的,但既不是函式引數也不是函式的區域性變數的變數。
function outter() {
var a = 123;
function inner() {
console.log(a);
}
return inner;
}
var foo = outter();
foo(); // 123
複製程式碼
對於 inner 函式而言,變數 a, 不是它的函式引數,也不是它的區域性變數,a 就是自由變數。
五、閉包的用處和缺點
從閉包的特點可以看出,自由變數儲存在了記憶體中,並能間接訪問。
那麼閉包的作用就是:
隱藏私有變數,解決變數名稱空間汙染的問題。
缺點
如果閉包過多,變數常駐記憶體,肯定會佔用大量記憶體空間。
總結
由於 JS 是函式式語言,當函式記住並訪問所在詞法作用域的自由變數時,就產生了閉包,即使函式是在當前詞法作用域外執行。
閉包在 JS 程式碼中非常常見,不必把它想得太玄乎。
歡迎關注我的個人公眾號“謝南波”,專注分享原創文章。