闭包(Closure)是JavaScript中一个非常重要且强大的概念,理解闭包对于掌握JavaScript高级特性至关重要。下面我将从多个角度详细解释闭包的概念、原理和应用。
一、什么是闭包?
闭包是指那些能够访问独立 (自由) 变量的函数,或者说函数定义时的词法环境(lexical environment)而非调用时的环境。
访问独立变量:函数能访问不属于自己作用域的变量 。
定义时而非调用时:这就是 JavaScript 的词法作用域规则。
闭包 = 函数 + 函数定义时所在的词法环境
本质:函数能记住并访问创建时的词法作用域,即使在作用域外执行,依然可以访问外部变量,这就是闭包。
1.1 词法作用域(Lexical Scoping)
JavaScript 的作用域是"静态"的。
函数在定义的那一刻,就确定了它能访问哪些变量,跟在哪里调用无关。
=> 函数的作用域由其在代码中的书写位置决定(定义时)。
javascript
function outer() {
const name = 'Outer';
// inner 函数就是闭包
// inner函数形成了对outer函数中name变量的闭包
function inner() {
console.log(name); // 访问外部函数的变量
}
return inner;
}
const closureFunc = outer();
closureFunc(); // 输出: Outer
在上面的例子中,inner函数在 outer函数内部定义,它就"记住"了 outer的作用域。即便 outer执行完了,inner依然能访问 name。
二、 底层揭秘:V8 引擎眼中的闭包
2.1 关键角色:\[Environment]
在 JS 引擎(如 V8)中,每个函数都有一个内部隐藏属性 \[Environment]。
\[Environment] 是函数在创建时 保存的一个引用,指向其外层的词法环境(Lexical Environment) 。
当函数执行时,会创建一个 执行上下文(Execution Context),其中包含:
- 词法环境(Lexical Environment) :存储变量 (let/const/function)
- 函数定义时(不是调用时),会通过内部属性 \[Environment] 保存当前词法环境的引用 。
- 变量环境(Variable Environment):存储 var
- outer 指针:指向上一层词法环境,形成作用域链
text
全局环境 ← outer 环境 ← inner 环境
- outer 执行完毕,执行上下文弹出调用栈
- 但 inner 的 \[Environment] 仍引用 outer 的词法环境
- 垃圾回收机制(GC)不会回收该环境
- 变量会被保留,不会销毁
2.2 闭包的形成过程
我们用一段经典代码来拆解:
js
function createCounter() {
let count = 0; // 被闭包"捕获"
return function() {
count++;
return count;
};
}
const counter = createCounter();
counter();
Step 1:函数定义阶段
createCounter 定义,内部匿名函数定义时,它的 \[Environment] 指向 createCounter 的词法环境。
Step 2:执行 createCounter()
- 创建 createCounter 执行上下文
- 创建词法环境,包含 count = 0
- 返回内部匿名函数
- 匿名函数带着 \[Environment] 一起被返回
Step 3:createCounter执行完毕
- 正常情况下,函数执行完,执行上下文出栈,局部变量销毁
- 但闭包阻止了销毁
- 因为匿名函数的 \[Environment] 仍在引用 count
- GC(V8 的垃圾回收器) 判定变量可达 → 词法环境被保留
Step 4:执行 counter()
- 匿名函数执行
- 在自身作用域找不到 count
- 沿着 \[Environment] 找到保留的词法环境
- 读取并修改 count
✅ 结论:闭包在 V8 中,本质上是被函数捕获并延长生命周期的词法环境。
三、 作用域链 + 闭包
text
Global Context (全局执行上下文)
├── LexicalEnvironment (词法环境)
│ ├── createCounter: function
│ └── counter: function (指向 inner (内部)函数)
│
└── createCounter 执行后 (环境未被销毁!)
├── LexicalEnvironment (词法环境 依然存活)
│ ├── count = 1 (被修改后的值)
│ └── inner: function (内部函数)
│ └── [[Environment]] ──────────────┐
│
counter() 执行时: ▼
inner Execution Context(内部函数执行上下文)
├── LexicalEnvironment (自身词法环境)
└── Outer Reference (作用域链) ──► createCounter 的 LexicalEnvironment
作用域链本质:沿着 \[Environment] 一层层向上查找变量 。
闭包本质:让外层作用域不会被回收。
四、 闭包的经典应用场景
4.1 模块模式(Module Pattern)
这是早期 JS 实现私有变量的唯一手段。
js
const myModule = (function() {
let privateVar = '我是私有的'; // 外部无法直接访问
function privateMethod() {
console.log(privateVar);
}
return {
publicMethod: function() {
privateMethod();
}
};
})();
myModule.publicMethod(); // 我是私有的
// myModule.privateVar; // undefined
// myModule.privateMethod(); // 报错
4.2 函数工厂 & 柯里化(Currying)
js
// 函数柯里化
// 1. 定义外层函数 multiply(a),
// 接收第一个参数,暂存起来
function multiply(a) {
// 2. 返回新函数,形成闭包
// 内部函数会记住外层作用域的 a
return function(b) {
return a * b;
};
}
// 3. 执行 multiply(2)
// a = 2 被闭包保留,不会销毁
// 返回的函数赋值给 double
const double = multiply(2);
// 4. 执行 double(5)
// 使用闭包中保存的 a=2
double(5); // 10
柯里化本质:
把多参函数拆成一系列单参函数,利用闭包记住前面的参数。
好处:参数复用、逻辑复用、批量生成工具函数。
4.3 防抖与节流(Debounce & Throttle)
前端性能优化高频方案,闭包用于保存定时器 / 时间戳。
js
// 防抖函数示例
// 防抖:延迟 delay 后执行,若期间再次触发则重新计时
function debounce(fn, delay) {
// 闭包保存定时器 ID,避免多次触发重复执行
let timer = null;
return function (...args) {
// 清除之前的定时器
clearTimeout(timer);
// 重新设置新定时器
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 使用示例
const handleInput = debounce(function () {
console.log('发送搜索请求');
}, 500);
// 输入框频繁输入时,只会在停止输入 500ms 后执行一次
input.addEventListener('input', handleInput);
// 节流(Throttle)
// 核心思想:在 指定时间内,函数只执行一次,不管触发多频繁,都按固定频率执行。
// 常用于:滚动监听、窗口 resize、高频点击、鼠标移动等。
function throttle(fn, interval) {
// 用闭包保存上一次执行的时间戳
let lastTime = 0;
return function (...args) {
const now = Date.now();
// 判断当前时间 - 上次执行时间 >= 间隔时间
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now; // 更新最后执行时间
}
};
}
// 使用示例:滚动节流
const handleScroll = throttle(function () {
console.log('滚动触发了', scrollY);
}, 300);
window.addEventListener('scroll', handleScroll);
防抖:等待一段时间,只执行最后一次
节流:固定时间内,只执行一次
两者都必须依靠闭包保存状态(timer /lastTime),否则无法实现。
五、 踩坑指南:闭包的内存陷阱
闭包虽好,但不能滥用。
5.1 循环中的经典坑
js
// 错误示例
for (var i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i); // 全部输出 6
}, i * 1000);
}
// 原因:var没有块级作用域,i是全局变量,所有回调函数共享同一个 i。
✅ 解决方案 1:IIFE(立即调用函数表达式,用于创建独立闭包作用域。)
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 1, 2, 3, 4, 5
}, j * 1000);
})(i);
}
✅ 解决方案 2(推荐):let块级作用域
for (let i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i); // 1, 2, 3, 4, 5
}, i * 1000);
}
(注:let 在循环中,每一轮都会创建一个新的、独立的词法环境,相当于自动闭包。)
5.2 内存泄漏风险
如果一个闭包持有巨大的数据结构,且长期不释放,会导致内存占用过高。
js
function heavyTask() {
const bigData = new Array(10000000); // 大对象
return function() { console.log('done'); };
}
const task = heavyTask();
// 即使 heavyTask 执行完了,bigData 依然存在
// 内存不释放 → 页面卡顿
task = null; // 手动切断引用,让 GC 回收
JavaScript 闭包保留的是「整个词法环境」,不是「用到的变量」。
只要返回内部函数 → 形成闭包 → 外层所有变量都会被保留。
无论内部函数有没有使用,都不会被回收。
5.3过多闭包可能导致性能问题
闭包变量需要沿作用域链查找,比局部变量稍慢。
大量长期驻留的闭包会增加内存压力,注意及时释放。
六、 总结
闭包的本质:函数 + 定义时的词法环境 的结合体。
形成原理:基于词法作用域,函数通过 \[Environment] 引用外部环境。
V8 视角:闭包是未被垃圾回收的词法环境。
核心价值:数据封装、私有变量、状态持久化。
注意事项:警惕循环陷阱、内存占用,不用时及时置 null 释放。