September 19, 2023

Viiisit [JavaScript] - Closure!

#javascript

上篇簡單講述了閉包的特性之後,今天來點不同的舉例來加深印象!

先來看看以下程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function createFunctionArray() {
var functionArray = []; // 創建一個函式陣列,用於存放多個函式

for (var i = 0; i < 5; i++) {
// 在迴圈中,我們創建一個匿名函式,該函式將輸出目前的索引值
functionArray.push(
function printIndex() {
console.log(i);
}
);
}

return functionArray;
}

var functions = createFunctionArray(); // 創建一個包含多個函式的陣列

functions[0](); // 呼叫陣列中的第一個函式,將輸出 5
functions[1](); // 呼叫陣列中的第二個函式,將輸出 5

是不是會很好奇,為什麼輸出都是 5

回想一下閉包的特性,雖然上面的程式碼似乎應該依次輸出 01
但實際上它們都輸出 5 的原因是因為在迴圈中創建的匿名函式捕獲了變數 i 的引用,而不是其值。

createFunctionArray 函式中,我們創建了一個函式陣列 functionArray
並使用一個迴圈來添加匿名函式到這個陣列中。這些匿名函式都捕獲了外部作用域的變數 i

當迴圈完成並退出後,i 的值等於 5,因為這是使迴圈停止的條件。
由於這些匿名函式仍然引用相同的 i,當呼叫這些函式時,都將使用當前的 i 值,即 5
這就是為什麼無論呼叫 functions[0]() 還是 functions[1](),都輸出 5 的原因。


那麼要如何讓上面的輸出結果是 01呢?

我們可以在每次迴圈迭代時創建一個新的作用域,這樣每個匿名函式都會捕獲不同的 i 值。

使用 let 創建的 i 變數在每次迴圈迭代時都有自己的區塊作用域,
因此每個匿名函式都能正確地捕獲到其自己的 i 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createFunctionArray() {
var functionArray = [];

for (var i = 0; i < 5; i++) {
(function (index) { // 使用IIFE創建新的作用域
functionArray.push(function() {
console.log(index);
});
})(i); // 將i作為參數傳遞給IIFE
}

return functionArray;
}

var functions = createFunctionArray();

functions[0](); // 輸出 0
functions[1](); // 輸出 1

這樣做之後,每個匿名函式都捕獲了它自己的 index 值,使得輸出正確。

IIFE 通常用於

  • 創建私有作用域: IIFE 創建了一個獨立的作用域,其中的變數在函式執行後會被銷毀。
    這有助於防止變數污染全域作用域。
  • 模組化程式碼: IIFE 可用於創建模組化的程式碼區塊,其中可以定義私有變數和函式,
    並通過返回公共接口來封裝它們,以供外部使用。

閉包實際應用

  1. 封裝私有變數和函式:
    閉包可以用來創建具有私有變數的函式,以增加安全性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function createCounter() {
    let count = 0;
    return {
    increment: function () {
    count++;
    },
    getCount: function () {
    return count;
    }
    };
    }

    const counter = createCounter();
    counter.increment();
    console.log(counter.getCount()); // 輸出 1
  2. 事件處理程序:
    閉包可用於處理事件。當使用者點擊按鈕時,可以使用閉包來記錄點擊次數。

    1
    2
    3
    4
    5
    6
    7
    const button = document.getElementById('likeButton');
    let clickCount = 0;

    button.addEventListener('click', function () {
    clickCount++;
    console.log(`點擊次數:${clickCount}`);
    });
  3. setTimeout 和 setInterval:
    使用閉包可以創建具有狀態的定時任務。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function createTimer() {
    let seconds = 0;
    return function () {
    seconds++;
    console.log(`過去秒數:${seconds}`);
    };
    }

    const timer = createTimer();
    setInterval(timer, 1000);
  4. 模組模式:
    閉包可用於創建模組,將相關的函式和數據封裝在一起,以提供更好的代碼組織。
    假設我們正在建立一個計數器模組:

    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
    29
    30
    31
    32
    33
    const CounterModule = (function () {
    // 私有變數
    let count = 0;

    // 增加
    function increment() {
    count++;
    }

    // 減少
    function decrement() {
    count--;
    }

    // 獲取當前值
    function getCount() {
    return count;
    }

    // 公開的部分
    return {
    increment: increment,
    decrement: decrement,
    getCount: getCount
    };
    })();

    // 使用計數器模組
    CounterModule.increment();
    CounterModule.increment();
    console.log(CounterModule.getCount()); // 輸出 2
    CounterModule.decrement();
    console.log(CounterModule.getCount()); // 輸出 1

    在這個例子中,CounterModule 是一個使用閉包實現的模組,
    包含了私有變數 count 和三個公開方法:incrementdecrementgetCount
    這樣,我們可以使用模組來管理計數器的狀態,同時保護了 count 變數,
    使其無法被外部直接訪問或修改,提供了更好的代碼組織和隔離。

  5. 快取:
    閉包可用於創建簡單的快取,以避免重複計算。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function createCache() {
    const cache = {};
    return function (key) {
    if (key in cache) {
    return cache[key];
    } else {
    const result = /* 計算結果 */;
    cache[key] = result;
    return result;
    }
    };
    }

    const getValue = createCache();
    console.log(getValue('data')); // 計算並快取結果
    console.log(getValue('data')); // 直接使用快取的結果

深入了解不同的例子與閉包實際應用之後,相信大家有對閉包留下深刻的印象!
今天就先分享到這,我們下篇見!


參考資料:
MDN - Closures
[筆記]-JavaScript 閉包(Closure)是什麼?關於閉包的3件事
[筆記] 談談JavaScript中closure的概念 – Part 2
[JS] 深入淺出 JavaScript 閉包(closure)