闭包
约 1793 字大约 6 分钟
2026-04-16
1. 闭包的定义
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
--《你不知道的 JavaScript》
闭包是 JavaScript 中一个强大的特性,也是面试中的高频考点。简单来说,闭包就是:
- 一个函数能够访问其词法作用域外部的变量
- 即使这些变量在函数执行完毕后本应被销毁,闭包仍然能够保持对这些变量的引用
2. 闭包的形成过程
2.1 作用域链的概念
每创建一个函数,会形成一条作用域链,从内到外依次是:
- 函数自身的执行上下文(AO)
- 外层函数的执行上下文
- 全局执行上下文(GO)
当函数访问变量时,会沿着这条作用域链从内到外查找。
function outer() {
const a = 1;
function inner() {
const b = 2;
console.log(a); // 沿着作用域链查找 a
}
inner();
}
outer();2.2 函数作为返回值被保存到外部
闭包形成的关键在于:函数被作为返回值保存到外部变量。
function fn1() {
const a = 1;
return function fn2() {
console.log(a);
};
}
const fn3 = fn1(); // fn1 执行完毕,返回的 fn2 被保存到 fn3
fn3(); // 仍然可以访问 a2.3 外部函数执行完毕后,其作用域是否被销毁
正常情况下,当函数执行完毕后,其作用域(AO)会被垃圾回收器释放。
但闭包特殊的地方在于:即使外部函数执行完毕,只要内部函数还持有对其作用域的引用,该作用域就不会被销毁。
function fn1() {
const a = 1;
return function fn2() {
console.log(a);
};
}
const fn3 = fn1();
// fn1 执行完毕后,其 AO 本应被销毁
// 但由于 fn2 仍然持有对 fn1 AO 的引用,所以 fn1 的 AO 不会被销毁
// fn2 形成闭包,保持对 a 的引用3. 闭包的典型应用场景
3.1 模块模式(私有变量)
利用闭包实现模块的私有变量和私有方法。
const Counter = (function() {
let count = 0; // 私有变量
return {
increment: function() {
count++;
console.log(count);
},
decrement: function() {
count--;
console.log(count);
},
getCount: function() {
return count;
}
};
})();
Counter.increment(); // 1
Counter.increment(); // 2
Counter.getCount(); // 2
console.log(count); // ReferenceError: count is not defined3.2 数据缓存 / 记忆函数
利用闭包缓存计算结果,避免重复计算。
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('从缓存读取');
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoizedFib = memoize(fibonacci);
memoizedFib(10); // 首次计算
memoizedFib(10); // 从缓存读取3.3 柯里化
将多参数函数转化为多个单参数函数的链式调用。
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
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)); // 63.4 防抖和节流
防抖和节流是性能优化的重要手段,都依赖闭包来维护状态。
// 防抖:防止重复点击,在指定时间后才执行
function debounce(fn, delay) {
let timeout = null;
return function(...args) {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 节流:指定时间间隔内只执行一次
function throttle(fn, interval) {
let canRun = true;
return function(...args) {
if (!canRun) return;
canRun = false;
setTimeout(() => {
fn.apply(this, args);
canRun = true;
}, interval);
};
}4. 经典面试题分析
4.1 for 循环 + setTimeout
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 100);
}
// 输出:5 5 5 5 5分析:
var是函数作用域,不是块级作用域- 循环结束后,
i的值是 5 - setTimeout 是异步的,等到执行时,访问的都是同一个
i
解决方案一:使用 let
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 100);
}
// 输出:0 1 2 3 4let 是块级作用域,每次循环都会创建一个新的 i,形成 5 个闭包。
解决方案二:使用立即执行函数(IIFE)
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(() => {
console.log(j);
}, 100);
})(i);
}
// 输出:0 1 2 3 4解决方案三:使用 bind
for (var i = 0; i < 5; i++) {
setTimeout(console.log.bind(null, i), 100);
}
// 输出:0 1 2 3 44.2 var 与 let 的差异
| 特性 | var | let |
|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 |
| 变量提升 | 声明提升,初始化不提升 | 暂时性死区(TDZ) |
| 重复声明 | 允许 | 不允许 |
| 全局属性 | 会成为 window 属性 | 不会 |
// var 变量提升
console.log(a); // undefined
var a = 1;
// let 暂时性死区
console.log(b); // ReferenceError
let b = 2;4.3 闭包引发的内存问题
function createFunctions() {
const arr = new Array(10000); // 占用大量内存
return function() {
console.log(arr.length);
};
}
const fn = createFunctions();
// 即使 fn 很小,但它持有了对 createFunctions 作用域的引用
// 导致 10000 个元素的数组无法被回收5. 闭包的优缺点
5.1 优点
- 封装私有变量:模块化编程,隐藏实现细节
- 记忆函数状态:保持函数的状态不被垃圾回收
- 实现高级函数:如防抖、节流、柯里化等
5.2 缺点
- 内存泄漏风险:闭包会持有对外部变量的引用,如果闭包长期存在,这些变量就无法被回收
- 内存占用增加:闭包会导致外部函数的作用域无法被销毁,直到闭包被释放
- 性能问题:过度使用闭包可能导致内存占用过大,影响性能
6. 如何避免闭包造成的内存问题
6.1 手动解除引用
在不需要闭包时,手动将引用置为 null。
function heavy() {
const data = new Array(100000);
return function() {
console.log(data.length);
};
}
let fn = heavy();
// 使用完毕后
fn = null; // 解除引用,让垃圾回收器回收内存6.2 使用块级作用域
在不需要的地方使用 let 替代 var,让变量在块级作用域结束后自动释放。
// 优化前
function process() {
for (var i = 0; i < 1000; i++) {
// ...
}
// i 仍然存在,浪费内存
}
// 优化后
function process() {
for (let i = 0; i < 1000; i++) {
// ...
}
// i 在循环结束后自动释放
}6.3 避免在循环中创建闭包
循环中创建闭包时,使用立即执行函数或 let 替代 var。
// 优化前
for (var i = 0; i < 5; i++) {
arr[i] = function() {
return i;
};
}
// 优化后:使用 let
for (let i = 0; i < 5; i++) {
arr[i] = function() {
return i;
};
}6.4 及时清理事件监听
在使用 addEventListener 时,在不需要时及时移除。
function setupHandler() {
const data = { /* 大对象 */ };
function handler() {
console.log(data);
}
document.addEventListener('click', handler);
// 返回清理函数
return function cleanup() {
document.removeEventListener('click', handler);
// 手动解除引用
// data = null;
};
}
const cleanup = setupHandler();
// 需要清理时
cleanup();总结
闭包是 JavaScript 的核心概念之一,理解闭包的形成原理对于深入学习 JavaScript 至关重要。在实际开发中,我们应该:
- 合理利用闭包实现模块化和私有变量
- 注意避免闭包导致的内存泄漏
- 在不需要闭包时及时解除引用
参考资料
