跳至主要内容

閉包

TL;DR

  • 閉包(closure)是函式以及該函式被宣告時所在的作用域環境(lexical environment)的組合
  • 要形成閉包須將函式宣告在另一個函式當中,內部函式能夠取得本身作用域以外的變數,就算外部函式已經被執行
  • 經常被應用在狀態保存緩存機制模擬私有變數以及柯里化
  • closure 主要用來封裝私有變數和方法;Class 主要用來建立一個建立物件的藍圖

什麼是閉包 (closure)

根據 MDN 的定義,閉包(Closure)是函式以及該函式被宣告時所在的作用域環境(lexical environment)的組合

簡單來說,要形成閉包須將函式宣告在另一個函式當中,內部函式能夠取得本身作用域以外的變數,就算外部函式已經被執行

function outterFn() {
const name = 'jordan';
function innerFn() {
console.log(name);
}
return innerFn;
}

// 閉包 = innerFn + outterFn 的作用域環境
const innerFn = outterFn();

// 就算 outterFn 已經被執行,innerFn 仍能夠取得 name 參數
innerFn(); // jordan

實際應用

  1. 狀態保存

    React 中的 useState 就是透過 closure 的概念實作,以下為一個簡易版的  useState ,從下方程式碼可以發現,getStatesetState  可以透過 closure 的特性,讓內部函式記住外部的變數,因此可以透過呼叫 setState 後,改變 state 的值,也可以透過 getState取得 state 最新的值。

    function useState(initialState) {
    let state = initialState;

    function getState() {
    return state;
    }

    function setState(updatedState) {
    state = updatedState;
    }

    return [getState, setState];
    }

    const [count, setCount] = useState(0);

    count(); // 0
    setCount(1);
    count(); // 1
  2. 緩存機制 (memoization)

    緩存機制也是透過 closure 讓內部函式記住外部的變數的特性,將計算過的值儲存在 cache 物件中,當下次使用相同的引數 (argument)呼叫時,就可以不用重新計算,直接取得已經儲存在 cache 物件的值。

    function memoize(cb) {
    const cache = {};
    return function (...args) {
    const key = JSON.stringify(args);

    if (key in cache) return cache[key];

    const result = cb.apply(this, args);

    cache[key] = result;

    return result;
    };
    }
  3. 模擬私有變數

    有時候我們不想讓暴露某些程式碼給外部使用,雖然 JavaScript 並不支援私有變數,但我們可以透過閉包達到類似的效果。

    const counter = function () {
    let privateCounter = 0;

    return {
    increment() {
    privateCounter += 1;
    },
    decrement() {
    privateCounter -= 1;
    },
    value() {
    return privateCounter;
    },
    };
    };

    // privateCounter 無法被外部修改
    // 因為 closure 的關係 increment 與 decrement 可以存取到 privateCounter
    // 因此要修改 privateCounter 只能夠透過 increment 與 decrement

    const counter1 = counter();
    counter1.increment();
    counter1.value(); // 1
    counter1.privateCounter; // undefined
  4. 柯里化

    const add = function (x) {
    return function (y) {
    return x + y;
    };
    };

    // 呼叫 add 函式並傳入引數 1
    // 呼叫 increment 函式,increment 函式能夠取得之前傳入的引數 1

    const increment = add(1);

    increment(2); // 3

closure vs. Class

在 ES6 以前,JavaScript 還沒有 Class,我們如果要模擬 OOP 的行為,一般都是透過 closure 達成。

closure

function counter() {
let count = 0;
return {
increment: () => count++,
};
}

const counter1 = counter();
counter1.increment(); // 0
counter1.increment(); // 1

class

class Counter {
constructor() {
this.count = 0;
}
increment() {
return this.count++;
}
}

const counter2 = new Counter();
counter2.increment(); // 0
counter2.increment(); // 1

雖然 closure 和 Class 在封裝邏輯和私有變數有許多相似之處,但它們是不同的概念,有著不同的用途:

  • closure:封裝私有變數和方法
  • Class:建立一個建立物件的藍圖

closure 和 Class 還有一個很大的不同,Class 創造出來的實例每次呼叫都是同一個 Class 方法,而透過 closure 建立的實例每次都是回傳一個全新的物件,因此每次實例呼叫的都是不同的方法。

// 透過 closure 建立
const counter1 = count();
const counter2 = count();

console.log(counter1.increment === counter2.increment); // false

// 透過 class 建立
const counter3 = new Count();
const counter4 = new Count();
console.log(counter3.increment === counter4.increment); // true

參考來源:

https://medium.com/@sustained_salmon_fly_484/javascript 的物件導向-closure-new-class-809e0970d566

https://www.explainthis.io/en/interview-guides/javascript/what-is-closure

https://www.explainthis.io/zh-hant/interview-guides/javascript/what-is-closure