JS闭包五层深度拆解与实战

5 阅读7分钟

很多前端小伙伴面试被问闭包,只会背一句 “函数嵌套函数,内部函数访问外部变量就是闭包”,面试官一听就知道你只会皮毛。

今天我们从入门概念 → 内存原理 → 执行上下文 → 底层机制 → V8 引擎性能优化五层,彻底吃透 JS 闭包,全程配套可运行代码、真实业务场景、面试高频坑点,看完直接碾压闭包面试 + 写出高性能生产代码~


第一层:基础认知 | 初中级前端必会

闭包官方定义

闭包 = 函数 + 可以访问外部函数作用域变量的能力哪怕外部函数已经执行结束,内部函数依然可以记住、访问外部函数的变量。

基础代码示例

javascript

// 基础闭包演示
function outer() {
  // 外部函数私有变量
  const count = 0;
  // 内部函数,形成闭包
  function inner() {
    console.log(count); // 内部可以持续访问外部count
  }
  return inner;
}

// 调用外部函数,外部函数执行完毕,调用栈弹出
const closureFn = outer();
// 闭包依然保留对count的访问能力
closureFn(); // 输出 0

闭包常见业务用途

  1. 数据私有化、封装私有变量
  2. 经典模块模式
  3. 回调函数、异步场景保存上下文
  4. 函数柯里化、节流防抖高阶函数

面试必问基础坑点

1. 闭包捕获的是引用,不是值

很多新手踩坑的根本原因:闭包永远捕获变量的引用地址,不是当前快照值。

javascript

// 错误写法 循环闭包翻车经典案例
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 全部输出 3 3 3
  }, 100);
}

// 正确修复方案1:let块级作用域
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 正常输出 0 1 2
  }, 100);
}

// 正确修复方案2:IIFE闭包隔离
for (var i = 0; i < 3; i++) {
  (function (index) {
    setTimeout(() => console.log(index), 100);
  })(i)
}

翻车原因:var 没有块级作用域,定时器回调形成闭包,全部共用同一个 i 的引用,循环结束 i 已经变成 3。


第二层:作用域链与内存 | 3 年 + 前端都模糊的核心

核心原理

外部函数执行完成后,执行上下文会从调用栈销毁弹出,但是闭包引用的活动对象 AO / 变量对象,会常驻堆内存,不会被 GC 回收。

多闭包共享变量内存分配

多个闭包引用同一个外部自由变量时,所有闭包共用同一块堆内存地址,一处修改,全部生效。

javascript

function createCounter() {
  let num = 0;
  // 两个闭包,共享同一个num
  return {
    add() { num++; console.log(num) },
    reduce() { num--; console.log(num) }
  }
}

const counter = createCounter();
counter.add(); // 1
counter.add(); // 2
counter.reduce(); // 1

闭包回收与内存泄漏

1. 置为 null 真的能立刻回收吗?

将闭包函数手动赋值为 null,只是断开函数的引用,只有当堆内存没有任何引用的时候,下一次 GC 扫描才会回收,不是立刻释放。

2. 闭包导致内存泄漏高频场景

  1. 长期常驻的 DOM 事件回调绑定闭包
  2. 全局常驻定时器、延时器闭包未销毁
  3. 全局缓存、大量数据常驻闭包引用
  4. Vue/React 组件销毁后闭包回调未解绑

内存泄漏修复示例

javascript

// 内存泄漏场景
function leakDemo() {
  const bigData = new Array(1000000).fill('超大内存数据');
  document.addEventListener('click', () => {
    console.log(bigData.length)
  })
}

// 优化写法:销毁时解绑+清空引用
let handler = null;
function goodDemo() {
  const bigData = new Array(1000000).fill('超大内存数据');
  handler = () => console.log(bigData.length);
  document.addEventListener('click', handler);
}

// 页面卸载/组件销毁,彻底释放内存
function destroy() {
  document.removeEventListener('click', handler);
  handler = null; // 断开闭包引用,GC正常回收bigData
}

第三层:闭包与执行上下文 | 进阶真功夫

调用栈与词法环境区别

  • 调用栈:函数执行时入栈,执行结束立刻出栈销毁
  • 词法环境(LexicalEnvironment) :存在堆内存,只要闭包引用存在,就永久保留

多闭包竞争同一个自由变量

javascript

function outer() {
  let val = 10;
  // 两个闭包,同时读写同一个val
  const fn1 = () => val++;
  const fn2 = () => console.log(val);
  return [fn1, fn2]
}

const [f1, f2] = outer();
f1();
f1();
f2(); // 输出12

