前端:閉包
轉自:https://juejin.cn/post/7085560639317344269#heading-6
什麼是閉包?
- ✅ 官方說法:閉包就是指有權訪問另一個函式作用域中的變數的函式。
- ✅ MDN說法:閉包是一種特殊的物件。它由兩部分構成:函式,以及建立該函式的環境。環境由閉包建立時在作用域中的任何區域性變數組成。
- ✅ 自我理解:把函式執行,形成私有上下文,並且儲存和保護私有變數的機制,稱之為“閉包” ,它是一種機制。
函式的執行看閉包
過程
- 形成私有上下文
- 進棧執行
- 一些列操作
- 初始化作用域鏈(兩頭<當前作用域,上級作用域>)
- 初始化this
- 初始化arguments
- 賦值形參
- 變數提升
- 程式碼執行
- 遇到變數就先看是否是自己私有的,不是自己私有的按照作用域鏈上查詢,如果不是上級的就繼續線上查詢,,直到 EC(G),變數的查詢其實就是一個作用域鏈的拼接過程,拼接查詢的鏈式就是作用域鏈。
- 正常情況下,程式碼執行完成之後,私有上下文出棧被回收。但是遇到特殊情況,如果當前私有上下文執行完成之後中的某個東西被執行上下文以外的東西佔用,則當前私有上下文就不會出棧釋放,也就是形成了不被銷燬的上下文,閉包。
三種情況
- 當前上下文的某些東西被上下文以外的某些東西佔用,那麼當前上下文就不會被釋放。
- 如果沒有被佔用就是執行完成之後就被釋放。
- 除了這上面兩種情況還有一種情況是,上下文沒有被佔用,但是要緊接著被用一次,這樣沒有用完之前是不能釋放的,用完在釋放,這樣就形成了一個臨時不被釋放 )
函式每一次執行 都是從新形成一個全新的私有上下文,和之前執行產生的上下文沒有必然的聯絡
閉包的作用
函式執行會形成全新的私有上下文,這個上下文可能被釋放,也可能不被釋放,不論是否被釋放,它的作用是:
- 保護:劃分一個獨立的程式碼執行區域,在這個區域中有自己私有變數儲存的空間,而用到的私有變數和其它區域中的變數不會有任何的衝突(防止全域性變數汙染)
- 儲存:如果上下文不被銷燬,那麼儲存的私有變數的值也不會被銷燬,可以被其下級上下文中調取使用
市面上一般認為只有形成的私有上下文不被釋放,才算是閉包(因為如果一但釋放,之前的東西也就不存在了);還有人認為,只有一下級上下文用到了此上下文中的動西才算閉包;
練習題
let x = 5; const fn = function fn(x) { return function(y) { console.log(y + (++x)); } }
let f = fn(6);
f(7); // 14
fn(8)(9); // 18
f(10); // 18
console.log(x); // 5
複製程式碼
- 變數提升(跳過,沒有var、function...)
- 程式碼執行
-
x -> 5
-
fn -> 0x 001
-
fn (6) 函式執行
- 建立私有上下文,建立活動物件 AO(fn1)
- 初始化作用域鏈,<<EC(fn1), EC(G)>>
- 初始化 this
- 初始化 arguments
- 形參賦值
- 變數提升
- 函式執行,返回一個小函式,為小函式建立新的堆記憶體儲存函式內容(作用域,程式碼字串,鍵值對)
-
f = 返回的小函式,也就是新的函式執行,f -> 0x 002,由於0x 002 被 f 佔用,所用0x 002 不能被釋放,所以它的上下文(作用域)也不能被釋放。
-
f(7) 函式執行
- 建立私有上下文,建立活動物件 AO(f1)
- 初始化作用域鏈,<<EC(f1), EC(fn1)>>
- 初始化 this
- 初始化 arguments
- 形參賦值
- 變數提升
- 函式執行,y = 7,x 當前作用域沒有,沿著作用域鏈往上找,所以在 EC(fn1) 中查詢 x = 6, y +(++ x) = 14
-
fn(8) 函式執行
- 建立私有上下文,建立活動物件 AO(fn2)
- 初始化作用域鏈,<<EC(fn2), EC(G)>>
- 初始化 this
- 初始化 arguments
- 形參賦值
- 變數提升
- 函式執行,返回一個小函式,為小函式建立新的堆記憶體儲存函式內容(作用域,程式碼字串,鍵值對)
-
fn(8)(9) 函式執行
- 建立私有上下文,建立活動物件 AO(f2)
- 初始化作用域鏈,<<EC(f2), EC(fn2)>>
- 初始化 this
- 初始化 arguments
- 形參賦值
- 變數提升
- 函式執行,y = 9,x 當前作用域沒有,沿著作用域鏈往上找,所以在 EC(fn2) 中查詢 x = 8, y +(++ x) = 18
-
f(10) 函式執行
- 建立私有上下文,建立活動物件 AO(f3)
- 初始化作用域鏈,<<EC(f3), EC(fn1)>>
- 初始化 this
- 初始化 arguments
- 形參賦值
- 變數提升
- 函式執行,y = 10,x 當前作用域沒有,沿著作用域鏈往上找,所以在 EC(fn1) 中查詢 x = 7, y +(++ x) = 18
-
下面子看一個例子,看大家能不能自己跟著上一個例子的思路來解出答案了。
let a = 0,
b = 0;
let A = function(a) {
A = function (b) {
console.log(a + b++);
}
console.log(a);
}
A(1); // 1
A(2); // 3
複製程式碼
面試中如何回答閉包問題
其實在面試中存在很多的閉包問題,筆試、問答都有可能,並且面試可能不會直接問閉包,問的是其他問題,但是可能會用到閉包。
在面試中如果是如上練習題的筆試或者看程式碼說結果的問題,那可參照上面的思路,這類題目不論多麼複雜,萬變不離其宗。
如果在面試中是問答,不要只會回答閉包的概念。還要學會從理論、底層執行機制、實踐等角度來回答閉包,不僅能看出你基礎能力的深度,也能看出你基礎能力的廣度。 在我之前的文章 “四說閉包” 驚豔面試官|8月更文挑戰中有較為深入的理解
- 先說閉包是什麼?
- 在說函式的建立和執行看閉包(引述:堆疊、EC、AO、VO、scope)
- 然後說閉包的作用以及在專案中的引用場景,以及帶來的問題
- 最後說閉包引發的高階程式設計技巧,在框架原始碼中的使用,或者自己寫類庫的怎麼使用
總結
閉包不管是日常的研發過程中還是面試中都是一個常見的場景,深入理解必將讓你在日常開發和麵試中如魚得水。