原生JS之還原閉包的本質

查小小飛發表於2019-11-01

剝離它天生的驕傲,還原閉包的本質。

程式碼中的閉包

先不去解釋閉包這個名詞,看一段JS程式碼:

訪問函式中的變數

原生JS之還原閉包的本質

基本常識,你無法直接訪問函式體中的變數,如果你知道JS中的作用域規則的話。

我們修改程式碼後,再次訪問函式中的變數

原生JS之還原閉包的本質
結果是可以訪問到這個函式中的變數。

這看上去沒有什麼新的內容,如果你瞭解JS的一些基本概念和語法,這再簡單不過了,但是,這裡面第二段程式碼裡面就已經有了閉包,你只是不知道而已,實際上就算你不知道閉包,仍然可以去寫程式碼,完成你的需求,而不是看到一個需求,想:我要用閉包。你按照需求寫程式碼,就能夠寫出閉包,雖然你不知道自己寫的就是閉包,那麼第二段程式碼中的閉包是什麼呢?

程式碼中的閉包

原生JS之還原閉包的本質
程式碼中的變數name 和 函式 returnName 就構成了一個閉包。好了,你到目前為止不需要知道什麼是閉包,不需要知道為什麼上面兩個變數就構成了閉包,你只需要知道和理解的是,這兩段程式碼的執行原理和執行結果就可以了。如果不知道,就關掉網頁,重頭學。

閉包的定義

在電腦科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函式閉包(function closures),是引用了自由變數的函式。這個被引用的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函式和與其相關的引用環境組合而成的實體。閉包在執行時可以有多個例項,不同的引用環境和相同的函式組合可以產生不同的例項。—— 維基百科

函式與對其狀態即詞法環境(lexical environment)的引用共同構成閉包(closure)。也就是說,閉包可以讓你從內部函式訪問外部函式作用域。在JavaScript,函式在每次建立時生成閉包。—— MDN

我個人覺得這些解釋很好,但如果你是剛剛接觸JS,那麼就不太接受,下面我用自己的理解說一下對閉包的理解。 你需要知道的幾個前置知識:

  • JS的作用域及作用域鏈
  • 函式作用域
  • 變數的生命週期

在上面的兩段程式碼中,第一段程式碼函式外部是無法訪問函式內部的變數的(var定義的),要想訪問到這個變數,那麼需要在函式中再定義一個函式,這樣,這個“子函式”就能夠訪問到“父函式”裡面的變數了。為了能夠在外面使用到這個函式,就需要將這個“子函式”作為“父函式”的返回值return 出來給外面使用。這樣就構成了一個閉包,不要急,你或許會迷糊,這和沒解釋一樣啊,這些我都知道啊,是的,你不要懷疑你自己,你已經知道了什麼是閉包,但你不知道那就是閉包。(哈哈哈)。

可以給閉包一個最簡潔的定義:

閉包 = 函式 + 被引用的自由變數

在第二段程式碼中,函式是 returnName ,這個函式引用的自由變數是 name

其實,你只要理解了作用域和作用域鏈,就能夠理解閉包,只是,你不會也不需要刻意寫閉包而已。

閉包的作用

1.訪問函式內部的變數

其實很多所謂的閉包的許多作用只是這兩個的延申,比如模擬私有方法,你按照自己的需求寫出了程式碼,只不過寫的“恰巧”實現了閉包。

2.延長變數的生命週期

變數一般在其環境消失後,變數隨之銷燬,無法訪問到了。所以當函式執行完畢後,函式裡面的變數就會隨之銷燬。函式被呼叫時,才會產生一個執行上下文(環境), 但是如果該變數被引用了,則該變數就不會被銷燬,閉包就是引用了外部的變數的函式,這樣這個外部變數就不會被銷燬。

(以下程式碼及文字摘自阮一峰部落格) 看程式碼:

原生JS之還原閉包的本質
在這段程式碼中,result實際上就是閉包f2函式。它一共執行了兩次,第一次的值是999,第二次的值是1000。這證明了,函式f1中的區域性變數n一直儲存在記憶體中,並沒有在f1呼叫後被自動清除。

為什麼會這樣呢?原因就在於f1f2的父函式,而f2被賦給了一個全域性變數,這導致f2始終在記憶體中,而f2的存在依賴於f1,因此f1也始終在記憶體中,不會在呼叫結束後,被垃圾回收機制(garbage collection)回收。

這段程式碼中另一個值得注意的地方,就是"nAdd=function(){n+=1}"這一行,首先在nAdd前面沒有使用var關鍵字,因此nAdd是一個全域性變數,而不是區域性變數。其次,nAdd的值是一個匿名函式(anonymous function),而這個匿名函式本身也是一個閉包,所以nAdd相當於是一個setter,可以在函式外部對函式內部的區域性變數進行操作。

一些注意的點

1)由於閉包會使得函式中的變數都被儲存在記憶體中,記憶體消耗很大,所以不能濫用閉包,否則會造成網頁的效能問題,在IE中可能導致記憶體洩露。解決方法是,在退出函式之前,將不使用的區域性變數全部刪除。

特別要注意的是,閉包不會導致記憶體洩漏的問題,那是IE瀏覽器的問題,不是閉包的問題。

2)閉包會在父函式外部,改變父函式內部變數的值。所以,如果你把父函式當作物件(object)使用,把閉包當作它的公用方法(Public Method),把內部變數當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函式內部變數的值。

一些廢話

在剛剛開始學習JavaScript的時候,會碰到各種技術名詞,其中“閉包”應該算是最最響亮的一個了,我可能是完美主義,學習技術的時候總會力求“全面”,會研究每一個技術名詞,概念,以為只有這樣才算掌握了知識, 之到我接觸到“閉包”後,我發現我很多學習方法都是不科學的,因為技術概念和名詞都是技術人對某種現象,某些特性的一共歸納總結,是馬後炮式的歸納總結,如果我總是去想搞清楚這些概念到底是什麼意思,是沒有必要的,很多時候,其實你已經掌握了,但你自己未必知道,閉包就是這樣一個名詞。忘記各種名詞和概念,學知識本身,而不是死摳概念,這是我的一點感悟。

相關文章