JavaScript - Closure(閉包)

參考來源一
參考來源二
參考來源三
參考來源四
參考來源五

閉包是 JavaScript 最強大的特性之一。JavaScript 允許巢狀函式(nesting of functions)並給予內部函式完全訪問(full access)所有變數、與外部函式定義的函式(還有所有外部函式內的變數與函式)不過,外部函式並不能訪問內部函式的變數與函式。這保障了內部函式的變數安全。另外,由於內部函式能訪問外部函式定義的變數與函式,將存活得比外部函式還久。

MDN

範圍鏈複習

閉包概念與範圍鏈關係密切。範圍鏈的一個要點是「變數有效範圍的最小單位是 function」。例如以下例子,內層的 function inner 可以讀取外層宣告的變數,但外層 function outer 存取不到內層宣告的變數。若是在自己層級找不到,就會一層一層往外找,直到全域為止。

1
2
3
4
5
6
7
8
9
function outer() {
var b = a * 2; // 此處無法取得變數 c , 但可以向外找到變數 a
function inner(c) {
console.log(a, b, c); //因為範圍鏈的關係,即使只有對c定義,但可以向上取得a,b,c
}
inner(b * 3);
}
var a = 1; //globe這層只有a
outer(a);

閉包

底下例子透過 呼叫 outer() 後 return 的結果,把原本外層存取不到的 inner 取得。因為範圍鏈的變數有效範圍是在函式定義的當下決定,不是呼叫的時候決定。所以即使在全域透唾 innerFunc() 呼叫內部的 inner(),實際取得的 msg 仍然是內層的 “local” 字串。

1
2
3
4
5
6
7
8
9
10
11
12
13
var msg = "global";

function outer() {
var msg = "local";
function inner() {
return msg;
}
return inner;
}

var innerFunc = outer();
var result = innerFunc();
console.log(innerFunc());

當 inner 被回傳後,除了自己本身的程式碼,也取得內部函式「當時環境」的變數值,記住執行當時的環境,這就是「閉包」。在呼叫函式以前,範圍鏈就被建立,所以藉由在函式 outer 裡面「回傳」另一個內部函式給外層的範圍,使得外層也可以透過「範圍鏈」取得內部的變數 msg。

  • 不使用 closure
    同時計算 counter 1 跟 counter 2 的函式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//  countDog  的函式
var count = 0;
function countDog() {
count += 1;
console.log(count + " dog(s)");
}

// countCat 的函式
var count = 0;
function countCat() {
count += 1;
console.log(count + " cat(s)");
}
countCat(); //1cat
countCat(); //2cat
countCat(); //3cat

但是當執行了 countDogs()跟 conuntCats(),會讓 count 增加。因為 count 是 全域變數,兩個函式執行時都會用到這個變數而變成重複計算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var count = 0;

function countDogs() {
count += 1;
console.log(count + " dog(s)");
}

var count = 0;

function countCats() {
count += 1;
console.log(count + " cat(s)");
}
countCats(); // 1 cat(s)
countCats(); // 2 cat(s)
countCats(); // 3 cat(s)

countDogs(); // 4 dog(s),我希望是 1 dog(s)
countDogs(); // 5 dog(s),我希望是 2 dog(s)

countCats(); // 6 cat(s),我希望是 4 cat(s)
  • 使用 closure
    Closure 就能解決這個問題。利用閉包(closure)的作法,讓函式有自己私有變數,簡單來說就是 countDogs 裡面能有一個計算 dogs 的 count 變數;而 countCats 裡面也能有一個計算 cats 的 count 變數,兩者是不會互相干擾的。
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
26
27
28
function dogHouse() {
var count = 0;
function countDogs() {
count += 1;
console.log(count + "dogs");
}
return countDogs;
}

function catHouse() {
var count = 0;
function countCats() {
count += 1;
console.log(count + "cats");
}
return countCats;
}

const countDogs = dogHouse();
const countCats = catHouse();

countDogs(); //1dogs
countDogs(); //2dogs
countDogs(); //3dogs

countCats(); //1cats
countCats(); //2cats
countDogs(); //4dogs

這樣寫就把關於計算貓與狗個別的 count 關閉在 catHouse() 與 dogHouse() 中,當看到一個 function 中內 return 了另一個 function,通常就是有用到 closure。
而在 dogHouse 這個函式中存在 count 這個變數,由於 JavaScript 變數會被縮限在函式的執行環境中,因此這個 count 的值只有在 dogHouse 裡面才能被取用,在 dogHouse 函式外是取用不到這個值的。
最後因為我們要能夠執行在 dogHouse 中真正核心 countDogs() 這個函式,因此我們會在最後把這個函式給 return 出來,好讓我們可以在外面去呼叫到 dogHouse 裡面的這個 countDogs() 函式:

接著,當我們在使用閉包時,我們先把存在 dogHouse 裡面的 countDogs 拿出來用,並一樣命名為 countDogs(這裡變數名稱可以自己取),因此當我執行全域中的 countDogs 時,實際上會執行的是 dogHouse 裡面的 countDogs 函式:

進一步簡化程式

如果熟悉在 closure 中會 return 一個 function 出來,可以不欲為裡面的函式命名,可以使用匿名函式的方式直接回傳出來。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function dogHouse() {
var count = 0;
// 把原本 countDogs 函式改成匿名函式直接放進來
return function () {
count += 1;
console.log(count + " dogs");
};
}

function catHouse() {
var count = 0;
// 把原本 countCats 函式改成匿名函式直接放進來
return function () {
count += 1;
console.log(count + " cats");
};
}

const countDogs = dogHouse();
const countCats = catHouse();
countDogs(); //1dogs
countDogs(); //2dogs
countDogs(); //3dogs

透過函式參數的方式把值帶入 closure 中,所以實際上只需要一個 counter,用不同的參數區分就可以記錄不同動物種類。

1
2
3
4
5
6
7
8
9
10
11
12
function createCounter(name) {
var count = 0;
return function () {
count++;
console.log(count + name);
};
}
const dogCounter = createCounter("dog");
const catCounter = createCounter("cat");
dogCounter(); // 1 dog
dogCounter(); // 2 dog
catCounter(); // 1 cat

常見誤區

1
2
3
4
5
6
7
8
9
10
11
12
13
var msg = "global";

function outer() {
var msg = "local";
function inner() {
return msg;
}
return inner;
}

var innerFunc = outer();
var result = innerFunc();
console.log(outer());

呼叫 outer()之後應該要出現 “local” 才對,但為什麼會出現

1
2
3
ƒ inner() {
return msg;
}

因為直接呼叫 outer()的時候,回傳的其實是

1
2
3
function inner() {
return msg;
}

如果在呼叫 outer()後面再加上一個小括號,就會等於立即執行 outer

1
2
3
4
5
6
7
8
9
10
11
12
13
var msg = "global";

function outer() {
var msg = "local";
function inner() {
return msg;
}
return inner;
}

var innerFunc = outer();
var result = innerFunc();
console.log(outer()());

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2020 CYC'S BLOG All Rights Reserved.

UV : | PV :