1. 概述
1.1 文档目的
本文档系统阐述 JavaScript 闭包(Closure)的定义、原理、应用场景及常见问题,为开发团队提供统一的技术参考标准。
1.2 术语定义
| 术语 | 定义 |
|---|---|
| 闭包(Closure) | 函数与其词法作用域的组合,使函数能够访问定义时的外部变量 |
| 词法作用域(Lexical Scope) | 作用域在代码编写时确定,而非运行时 |
| 词法环境(Lexical Environment) | 由环境记录器和对外部环境的引用组成 |
| 执行上下文(Execution Context) | 代码执行时的运行环境,包含变量对象、作用域链、this 等 |
| 垃圾回收(GC) | 自动回收不再被引用的内存空间 |
1.3 闭包判定标准
满足以下三个条件即构成闭包:
- 存在嵌套函数(函数内部定义函数)
- 内部函数引用了外部函数的变量
- 内部函数在外部函数作用域之外被调用或引用
2. 核心概念
2.1 定义
闭包是指一个函数能够记住并访问它的词法作用域,即使这个函数在当前词法作用域之外执行。
公式化表达:闭包 = 函数 + 该函数能访问的外部词法环境
2.2 基础示例
function outer() {
let count = 0; // outer 的局部变量
function inner() {
count++; // inner 访问了 outer 的变量
return count;
}
return inner; // 返回 inner 函数
}
const counter = outer(); // outer 执行完毕,count 未被销毁
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
2.3 关键特征
| 特征 | 说明 |
|---|---|
| 变量持久化 | 外部函数的变量在函数执行完毕后仍然存活 |
| 数据隔离 | 每个闭包实例拥有独立的变量副本 |
| 作用域链访问 | 内部函数可沿作用域链访问所有外部变量 |
| 私有性 | 外部无法直接访问闭包内部的变量 |
3. 闭包的作用
3.1 数据封装与私有变量
JavaScript 没有原生私有属性机制,闭包是实现数据封装的经典方案。
function createPerson(name) {
let _age = 0; // 私有变量,外部无法直接访问
return {
getName: () => name,
getAge: () => _age,
growUp: () => _age++
};
}
const person = createPerson('Alice');
console.log(person.getAge()); // 0
person.growUp();
console.log(person.getAge()); // 1
console.log(person._age); // undefined,无法直接访问
适用场景:
- 模块模式(Module Pattern)
- 类的私有属性和方法
- 配置对象的不可变封装
3.2 函数工厂与柯里化
通过闭包创建带预设参数的函数。
function makeMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = makeMultiplier(2);
const triple = makeMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
适用场景:
- 参数预设(Partial Application)
- 函数柯里化(Currying)
- 配置化函数生成
3.3 异步回调中的状态保持
在异步操作中保持循环变量的当前值。
for (var i = 1; i <= 3; i++) {
(function(savedI) {
setTimeout(() => {
console.log(savedI); // 1, 2, 3
}, 100);
})(i);
}
适用场景:
- 循环中的异步操作
- 事件处理器的参数绑定
- 定时器回调
3.4 防抖与节流
利用闭包保存中间状态,实现性能优化。
function debounce(fn, delay) {
let timer = null; // 闭包保存 timer 状态
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 使用
const handleSearch = debounce((query) => {
console.log('搜索:', query);
}, 300);
适用场景:
- 搜索框输入防抖
- 滚动事件节流
- 按钮点击防重
3.5 单例模式实现
const Singleton = (function() {
let instance = null;
function createInstance() {
return { data: '唯一实例' };
}
return {
getInstance: function() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
4. 实现原理
4.1 词法作用域机制
JavaScript 采用词法作用域(Lexical Scoping),即作用域在函数定义时确定,而非调用时。
函数在创建时会保存一个内部属性 [[Environment]],指向其定义时的词法环境。这一机制是闭包实现的底层基础。
4.2 执行过程分析
以以下代码为例:
function foo() {
let a = 1;
function bar() {
console.log(a);
}
return bar;
}
const baz = foo();
baz(); // 1
执行步骤:
| 步骤 | 操作 | 内存状态 |
|---|---|---|
| 1 | 定义 foo,foo.[[Environment]] 指向全局环境 | 全局词法环境 |
| 2 | 调用 foo(),创建新的执行上下文 | 新增 foo 词法环境 |
| 3 | foo 内定义 bar,bar.[[Environment]] 指向 foo 的词法环境 | bar 持有对 foo 环境的引用 |
| 4 | foo 返回 bar 并执行完毕,执行上下文弹出 | foo 的执行上下文销毁 |
| 5 | foo 的词法环境仍被 bar 引用,因此保留 | a 仍存在于内存中 |
| 6 | 调用 baz(),引擎沿作用域链找到 a | 正常访问 a |
4.3 内存模型
┌─────────────────────────────────────────┐
│ 全局执行上下文 │
│ ┌─────────────────────────────────┐ │
│ │ 全局词法环境 │ │
│ │ foo: <function object> │ │
│ │ baz: <function object> │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ foo 词法环境(仍存活) │
│ ┌─────────────────────────────────┐ │
│ │ a: 1 │ │
│ │ bar: <function object> │ │
│ │ 外部引用 → 全局词法环境 │ │
│ └─────────────────────────────────┘ │
│ ↑ 被 baz(即 bar)引用 │
└─────────────────────────────────────────┘
4.4 垃圾回收影响
正常情况下,函数执行完毕后其词法环境会被垃圾回收。但在闭包场景中:
- 内部函数持有外部词法环境的引用
- 只要内部函数可达,外部词法环境就不会被回收
- 这既是闭包实现的基础,也是内存泄漏的根源
5. 常见问题与解决方案
5.1 内存泄漏
问题描述
闭包引用的变量不会被垃圾回收。如果闭包持有大量数据且长期存活,会造成内存泄漏。
// ❌ 错误示例:意外持有大量数据
function processBigData() {
const hugeArray = new Array(1000000).fill('x');
return function() {
console.log('done'); // 未使用 hugeArray,但整个作用域被持有
};
}
const leak = processBigData();
// hugeArray 永远不会被释放
解决方案
方案一:只引用必要变量
// ✅ 正确做法
function processBigData() {
const hugeArray = new Array(1000000).fill('x');
const result = hugeArray.length; // 只保存需要的数据
return function() {
console.log(result); // 不再引用 hugeArray
};
}
方案二:及时释放引用
let handler = processBigData();
// 使用完毕后
handler = null; // 帮助垃圾回收
方案三:使用 WeakMap/WeakSet
const cache = new WeakMap();
function process(obj) {
if (!cache.has(obj)) {
const result = heavyComputation(obj);
cache.set(obj, result);
}
return cache.get(obj);
}
// 当 obj 不再被引用时,缓存条目自动被回收
5.2 循环中的变量共享
问题描述
使用 var 在循环中创建闭包,所有闭包共享同一个变量。
// ❌ 错误:输出 3, 3, 3
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
原因分析:var 没有块级作用域,三个闭包共享同一个 i,循环结束时 i = 3。
解决方案
方案一:使用 let(推荐)
// ✅ 推荐方案
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 0, 1, 2
}
let 具有块级作用域,每次迭代都会创建一个新的绑定。
方案二:使用 IIFE(立即执行函数)
// ✅ 兼容方案
for (var i = 0; i < 3; i++) {
(function(savedI) {
setTimeout(() => console.log(savedI), 100);
})(i);
}
方案对比:
| 方案 | 兼容性 | 代码量 | 推荐度 |
|---|---|---|---|
let | ES6+ | 最少 | ⭐⭐⭐⭐⭐ |
| IIFE | 全平台 | 较多 | ⭐⭐⭐ |
forEach | ES5+ | 中等 | ⭐⭐⭐⭐ |
5.3 this 指向问题
问题描述
闭包不会继承外层函数的 this,其 this 取决于调用方式。
const obj = {
name: 'Alice',
getName: function() {
return function() {
console.log(this.name); // undefined
};
}
};
obj.getName()(); // this 指向全局对象
解决方案
方案一:使用箭头函数
const obj = {
name: 'Alice',
getName: function() {
return () => {
console.log(this.name); // Alice
};
}
};
箭头函数没有自己的 this,会继承外层函数的 this。
方案二:保存 this 引用
const obj = {
name: 'Alice',
getName: function() {
const self = this;
return function() {
console.log(self.name); // Alice
};
}
};
方案三:使用 bind
const obj = {
name: 'Alice',
getName: function() {
return function() {
console.log(this.name);
}.bind(this);
}
};
5.4 性能问题
问题描述
- 过度使用闭包会导致作用域链过长,增加变量查找时间
- 每个闭包实例都持有独立的作用域链副本,增加内存开销
解决方案
// ❌ 避免:嵌套过深的闭包
function a() {
const x = 1;
return function b() {
const y = 2;
return function c() {
const z = 3;
return function d() {
return x + y + z; // 作用域链过长
};
};
};
}
// ✅ 推荐:扁平化设计,必要时解耦
function createCalculator(base) {
return {
add: (n) => base + n,
multiply: (n) => base * n
};
}
6. 最佳实践
6.1 编码规范
- 最小化引用范围:闭包中只引用必要的变量,避免持有整个作用域
- 及时释放引用:不再使用的闭包引用应置为
null - 优先使用 let/const:避免
var导致的变量提升问题 - 箭头函数处理 this:在需要继承
this的场景使用箭头函数 - 避免深层嵌套:超过 3 层的嵌套闭包应考虑重构
6.2 代码模板
安全计数器
function createCounter(initial = 0) {
let count = initial;
return {
increment: () => ++count,
decrement: () => --count,
getValue: () => count,
reset: (value = initial) => { count = value; },
setValue: (value) => { count = value; }
};
}
// 使用
const counter = createCounter(10);
counter.increment();
console.log(counter.getValue()); // 11
模块模式
const myModule = (function() {
// 私有变量
let privateVar = 0;
// 私有方法
function privateMethod() {
return privateVar * 2;
}
// 公有接口
return {
publicMethod: function() {
privateVar++;
return privateMethod();
},
getVar: function() {
return privateVar;
}
};
})();
缓存函数(Memoization)
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// 使用
const fibonacci = memoize(function(n) {
if (n < 2) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
6.3 调试技巧
在 Chrome DevTools 中查看闭包:
- 在闭包函数内设置断点
- 打开 Scope 面板,查看 Closure 部分
- 可以看到闭包引用的所有外部变量
Scope
├── Local
│ └── this: Window
├── Closure (outer)
│ └── count: 2
└── Global
└── ...
7. 附录
7.1 常见问题速查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 内存持续增长 | 闭包持有大对象 | 只引用必要变量,及时释放 |
| 循环中输出相同值 | var 变量共享 | 改用 let 或 IIFE |
this 为 undefined | 闭包中 this 指向全局 | 使用箭头函数或保存 self |
| 闭包变量值不符合预期 | 异步执行时变量已改变 | 使用 IIFE 或 let 创建新绑定 |
7.2 相关概念
- 作用域链(Scope Chain):变量查找的路径,由当前环境和所有外部环境组成
- 执行栈(Call Stack):管理函数调用的后进先出结构
- 词法环境(Lexical Environment):由环境记录器和外部引用组成的规范类型
- 变量对象(Variable Object):存储执行上下文中变量和函数声明的对象