[JavaScriptWeird]No.35 瞭解閉包(二)

前言

接續上篇內容,這篇將用幾個經典範例用來更深入了解閉包。

典型的例子:

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++){
arr.push(
function(){
console.log(i);
}
);
}
return arr;
}

var fs = buildFunctions();
fs[0](); // 3
fs[1](); // 3
fs[2](); // 3

做為人類,我們預期三個結果應該會是 0 、 1 、 2,但實際上卻是回傳 3 。

結合我們之前的觀念,當程式碼執行到 arr.push 時,匿名函式被創造,但是必須要注意的是在此時它並沒有被執行,只是把匿名函式放入陣列,接著回傳 arr

我們宣告變數 fs 指向 buildFunctions 函式,並且進行呼叫, for 迴圈執行完畢後 i 的值就是 3 被保存在 buildFunctions 函式的執行環境內。

而我們呼叫了匿名函式,由於函式內部沒有 i 變數,因此會轉而向外部尋找,此時雖然外部 buildFunctions 函式的執行環境不存在,但是因為閉包,所以仍然可以取得 i 的值,所以輸出才是 3 。

而其餘的呼叫也因為需要的變數都是 i ,所以指向相同的外部環境尋找變數,因此結果都是一樣的。

如何修正成為我們預期的結果?

如同我們在 IIFE 章節得出的結果,可以使用 ES6 新增的 let 來修改程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function buildFunctions() {  
var arr = [];
for(let i = 0 ; i < 3 ; i++){
arr.push(
function(){
console.log(i);
}
);
}
return arr;
}

var fs = buildFunctions();
fs[0](); // 0
fs[1](); // 1
fs[2](); // 2

let 的範圍只有 for 迴圈的 {} 內,而每次進行迴圈時都會在執行環境內不同的記憶體位址建立 i ,所以當這個匿名函式被呼叫,每次都會指向不同的記憶體位址。

ES5 的作法 IIFE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function buildFunctions() {  
var arr = [];
for(var i = 0 ; i < 3 ; i++){
arr.push(
(function(i){
return function(){
console.log(i);
}
})(i)
);
}
return arr;
}

var fs = buildFunctions();
fs[0](); // 0
fs[1](); // 1
fs[2](); // 2

原理很簡單,先前例子 i 的值會相同,是因為尋找到相同外部環境的同樣變數值。

那麼只要個別創造不同的執行環境保存變數 i 就解決問題了。 IIFE 可以有效地解決這個問題,因為只要函式被呼叫了,就會建立一個執行環境。

像這樣,每次進行迴圈時,都有一個 IIFE 被執行,建立了執行環境,保存當下 i 的值,然而當內部的匿名函式被呼叫時,不再需要跑到最外層去存找 i ,而是在 IIFE 那一層就可以找到相應的 i

0%