天天看點

JavaScript閉包由淺入深

JavaScript中的閉包并非單一的概念,它涉及到作用域、作用域鍊、執行上下文、記憶體管理等多種知識。

如果在一個函數中我們傳回了另一個函數,且這個傳回的内層函數使用了外層函數的變量,那麼外界便能夠通過這個傳回的函數擷取原函數内部的變量值,則我們将傳回的函數稱為原函數的一個閉包。

這概念看起來是不是有點繞呢?????那麼我們來看一下下面這個例子:

上面是一個簡單的閉包示例,在外部函數outFun内傳回了箭頭函數,我們将其指派給innerFun,調用innerFun我們可以通路到outFun函數内的變量num,innerFun就是outFun的一個閉包。

看了上面的例子想必你對于閉包已經有了基本的了解????

那麼就有小夥伴要問了:為什麼可以在函數外部通路到outFun中的局部變量呢?

一般來講,在一個函數執行完畢後,會從函數執行棧中出棧,函數内的局部變量在下一個垃圾回收(GC)節點會被回收,同時該函數對應的執行上下文會被銷毀,是以我們無法在外界通路函數内部定義的變量,也就形成了所謂的函數作用域。

但是我們根據詞法作用域的規則,内部函數總是可以通路其外部函數中聲明的變量。通過innerFun通路outFun中的變量,引用的變量仍然儲存在記憶體中,垃圾回收機制無法将其回收,形成了閉包。

接下來我們通過一道經典面試題來深入了解下閉包:

上面這道題輸出了什麼?

上面的問題你答對了嗎?那麼我們進入下一步????

如果我們想要輸出5 0 1 2 3 4,該如何對上面的代碼進行改造呢?

熟悉setTimeoutAPI的小夥伴會給出以下方案:

setTimeout的第三個參數為回調函數的傳入參數。

熟悉ES6的小夥伴應該會使用let來實作這個需求吧?

使用let替代var,在每一次循環内let都會形成一個塊級作用域,進行重新指派,但這種解決方案會存在一個問題,因為 i 隻會存在于循環内部,是以在外部的 console.log 并無法通路到内部的 i ,這并不算是一個完美的解決方案。

要讓外部通路到函數内部的變量,你是不是想到什麼了呢?沒錯!就是我們的主角閉包 ????

利用IIFE (Immediately Invoked Function Expression:聲明即執行的函數表達式)來執行将每一次循環的變量傳入定時任務中可以達到我們想要的效果。

如果更進一步,我們不僅想要輸出5 0 1 2 3 4,同時希望它們的輸出間隔都為1秒呢?

我們可以修改定時器的時間來實作該需求:

既然我們每一個循環都有一個異步操作,那麼我們能不能使用Promise來實作這個需求呢?

我們可以使用一個數組存放Promise對象,使用Promise.allAPI來實作這個需求:

在 for 循環中使用了var進行聲明而不使用let進行聲明,是因為在執行Promise.all後之後的回調還需要執行一次定時任務,若使用let則無法拿到函數作用域内的i變量,若使用let最後結果将輸入 0 而不是 5 。

既然我們使用了Promise,那麼不妨将其優化一下,不使用Promise.all而使用async/await來實作:

這種寫法相對來說可讀性也有所提高,同時節省了數組所需的記憶體。

接下來我們回歸原點,來看一道與閉包相關的手寫代碼題:函數柯裡化

所謂的柯裡化(curry)就是将接受多個參數的函數通過閉包轉化為接收更少參數的函數,該函數傳回一個接收剩餘參數的函數。柯裡化函數能夠實作參數的複用。

我們來看一下具體的示例:

看到這裡你應該能明白什麼是柯裡化函數了吧?那麼接下來讓我們一起來嘗試實作一個柯裡化工具函數吧????

柯裡化實作思路: 調用時傳回一個柯裡化封裝後的函數carried 當傳入的args長度與原始函數所定義的func.length相同或更長,那麼直接将參數傳遞給它即可 否則傳回一個封裝後的偏函數,它将重新應用carried,将之前傳入的參數與新的參數一起傳入,直到傳入的參數總長度大于或等于func.length時才會擷取最終結果。

上面的代碼結構稍微複雜點,可以代入上面的例子以及實作思路反複咀嚼,能夠從根本上了解其實作思路你就赢了????

當然,凡事有好有壞,閉包也不能免俗。閉包在使用的同時存在記憶體洩漏的風險。

記憶體洩漏:指記憶體空間明明已經不再被使用,但由于某種原因并沒有被釋放的現象。

我們來看一下具體的例子:

使用setInterval後first内的value被引用,即使設定了first = null記憶體空間仍然無法釋放,每隔一秒仍然會在控制台輸出value值。這種情況需要使用clearInterval對其清除,占用的記憶體才能被釋放。

使用element.innerHTML = '',成功将button從dom中移除,但是事件處理函數仍在監聽它,element節點無法回收,記憶體被占用。這種情況需要使用removeEventListener函數去除事件監聽,防止記憶體洩漏。

使用element.parentNode.removeChild(element)将element從文檔流中去除,但是element仍然存在,該節點所占用的記憶體無法釋放。這種情況需要使用element = null來釋放記憶體。

到此為此我想你對于閉包造成記憶體洩漏的情況已經有了基本的認知,閉包雖好,使用的時候也要小心記憶體洩漏哦????

感謝你花時間讀到這裡。如果這一篇文章你覺得不錯或是對你有所幫助的話,請給筆者一個贊:+1:,如果對文中内容有任何疑問,歡迎評論區留言評論????

繼續閱讀