JavaScript] - Closure
上篇簡單講述了閉包的特性之後,今天來點不同的舉例來加深印象! 先來看看以下程式碼: 是不是會很好奇,為什麼輸出都是 ? 回想一下閉包的特性,雖然上面的程式碼似乎應該依次輸出 、, 但實際上它們都輸出 的原因是因為在迴圈中創建的匿名函式捕獲了變數 的引用,而不是其值。 在 函式中,我們創建了一個函式陣...
上篇簡單講述了閉包的特性之後,今天來點不同的舉例來加深印象!
先來看看以下程式碼:
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?
回想一下閉包的特性,雖然上面的程式碼似乎應該依次輸出 0、1,
但實際上它們都輸出 5 的原因是因為在迴圈中創建的匿名函式捕獲了變數 i 的引用,而不是其值。
在 createFunctionArray 函式中,我們創建了一個函式陣列 functionArray,
並使用一個迴圈來添加匿名函式到這個陣列中。這些匿名函式都捕獲了外部作用域的變數 i。
當迴圈完成並退出後,i 的值等於 5,因為這是使迴圈停止的條件。
由於這些匿名函式仍然引用相同的 i,當呼叫這些函式時,都將使用當前的 i 值,即 5。
這就是為什麼無論呼叫 functions[0]() 還是 functions[1](),都輸出 5 的原因。
那麼要如何讓上面的輸出結果是 0、1呢?
我們可以在每次迴圈迭代時創建一個新的作用域,這樣每個匿名函式都會捕獲不同的 i 值。
- 方式一:使用
let來產生區塊作用域
function createFunctionArray() {
var functionArray = [];
for (let i = 0; i < 5; i++) { // 使用 let 創建區塊作用域
functionArray.push(function() {
console.log(i);
});
}
return functionArray;
}
var functions = createFunctionArray();
functions[0](); // 輸出 0
functions[1](); // 輸出 1
使用 let 創建的 i 變數在每次迴圈迭代時都有自己的區塊作用域,
因此每個匿名函式都能正確地捕獲到其自己的 i 值。
- 方式二:IIFE (Immediately Invoked Function Expression)(立即呼叫函式表示法)
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 可用於創建模組化的程式碼區塊,其中可以定義私有變數和函式, 並通過返回公共接口來封裝它們,以供外部使用。
閉包實際應用
-
封裝私有變數和函式: 閉包可以用來創建具有私有變數的函式,以增加安全性。
function createCounter() { let count = 0; return { increment: function () { count++; }, getCount: function () { return count; } }; } const counter = createCounter(); counter.increment(); console.log(counter.getCount()); // 輸出 1 -
事件處理程序: 閉包可用於處理事件。當使用者點擊按鈕時,可以使用閉包來記錄點擊次數。
const button = document.getElementById('likeButton'); let clickCount = 0; button.addEventListener('click', function () { clickCount++; console.log(`點擊次數:${clickCount}`); }); -
setTimeout 和 setInterval: 使用閉包可以創建具有狀態的定時任務。
function createTimer() { let seconds = 0; return function () { seconds++; console.log(`過去秒數:${seconds}`); }; } const timer = createTimer(); setInterval(timer, 1000); -
模組模式: 閉包可用於創建模組,將相關的函式和數據封裝在一起,以提供更好的代碼組織。 假設我們正在建立一個計數器模組:
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和三個公開方法:increment、decrement和getCount。 這樣,我們可以使用模組來管理計數器的狀態,同時保護了count變數, 使其無法被外部直接訪問或修改,提供了更好的代碼組織和隔離。 -
快取: 閉包可用於創建簡單的快取,以避免重複計算。
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)