小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
的执行上下文,但依然能访问其局部变量。这是因为它保存了定义时的作用域链!
🎯 二、闭包产生的三大条件
- 函数嵌套函数
- 内部函数引用了外部变量
- 内部函数"逃逸"了出去(作为返回值/赋值/回调)
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 对象保存必要变量 |
内存机制 | 被闭包捕获的变量提升到堆中 |
应用场景 | 缓存、私有变量、柯里化、模块化 |
风险 | 内存泄漏,需释放引用 |