閉包,是真的美

發表於2018-04-11

歡迎評論和star

寫這篇文章時的心情是十分忐忑的,因為對於我們今天的主角:閉包,很多小夥伴都寫過關於它的文章,相信大家也讀過不少,那些文章到底有沒有把JS中這個近似神話的東西講清楚,說實心裡,真的有,但為數不多。

寫這篇文章的初衷:讓所有看到這篇文章的小夥伴都徹徹底底的理解閉包 => 提高JS水平 => 能夠寫出更高質量的JS程式碼。

開文之所以說心情是忐忑的,就是怕達不到我寫該文的初衷,但是我有信心同時我也會努力的完成我的目標。如行文中有絲毫誤人子弟的陳述,歡迎大家指正,在這感激不盡。

我們開始吧:

相信眾多JS的lovers都聽說過這句話:閉包很重要但是很難理解

我起初也是這麼覺得,但是當我努力學習了JS的一些深層的原理以後我倒覺得閉包並不是那麼不好理解,反倒是讓我感覺出一種很美的感覺。當我徹底理解閉包的那一剎那,心中油然產生一種十分愉悅感覺,就像**”酒酣尚醉,花未全開”**那種美景一樣。

撥開閉包神祕的面紗

我們先看一個閉包的例子:

大家肯定都寫過類似的程式碼,相信很多小夥伴也知道這段程式碼應用了閉包,but, Why does the closure be generated and Where is closure?

來,我們慢慢分析:

首先必須先知道閉包是什麼,才能分析出閉包為什麼產生和閉包到底在哪?

當一個函式能夠記住並訪問到其所在的詞法作用域及作用域鏈,特別強調是在其定義的作用域外進行的訪問,此時該函式和其上層執行上下文共同構成閉包。

需要明確的幾點:

  1. 閉包一定是函式物件(wintercn大大的閉包考證
  2. 閉包和詞法作用域,作用域鏈,垃圾回收機制息息相關
  3. 當函式一定是在其定義的作用域外進行的訪問時,才產生閉包
  4. 閉包是由該函式和其上層執行上下文共同構成(這點稍後我會說明)

閉包是什麼,我們說清楚了,下面我們看下閉包是如何產生的。

接下來,我預設你已經讀過我之前的兩篇文章 原來JavaScript內部是這樣執行的徹底搞懂JavaScript作用域 , 建議先進行閱讀理解JS執行機制和作用域等相關知識,再理解閉包,否則可能會理解的不透徹。

現在我假設JS引擎執行到這行程式碼

let baz = foo();

此時,JS的作用域氣泡是這樣的:

closure

這個時候foo函式已經執行完,JS的垃圾回收機制應該會自動將其標記為”離開環境”,等待回收機制下次執行,將其記憶體進行釋放(標記清除)。

但是,我們仔細看圖中粉色的箭頭,我們將bar的引用指向baz,正是這種引用賦值,阻止了垃圾回收機制將foo進行回收,從而導致bar的整條作用域鏈都被儲存下來

接下來,baz()執行,bar進入執行棧,閉包(foo)形成,此時bar中依舊可以訪問到其父作用域氣泡中的變數a。

這樣說可能不是很清晰,接下來我們藉助chrome的除錯工具看下閉包產生的過程。

當JS引擎執行到這行程式碼let baz = foo();時:

closure

圖中所示,let baz = foo();已經執行完,即將執行baz();,此時Call Stack中只有全域性上下文。

接下來baz();執行:

closure

我們可以看到,此時bar進入Call Stack中,並且Closure(foo)形成。

針對上面我提到的幾點進行下說明:

  1. 上述第二點(閉包和詞法作用域,作用域鏈,垃圾回收機制息息相關)大家應該都清楚了
  2. 上述第三點,當函式baz執行時,閉包才生成
  3. 上述第四點,閉包是foo,並不是bar,很多書(《you dont know JavaScript》《JavaScript高階程式設計》)中,都強調儲存下來的引用,即上例中的bar是閉包,而chrome認為被儲存下來的封閉空間foo是閉包,針對這點我贊同chrome的判斷(僅為自己的理解,如有不同意見,歡迎來討論)

閉包的藝術性

我相信這個世界上最美的事物往往就存在我們身邊,通常它並不是那麼神祕,那麼不可見,只是我們缺少了一雙發現美的眼睛。

生活中,我們抽出一段時間放慢腳步,細細品味我們所過的每一分每一秒,會收穫到生活給我們的另一層樂趣。

閉包也一樣,它不是很神祕,反而是在我們的程式中隨處可見,當我們靜下心來,品味閉包的味道,發現它散發出一種藝術的美,樸實、精巧又不失優雅。

closure

細想,在我們作用域氣泡模型中,作用域鏈讓我們的內部bar氣泡能夠”看到”外面的世界,而閉包則讓我們的外部作用域能夠”關注到”內部的情況成為可能。可見,只要我們願意,內心世界和外面世界是可以相通的

閉包的應用的注意事項

閉包,在JS中絕對是一個高貴的存在,它讓很多不可能實現的程式碼成為可能,但是物雖好,也要合理使用,不然不但不能達到我們想要的效果,有的時候可能還會適得其反。

  • 記憶體洩漏(Memory Leak)JavaScript分配給Web瀏覽器的可用記憶體數量通常比分配給桌面應用程式的少,這樣做主要是防止JavaScript的網頁耗盡全部系統記憶體而導致系統崩潰。因此,要想使頁面具有更好的效能,就必須確保頁面佔用最少的記憶體資源,也就是說,我們應該保證執行程式碼只儲存有用的資料,一旦資料不再有用,我們就應該讓垃圾回收機制對其進行回收,釋放記憶體。

    我們現在都知道了閉包阻止了垃圾回收機制對變數進行回收,因此變數會永遠存在記憶體中,即使當變數不再被使用時,這樣會造成記憶體洩漏,會嚴重影響頁面的效能。因此當變數物件不再適用時,我們要將其釋放。

    我們拿上面程式碼舉例:

    關於記憶體洩漏,推薦 阮一峰老師部落格

閉包的應用

  1. 模組一個模組應該具有私有屬性、私有方法和公有屬性、公有方法。而閉包能很好的將模組的公有屬性、方法暴露出來。

    “return”關鍵字將物件引用匯出賦值給myModule,從而應用到閉包。

  2. 延時器(setTimeout)、計數器(setInterval)這裡簡單寫一個常見的關於閉包的面試題。

    答案大家都知道:每秒鐘輸出一個5,一共輸出5次

    那麼如何做到每秒鐘輸出一個數,以此為0,1,2,3,4呢?

    我們這裡只介紹閉包的解決方法,其他類似塊作用域等等的解決方法,我們這裡不討論。

    “setTimeout”方法裡應用了閉包,使其內部能夠記住每次迴圈所在的詞法作用域和作用域鏈。

    由於setTimeout中的回撥函式會在當前任務佇列的尾部進行執行,因此上面第一個例子中每次迴圈中的setTimeout回撥函式記住的i的值是for迴圈作用域中的值,此時都是5,而第二個例子記住的i的數為setTimeout的父級作用域自執行函式中的j的值,依次為0,1,2,3,4。

  3. 監聽器

=- 關於閉包,我覺得我說清楚了,你看清楚了嗎?留言告訴我吧 -=

如果你覺得寫的還不是很爛,請關注我的 github 吧,讓我們一起成長。。。

相關文章