
閉包是由英文的 closure 直接翻譯過來的,所以從字面上看起來,可能也不知道閉包是什麼。
closure 是函式在與其語彙範疇之外被調用,也仍記得並能夠被存取的能力,可說是指向特定範疇的參考。
1 2 3 4 5 6 7 8 9
| function foo() { var a = 2; function bar() { console.log(a); } return bar; } var baz = foo(); baz();
|
bar() 函式能夠存取 foo 的內層範疇。
一般來說我們預期 foo() 的整個內層範疇都會消失,基本上函式離開執行環境時,也會同時將佔用的記憶體空間給釋放出來,但 closure 不會讓這件事發生,foo() 內層的範疇實際上仍在使用,因此不會消失。誰在使用它呢? 就是 bar() 函式本身。
1 2 3 4 5 6 7 8 9 10
| function foo() { var a = 2; function baz() { console.log(a); } bar(baz); } function bar(fn) { fn(); }
|
我們將內層函式 baz 傳給了 bar,並呼叫 bar 的內層函式 fn,包覆 foo() 內層範疇的 closure 就能藉由存取 a 來觀察。
而這些到處傳遞的函式也可以是間接的
1 2 3 4 5 6 7 8 9 10 11 12 13
| var fn; function foo() { var a = 2; function baz() { console.log(a); } fn = baz; } function bar() { fn(); } foo(); bar();
|
不管我們使用了何種方法,將一個內層函式運送到其語彙範疇之外,它依然會保留對他原本宣告處的一個範疇參考。
迴圈與 Closure
closure 最經典的範例
1 2 3 4 5
| for (var i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i); }, i * 1000); }
|
我們預期它會每秒印出 1、2、3、4、5,但實際上是每秒印出了五次 6!?
這是因為 setTimeout 在迴圈結束後才執行,因此每次都會出現 6,那到底我們程式中缺少了什麼呢?
我們想要讓迴圈每次迭代都能在該次迭代進行時捕捉到他自己的 i 的一份拷貝,然而,那些計時器函式全都是在迴圈完成後執行,所有的那五個函式,雖然都是在各自的迴圈迭代中分別定義的,但它們都會覆蓋同一個共同的全域範疇,其中實際上只有一個 i 存在。
我們需要更多已被封閉包圍的範疇,也就是說,每次迭代都需要一個新的 closure scope
解決方式 IIFE:
1 2 3 4 5 6 7
| for (var i = 1; i <= 5; i++) { (function (j) { setTimeout(function timer() { console.log(j); }, j * 1000); })(i); }
|
利用 IIFE 建立專屬範疇,並有自己的變數,來放置每次迭代 i 的一份拷貝
重返區塊範疇
我們用了 IIFE 來建立專屬每次迭代的新範疇,實際上,我們需要的是各次迭代專屬的區塊範疇。
let 宣告會劫持一個區塊,且每次重新宣告變數 i,並將上一次迭代的結果作為這一次的初始值。
1 2 3 4 5
| for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i); }, i * 1000); }
|
區塊範疇和 closure 攜手合作,解決了所有的問題。
模組
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function CoolModule() { var something = 'cool'; var another = [1, 2, 3];
function doSomething() { console.log(something); }
function doAnother() { console.log(another.join('!')); }
return { doSomething: doSomething, doAnother: doAnother, }; }
var foo = CoolModule(); foo.doSomething(); foo.doAnother();
|
模組模式又稱揭露模組(revealing module),可以將內層函式的資料保持隱藏和私有
,調用時只回傳對外公開的 API,
這個物件回傳最終會指定給外層變數 foo,就能存取 API 的特性方法,例如 foo.doSomething()
要行使模組模式,有兩個必要條件:
- 必須有一個外層的包含函式,而它必須至少被調用一次(每次都會建立一個新的模組實體)
- 這個包含函式至少得回傳一個內層函式,如此這個內層函式才能有覆蓋那個私有範疇的 closure,因此得以存取或修改那個私有狀態
帶有一個函式特性的物件本身並不是一個真正的模組,從一次函式調用所回傳的物件,如果其上只有資料特性,沒有產生 closure 的函式,那它也不是一個真正的模組。
CoolModule()展示了一個獨立的模組創造器,可以被調用數次,每次都會建立一個新的模組實體。
如果只想要單一個實體的時候:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| var foo = (function CoolModule() { var something = 'cool'; var another = [1, 2, 3];
function doSomething() { console.log(something); }
function doAnother() { console.log(another.join('!')); }
return { doSomething: doSomething, doAnother: doAnother, }; })();
foo.doSomething(); foo.doAnother();
|
模組函式變成了一個 IIFE,即刻調用,並將回傳值直接指定給我們單一個模組實體式別字 foo
模組只是函式,所以能夠接收參數:
1 2 3 4 5 6 7 8 9 10 11 12 13
| function CoolMoudule(id) { function identify() { console.log(id); } return { identify: identify, }; } var foo1 = CoolMoudule('foo 1'); var foo2 = CoolMoudule('foo 2');
foo1.identify(); foo2.identify();
|
為公開 API 的回傳物件取個名稱:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| var foo = (function CoolModule(id) { function change() { publicAPI.identify = identify2; }
function identify1() { console.log(id); }
function identify2() { console.log(id.toUpperCase()); }
var publicAPI = { change: change, identify: identify1, };
return publicAPI; })('foo module');
foo.identify(); foo.change(); foo.identify();
|
在模組實體中保留對公開 API 物件的一個內層參考,就能夠從內部修改那個模組實體,包括新增與移除方法和特性,還有變更它們的值。
參考書籍: 你不知道的 JS-範疇與 Closures
🚀線上課程分享
線上課程可以加速學習的時間,省去了不少看文件的時間XD,以下是我推薦的一些課程
想學習更多關於前後端的線上課程,可以參考看看。
Hahow 有各式各樣類型的課程,而且是無限次數觀看,對學生或上班族而言,不用擔心被時間綁住
如果你是初學者,非常推薦六角學院哦!
剛開始轉職也是上了六角的課,非常的淺顯易懂,最重要的是,隨時還有線上的助教幫你解決問題!
Udemy 裡的課程非常的多,品質普遍不錯,且價格都滿實惠的,CP值很高!
也是很多工程師推薦的線上課程網站。