閉包是由英文的 closure 直接翻譯過來的,所以從字面上看起來,可能也不知道閉包是什麼。
closure 是函式在與其語彙範疇之外被調用,也仍記得並能夠被存取的能力,可說是指向特定範疇的參考。
1 | function foo() { |
bar() 函式能夠存取 foo 的內層範疇。
一般來說我們預期 foo() 的整個內層範疇都會消失,基本上函式離開執行環境時,也會同時將佔用的記憶體空間給釋放出來,但 closure 不會讓這件事發生,foo() 內層的範疇實際上仍在使用,因此不會消失。誰在使用它呢? 就是 bar() 函式本身。
1 | function foo() { |
我們將內層函式 baz 傳給了 bar,並呼叫 bar 的內層函式 fn,包覆 foo() 內層範疇的 closure 就能藉由存取 a 來觀察。
而這些到處傳遞的函式也可以是間接的
1 | var fn; |
不管我們使用了何種方法,將一個內層函式運送到其語彙範疇之外,它依然會保留對他原本宣告處的一個範疇參考。
迴圈與 Closure
closure 最經典的範例
1 | for (var i = 1; i <= 5; i++) { |
我們預期它會每秒印出 1、2、3、4、5,但實際上是每秒印出了五次 6!?
這是因為 setTimeout 在迴圈結束後才執行,因此每次都會出現 6,那到底我們程式中缺少了什麼呢?
我們想要讓迴圈每次迭代都能在該次迭代進行時捕捉到他自己的 i 的一份拷貝,然而,那些計時器函式全都是在迴圈完成後執行,所有的那五個函式,雖然都是在各自的迴圈迭代中分別定義的,但它們都會覆蓋同一個共同的全域範疇,其中實際上只有一個 i 存在。
我們需要更多已被封閉包圍的範疇,也就是說,每次迭代都需要一個新的 closure scope
解決方式 IIFE:
1 | for (var i = 1; i <= 5; i++) { |
利用 IIFE 建立專屬範疇,並有自己的變數,來放置每次迭代 i 的一份拷貝
重返區塊範疇
我們用了 IIFE 來建立專屬每次迭代的新範疇,實際上,我們需要的是各次迭代專屬的區塊範疇。
let 宣告會劫持一個區塊,且每次重新宣告變數 i,並將上一次迭代的結果作為這一次的初始值。
1 | for (let i = 1; i <= 5; i++) { |
區塊範疇和 closure 攜手合作,解決了所有的問題。
模組
1 | function CoolModule() { |
模組模式又稱揭露模組(revealing module),可以將內層函式的資料保持隱藏和私有
,調用時只回傳對外公開的 API,
這個物件回傳最終會指定給外層變數 foo,就能存取 API 的特性方法,例如 foo.doSomething()
要行使模組模式,有兩個必要條件:
- 必須有一個外層的包含函式,而它必須至少被調用一次(每次都會建立一個新的模組實體)
- 這個包含函式至少得回傳一個內層函式,如此這個內層函式才能有覆蓋那個私有範疇的 closure,因此得以存取或修改那個私有狀態
帶有一個函式特性的物件本身並不是一個真正的模組,從一次函式調用所回傳的物件,如果其上只有資料特性,沒有產生 closure 的函式,那它也不是一個真正的模組。
CoolModule()展示了一個獨立的模組創造器,可以被調用數次,每次都會建立一個新的模組實體。
如果只想要單一個實體的時候:
1 | var foo = (function CoolModule() { |
模組函式變成了一個 IIFE,即刻調用,並將回傳值直接指定給我們單一個模組實體式別字 foo
模組只是函式,所以能夠接收參數:
1 | function CoolMoudule(id) { |
為公開 API 的回傳物件取個名稱:
1 | var foo = (function CoolModule(id) { |
在模組實體中保留對公開 API 物件的一個內層參考,就能夠從內部修改那個模組實體,包括新增與移除方法和特性,還有變更它們的值。
參考書籍: 你不知道的 JS-範疇與 Closures
Hey!想學習更多前端知識嗎?
最近 Lala 開了前端課程 👉【實地掌握RWD - 12小時新手實戰班】👈無論您是 0 基礎新手,又或是想學 RWD 的初學者,
我們將帶你從零開始,深入了解並掌握 RWD 響應式網頁設計的核心技術,快來一起看看吧 😊