這是上月面試碰到的一道面試題,作為一個有着十年開發經驗七年前端經驗的大齡青年,碰到這種沒有一點鋪墊的八股文,真的只想説一句毀滅吧。
記得以前剛做前端不久的時候,就在百度搜閉包,大多搜到的結果都説的是閉包是什麼樣子的,比如説在函數外部可以訪問到函數內部的變量,又或者説閉包會導致什麼問題,比如會影響GC回收。總之沒什麼標準説法,就好像現在網上的吐槽,你背面試題沒用,得和麪試官背的同一套才行,但是天知道面試官學的是哪一套。
《JavaScript高級程序設計》作為前端的必看書籍,算是比較受認可的正經學習途徑。它給出瞭如下關於閉包的定義:
閉包指的是那些引用了另一個函數作用域中變量的函數,通常是在嵌套函數中實現的。
這裏面有兩個關鍵詞:作用域和函數。
眾所周知,JS中聲明一個函數,會在函數內部形成一個新的作用域,在這個作用域中可以訪問包含了這個函數聲明的外部作用域中的變量和函數,形成一個作用域鏈,但正常情況下,外部是訪問不到這個函數作用域內部的變量和函數的。
在函數執行時,要從作用域鏈中查找變量,以便讀、寫值。
每個函數執行時,其執行上下文中都會有一個包含其中變量的對象。全局上下文中的叫變量對象,它會在代碼執行期間始終存在;而函數局部上下文中的叫活動對象,只在函數執行期間存在。
作用域鏈其實是一個包含指針的列表,每個指針分別指向一個變量對象。
比如:
function compare(value1, value2) {
if(value1 < value2) {
return -1;
} else if(value1 > value2) {
return 1;
} else {
return 0;
}
}
let result = compare(5, 10);
- 在定義compare()函數時,就會為它創建作用域鏈,預裝載全局變量對象,並保存在內部的
[[Scope]]中; - 在調用compare()函數時,會創建相應的執行上下文,然後通過複製函數的
[[Scope]]來創建其作用域鏈;接着會創建函數的活動對象(用作變量對象)並將其推入作用域鏈的前端。
函數執行完畢後,局部活動對象會被銷燬,內部作用域會被銷燬,內存中就只剩下全局作用域。
閉包就製造了一個例外的情況,使得在函數作用域外部,也能訪問到函數內部的變量,這是因為通過某種方式(比如函數返回值)把函數內部變量的引用暴露給外部,並且外部也存在對其內部變量的引用,即函數外部存在對函數內部作用域的引用,使得函數執行完畢後,作用域也無法被銷燬。這就會影響內存回收,可能造成內存泄漏,所以建議僅在十分必要時使用。
閉包的例子:
setTimeout(() => console.log(value * 2), 1000);
前置知識,函數的參數也存在於函數的內部作用域。
這裏調用了setTimeout定時器方法,它的第一個參數是函數(或者是可以通過eval轉為函數的string),真正執行這個函數的是瀏覽器運行時,所以在setTimeout外部存在對這個函數的引用,即在setTimeout外部存在對其內部作用域的引用,生成了一個閉包,當這個函數出列被執行後,setTimeout的作用域鏈才會被銷燬。
雖然閉包有其弊端,但是也有可以利用的地方。
- 防止變量外溢,全局污染
在前端開發中,經常會使用一些三方庫,這很可能會造成與業務代碼的衝突,此時三方庫可以藉助立即執行函數(IIFE),將必要的變量通過函數返回,或者掛在某個特定的全局變量上暴露給外部,而其他內部變量則不會被訪問到,也就不會與業務代碼衝突;這在團隊協作中也很有用,防止與他人的代碼造成不必要的衝突。
- 訪問私有變量
有時我們想更規範代碼,將相關業務的代碼放在一起,此時可以通過函數來將這些代碼集合起來,這樣外部也不能輕易修改這部分代碼的變量數據,保障了數據的安全性。但有時我們需要訪問內部的私有變量,此時可以通過創建能夠訪問函數私有變量/函數的公有方法,並對外暴露此方法的引用形成閉包,來達到訪問內部私有變量的目的。
- 創建模塊
屬於對訪問私有變量的一種擴展使用。一個模塊通常用對象來表示,就是一些變量和一些函數的集合,通過對象的屬性來訪問這些變量和函數。
模塊可能有一些初始化操作,和一些不想暴露給外部的私有變量/函數,就可以通過立即執行函數,將這些操作和變量/函數限制在內部作用域,在執行後返回一個模塊對象,在這個對象上可以暴露一些公有屬性和公有方法。