閉包
閉包的官方定義是:一個表達式(通常是一個函數),它具有多個變量,并綁定到一個包含這些變量的環境。
在 JavaScript 中,閉包指的是函數即使在執行并離開其定義的詞法作用域后,仍能夠訪問該作用域的能力。這是因為當函數被創建時,它會生成一個閉包,其中包含對當前函數定義環境的引用,從而使函數能夠繼續訪問該環境中的變量。
以下是一個閉包的示例:
function makeCounter() {
var count = 0;
return function() {
return ++count;
};
}
var counter = makeCounter();
console.log(counter()); // 輸出:1
console.log(counter()); // 輸出:2
console.log(counter()); // 輸出:3
在這個示例中,makeCounter
函數返回一個匿名函數,并在makeCounter
函數的作用域內定義了一個count
變量。當makeCounter
函數執行時,它返回匿名函數并將其賦值給變量counter
。每次調用counter
函數時,它都會訪問makeCounter
函數作用域內的count
變量并遞增它。由于匿名函數在創建時形成了一個閉包,所以即使makeCounter
函數執行完畢,counter
函數仍然可以訪問makeCounter
函數作用域內的count
變量,從而實現了計數器的功能。
閉包具有以下特點:
- 閉包可以訪問外部函數作用域中的變量,即使外部函數已經返回。
- 閉包持有對外部函數作用域的引用,這可能導致內存泄漏。
- 閉包可以在多個函數之間共享狀態,但需要注意避免意外修改該狀態。
由于閉包的特殊性質,它們在 JavaScript 中被廣泛用于實現模塊化、封裝私有變量等。然而,由于閉包可能導致內存泄漏等問題,使用時應謹慎。
閉包的實現原理
閉包的實現原理基于 JavaScript 的函數作用域和作用域鏈機制。當函數被定義時,它會創建一個新的作用域,并在該作用域中保存當前的變量環境。當函數執行時,它會創建一個新的執行環境,并在該執行環境中保存當前的作用域鏈。函數執行完畢后,它會銷毀執行環境和作用域鏈,但作用域中的變量仍然保存在內存中。
當函數返回一個內部函數時,內部函數仍然可以訪問外部函數的作用域和變量,因為其作用域鏈包含外部函數的作用域鏈。這就創建了一個閉包,使得內部函數可以訪問外部函數的變量,并且這些變量直到內部函數被銷毀時才會被釋放。
以下是一個演示閉包實現原理的示例:
function outer() {
let x = 10;
return function inner() {
console.log(x);
};
}
const innerFn = outer();
innerFn(); // 輸出 10
在這個示例中,outer
函數返回一個內部函數inner
,它可以訪問outer
函數中的變量x
。在outer
函數執行完畢后,變量x
仍然保存在內存中,因為inner
函數形成了一個閉包,并且可以訪問outer
函數的變量和作用域。
閉包的使用場景
實現私有變量、方法和模塊化
// 模塊化計數器
const counterModule = (function() {
let count = 0; // 私有變量
function increment() { // 私有方法
count++;
console.log(`counter value: ${count}`);
}
function reset() { // 私有方法
count = 0;
console.log('counter is reset');
}
return { // 暴露公共方法
increment,
reset
}
})();
// 使用模塊
counterModule.increment(); // counter value: 1
counterModule.increment(); // counter value: 2
counterModule.reset(); // counter is reset
在上述代碼中,我們使用立即調用函數表達式(IIFE)返回一個包含兩個公共方法increment
和reset
的對象,這些方法可以從外部訪問。count
變量以及increment
和reset
方法是私有的。這樣,我們可以以模塊化的方式組織代碼,避免全局變量污染,同時保護私有變量和方法免受外部干擾。
實現函數記憶化
function memoize(fn) {
const cache = {}; // 緩存計算結果
return function(...args) {
const key = JSON.stringify(args); // 將參數轉換為緩存鍵
if (cache[key] === undefined) { // 如果結果不存在,則計算并緩存
cache[key] = fn.apply(this, args);
}
return cache[key]; // 返回緩存的計算結果
};
}
function factorial(n) {
console.log(`calculating ${n} factorial`);
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}
const memoizedFactorial = memoize(factorial);
console.log(memoizedFactorial(5)); // calculating 5 factorial 120
console.log(memoizedFactorial(5)); // 120
console.log(memoizedFactorial(3)); // calculating 3 factorial 6
console.log(memoizedFactorial(3)); // 6
在上述代碼中,我們定義了一個memoize
函數,它接受一個函數fn
作為參數,并返回一個新函數,該新函數緩存fn
的計算結果以避免重復計算。具體來說,我們使用一個名為cache
的對象來存儲計算結果,并返回一個在外部memoize
函數中引用cache
對象和fn
函數的閉包。在閉包中,我們使用JSON.stringify
將輸入參數轉換為字符串作為緩存鍵,然后檢查結果是否已在緩存中。如果是,則直接返回;否則,計算結果并將其保存到緩存中。最后,我們可以使用memoize
函數包裝任何需要記憶化的函數以避免重復計算。在上述代碼中,我們使用memoize
函數包裝了一個計算階乘的函數factorial
,并將其命名為memoizedFactorial
。我們可以看到,第一次計算一個數的階乘時,它會輸出正在計算的消息。然而,當我們再次計算相同的數時,它不會輸出消息,因為結果已經被緩存。
避免循環中的作用域問題
此技術還可用于避免循環中的作用域問題。當我們在循環中定義一個函數時,它可能會引用循環變量,但由于 JavaScript 的作用域規則,當函數被調用時,循環變量的值可能不是我們期望的。通過使用閉包為循環的每次迭代創建一個新的作用域,我們可以避免這個問題。
function createFunctions() {
const result = [];
for (var i = 0; i < 5; i++) {
result[i] = function(num) {
return function() {
return num;
};
}(i);
}
return result;
}
const funcs = createFunctions();
console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2
console.log(funcs[3]()); // 3
console.log(funcs[4]()); // 4
在上述代碼中,我們定義了一個createFunctions
函數,它返回一個包含五個函數的數組。這些函數的目的是返回它們在數組中的索引。我們使用閉包來避免循環中的作用域問題。具體來說,我們在循環中定義一個立即調用的匿名函數,它接受一個參數num
并返回一個新函數,該新函數始終返回num
。然后,我們立即調用這個匿名函數,將i
作為參數傳遞,并將返回的函數保存到數組result
的相應位置。由于匿名函數返回一個新函數,并且這個新函數引用外部函數createFunctions
中的變量num
,每個函數都會記錄其在數組中的索引。因此,當我們調用這些函數時,它們將返回它們在數組中的索引,而不是循環變量i
的值。最后,我們使用createFunctions
函數創建一個包含五個返回自身索引的函數的數組,并分別調用這些函數,輸出它們的返回值。
在異步編程中保存狀態
function createIncrementer() {
let count = 0;
function increment() {
count++;
console.log(`Count: ${count}`);
}
return {
incrementAsync() {
setTimeout(() => {
increment();
}, 1000);
}
};
}
const incrementer = createIncrementer();
incrementer.incrementAsync(); // Count: 1
incrementer.incrementAsync(); // Count: 2
incrementer.incrementAsync(); // Count: 3
在上述代碼中,我們定義了一個createIncrementer
函數,它返回一個包含incrementAsync
方法的對象。該方法將在 1 秒后調用內部的increment
函數。increment
函數通過閉包訪問在外部函數createIncrementer
中定義的變量count
,因此它可以在多次調用incrementAsync
方法之間持續跟蹤計數。我們創建了一個incrementer
對象,并多次調用其incrementAsync
方法,每次都會在 1 秒后輸出當前的計數值。請注意,在此過程中我們沒有顯式傳遞任何參數,而是使用閉包來維護計數狀態,從而避免了在異步編程中手動傳遞狀態的麻煩。
實現函數柯里化
實現以下代碼的柯里化函數。
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum);
curriedSum(1, 2, 3); // 6
curriedSum(1)(2, 3); // 6
curriedSum(1, 2)(3); // 6
curriedSum(1)(2)(3); // 6
實現高階函數
計算執行時間的高階函數
function timingDecorator(fn) {
return function() {
console.time("timing");
const result = fn.apply(this, arguments);
console.timeEnd("timing");
return result;
};
}
const add = function(x, y) {
return x + y;
};
const timingAdd = timingDecorator(add);
console.log(timingAdd(1, 2));
緩存返回結果的高階函數
function memoizeDecorator(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const fibonacci = function(n) {
if (n < 2) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
};
const memoizeFibonacci = memoizeDecorator(fibonacci);
console.log(memoizeFibonacci(10));
實現延遲執行函數
function delayDecorator(fn, delay) {
return function() {
const args = arguments;
setTimeout(function() {
fn.apply(this, args);
}, delay);
};
}
const sayHello = function(name) {
console.log(`Hello, ${name}!`);
};
const delayedHello = delayDecorator(sayHello, 1000);
delayedHello("John");
實現生成器
function makeGenerator(array) {
let index = 0;
return function() {
if (index < array.length) {
return { value: array[index++], done: false };
} else {
return { done: true };
}
};
}
const generator = makeGenerator([1, 2, 3]);
let result = generator();
while (!result.done) {
console.log(result.value);
result = generator();
}
在這個示例中,我們定義了一個名為makeGenerator
的函數,它接受一個數組作為參數并返回一個新函數。這個新函數使用閉包在內部保存數組索引,并根據索引依次返回數組中的每個元素,直到所有元素都被返回。我們將數組[1, 2, 3]
傳遞給makeGenerator
函數,并將返回的函數賦值給變量generator
。然后,我們調用generator
函數逐個檢索數組中的元素并將它們輸出到控制臺。這樣,我們可以輕松地使用閉包實現生成器,并以惰性方式逐個生成值,避免了一次性計算所有值所帶來的性能和內存消耗問題。同時,使用閉包可以維護函數的狀態和作用域,避免全局變量污染和變量沖突等問題。
實現事件監聽器
function createEventListener(element, eventName, handler) {
element.addEventListener(eventName, handler);
return function() {
element.removeEventListener(eventName, handler);
};
}
const button = document.getElementById("myButton");
const onClick = function() {
console.log("Button clicked!");
};
const removeEventListener = createEventListener(button, "click", onClick);
setTimeout(function() {
removeEventListener();
}, 5000);
在這個示例中,我們定義了一個createEventListener
函數,它接受一個 DOM 元素、一個事件名稱和一個事件處理函數作為參數,并返回一個新函數。這個新函數使用閉包在內部保存 DOM 元素、事件名稱和事件處理函數,并在執行時向 DOM 元素添加一個事件監聽器。我們將一個按鈕元素、一個點擊事件處理函數和事件名稱“click”傳遞給createEventListener
函數,并將返回的函數賦值給removeEventListener
。然后,我們在一段時間后通過調用removeEventListener
函數手動移除事件監聽器,以停止響應按鈕點擊事件。這樣,我們可以使用閉包輕松實現事件監聽器,并靈活控制其生命周期,避免內存泄漏和性能問題。使用閉包還允許我們維護函數的狀態和作用域,避免全局變量污染和變量沖突。
該文章在 2024/11/6 10:33:26 編輯過