徹底理解js中的閉包

dovlie發表於2017-07-29

閉包是js的一個難點也是它的一個特色,是我們必須掌握的js高階特性,那麼什麼是閉包呢?它又有什麼用呢?

我們都知道,js的作用域分兩種,全域性和區域性,基於我們所熟悉的作用域鏈相關知識,我們知道在js作用域環境中訪問變數的權利是由內向外的,內部作用域可以獲得當前作用域下的變數並且可以獲得當前包含當前作用域的外層作用域下的變數,反之則不能,也就是說在外層作用域下無法獲取內層作用域下的變數,同樣在不同的函式作用域中也是不能相互訪問彼此變數的,那麼我們想在一個函式內部也有限權訪問另一個函式內部的變數該怎麼辦呢?閉包就是用來解決這一需求的,閉包的本質就是在一個函式內部建立另一個函式。

 

我們首先知道閉包有3個特性:

①函式巢狀函式

②函式內部可以引用函式外部的引數和變數

③引數和變數不會被垃圾回收機制回收

 

本文我們以閉包兩種的主要形式來學習

 

①函式作為返回值

在這段程式碼中,a()中的返回值是一個匿名函式,這個函式在a()作用域內部,所以它可以獲取a()作用域下變數name的值,將這個值作為返回值賦給全域性作用域下的變數b,實現了在全域性變數下獲取到區域性變數中的變數的值

再來看一個閉包的經典例子

一般情況下,在函式fn執行完後,就應該連同它裡面的變數一同被銷燬,但是在這個例子中,匿名函式作為fn的返回值被賦值給了fn1,這時候相當於fn1=function(){var n = 0 ... },並且匿名函式內部引用著fn裡的變數num,所以變數num無法被銷燬,而變數n是每次被呼叫時新建立的,所以每次fn1執行完後它就把屬於自己的變數連同自己一起銷燬,於是乎最後就剩下孤零零的num,於是這裡就產生了記憶體消耗的問題

 

再來看一個經典例子-定時器與閉包

寫一個for迴圈,讓它按順序列印出當前迴圈次數

按照預期它應該依次輸出1 2 3 4 5,而結果它輸出了五次5,這是為什麼呢?原來由於js是單執行緒的,所以在執行for迴圈的時候定時器setTimeout被安排到任務佇列中排隊等待執行,而在等待過程中for迴圈就已經在執行,等到setTimeout可以執行的時候,for迴圈已經結束,i的值也已經程式設計5,所以列印出來五個5,那麼我們為了實現預期結果應該怎麼改這段程式碼呢?(ps:如果把for迴圈裡面的var變成let,也能實現預期結果)

 

引入閉包來儲存變數i,將setTimeout放入立即執行函式中,將for迴圈中的迴圈值i作為引數傳遞,100毫秒後同時列印出1 2 3 4 5

那如果我們想實現每隔100毫秒分別依次輸出數字,又該怎麼改呢?

在這段程式碼中,相當於同時啟動3個定時器,i*100是為4個定時器分別設定了不同的時間,同時啟動,但是執行時間不同,每個定時器間隔都是100毫秒,實現了每隔100毫秒就執行一次列印的效果。

 

②閉包作為引數傳遞

在這段程式碼中,函式fn1作為引數傳入立即執行函式中,在執行到fn2(30)的時候,30作為引數傳入fn1中,這時候if(x>num)中的num取的並不是立即執行函式中的num,而是取建立函式的作用域中的num這裡函式建立的作用域是全域性作用域下,所以num取的是全域性作用域中的值15,即30>15,列印30

 

最後總結一下閉包的好處與壞處

好處

①保護函式內的變數安全 ,實現封裝,防止變數流入其他環境發生命名衝突

②在記憶體中維持一個變數,可以做快取(但使用多了同時也是一項缺點,消耗記憶體)

③匿名自執行函式可以減少記憶體消耗

壞處

①其中一點上面已經有體現了,就是被引用的私有變數不能被銷燬,增大了記憶體消耗,造成記憶體洩漏,解決方法是可以在使用完變數後手動為它賦值為null;

②其次由於閉包涉及跨域訪問,所以會導致效能損失,我們可以通過把跨作用域變數儲存在區域性變數中,然後直接訪問區域性變數,來減輕對執行速度的影響

 

 

相關文章