JavaScript
[JS] 閉包 (Closure)

閉包 (Closure)

JavaScript 的閉包(closure)是指在函數內部創建的函數,它可以訪問其外部函數作用域中的變量。閉包的特性之一是它可以捕獲並維持其創建時的作用域鏈。 React 中的 useState 就是透過閉包來實作。

什麼是閉包?

在 MDN 文件中,閉包被定義為函式及其宣告時所在的作用域環境的組合。簡單來說,閉包就是內部函式能夠存取外部函式的變數並記住它。閉包常被用來實現狀態保存

以下是一個簡單的例子。在下方程式碼中,inner 函式可以訪問 outer 函式的 a 變數,並將其保存在記憶體中。每次呼叫 inner 時,a 的狀態都會被記住,因此結果是遞增的。

function outer() {
  let a = 0;
  function inner() {
    a += 1;
    console.log(a);
  }
  return inner;
}
 
const inner = outer();
 
inner(); // 1
inner(); // 2
inner(); // 3

總結來說,閉包允許內部函式存取外部函式的作用域,並記住外部函式的變數。

閉包的應用

1. 狀態保存

在程式開發中,常需要管理狀態。React 提供的 useState 函式庫可用來管理狀態。以下是一個簡化版的 useState 模擬。函式 getState 和 setState 可以在外部函式中取得和更新狀態。

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
setCount(500);
count(); // 500

React 核心團隊成員 Sebastian Markbåge 分享了一段程式碼,說明了 React 中的伺服器動作,可以利用閉包進行版本檢查。以下程式碼中的 verifiedVersion 記住了首次渲染時的版本,內部函式 publish 可以取得 verifiedVersion。

closure

2. 緩存機制

閉包能夠實現緩存機制,因為內部函式可以記住外部變數。下面的範例中,由於閉包的原理,cache 變數可以被回傳的箭頭函式取得和記住,因此可以重複使用 cache 來存放想要緩存的東西。

function cached(fn) {
  const cache = {};
 
  return (...args) => {
    const key = JSON.stringify(args);
 
    if (key in cache) {
      return cache[key];
    } else {
      const val = fn(...args);
      cache[key] = val;
      return val;
    }
  };
}

3. 模擬私有變數

JavaScript 沒有直接支援私有變數,但可以利用閉包實現類似的功能。下面的範例中,privateCounter 無法被外部修改,因為 increment 和 decrement 可以存取到 privateCounter,使其只能透過這兩個函式來修改。

// privateCounter 沒被法被外部修改,
// 因為閉包的關係 increment 與 decrement 可以存取到 privateCounter
// 因此 privateCounter 只能夠透過 increment 與 decrement 來改,這能有效避免被誤觸到
var counter = (function () {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function () {
      changeBy(1);
    },
    decrement: function () {
      changeBy(-1);
    },
    value: function () {
      return privateCounter;
    },
  };
})();
 
console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1

閉包的缺點: 內存洩漏

雖然閉包有許多優點,但也存在缺點。從記憶體的角度來看,閉包可能導致內存洩漏,因為內部函式會記得外部變數,使其常駐在記憶體中。需要注意避免過度使用。

在這個例子中,longArray 並未被使用,但由於閉包的存在,它仍然被 addNumbers 函式記憶。即使 longArray 沒有使用到,但它仍然佔用著記憶體,這就是典型的內存洩漏。

function outer() {
  const longArray = [];
  return function inner(num) {
    longArray.push(num);
  };
}
const addNumbers = outer();
 
for (let i = 0; i < 100000000; i++) {
  addNumbers(i);
}

Factory Function

A factory function is a function that returns a new object.

垃圾回收機制 (Garbage Collection)

關於 JavaScript 閉包的垃圾回收(garbage collection)特性,可以從以下幾個方面來說明:

  1. 變量引用的維持:閉包中引用的外部變量會使得這些變量的作用域鏈被保留在內存中,即使外部函數執行完畢,這些變量也不會被釋放。

  2. 內存洩漏風險:如果閉包中引用的外部變量是一個較大的對象或者數組,並且這個閉包長時間存在,那麼這個對象或數組也會被保留在內存中,可能導致內存洩漏問題。

  3. 引用計數算法:JavaScript 的垃圾回收器通常使用引用計數算法來判斷對象是否可以被回收。在閉包中,如果存在循環引用(例如一個對象引用了閉包中的函數,而閉包中的函數又引用了這個對象),那麼這些對象將無法被正常回收,即使它們已經不再被程式所使用。

  4. 手動解除引用:為了避免閉包導致的內存洩漏問題,開發者可以通過手動解除對閉包的引用來釋放相關的內存,例如將閉包賦值為 null。這樣一來,閉包中引用的外部變量及其作用域鏈就會被釋放,相應的內存也會被回收。

結論,閉包在 JavaScript 中是一種強大的特性,但在使用時需要注意內存管理,特別是在涉及到長時間存在的閉包時,要注意避免因閉包而導致的內存洩漏問題。

References