JavaScript - 閉包(closure)

壹、範圍鏈 Scope Chain

以 function 為界線,在 function 內層如果找不到變數可以一直向外層找變數,但 function 外層不能讀取到 function 內層的變數。

「切分變數有效範圍的最小單位是 “function” 」

但如果你無論如何都想要讀取到要怎麼辦呢? 這時候就不得不提閉包了。
閉包除了可以 在外層讀取到內的的變數 外,
還可以建立屬於自己的異世界 儲存建立環境時的變數值

趕快來探索這個異世界的奧秘吧

二、閉包

因為 function 內部的變數無法被外部讀取,因此在使用 function 時常常需要在外層宣告了很多變數,如果是大型的案子可能會造成衝突。

以這個程式碼為例,變數都必須先宣告在函式外

1
2
3
4
5
6
7
8
9
10
11
12
13
var LeoTeam = 4 // Leo 的團隊有 4 個人
var TomTeam = 5 // Tom 的團隊有 5 個人
function addLeo(num){
LeoTeam = LeoTeam + num;
}
function addTom(num){
TomTeam = TomTeam + num;
}

addLeo(2); // Leo 的團隊今天多招募了兩個人
//一天過後
addTom(1); // Tom 的團隊隔天多招募了一個人
console.log('LeoTeam:'+LeoTeam, 'TomTeam:'+TomTeam);

如果我不想預先宣告變數,並且變數不需要宣告為全域變數,我可以…


使用閉包


1. 工廠模式

如此一來不管要創造幾個團隊,或是這個團隊的初始是多少人,後續要增加多少人,都可以很有彈性的變動,而這個方法又稱為閉包的 工廠模式,顧名思義就像工廠生產線一樣,很快速地去產出一個團隊。

1
2
3
4
5
6
7
8
9
10
11
12
13
function team(initNum){
let Num = initNum || 1; // 如果沒有給 initNum 的值,一開始就給預設值 1
return function(addPeople){
Num = Num + addPeople
return Num
}
}
let LeoTeam = team(4)
let TomTeam = team(5)
LeoTeam(2); // 6
TomTeam(1); // 6
TomTeam(1); // 7
TomTeam(1); // 8

2. 私有方法

另外介紹一下 私有方法,比較像針對特定對象,去做客製化處理。

像下面這個例子,就可以去將團隊的人數作增減,不只是多幾人,也可以少幾人。
私有方法是將 多個函式寫在 return 中,選定需要的函式功能去執行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function team(initNum){
let Num = initNum || 1; // 如果沒有給 initNum 的值,一開始就給預設值 1
return{
increase: function(addPeople){
Num += addPeople;
},
decrease: function(addPeople){
Num -= addPeople;
},
value: function(){
return Num;
},
}
}
let LeoTeam = team(4)

LeoTeam.increase(3); // Leo 的團隊多聘請了 3 人
LeoTeam.decrease(2); // Leo 的團隊離職了 2 人
LeoTeam.increase(10); // Leo 的團隊多聘請了 10 人
LeoTeam.decrease(1); // Leo 的團隊離職了 1 人
LeoTeam.value(); //回傳目前團隊人數

3. 閉包的經典題目

下面這段程式碼,大家預期 fn[0]、fn[1]、fn[2] 會出現什麼數字呢?
如果覺得是 0、1、2 的話….你再想想~~

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

var fn = arrFunction();
console.log(fn);
fn[0]();
fn[1]();
fn[2]();

.
.
.
.
.
.
.
.
.
.
.

答案會是 3、3、3 喔~
為什麼會是 333,這不合理阿!
讓我慢慢道來

我們可以將這個程式碼分為兩段來看

第一段是 var fn = arrFunction();

這裡就會執行了 for 迴圈,因此這邊的 i 就已經跑完了,
你或許會有疑問,i 不是小於 3 嗎? 為什麼最後 i 會跑到 3,
這裡可以把 for 想成一個條件式,i 的值小於 3 他就會繼續跑下一個迴圈,
但如果 i 等於 3 時,就會離開這個迴圈,但這時 i 的值就會是 3

第二段是 fn[0]()

console.log(i)會在這邊執行

記得第一段的 i 的值目前這個狀態是多少嗎??
對,就是 3 喔。
因此當 fn[0] () 去找 i 的值的時候,在他的內部找不到 i,
因此透過範圍鏈到之前 fn = arrFunction() 的執行環境建立的記憶體位置去找 i,
這時找到的 i 就會是 3。
之後 fn[1] () 、fn[2] () 一樣會到同一個記憶體位置找到 i = 3。

因此,最後 console.log 出來的值會是 333。

那有什麼辦法可以解決呢??

在這邊提供兩個解法:

1. 使用 ES6 的 let

let 宣告變數的方法,其中一個特性是,let 的作用域 (scope) 會改以 {} 為範圍,而不是傳統的 var 的作用域為 function(){..} 的大括號內為範圍。

因此在執行第一段程式 var fn = arrFunction(); 時,每次 for 迴圈執行的 i 值都會被存到不同的記憶體內,這三個不同的記憶體位置裡面的 i 分別為 0、1、2,
( 對照原本使用 var 時,因為作用域是在 function 內,記憶體只會被放到同一個位置裡面。 )

當執行 fn[0] ()、 fn[1] () 、fn[2] () 時,就會到這三個記憶體位置取出三個不同的 i 值。

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

var fn = arrFunction();
console.log(fn);
fn[0](); // 0
fn[1](); // 1
fn[2](); // 2

2. 使用 立即函式 IIFL

透過立即函式閉包的方式,
在第一段程式 var fn = arrFunction();
在跑迴圈的當下,因為立即函式的關係,跑一次迴圈,就建立一個自己的執行環境,跑三次就建立三個不同的執行環境,而每個執行環境都有自己的記憶體位置,i值就會存在這裡。

因此,執行第二段程式時,
fn[0] ()、 fn[1] () 、fn[2] (),他會去跑
return function(){
console.log(j)
}
而其中的j值,會分別從三個不同的記憶體中去找。

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

var fn = arrFunction();
console.log(fn);
fn[0](); // 0
fn[1](); // 1
fn[2](); // 2

參考資料:

重新認識 JavaScript: Day 19 閉包 Closure
六角學院: JavaScript 核心篇
JavaScript 全攻略:克服 JS 的奇怪部分

0%