首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

JavaScript 閉包深度解析:原理、應用模式與記憶體陷阱防範

     編輯:Java 相關知識

什麼是閉包?

閉包 = 函數 + 該函數定義時的詞法環境。簡單說:內部函數引用了外部函數的變數,並被外部函數以外的範圍引用時,這些變數不會隨外部函數結束而銷毀,而是被「閉包」保存起來。

底層機制:作用域鏈與 [[Environment]]

  • 每個函數建立時,會捕獲當前的詞法環境 (Lexical Environment) 參考,存於內部屬性 `[[Environment]]`。
  • 執行時,建立新的函數執行上下文,其外部環境參考指向 `[[Environment]]`,形成作用域鏈。
  • 變數查找順序:當前執行上下文 → `[[Environment]]` → 外層 `[[Environment]]` ... → 全域環境。

經典應用模式

1. 封裝私有狀態 (模組模式)

const createCounter = () => {
    let count = 0; // 私有變數,外部無法直接存取
    return {
        increment: () => ++count,
        decrement: () => --count,
        getValue: () => count
    };
};

const counterA = createCounter();
counterA.increment(); // 1
counterA.increment(); // 2
// count 變數被閉包保護,counterA 與 counterB 狀態隔離

2. 事件處理器綁定參數 (偏函數應用)

// 錯誤寫法:迴圈中 var 共享同一變數
for (var i = 0; i < 3; i++) {
    btns[i].onclick = () => console.log(i); // 全印 3
}

// 正確寫法 1:IIFE 建立新作用域
for (var i = 0; i < 3; i++) {
    ((index) => {
        btns[index].onclick = () => console.log(index); // 印 0,1,2
    })(i);
}

// 正確寫法 2:let 區塊作用域 (ES6+)
for (let i = 0; i < 3; i++) {
    btns[i].onclick = () => console.log(i); // 印 0,1,2
}

3. 延遲執行與 Curry 化

const delay = (fn, ms, ...args) => setTimeout(() => fn(...args), ms);

const greet = (name, greeting) => console.log(`${greeting}, ${name}!`);
delay(greet, 1000, 'Alice', 'Hello'); // 1秒後印出

// Curry 化:預設參數
const format = (prefix, suffix) => (str) => `${prefix}${str}${suffix}`;
const wrapBracket = format('[', ']');
wrapBracket('content'); // "[content]"

4. 記憶化

const memoize = (fn) => {
    const cache = new Map(); // 閉包保存快取
    return (...args) => {
        const key = JSON.stringify(args);
        if (cache.has(key)) return cache.get(key);
        const result = fn(...args);
        cache.set(key, result);
        return result;
    };
};

const fib = memoize(n => n <= 1 ? n : fib(n-1) + fib(n-2));

記憶體洩漏陷阱與防範

1. 循環引用 (舊版 IE) / 現代瀏覽器的無意保留

雖然現代 GC (Mark-and-Sweep) 能處理循環引用,但閉包讓變數存活時間延長,若不再需要卻未釋放引用,即造成洩漏。

// 典型洩漏:DOM 事件監聽器形成閉包,保有大量資料
function attachBigData(element, hugeData) {
    element.onclick = () => {
        // hugeData 被閉包捕獲,即使 element 從 DOM 移除,
        // 若事件監聽器未移除,hugeData 無法被 GC
        process(hugeData); 
    };
}

// 修正:使用 WeakMap 或移除監聽器
const dataMap = new WeakMap(); // 不阻礙 GC
function attachSafe(element, hugeData) {
    dataMap.set(element, hugeData);
    element.onclick = () => process(dataMap.get(element));
}
// element 移除且無其他引用時,hugeData 可被回收

2. 類別方法定義在建構式中 (每實例重建函數)

// 記憶體浪費:每個實例擁有獨立的 method 函數物件
class BadExample {
    constructor(data) {
        this.data = data;
        this.method = () => console.log(this.data); // 箭頭函數建立閉包
    }
}

// 優化:方法定義在 prototype 上
class GoodExample {
    constructor(data) { this.data = data; }
    method() { console.log(this.data); } // 共享同一函數
}

最佳實踐清單

  1. 善用 `let`/`const` 區塊作用域,減少 IIFE 需求。
  2. 事件監聽器務必 `removeEventListener`,或使用 `AbortController` (現代 API)。
  3. 大型資料用 `WeakMap`/`WeakRef` 關聯,不阻礙 GC。
  4. 類別方法寫在 `prototype` 或 `class` 體內,避免建構式內建立閉包函數。
  5. 閉包變數僅保存必要資料,避免捕獲巨大物件或整個 `this`。

總結

閉包是 JavaScript 函數式特性的核心,賦予了函數「記憶」出生環境的能力。理解其背後的作用域鏈機制,善用模組模式、Curry 化、記憶化等模式,並警惕非預期的變數捕獲導致記憶體洩漏,是進階前端工程師的必修課。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved