一、閉包是什么?
閉包是指函數可以“記住”并訪問它定義時的詞法作用域,即使這個函數在其作用域鏈之外執行。
簡單說:函數 A 在函數 B 中被定義,并在函數 B 外部被調用,它依然能訪問函數 B 中的變量,這就是閉包。
示例:
function outer() {let count = 0;return function inner() {count++;console.log(count);};
}const counter = outer(); // outer 執行,返回 inner
counter(); // 1
counter(); // 2
- counter 是 inner 函數。
- 雖然 outer 已經執行完畢,但 inner 仍能訪問 outer 中的變量 count。
- 因為 JS 引擎保留了 outer 的詞法作用域 —— 這就是閉包。
二、閉包的核心特性
- 函數嵌套函數
- 內部函數引用了外部函數的變量
- 外部函數執行后,其內部作用域仍被保留
三、常見的閉包封裝類型
1. 封裝私有變量
- 避免全局污染,創建私有作用域,保護變量不被外部修改。
示例:
function createCounter() {let count = 0;return {increment() {count++;console.log(count);},decrement() {count--;console.log(count);}};
}const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1
2. 防抖函數(Debounce)
- 控制函數的執行頻率,避免頻繁觸發(如搜索輸入框)
示例:
function debounce(fn, delay) {let timer = null;return function (...args) {clearTimeout(timer);timer = setTimeout(() => {fn.apply(this, args);}, delay);};
}
實際應用場景:
- 搜索輸入聯想(autocomplete)
- 描述:用戶輸入關鍵詞時,實時發送接口請求搜索建議;
- 問題:用戶每打一個字就觸發請求,接口壓力大;
- 防抖:用戶停止輸入一段時間(如 300ms)后再發起請求。
<input type="text" oninput="debounceSearch(event)">const debounceSearch = debounce((e) => {searchAPI(e.target.value);
}, 300);
- 窗口大小調整(resize)事件
- 描述:監聽 window.onresize 調整布局;
- 問題:調整過程中會瘋狂觸發;
- 防抖:等用戶停止調整再觸發邏輯。
window.addEventListener('resize', debounce(() => {updateLayout();
}, 200));
- 表單校驗(輸入完成后校驗)
- 輸入過程中不斷校驗字段太頻繁;
- 防抖校驗:等用戶停止輸入后再校驗格式/是否重復。
3. 節流函數(Throttle)
- 限制函數在某段時間內只執行一次,常用于滾動/resize等事件。
示例:
function throttle(fn, delay) {let lastTime = 0;return function (...args) {const now = Date.now();if (now - lastTime > delay) {fn.apply(this, args);lastTime = now;}};
}
實際應用場景:
- 頁面滾動事件(scroll)
- 描述:滾動時觸發監聽函數,計算位置、懶加載、吸頂等;
- 問題:滾動過程中觸發頻繁,影響性能;
- 節流:限制函數每隔 100ms 觸發一次。
window.addEventListener('scroll', throttle(() => {checkLoadMore();
}, 100));
- 按鈕點擊防止重復提交
- 描述:用戶頻繁點擊“提交”按鈕;
- 節流:按鈕點擊1秒內只能觸發一次。
<button onclick="throttleSubmit()">提交</button>const throttleSubmit = throttle(() => {submitForm();
}, 1000);
- 拖拽事件
- 拖動一個 DOM 元素時,mousemove 觸發頻繁;
- 節流避免頻繁 DOM 操作。
防抖和節流小結
技術 | 關鍵詞 | 作用 |
---|---|---|
防抖(debounce) | 最后一次 | 等用戶停止操作一段時間后再執行 |
節流(throttle) | 每隔一次 | 控制函數在一定時間內最多執行一次 |
4. 緩存函數(記憶函數)
- 對重復計算進行緩存優化(如遞歸計算斐波那契數)
示例:
function memoize(fn) {const cache = {};return function (key) {if (cache[key] !== undefined) {return cache[key];}const result = fn(key);cache[key] = result;return result;};
}const fib = memoize(function(n) {console.log("計算 fib(" + n + ")");if (n <= 1) return n;return fib(n - 1) + fib(n - 2);
});console.log(fib(5)); // 會打印很多“計算 fib(n)”
console.log(fib(5)); // 這次會直接用緩存,打印很少
5. 單例模式封裝
- 只創建一次實例(如彈窗、全局組件)
示例:
function getSingleton(fn) {let instance;return function (...args) {if (!instance) {instance = fn.apply(this, args);}return instance;};
}const createDialog = getSingleton(() => {const div = document.createElement('div');div.innerHTML = '我是彈窗';document.body.appendChild(div);return div;
});
- 第一次調用:instance 為 undefined,會執行 fn,并把返回值保存在 instance 中;
- 后續調用:直接返回第一次的 instance,不再執行 fn。
6. 柯里化函數(Currying)
- 把接受多個參數的函數,轉換成一系列接受單個參數的函數。
示例:
function curry(fn) {return function curried(...args) {if (args.length >= fn.length) {return fn(...args);}return function (...next) {return curried(...args, ...next);};};
}function add(a, b, c) {return a + b + c;
}const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 也可以,輸出 6
console.log(curriedAdd(1)(2, 3)); // 也可以,輸出 6
7. 工廠函數(封裝狀態)
- 用于組件、模塊創建,管理狀態
示例:
function createUser(name) {let _name = name;return {getName() {return _name;},setName(newName) {_name = newName;}};
}const user = createUser('77');
console.log(user.getName()); // 77
user.setName('88');
console.log(user.getName()); // 88
8. 延遲計算 / 延遲執行
- 按需執行邏輯,提高性能或解決作用域問題
示例:
function lazy(fn) {let cached = null;return function () {if (cached === null) {cached = fn();}return cached;};
}const getConfig = lazy(() => {console.log('讀取配置...');return { env: 'prod' };
});getConfig(); // 讀取配置...
getConfig(); // (不再重復執行)
四、閉包的注意事項
- 內存泄漏風險:閉包會長期保留作用域鏈,如果引用過多變量未釋放,可能導致內存問題。
- 調試難度稍高:變量作用域不明顯時,排查問題可能會變復雜。