彻底搞懂 JavaScript 闭包:原理、陷阱与内存优化全解析

一、闭包的定义

闭包(Closure) 是指 有权访问另一个函数作用域中的变量的函数

简单来说,就是函数可以"记住"定义时的作用域,而不是调用时的作用域

scss 复制代码
function outer() {
  let count = 0;
  return function inner() {
    count++;
    console.log(count);
  }
}

const fn = outer();
fn(); // 1
fn(); // 2

这里 inner 就是闭包,它可以访问 outer 的变量 count


二、闭包的形成条件

闭包产生通常需要三个条件:

  1. 函数嵌套:有函数内部函数。
  2. 内部函数引用外部变量
  3. 外部函数返回内部函数或将内部函数传出

三、闭包的作用

  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.decrement()); // 0

外部无法直接访问 count,只能通过提供的方法操作。

  1. 函数工厂 / 动态生成函数
javascript 复制代码
function multiplyBy(n) {
  return function(x) {
    return x * n;
  }
}
const double = multiplyBy(2);
console.log(double(5)); // 10
  1. 保持状态(常用于异步、事件回调)

四、闭包的面试常考点

1. 循环中的闭包问题

javascript 复制代码
for (var i = 0; i < 3; i++) {
  setTimeout(function() { console.log(i); }, 100);
}
// 输出:3 3 3

原因var 是函数作用域,闭包引用的是同一个 i
解决方案

  • 使用 let(块级作用域):
javascript 复制代码
for (let i = 0; i < 3; i++) {
  setTimeout(function() { console.log(i); }, 100);
}
// 输出:0 1 2
  • 或使用立即执行函数(IIFE):
css 复制代码
for (var i = 0; i < 3; i++) {
  (function(i){
    setTimeout(function() { console.log(i); }, 100);
  })(i);
}

2. 闭包与内存泄漏

闭包会 引用外部作用域变量 ,可能导致 垃圾回收无法释放

  • 避免过度使用全局闭包。
  • 在不需要时,手动清理引用。

3. 面试题示例

题1:输出结果?

ini 复制代码
function makeFunc() {
  let name = "Yo Yo";
  return function() {
    console.log(name);
  }
}
const func = makeFunc();
func();

答案Yo Yo

因为闭包保持了对 name 的引用。

题2:循环闭包输出?

scss 复制代码
var funcs = [];
for (var i = 0; i < 3; i++) {
  funcs[i] = function() { console.log(i); };
}
funcs[0](); funcs[1](); funcs[2]();

答案3 3 3

var 是函数作用域,闭包引用的是同一个 i

题3:使用闭包实现私有变量

javascript 复制代码
function Counter() {
  let count = 0;
  this.increment = function() { count++; return count; }
  this.decrement = function() { count--; return count; }
}
const c = new Counter();
console.log(c.increment()); // 1

题4:闭包结合 setTimeout

css 复制代码
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}

答案0 1 2

let 作用域绑定每次循环的 i


五、闭包引起内存泄漏问题

1 为什么闭包会造成内存无法释放?

闭包之所以"强大",是因为它让内部函数持续引用 外部作用域中的变量。

但也正因为这个引用存在,即使外层函数已经执行完毕,外层变量仍然被保存在内存中,不能被垃圾回收(GC)释放。

示例

javascript 复制代码
function outer() {
  let bigData = new Array(1000000).fill('数据');
  return function inner() {
    console.log(bigData.length);
  }
}
const fn = outer();
// outer 已经执行完毕,但 bigData 仍被 inner 引用,无法回收

这里 bigData 占用大量内存,而 fn 一直持有引用,GC 就不会释放。


2 在不需要时,如何"手动清理引用"

关键思路:让闭包不再引用外部变量

✅ 方式 1:将闭包变量置为 null

javascript 复制代码
function outer() {
  let bigData = new Array(1000000).fill('数据');
  return function inner() {
    console.log(bigData.length);
    bigData = null; // 手动解除引用
  }
}

const fn = outer();
fn(); // 使用一次

调用 fn 后手动将 bigData = null,让 GC 能识别为可回收。


✅ 方式 2:将闭包函数引用置为 null

php 复制代码
let fn = (function() {
  let data = { name: 'Yo Yo' };
  return function() {
    console.log(data);
  }
})();

fn();      // 使用
fn = null; // 手动解除引用,闭包随之释放

一旦 fn 没有被引用,整个闭包作用域都会被 GC。


✅ 方式 3:避免不必要的全局闭包

不要让闭包函数挂在全局对象上,比如:

javascript 复制代码
window.fn = (function() {
  let cache = [];
  return function() { cache.push(Date.now()); }
})();

改进:

javascript 复制代码
(function() {
  let cache = [];
  document.body.addEventListener('click', function() {
    cache.push(Date.now());
  });
})();

只在局部使用闭包,事件解绑或模块销毁时自然会释放。


✅ 方式 4:组件或页面卸载时清理(在框架中)

比如 React、Vue 中使用闭包保存状态时,在组件卸载阶段清理:

React 示例

javascript 复制代码
useEffect(() => {
  let timer = setInterval(() => console.log('run'), 1000);
  return () => clearInterval(timer); // 清理引用
}, []);

如果闭包里引用了外部变量(如 DOM、定时器),必须在 cleanup 阶段清理,否则内存会持续增长。


3、判断闭包导致的内存泄漏的常见迹象

  • 控制台内存面板中,执行某函数后内存不下降;
  • 页面关闭某功能后,内存占用仍持续增加;
  • 浏览器 Performance 记录中,Detached DOM 节点未被释放。

总结要点

  • 闭包 = 函数 + 对外部变量的引用

  • 好处

    • 数据私有化
    • 保存状态
    • 动态函数生成
  • 注意

    • 循环变量问题
    • 内存泄漏
    • 不要滥用全局闭包
相关推荐
茄汁面6 小时前
Angular(TypeScript ) 中基于 ExcelJS 实现导入模板下载功能(带样式与数据验证)
前端·javascript·node.js
前端九哥6 小时前
老板:就是你小子删光了try-catch?
前端·javascript
杰出的胡兵6 小时前
2v1带您实战12nm高级数字后端
前端·soc·数字后端·数字ic后端·芯片设计全流程培训
Achieve前端实验室6 小时前
深入浅出 ES Module
前端·javascript·前端框架
炳子6 小时前
小程序地图组件(map)中使用全屏预览图片(previewImage)的问题解决方案
前端
Onion6 小时前
BroadcastChannel 使用:优缺点、场景示例与最佳实践
前端·javascript
东东2336 小时前
搭建 Vite + React 服务端渲染(SSR)环境
前端·javascript·react.js
上车函予6 小时前
点击即扩散:使用 View Transition API 实现 UnoCSS 官网同款主题切换动画
前端·javascript·css
星链引擎6 小时前
生成式 AI 驱动下的智能聊天机器人 技术架构核心实现与场景落地
前端