Jeff的隨手筆記

學習當一個前端工程師

0%

『JavaScript 基礎』 Closures

Imgur

這是一門在Udemy 課程,是同學介紹的。主要是因為想要運用到過年前這段時間好好的增進自己的JavaScript的基礎能力,讓自己能往前端工程師更近一步。主要還是會以筆記的形式做呈現!


在正式上課前,講師先給我們看了一段code:

1
2
3
4
5
6
7
8
9
10
function greet(whattosay) {

return function(name) {
console.log(whattosay + ' ' + name);
}

}

greet('Hi')('Tony')
// 輸出會是:Hi Tony

好像有點難懂,我們換另一種寫法:

1
2
3
4
5
6
7
8
9
10
11
function greet(whattosay) {

return function(name) {
console.log(whattosay + ' ' + name);
}

}

var sayHi = greet('Hi');
sayHi('Tony');
//輸出會是: Hi Tony

看起來好像很合理,但如果仔細一想,有一個疑問:為什麼sayHi會知道whattosay這個參數。

我的想法是:whattosay是在我們called greet function時創建的,而當greet這個function執行完成後參數whattosay應該會從execution stack 離開才對。為什麼sayHi還能找到呢?

這就是我們要學的closures所帶來的結果。

|Closures的底層原理

當我們執行整段code時,我們知道整段code的global execution context會被建立。

當我們來到 sayHi = greet 時,他會invokes greet這個function,創建新的execution context
Imgur

當執行到function greet(whattosay)時,javaScript引擎會注意到這裡有一個parameters(參數),因此把他放到了execution context裡。
Imgur

當再往下執行後,發現了return ,因此回傳了後面整段function,所以整個greet()就會從stack彈出。

但要記得,我們說過每個execution context在記憶體裡都會有一個空間,在正常情況下,JavaScript引擎會透過garbage collection來清除內容,但在execution context抽離的當下,雖然execution context已經不在了,但裡面的變數還是儲存在那個記憶體位置。
Imgur

當我們繼續往下執行到sayHi('Tony')時,我們建立了一個給匿名函式的execution context,同時裡面帶有變數name。
Imgur

當我們執行這個這個匿名函式的console.log(whattosay + ' ' + name)時,JavaScript引擎就會過scope chain的方式來尋找whattosay這個變數。這時候雖然我們的greet這個function的execution context已經不在了,但其實在這個記憶體位置仍然留有參照(reference),所以在greet function裡面所建立的函式仍然可以找得到whattosay這個變數。
Imgur

到這邊就是整個closure的底層原理。
Imgur

|Closure的練習

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function buildFunctions() {

var arr = [];

for (var i = 0; i < 3; i++) {

arr.push(
function() {
console.log(i);
}
)
}

return arr;
}

var fs = buildFunctions();

fs[0]();
fs[1]();
fs[2]();

看到第一眼本來很直覺的想要說答案是0,1,2,但在仔細看了一遍後注意到arr裡面放的是function而不是number。所以console.log(i)並不會在被執行,而是當我們程式走到fs[0]()時,他才會去執行console.log(i),所以這時要套用到剛剛學習的觀念,透過scope chain去找到i的值。

buildFunctions這個function執行for迴圈時,每當執行一次,就會把 function( ){console.log(i)} 儲存到陣列中,但要注意的是這時候這個被儲到陣列中的function並沒有執行(invoke),而是只是儲存在裡面而已,因為它沒有透過括號 ( ) 來執行;然後 i 會繼續累加,當 i 累加到3的時候,因為不符合 i < 3 所以會跳出迴圈。因此i就會是等於3,而arr會是長這樣:

1
arr = [f0, f1, f2]

Imgur

因此當我們透過scope chain去找i時就會得到i = 3,之後再帶入到程式碼裡面就會得到答案3。由於f0、f1跟f2都擁有同樣的outer environment reference,因此答案會是:3,3,3。

那如果這時候我們要讓他輸出的結果變成 0, 1, 2時,我們該怎麼做?

這邊提到了2個做法,第一個是使用let,第二個就是使用IIFEs

先看let:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function buildFunctions() {

var arr = [];

for (var i = 0; i < 3; i++) {
let j = i;
arr.push(
function() {
console.log(j);
}
)

}

return arr;
}

透過let,可以讓每次跑的迴圈都建立在一個新的記憶體位置,因此最後指到的地方會是不一樣的,於是可以輸出0, 1, 2的結果。

使用IIFEs就稍微複雜了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function buildFunctions() {

var arr = [];

for (var i = 0; i < 3; i++) {
arr.push(
(function(j) {
return function() {
console.log(j);
}
}(i))
)

}

return arr;
}

(function(j){...})(i)因為這段是IIFEs,所以他會直接被執行並且會把變數i帶到function裏面,這也就導致在一開始原本一樣的outer environment reference變成不一樣,因此當我們要再去outer environment reference 找參數時,就會因為參數的值不同,得到的輸出就不會再是都一樣的。

|Closure的進階練習

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function makeGreeting(language) {

return function(firstname, lastname) {

if (language === 'en') {
console.log('Hello ' + firstname + ' ' + lastname);
}

if (language === 'es') {
console.log('Hola ' + firstname + ' ' + lastname);
}

}

}

var greetEnglish = makeGreeting('en');
var greetSpanish = makeGreeting('es');

greetEnglish('John', 'Doe');
greetSpanish('John', 'Doe');

閉包的使用當然有很多種,例如上面這段code,雖然比前一個練習更複雜,但我們只要記得:

每執行一次函式,就會產生一個新的execution context,即使有多個參數值被儲存在記憶體中,JavaScript引擎會自己找到屬於該execution context的變數。

丨Closure的總結

closure是JavaScript引擎的一種特性,並不是說你需要去創造它或執行它。透過closure這樣的特性,我們可以確保當我們在執行function的時候,JavaScript引擎能夠找到其相對應的變數,也就是說,不論某一個function是不是已經執行完畢,是不是已經抽離execution stack,JavaScript引擎仍然可以找到外面的變數。

參考文章:https://pjchender.blogspot.com/2016/06/closuresclosurescallback.html