结论:同一个外部变量,所有闭包读写的永远是最新值,互相影响。

闭包 + IIFE 模拟块级作用域

ES6 之前没有 let/const,前端全靠 IIFE + 闭包实现私有作用域隔离,解决 var 全局污染问题。

javascript

// IIFE + 闭包 经典实现
(function() {
  var privateVal = '我是IIFE私有变量';
  window.getVal = function() {
    return privateVal
  }
})()

console.log(getVal()); // 正常访问
console.log(privateVal); // 报错,外部无法直接访问

变量查找:编译时还是运行时?

结论:词法作用域,编译期就已经确定作用域链,和函数在哪调用完全无关

javascript

const a = '全局变量';
function outer() {
  const a = '外部函数变量';
  function inner() {
    console.log(a)
  }
  return inner;
}

const test = outer();
// 哪怕函数在全局调用,依然输出 外部函数变量
test();

闭包的变量查找,在定义位置决定,不是调用位置,这就是静态词法作用域。


第四层:进阶业务实战|Node 端避坑

Node.js 服务端闭包注意事项

  1. 请求之间数据隔离HTTP 服务千万不要用全局闭包缓存请求变量,会导致不同用户请求数据串扰、数据错乱。

javascript

// ❌ 高危错误写法:全局闭包导致请求污染
let userInfo;
app.get('/api', (req, res) => {
  userInfo = req.query; // 全局共用,并发请求数据错乱
})

// ✅ 正确写法:请求内局部变量,天然隔离
app.get('/api', (req, res) => {
  const userInfo = req.query; // 单次请求私有,互不干扰
})
  1. 内存持续增长监控长连接、常驻后台服务,大量闭包堆积会导致内存持续上涨、服务 OOM 崩溃,需要定期做内存快照、清理无用闭包引用。

第五层:性能与 V8 底层优化|高级天花板

V8 引擎闭包优化机制

  1. 上下文内联:如果闭包只引用少量简单变量,V8 会做优化内联,减少内存开销

  2. 逃逸分析:V8 会分析变量会不会被外部逃逸引用

    • 未逃逸:直接分配在栈内存,执行完立刻回收,零 GC 压力
    • 逃逸:分配到堆内存,生成闭包词法环境
  3. 闭包优化消除:完全没有被外部引用的闭包,V8 会直接抹除,不生成额外内存结构

大量闭包的性能危害

列表循环里疯狂创建闭包,会疯狂增加堆内存占用、频繁触发 GC 卡顿

javascript

// ❌ 性能极差写法:循环疯狂新建闭包
const list = [1,2,3,4,5,10000...];
list.forEach(item => {
  // 每次循环新建独立闭包
  const cb = () => console.log(item);
})

// ✅ 性能优化:外部复用同一个函数,避免重复创建闭包
const logItem = (item) => console.log(item);
list.forEach(logItem);

闭包 vs eval /new Function 性能差距

  • 标准闭包:V8 完全可优化、作用域可预测、性能极高

  • eval / new Function:

    1. 完全破坏词法作用域,V8 无法做逃逸分析、内联优化
    2. 禁用引擎所有编译器优化,性能断崖式下跌
    3. 严重内存泄漏、安全风险极高

面试高频闭包灵魂拷问汇总

  1. Q:闭包一定会内存泄漏吗?A:不会,只有无用引用常驻、没有断开引用才会泄漏,合理使用闭包完全没问题。
  2. Q:let 解决循环闭包问题的原理?A:let 有块级作用域,每次循环都会生成独立的词法环境,每个回调绑定独立变量。
  3. Q:闭包的优缺点?✅ 优点:数据私有化、封装、高阶函数、灵活保留上下文❌ 缺点:常驻堆内存,滥用增加内存开销、不当使用造成内存泄漏

最后总结

表格

层级核心掌握点面试段位
第一层闭包基础概念、循环经典坑初中级
第二层作用域链、AO、内存分配、泄漏场景中级
第三层执行上下文、IIFE、静态词法作用域中高级
第四层Node 服务端闭包、并发数据隔离高级开发
第五层V8 逃逸分析、底层优化、性能调优资深 / 架构

不要再死记硬背闭包定义了,面试官要的从来不是背诵,而是你对底层原理、内存、性能、业务风险的完整理解,吃透这五层,闭包相关面试题全部拿捏~


写在最后

看完这篇,下次面试官再问闭包,你就可以从基础定义一路聊到 V8 引擎底层优化 + 业务避坑 + 性能调优,直接和普通拉开差距~

觉得内容有用,欢迎点赞收藏,后续更新 JS 进阶底层原理干货!