闭包 (Closure) 深度解析
闭包是 JavaScript 中最强大但也最容易让人困惑的特性之一。它是理解高级 JS 编程、内存管理以及框架原理(如 Vue/React 响应式)的基石。
一、一句话先记住
闭包 = 函数 + 该函数能访问的外部变量。
或者更通俗地说:闭包让函数拥有了“记忆”,即使外部函数已经执行结束,它依然能记住并访问定义时的外部环境。
二、闭包的产生条件
当满足以下三个条件时,就产生了闭包:
- 存在嵌套函数:函数 A 内部定义了函数 B。
- 内部函数引用外部变量:函数 B 访问了函数 A 中的局部变量。
- 内部函数被传出:函数 B 被作为返回值,或赋值给了全局变量/定时器等。
javascript
function outer() {
const name = "V8"; // 外部变量
return function inner() {
console.log(name); // 引用外部变量
};
}
const closureFn = outer(); // outer 执行结束,但 name 变量被 inner 留住了
closureFn(); // 输出: "V8"三、底层原理:作用域链 (Scope Chain)
在 JavaScript 中,每个函数在创建时都会关联一个 [[Scopes]] 属性。
- 当
inner函数被定义时,它会记录父级函数outer的作用域。 - 当
outer执行完毕弹出调用栈后,由于closureFn(即inner)依然存在且保留了对outer作用域的引用,导致outer的局部变量无法被销毁。
👉 关联知识:
- V8 执行流水线与调用栈:了解函数是如何压栈与弹栈的。
四、内存视角:闭包存哪了?
这是一个经典的面试题:闭包中的变量存放在栈里还是堆里?
1. 真相:存放在堆 (Heap) 中
通常情况下,函数的局部变量存放在**栈 (Stack)**中,函数执行完即销毁。 但 V8 引擎在解析代码时,如果发现内部函数引用了外部变量,它会做一个特殊处理:
- 在 堆内存 中创建一个名为
Closure的特殊对象。 - 将被引用的变量从栈中移动到这个堆对象里。
- 这样即使栈帧销毁了,内部函数依然可以通过指针在堆中找到这个变量。
👉 关联知识:
- V8 内存布局:栈与堆全景图:直观查看
Closure对象在堆中的位置。
五、闭包的常见应用场景
1. 模拟私有变量(模块化原型)
javascript
function createCounter() {
let count = 0; // 私有变量
return {
add() { count++; },
get() { return count; }
};
}
const counter = createCounter();
counter.add();
console.log(counter.get()); // 1
console.log(counter.count); // undefined (无法直接访问)2. 函数柯里化 (Currying)
javascript
function multiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplier(2);
console.log(double(5)); // 103. 异步回调与定时器
在 setTimeout 或 Promise.then 中,回调函数往往需要访问外部的上下文,这本质上都是利用了闭包。
六、闭包与内存泄漏
虽然闭包很有用,但如果使用不当,会导致内存无法及时回收。
1. 产生原因
只要闭包函数还被引用(如挂载在全局对象或 DOM 元素上),它所关联的外部变量就会一直常驻在 老生代堆内存 中。
2. 解决方案
手动解除引用:将闭包函数置为 null。
javascript
let fn = outer();
// ... 使用闭包
fn = null; // 此时闭包引用的变量将被 GC 回收👉 关联知识:
- 老生代 GC 算法:标记-清除与整理:了解 V8 如何回收这些常驻对象。
七、面试高频题
1. 说说你对闭包的理解?
白话回答:闭包就是能读取其他函数内部变量的函数。在底层,它是函数和其词法环境的组合。它能让变量常驻内存,常用于封装私有变量、防抖节流等场景。
2. 闭包会造成内存泄漏吗?
白话回答:闭包本身不是泄漏,它是 JS 引擎的正常特性。只有当闭包被不当持有(比如长期挂载在全局),导致它引用的变量无法被 GC 回收时,才会产生内存泄漏。
3. 循环中定时器输出 i 的问题?
javascript
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i); // 输出 6 6 6 6 6
}, i * 1000);
}原因:var 没有块级作用域,定时器回调执行时 i 已经变成了 6。 闭包解法:
javascript
for (let i = 1; i <= 5; i++) { // 使用 let 产生块级作用域闭包
setTimeout(() => console.log(i), i * 1000);
}八、总结
- 优点:数据私有化、延长变量寿命。
- 缺点:增加内存消耗、可能导致泄漏。
- 核心心智模型:函数背着一个看不见的“包”(Scopes),包里装着它诞生时的环境数据。