對於前端人員面試,出現頻率最多也是讓人最頭疼的就是面試官說:“請簡單談一談你對閉包的理解”。對於這一個幾乎快被人問爛的問題,屢屢出現在我們面試或被面試的過程中的原因很簡單--我們一直都在接觸閉包,卻很少去正確地對待它。 因為閉包是因為JS的一些語言特性而形成的,所以在談它之前我們首先要瞭解一下的知識 ...
對於前端人員面試,出現頻率最多也是讓人最頭疼的就是面試官說:“請簡單談一談你對閉包的理解”。對於這一個幾乎快被人問爛的問題,屢屢出現在我們面試或被面試的過程中的原因很簡單--我們一直都在接觸閉包,卻很少去正確地對待它。
因為閉包是因為JS的一些語言特性而形成的,所以在談它之前我們首先要瞭解一下的知識點
1.執行上下文
2.作用域
3.垃圾回收機制
4.函數嵌套
本文只會簡單的談涉及到的內容,如果知識點有遺漏的同學可以自行google,接下來讓我們進入正題!
1. 什麼是閉包?
關於什麼是閉包讓我們先看看《高級程式設計》和《JavaScript權威指南》中的說法:
《高程》: 閉包是指有權訪問另一個函數作用域中的變數的函數,創建閉包的常見方式,就是在一個函數內部創建另一個函數。
《權威指南》:和其他大多數現代編程語言一樣,JavaScript也採用詞法作用域,也就是說,函數的執行依賴於變數作用域,這個作用域是在函數定義時決定的,而不是函數調用時決定的。為了實現這種詞法作用域,JavaScript函數對象的內部狀態不僅包含函數的代碼邏輯,還必須引用當前的作用域鏈。函數對象可以通過作用域鏈互相關聯起來,函數體內部的變數都可以保存在函數作用域內,這種特性在電腦科學文獻中稱為“閉包”。
比較兩種說法《高程》中的說法太過抽象,我比較傾向於《權威指南》中的說法,現在讓我們剝繭抽絲,一步一步的解釋這種說法。
2. 解釋閉包,掃蕩閉包理解過程中產生的知識點
我們知道“JavaScript中沒有塊級作用域”,所謂“塊”,也就是大括弧“{}”中間的語句(但是在ES6中已經引入塊級作用域,這裡不做討論).我們還知道在JavaScript中,在函數裡面定義的變數,可以在函數裡面被訪問,但是在函數外無法訪問,這也就形成了函數作用域,即如下代碼所示
var i = 1;
if(true){
var j = 2;
}
console.log(i,j) // 1 2
function test(){
var z = 3;
}
test();
console.log(z); //Uncaught ReferenceError: z is not defined(…)
又因為函數是可以嵌套的,所以函數A裡面定義的函數B也能函數A內部聲明的變數,即如下代碼所示
function func() {
var num = 10;
function sub() {
console.log(num)
}
sub();
}
func(); //10
因為一層層的函數嵌套就形成了作用域鏈的概念,,但是如果我們僅僅只是這樣去理解或者解釋作用域或者作用域鏈就有些太過膚淺了,接下來,我們再在此基礎上添加執行上下文以及垃圾回收機制再去深入理解他們,先讓我們來看下麵的代碼
1 var a = 1, b= 1;
2 function func() {
3
4 var c = 10,
5 a = 10;
6
7 console.log(a);
8
9 function sub() {
10 var a = 100,
11 d = 100
12 console.log(a)
13 }
14 sub();
15 }
16 console.log(a); //1
17 func(); // 10 \n 100
可以從開始來瞭解一下這一段代碼的執行過程:
-
在載入程式時,已經確定了全局上下文環境,並隨著程式的執行而對變數就行賦值。
-
程式執行到第16行,調用console.log(a); 控制台輸出 1
-
程式執行到第17行,調用func(),生成此次調用func函數時的上下文環境,壓棧,並將此上下文環境設置為活動狀態
-
程式執行到第7行,調用console.log(a); 因為在此上下文環境中 a = 10, 控制台輸出 10
-
程式執行到第14行,調用sub(),生成此次調用sub函數的上下文環境,壓棧,並設置為活動狀態
-
程式執行到第12行,調用console.log(a); 因為在此上下文環境中 a = 100, 控制台輸出 100
-
程式執行到第14行,調用sub()結束,sub函數上下文環境被銷毀,回到func函數上下文環境,變為活動狀態。
-
程式執行到第17行,調用func()結束,func函數上下文環境被銷毀,全局上下文環境又回到活動狀態
-
over
寫到這裡先說一下,以上所說的和我們經常所說的閉包都沒有關係。。。>_< 別打臉!
但是!!不可避免的可以說,以上所說的都是閉包。讓我們反過來,看看閉包的定義:
《高程》:閉包是指有權訪問另一個函數作用域中的變數的函數,創建閉包的常見方式,就是在一個函數內部創建另一個函數,這不就是我們剛剛的函數嵌套嗎?
《權威指南》:和其他大多數現代編程語言一樣,JavaScript也採用詞法作用域,也就是說,函數的執行依賴於變數作用域,這個作用域是在函數定義時決定的,而不是函數調用時決定的。為了實現這種詞法作用域,JavaScript函數對象的內部狀態不僅包含函數的代碼邏輯,還必須引用當前的作用域鏈。函數對象可以通過作用域鏈互相關聯起來,函數體內部的變數都可以保存在函數作用域內,這種特性在電腦科學文獻中稱為“閉包”。 這不就是我們剛剛研究的作用域鏈嗎?
所有說,閉包的概念早就在我們的心中了,佛曰:不可說,不可說。。。
認真總結上面的知識點,並且在權威指南的定義中還給我們鞏固了另一個很重要的知識點:函數的執行依賴於變數作用域,這個作用域是在函數定義時決定的,而不是函數調用時決定的 , 所以當我們每次看到函數的調用環境和定義的環境不相同的時候是一定要謹慎謹慎再謹慎啊!
3. 閉包應用的場景
都說兩天腿的人好找,三條腿的蛤蟆難尋。既然閉包這麼好理解,那為啥老是出現在各種面試寶典中呢?其實這個很簡單,總會有基因突變的蛤蟆不是麽?>_<
因為JavaScript的另一大特性-函數是複雜數據類型的一種,所以就可以被返回,被傳遞,這就讓閉包進行了基因突變。
1. 函數作為返回值
function func(){
var max = 10;
return function(){
consloe.log(max++);
}
}
var f = func();
var f1 = func();
f(); // 10
f(); // 11
f1();//10
我們很多時候都會想當然的根據一句話去定義一個問題如:“函數調用完成之後,其執行上下文環境就會被銷毀”,但是在上面的代碼中,函數調用完成之後,其執行上下文環境不會接著被銷毀,並且連續執行兩次func會產生兩個func執行上下文環境
2.函數作為參數被傳遞
var max = 10;
function func(){
console.log(max);
}
function f1(fn){
var max =1000;
fn();
}
f1(func); //10
這就是我們上文所說的函數的執行依賴於變數作用域,這個作用域是在函數定義時決定的,而不是函數調用時決定的,雖然被作為參數傳遞到函數f1內部被調用,但是他的上一級作用域依然是全局作用域。
好吧,先寫到這吧,水水更健康。。。。