彻底搞懂 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 节点未被释放。

总结要点

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

  • 好处

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

    • 循环变量问题
    • 内存泄漏
    • 不要滥用全局闭包
相关推荐
aa小小8 小时前
localhost 访问异常排查笔记
前端
格子软件8 小时前
2026年GEO优化系统源码的分布式状态机深度拆解
java·前端·vue.js·vue·geo
陈随易8 小时前
Rust、Golang、MoonBit 编译成 WASM,体积和速度差距有多大?
前端·后端·程序员
IT_陈寒9 小时前
Python多线程的坑,我居然现在才踩到
前端·人工智能·后端
摇滚侠9 小时前
方法 A 等方法 B 执行完再执行 叫同步调用还是异步调用 JS 默认是同步调用还是异步调用
开发语言·javascript·ecmascript
触底反弹9 小时前
🔥 字符串算法面试三连击:反转、回文、回文变种,搞懂这三题稳了!
前端·javascript·算法
触底反弹10 小时前
AI Tool Use 深度解析:大模型是如何"突破物理限制"调用外部工具的?
javascript·人工智能·后端
竹林81810 小时前
从 RPC 超时到批量签名:我用 @solana/web3.js 重构了一个 NFT 铸造页面,踩了这些坑
前端·javascript
工业HMI实战笔记10 小时前
工业HMI界面布局“1核2辅”黄金结构,适配90%场景
前端·ui·性能优化·自动化·交互
优雅格子衫10 小时前
TypeScript 类的基本使用小结
javascript·ubuntu·typescript