Lala Code

Lala 的前端大補帖,歡迎一起鑽研前端技術😊

0%

搞懂JS-什麼是 closure 閉包

Imgur

閉包是由英文的 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(); // 2 closure

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(); // closure
}

我們將內層函式 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; // 指定 baz 給全域變數
}
function bar() {
fn(); // closure
}
foo();
bar(); // 2

不管我們使用了何種方法,將一個內層函式運送到其語彙範疇之外,它依然會保留對他原本宣告處的一個範疇參考。

迴圈與 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(); // cool
foo.doAnother(); // 1!2!3

模組模式又稱揭露模組(revealing module),可以將內層函式的資料保持隱藏和私有,調用時只回傳對外公開的 API,
這個物件回傳最終會指定給外層變數 foo,就能存取 API 的特性方法,例如 foo.doSomething()

要行使模組模式,有兩個必要條件:

  1. 必須有一個外層的包含函式,而它必須至少被調用一次(每次都會建立一個新的模組實體)
  2. 這個包含函式至少得回傳一個內層函式,如此這個內層函式才能有覆蓋那個私有範疇的 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(); // cool
foo.doAnother(); // 1!2!3

模組函式變成了一個 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(); // "foo 1"
foo2.identify(); // "foo 2"

為公開 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() {
// modify the public API
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 module
foo.change();
foo.identify(); // FOO MODULE

在模組實體中保留對公開 API 物件的一個內層參考,就能夠從內部修改那個模組實體,包括新增與移除方法和特性,還有變更它們的值。


參考書籍: 你不知道的 JS-範疇與 Closures




Hey!想學習更多前端知識嗎?

最近 Lala 開了前端課程 👉【實地掌握RWD - 12小時新手實戰班】👈
無論您是 0 基礎新手,又或是想學 RWD 的初學者,
我們將帶你從零開始,深入了解並掌握 RWD 響應式網頁設計的核心技術,快來一起看看吧 😊



🚀線上課程分享

線上課程可以加速學習的時間,省去了不少看文件的時間XD,以下是我推薦的一些課程
想學習更多關於前後端的線上課程,可以參考看看。

Hahow

Hahow 有各式各樣類型的課程,而且是無限次數觀看,對學生或上班族而言,不用擔心被時間綁住



六角學院

如果你是初學者,非常推薦六角學院哦!
剛開始轉職也是上了六角的課,非常的淺顯易懂,最重要的是,隨時還有線上的助教幫你解決問題!


Udemy

Udemy 裡的課程非常的多,品質普遍不錯,且價格都滿實惠的,CP值很高!
也是很多工程師推薦的線上課程網站。
❤️