闭包
闭包不是一个具体的技术,而是一种现象,是指在定义函数时,周围环境中的信息可以在函数中使用。换句话说,执行函数时,只要在函数中使用了外部的数据,就创建了闭包
实际用处:解决全局变量污染,手动创建的闭包用null销毁(解除引用)
闭包最核心的作用是:突破函数作用域的限制,让函数能 “记住” 并操作它定义时所在的外部环境中的变量,即使外部环境已经消失

那么多闭包,那岂不是占用内存空间么?
实际上,如果是自动形成的闭包,是会被销毁掉的
闭包是一个封闭的空间,里面存储了在其他地方会引用到的该作用域的值,在 JavaScript 中是通过作用域链来实现的闭包。
只要在函数中使用了外部的数据,就创建了闭包,这种情况下所创建的闭包,我们在编码时是不需要去关心的。
我们还可以通过一些手段手动创建闭包,从而让外部环境访问到函数内部的局部变量,让局部变量持续保存下来,不随着它的上下文环境一起销毁。
// 例子
for (var i = 0; i < 5; i++) {
setTimeout(function () { // 访问外层作用域的变量i(全局的),产生闭包
console.log(i)
}, 1000)
}
// 5 5 5 5:异步,循环结束才输出,输出时,找变量i,找到外层作用域的 i只有一个 ,此时i为5
// 闭包解决
for (var i = 0; i < 5; i++) {
(function (index) {
setTimeout(function () {
console.log(index) // 访问外层作用域的变量index(IIFE的局部变量),产生闭包
}, 1000)
})(i);
}
// ES6 的 let解决
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i) // 每个回调都产生闭包,访问外层作用域的变量i(let的块级作用域)
}, 1000)
}
补充
一、什么是闭包?
闭包是 JavaScript 中函数与其词法环境的组合——函数创建时会捕获其所处的词法作用域(定义时的作用域,而非执行时的作用域),即使函数在创建时的作用域之外执行,也能访问该作用域内的变量、参数和其他函数。
核心本质:
词法作用域是闭包的基础(函数的作用域由定义位置决定,而非调用位置);
闭包的核心是「函数+创建时的词法环境引用」,让函数突破定义时的作用域限制。
极简示例:
function outer() {
// 外部函数的局部变量(词法环境中的变量)
let count = 0;
// 内部函数(闭包):捕获 outer 的词法环境
function inner() {
count++; // 访问外部函数的局部变量
console.log(count);
}
return inner; // 外部函数返回内部函数(突破作用域限制)
}
// 调用 outer 后,inner 被赋值给 add,此时 outer 已执行完毕
const add = outer();
add(); // 1(inner 仍能访问 outer 的 count 变量)
add(); // 2(count 未被销毁,生命周期被延长)
二、闭包解决了什么问题?
闭包的核心价值是突破作用域限制,解决了 JavaScript 中「变量访问范围」和「生命周期」的关键问题,主要应用场景有 3 类:
1. 实现变量私有化,避免全局污染
JavaScript 早期没有「块级作用域」(ES6 前只有函数作用域和全局作用域),全局变量容易被篡改,闭包可以模拟「私有变量」,让变量仅能通过指定接口访问。
示例(模块化封装私有变量):
const counterModule = (function() {
// 私有变量(外部无法直接访问)
let privateCount = 0;
// 私有方法(仅内部可用)
function privateLog(msg) {
console.log(`${msg}: ${privateCount}`);
}
// 暴露公共接口(闭包:访问私有变量/方法)
return {
increment: function() {
privateCount++;
},
log: function() {
privateLog("当前计数"); // 1 → 2 → ...
}
};
})();
counterModule.increment();
counterModule.log(); // 当前计数: 1
console.log(counterModule.privateCount); // undefined(无法直接访问私有变量)
2. 延长变量的生命周期
函数执行完毕后,其作用域通常会被垃圾回收机制(GC)销毁,但闭包会引用该作用域的变量,导致作用域无法销毁,变量生命周期被延长。
示例(定时器/回调中访问局部变量):
function getTimer() {
let seconds = 0;
// 定时器回调是闭包,捕获 seconds
setInterval(function() {
seconds++;
console.log(`已运行 ${seconds} 秒`);
}, 1000);
}
getTimer(); // 每隔 1 秒输出,seconds 持续累加(未被 GC 回收)
3. 实现模块化(早期无 ES6 Module 时)
闭包是 JavaScript 早期实现模块化的核心方案(如 CommonJS 早期原理),通过「私有作用域+暴露公共接口」,将代码分割为独立模块,避免命名冲突。
示例(工具函数模块):
const mathTool = (function() {
const PI = 3.14159; // 私有常量
return {
circleArea: function(r) {
return PI * r * r; // 闭包访问 PI
},
circleCircumference: function(r) {
return 2 * PI * r;
}
};
})();
console.log(mathTool.circleArea(2)); // 12.56636
console.log(mathTool.PI); // undefined(私有变量不可访问)
三、闭包带来什么问题?
闭包的「延长变量生命周期」特性是把双刃剑,滥用会引发以下问题:
1. 内存泄漏(最核心问题)
闭包引用的词法环境(含变量、参数)会被持续持有,即使这些变量不再需要,也无法被 GC 回收,导致内存占用过高,长期积累可能引发页面卡顿、崩溃。
示例(不当闭包导致内存泄漏):
function createBigClosure() {
// 模拟大数据(100万条数据,占用大量内存)
const largeData = new Array(1000000).fill('large-data');
// 闭包引用 largeData,即使外部不需要,也无法回收
return function() {
console.log(largeData.length);
};
}
const unusedClosure = createBigClosure();
// 即使 unusedClosure 不再使用,largeData 仍被引用,无法 GC
// 解决:不再使用时手动释放引用 → unusedClosure = null;
闭包导致内存泄漏详细解释解析:
持有了不在需要的函数引用,会导致函数关联的词法环境无法销毁
当多个函数共享词法环境,会导致词法环境膨胀,从而导致出现无法触达也无法回收的内存空间
2. 变量共享陷阱(循环中常见)
若多个闭包共享同一个词法环境的变量,可能导致意外的变量覆盖,典型场景是「循环中创建闭包」(ES6 前 var 无块级作用域时)。
示例(循环闭包的变量共享问题):
// 期望输出 0、1、2,实际输出 3、3、3
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 所有闭包共享同一个 i(循环结束后 i=3)
}, 1000);
}
// 解决方案:
// 1. ES6 用 let 创建块级作用域(每个循环迭代有独立 i)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000); // 0、1、2
}
// 2. ES5 用 IIFE 传参(创建独立作用域)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 1000); // 0、1、2
})(i);
}
3. 调试复杂度提升
闭包的作用域链嵌套较深,变量的来源不直观(可能来自外层函数、外层的外层函数),调试时难以追踪变量的赋值和修改路径,增加排查问题的难度。
4. 性能损耗
闭包需要维护额外的词法环境引用,函数执行时会多一层作用域链查找(而非直接访问当前作用域变量),频繁创建或调用闭包可能带来轻微的性能开销(通常不明显,滥用时才会凸显)。
四、总结
闭包定义:函数+创建时的词法环境引用,让函数在定义作用域外仍能访问内部变量;
解决的问题:变量私有化、延长变量生命周期、实现模块化;
带来的问题:内存泄漏、变量共享陷阱、调试困难、轻微性能损耗;
使用建议:合理使用闭包(如模块化、私有变量场景),避免滥用;不再使用时手动释放闭包引用(
closure = null),减少内存泄漏风险。
闭包
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。



评论交流
欢迎留下你的想法