閉包 (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。
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)特性,可以從以下幾個方面來說明:
-
變量引用的維持:閉包中引用的外部變量會使得這些變量的作用域鏈被保留在內存中,即使外部函數執行完畢,這些變量也不會被釋放。
-
內存洩漏風險:如果閉包中引用的外部變量是一個較大的對象或者數組,並且這個閉包長時間存在,那麼這個對象或數組也會被保留在內存中,可能導致內存洩漏問題。
-
引用計數算法:JavaScript 的垃圾回收器通常使用引用計數算法來判斷對象是否可以被回收。在閉包中,如果存在循環引用(例如一個對象引用了閉包中的函數,而閉包中的函數又引用了這個對象),那麼這些對象將無法被正常回收,即使它們已經不再被程式所使用。
-
手動解除引用:為了避免閉包導致的內存洩漏問題,開發者可以通過手動解除對閉包的引用來釋放相關的內存,例如將閉包賦值為 null。這樣一來,閉包中引用的外部變量及其作用域鏈就會被釋放,相應的內存也會被回收。
結論,閉包在 JavaScript 中是一種強大的特性,但在使用時需要注意內存管理,特別是在涉及到長時間存在的閉包時,要注意避免因閉包而導致的內存洩漏問題。