JavaScript 核心觀念(38)-函式以及 This 的運作-閉包 Closure

前言

閉包的觀念在 JavaScript 也非常重要,其重要原因在於與記憶體有息息相關的關聯性。

閉包 Closure

首先閉包最主要觀念與記憶體有非常大的關係。

為什麼這樣說呢?首先在前面章節我們有講過執行堆疊觀念,當函式執行時會向下方一樣

執行堆疊運作模式

而當函式完成自己任務時,就會釋放記憶體

記憶體釋放

因此這行為又稱之為「Garbage collection」,又稱為GC。

那為什麼閉包與這個有關係呢?

首先讓我們來看一段範例,下方程式碼簡單描述一下就是 randomString 是一個產生隨機字串的函式,然後當執行 getData 它會隨機吐一組字串回傳並儲存到 demoData 陣列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function randomString(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}

function getData() {
var demoData = [];
for (let i = 0; i < 1000; i++) {
demoData.push(randomString(1000))
}
}
getData();

當我們執行上方函式時,其實完全不會佔用記憶體,其主要原因就如同上方所述,函式當執行完畢之後,記憶體是會被釋放的,但若將 var demoData = []; 改放置掛載在全域下會發生什麼事情?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function randomString(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
var demoData = [];
function getData() {
for (let i = 0; i < 1000; i++) {
demoData.push(randomString(1000))
}
}
getData();

在此可以看到 strings 暫用了 23 MB
(此為 Firefox 瀏覽器)

記憶體

接下來我們將 demoData 放回函式內再來看一次會發生什麼事情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function randomString(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
function getData() {
var demoData = [];
for (let i = 0; i < 1000; i++) {
demoData.push(randomString(1000))
}
}
getData();

記憶體

可以發現記憶體明顯少非常的多。

而在此閉包的概念在於記憶體是否可以再次被參考,而因為 var demoData = []; 是建立在 getData 底下,因此在函式結束任務被釋放記憶體之後就無法再次參考該變數,所以如果你想取得這個變數是無法的。

而在此所謂的記憶體是否可以再次的定義是什麼呢?讓我們加入 setTimeout 來試一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function randomString(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
function getData() {
var demoData = [];
for (let i = 0; i < 1000; i++) {
demoData.push(randomString(1000))
}
setTimeout(function() {
demoData;
}, 10000);
}
getData();

因此我們將 demoData 放進 setTimeout,預計在十秒之後會執行該變數,在這十秒期間我們先截圖第一次,在此可以看到記憶體暫用非常的大,因為目前被參考導致無法釋放

記憶體被參考無法釋放

但十秒過後就可以發現記憶體變化變了,最主要原因記憶體已經無法被參考,因為函式已經被釋放

記憶體被釋放

而在此 setTimeout 其實就是閉包的概念,因此 demoData 才會無法被釋放掉。

接下來來實際撰寫一個真正的閉包

1
2
3
4
5
6
7
8
9
function fn() {
var money = 100;
return function(num) {
money = money + num;
return money;
}
}

var a = fn();

上方就是一個標準的閉包,剛剛有提到閉包的最主要與記憶體有相關,而這邊是什麼意思呢?首先我們建立了一個 fn 函式並建立一個變數與回傳一個函式,而最後我們將 fn() 執行之後並儲存在變數 a 中,這時候我們可以嘗試執行一下 a,可以看到他是回傳內部的 function

函式

因此這個函式可以傳入參數,也就是 num,這時候當我們這樣撰寫,可以發現一件事情金額會不停累加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function fn() {
var money = 100;
return function(num) {
money = money + num;
return money;
}
}

var a = fn();

console.log(a(100)); // 200
console.log(a(100)); // 300
console.log(a(100)); // 400
console.log(a(100)); // 500

可以發現變數永遠都沒有被釋放,但你從外部是無法去取得 money 因為這只存在於 fn 函式內。

而為什麼會發生這種狀況呢?因為在我們執行 fnvar money = 100; 是被內部函式所參考,也就是 money = money + num;,因此 JavaScript 當發現這個變數是被參考的,那麼就不會被釋放掉該記憶體,而該記憶體就會放在瀏覽器的某處,因此我們就可以不停地使用該變數。

而使用這個閉包還有什麼好處?它可以建立屬於它自己的獨立閉包,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function fn() {
var money = 100;
return function(num) {
money = money + num;
return money;
}
}

var a = fn();

console.log(a(100)); // 200
console.log(a(100)); // 300
console.log(a(100)); // 400
console.log(a(100)); // 500

var b = fn();

console.log(b(500)); // 600
console.log(b(500)); // 1100
console.log(b(500)); // 1600
console.log(b(500)); // 2100

因此透過該方式,我們就可以建立獨一無二屬於它自己的函式,而在此閉包其實也有記憶體回收、範圍鏈、函式呼叫等觀念息息相關唷。

參考文獻