闭包全解与 V8 深潜

小Dora 的 JavaScript 修炼日记 · Day 4

"闭包就像前任,表面已经结束,其实在内存里还活着。"

------某资深垃圾回收器


🧨 前言:闭包不是语法糖,而是作用域的灵魂延续

闭包是 JS 世界的"神秘武器",它能穿越函数的生命周期、记住那些早该释放的变量,让你:

  • 封装数据而不泄露
  • 缓存结果而不重算
  • 模拟私有变量
  • 实现柯里化与高阶函数

今天我们不仅要掌握"用",更要深入理解闭包为何存在、怎么生成、如何释放,特别是在 V8 里的真面目


🧱 一、闭包到底是什么?

闭包(Closure)是函数 + 其定义时作用域环境的组合。

经典定义 👇

scss 复制代码
function outer() {
  let secret = 42;
  return function inner() {
    console.log(secret);
  };
}
const fn = outer();
fn(); // 输出 42

🧠 inner 就是闭包:它虽然脱离了 outer 的执行上下文,但依然能访问其局部变量。这是因为它保存了定义时的作用域链!


🎯 二、闭包产生的三大条件

  1. 函数嵌套函数
  2. 内部函数引用了外部变量
  3. 内部函数"逃逸"了出去(作为返回值/赋值/回调)
csharp 复制代码
function counter() {
  let count = 0;
  return function () {
    return ++count;
  };
}
const add = counter();
add(); // 1
add(); // 2

✅ 闭包使 count 的生命周期超出了其作用域块。


⚙️ 三、闭包 + V8 引擎底层原理

光会用闭包只能算"手艺人",理解 V8 是怎么管理作用域与内存的,才是"架构级别"的理解。


🧪 1. V8 创建闭包的内部流程

csharp 复制代码
function outer() {
  let a = 1;
  return function inner() {
    return a;
  };
}

🔍 作用域链:

lua 复制代码
inner.[[Scope]] → outer.LexicalEnv → global.LexicalEnv

在函数定义阶段:

  • inner 函数持有 [[Environment]] 属性(也称 [[Scope]])
  • 这个属性指向创建它时的词法环境

即使 outer 已执行完毕出栈,只要 inner 被引用,outer 的变量就会被挂在堆上存活。


🧠 2. 闭包变量存储在堆上还是栈上?

阶段 存储位置 描述
执行期间 栈中 正常上下文变量
被闭包捕获后 提升到堆中 Context 对象中存储
javascript 复制代码
function test() {
  let a = 1;
  return () => a;
}

🚨 a 本来在 test() 的栈帧中,返回箭头函数后被提升到堆中,由闭包 Context 引用。


📦 3. V8 的闭包优化技巧:Partial Context Allocation

V8 非常聪明:闭包只保存被引用的变量 ,未引用的变量不会保留在 Context 中。

ini 复制代码
function foo() {
  let used = 1;
  let unused = 2;
  return () => used;
}

✅ 最终 Context 中只有 used


♻️ 4. 闭包变量的 GC 机制

只有在闭包函数本身不再被引用时,其上下文才会被垃圾回收。

php 复制代码
let fn = (() => {
  let bigData = new Array(1e6).fill(0);
  return () => bigData;
})();
fn = null; // bigData 终于可以被 GC 了

🚧 不清除闭包就会导致 内存泄漏,特别是在事件监听器、定时器里。


🧩 四、典型闭包应用实战:高级封装姿势

✅ 1. 模拟私有变量(WeakMap / 闭包)

javascript 复制代码
function createCounter() {
  let count = 0;
  return {
    inc: () => ++count,
    dec: () => --count,
    get: () => count,
  };
}
const c = createCounter();

无论你怎么调 c.count = 1000,外部都改不了闭包变量。


✅ 2. 缓存函数(Memoize)

vbnet 复制代码
function memoize(fn) {
  const cache = new Map();
  return function (key) {
    if (cache.has(key)) return cache.get(key);
    const result = fn(key);
    cache.set(key, result);
    return result;
  };
}

闭包保留了 cache,实现函数级缓存,是大厂面试常考的高阶题型。


✅ 3. 柯里化(Currying)

javascript 复制代码
function add(a) {
  return function (b) {
    return a + b;
  };
}
add(1)(2); // 3

参数 a 被闭包捕获,生成一个新的函数环境,这就是函数式编程的基础。


🧠 五、大厂级闭包脑筋急转弯

❓ 面试题:下面输出什么?

css 复制代码
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。解决方法:

ini 复制代码
for (let i = 0; i < 3; i++) {
  funcs[i] = function () {
    console.log(i);
  };
}

或者手动闭包:

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

📋 闭包 + V8 专项 Checklist(Day4)

  • 我能准确描述闭包的三大触发条件?
  • 我理解 [[Scope]] 与作用域链的构建时机?
  • 我知道闭包变量从栈转移到堆的过程?
  • 我理解 V8 的 Context 优化机制?
  • 我能解释闭包 GC 释放条件?
  • 我写过闭包模拟私有变量、缓存函数?
  • 我能手绘闭包结构 + 内存图?
  • 我能在复杂嵌套中精确判断闭包行为?

✅ Day 4 总结打卡

概念 精炼理解
闭包 函数 + 其定义时作用域链
V8 中闭包 使用 Context 对象保存必要变量
内存机制 被闭包捕获的变量提升到堆中
应用场景 缓存、私有变量、柯里化、模块化
风险 内存泄漏,需释放引用

相关推荐
hard_coding_wang14 分钟前
使用layui的前端框架过程中,无法加载css和js怎么办?
javascript·前端框架·layui
香蕉可乐荷包蛋22 分钟前
vue3中ref和reactive的使用、优化
前端·javascript·vue.js
勤奋的知更鸟30 分钟前
JavaScript 性能优化实战:深入性能瓶颈,精炼优化技巧与最佳实践
开发语言·javascript·性能优化
耶啵奶膘34 分钟前
css——width: fit-content 宽度、自适应
前端·css
OEC小胖胖36 分钟前
前端框架状态管理对比:Redux、MobX、Vuex 等的优劣与选择
前端·前端框架·web
倔强青铜三1 小时前
苦练Python第20天:Python官方钦定的代码风格指南
人工智能·python·面试
字节架构前端1 小时前
k8s场景下的指标监控体系构建——Prometheus 简介
前端·架构
奕羽晨2 小时前
关于CSS的一些读书笔记
前端·css
倔强青铜三2 小时前
苦练Python第19天:断言与自定义异常
人工智能·python·面试