Javascript闭包
什么是闭包(Closure)?
- 闭包让函数可以访问它外部的变量,即使外部函数已经执行完毕。
function outer() {
let count = 0;
return function inner() {
count++;
console.log(count);
};
}
const counter = outer();
counter(); // 输出 1
counter(); // 输出 2
在上面的例子中,inner 函数形成了一个闭包,它捕获了外部函数 outer 中的变量 count。即使 outer 函数执行完毕,count 依然保存在内存中,因为 inner 函数仍然引用着它。
闭包的工作机制
JavaScript 的作用域是基于词法作用域,也就是说函数的作用域在函数定义时就已经确定。闭包的形成依赖于这一机制:
- 函数定义时,记录下其所在的作用域环境。
- 当函数执行时,可以访问定义时作用域中的变量。
- 即使外部函数执行结束,作用域链依然被闭包中的函数保留。
浏览器或 JavaScript 引擎通过作用域链保证闭包变量的生存周期不会被垃圾回收。
闭包的应用场景
1. 数据封装与私有变量
JavaScript 没有原生的私有属性,闭包可以实现变量的私有化,防止外部直接访问和修改。
function createCounter() {
let count = 0;
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
这里的 count 只能通过 increment 和 decrement 方法访问,避免了外部直接篡改。
2. 函数工厂
闭包可以用来创建带有预设参数的函数,称为函数工厂。
function multiplyBy(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplyBy(2); //multiplyBy(2)返回一个函数,这个函数会把输入的数字乘以2.
console.log(double(5)); // 10
const triple = multiplyBy(3);
console.log(triple(5)); // 15
multiplyBy 是一个函数工厂:它根据传入的 factor 生成一个新的函数。
闭包的作用:返回的函数记住了 factor,即使外层函数执行完毕,factor 仍然可用。
3. 事件处理和异步编程
闭包可以保存事件处理函数或异步回调中的变量状态。
for (var i = 1; i <= 3; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i);
}
// 输出:1, 2, 3,分别在1秒、2秒、3秒后
使用立即执行函数表达式(IIFE)形成闭包,避免了 var 声明变量被循环覆盖的问题。
使用闭包时的注意点
1. 内存泄漏风险
闭包持有外部变量引用,可能导致一些对象无法被垃圾回收。如果闭包过度使用且持有大量数据,可能造成内存泄漏。应避免在闭包中引用不必要的变量。
2. JavaScript 的垃圾回收机制
JavaScript 使用标记清除(Mark-and-Sweep)垃圾回收算法:
- 引擎从根对象(如全局对象、当前执行栈中的变量)开始标记所有可达的对象。
- 未被标记的对象视为不可达对象,会被回收释放内存。
闭包中的变量只要被闭包函数引用,变量就被视为“可达”,不会被回收。
3. 闭包内存回收的时机
闭包的内存回收依赖于闭包本身的生命周期:
- 闭包不再被任何变量引用时,闭包及其闭合的作用域变量才可被回收.
- 只要闭包函数仍被引用,闭合的数据就不能被回收。
示例:
function makeCounter() {
let count = 0;
return function() {
return ++count;
};
}
const counter = makeCounter();
console.log(counter()); // 1, // 此时 count 不会被回收
console.log(counter()); // 2
counter = null; // 解除引用
// 这时闭包函数和 count 变量都可以被回收
2. 性能开销
闭包会增加作用域链的查找成本。在性能敏感的代码中,合理使用闭包,避免过度嵌套。
3. 变量共享问题
在循环中使用闭包时,常见变量共享问题需要注意。ES6 的 let 关键字可以解决此类问题。
在 ES5 时代,如果你写:
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), i * 1000);
}
结果会是:
4
4
4
原因是:
- var 声明的变量没有块级作用域,整个循环里只有一个 i。
- 每次迭代的回调函数都共享同一个 i。
- 当定时器执行时,循环早已结束,i 已经变成 4,所以三个回调都打印 4。
✅ ES6 的解决方案:let
在 ES6 中,let 引入了 块级作用域。 在循环中使用 let i 时,每一次迭代都会创建一个新的 i 变量,作用域只属于这一轮循环。
所以这段代码:
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), i * 1000);
}
执行结果是:
Code
1 // 1秒后
2 // 2秒后
3 // 3秒后
原因:
- 每次循环迭代,都会生成一个新的 i,并绑定到对应的闭包里。 2.定时器回调函数捕获的是当前迭代的 i,不会和其他迭代共享。
- 因此三个回调分别打印 1、2、3。
🎯 总结逻辑 问题:用 var 时,闭包共享同一个循环变量,导致结果相同。
解决:用 let,每次迭代有独立的作用域,闭包捕获的是不同的变量。
参考链接: