JavaScript 闭包(Closure)
简介
**闭包(Closure)**是 JavaScript 中最强大和最容易让人困惑的概念之一。理解闭包对于掌握 JavaScript 至关重要。
参考文档:
js-function.md— 函数文档,包含闭包的基本介绍- 本文档是闭包的专题深入文档,从原理到实践全面展开
词法作用域(Lexical Scoping)
要理解闭包,首先需要理解词法作用域。
作用域链
let globalVar = 'global';
function outer() {
let outerVar = 'outer';
function inner() {
let innerVar = 'inner';
console.log(globalVar); // "global"(访问全局)
console.log(outerVar); // "outer"(访问外部函数)
console.log(innerVar); // "inner"(访问自身)
}
inner();
}
outer();
// console.log(outerVar); // ReferenceError(外部无法访问内部变量)
嵌套函数的作用域规则
function outer() {
let x = 10;
function inner() {
let y = 20;
console.log(x + y); // 30(inner 可以访问 outer 的 x)
}
inner();
// console.log(y); // ReferenceError(outer 不能访问 inner 的 y)
}
outer();
变量查找顺序
当在函数内使用一个变量时,查找顺序是:
- 先在当前函数作用域中查找
- 如果没找到,到外层函数作用域查找
- 继续向外,直到全局作用域
- 如果全局也没找到,报错
ReferenceError
let a = 'global-a';
function level1() {
let b = 'level1-b';
function level2() {
let c = 'level2-c';
function level3() {
let d = 'level3-d';
console.log(a); // "global-a"(从全局找到)
console.log(b); // "level1-b"(从 level1 找到)
console.log(c); // "level2-c"(从 level2 找到)
console.log(d); // "level3-d"(自身)
}
level3();
}
level2();
}
level1();
闭包的定义和原理
什么是闭包
闭包是指函数可以记住并访问它的词法作用域,即使函数在其词法作用域外执行。
function outer() {
let count = 0; // outer 的局部变量
function inner() {
count++; // inner 访问了 outer 的变量
console.log(count);
}
return inner; // 返回 inner 函数
}
let counter = outer(); // outer 执行完毕
counter(); // 1 ← 但 inner 仍然可以访问 count!
counter(); // 2 ← count 的值被"记住"了
counter(); // 3
发生了什么?
outer()执行时,创建了count = 0和inner函数outer()返回inner函数,赋值给counterouter()执行完毕,按理说其局部变量应该被销毁- 但
inner函数"关闭(closure)"了count变量,使其不会被垃圾回收 - 每次调用
counter(),仍然可以访问和修改count
闭包的构成
闭包 = 函数 + 其词法作用域的引用
function createClosure() {
let secret = '我是秘密';
return function() {
return secret; // 这个函数"关闭"了 secret
};
}
let getSecret = createClosure();
console.log(getSecret()); // "我是秘密"(即使 createClosure 已执行完)
每次调用产生独立的闭包
function makeCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
let counterA = makeCounter();
let counterB = makeCounter(); // 新的闭包,新的 count
console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1(独立的 count)
console.log(counterA()); // 3
console.log(counterB()); // 2
闭包的基本示例
最简单的闭包
function outer() {
let message = 'Hello';
function inner() {
console.log(message);
}
inner();
}
outer(); // "Hello"
这已经是一个闭包!
inner访问了outer的message变量。
闭包中的变量共享
function create() {
let value = 0;
return {
increment() {
value++;
return value;
},
decrement() {
value--;
return value;
},
getValue() {
return value;
}
};
}
let obj = create();
console.log(obj.increment()); // 1
console.log(obj.increment()); // 2
console.log(obj.decrement()); // 1
console.log(obj.getValue()); // 1
// value 不能直接访问
// console.log(obj.value); // undefined(私有变量)
闭包的生命周期
function outer() {
let hugeData = new Array(1000000).fill('*'); // 大量数据
let counter = 0;
return function() {
counter++;
console.log(counter);
// hugeData 也被闭包"关闭"了!
};
}
let fn = outer(); // hugeData 不会被释放
// 只要 fn 还存在,hugeData 就占用内存
fn = null; // 现在 fn 被释放,闭包消失,hugeData 可以被垃圾回收
闭包的实际应用
1. 数据封装(私有变量)
模拟私有属性和方法:
function createUser(name) {
let age = 0; // 私有变量,外部无法直接访问
return {
getName() {
return name; // name 也是闭包捕获的
},
getAge() {
return age;
},
setAge(newAge) {
if (newAge >= 0) {
age = newAge;
return true;
}
return false;
}
};
}
let user = createUser('Alice');
console.log(user.getName()); // "Alice"
console.log(user.getAge()); // 0
user.setAge(25);
console.log(user.getAge()); // 25
// 无法直接访问私有变量
// console.log(user.age); // undefined
// user.age = 30; // 无效
2. 函数工厂
创建特定功能的函数:
function multiplyBy(factor) {
return function(number) {
return number * factor; // factor 被闭包捕获
};
}
let double = multiplyBy(2);
let triple = multiplyBy(3);
let quadruple = multiplyBy(4);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(quadruple(5)); // 20
另一个例子——创建问候函数:
function greeter(greeting) {
return function(name) {
return `${greeting}, ${name}!`;
};
}
let sayHello = greeter('Hello');
let sayHi = greeter('Hi');
let sayBonjour = greeter('Bonjour');
console.log(sayHello('Alice')); // "Hello, Alice!"
console.log(sayHi('Bob')); // "Hi, Bob!"
console.log(sayBonjour('Charlie')); // "Bonjour, Charlie!"
3. 模块化(模块模式)
在 ES Modules 出现之前,闭包是实现模块化的主要方式:
let myModule = (function() {
let privateVar = '秘密数据'; // 私有变量
function privateMethod() { // 私有方法
console.log('私有方法');
}
return {
publicMethod() {
console.log('公共方法,可以访问:', privateVar);
privateMethod();
},
getSecret() {
return privateVar;
}
};
})(); // 立即执行
myModule.publicMethod(); // "公共方法,可以访问: 秘密数据" + "私有方法"
console.log(myModule.getSecret()); // "秘密数据"
// myModule.privateMethod(); // 错误:privateMethod 不是公共的
4. 定时器中的闭包
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i); // 全部输出 4!(经典闭包陷阱)
}, i * 1000);
}
// 输出:4 4 4(3 秒后)
// 原因:setTimeout 的回调捕获了同一个 i 的引用
// 当回调执行时,循环早已结束,i 已经是 4
解决方法见下文"闭包的常见陷阱"章节。
5. 事件监听中的闭包
function setupButtons() {
for (let i = 0; i < 3; i++) {
let button = document.createElement('button');
button.textContent = `按钮 ${i}`;
button.addEventListener('click', function() {
alert(`你点击了按钮 ${i}`); // i 被闭包捕获
});
document.body.appendChild(button);
}
}
// 每个按钮点击时,会显示正确的序号
// 因为 let 每次迭代创建新的词法作用域
6. 偏函数(Partial Application)
固定部分参数,产生新函数:
function partial(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn(...fixedArgs, ...remainingArgs); // fixedArgs 被闭包捕获
};
}
function add(a, b, c) {
return a + b + c;
}
let add5and10 = partial(add, 5, 10); // 固定前两个参数
console.log(add5and10(3)); // 18(5 + 10 + 3)
let add5 = partial(add, 5); // 只固定第一个参数
console.log(add5(10, 3)); // 18(5 + 10 + 3)
7. 柯里化(Currying)
将多参数函数转换为一系列单参数函数:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
};
}
function add(a, b, c) {
return a + b + c;
}
let curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
8. 记忆化(Memoization)
缓存函数结果,避免重复计算:
function memoize(fn) {
let cache = {}; // 闭包捕获的缓存对象
return function(...args) {
let key = JSON.stringify(args);
if (cache[key]) {
console.log('从缓存读取:', key);
return cache[key];
}
console.log('计算:', key);
let result = fn.apply(this, args);
cache[key] = result;
return result;
};
}
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
let memoizedFib = memoize(fibonacci);
console.log(memoizedFib(10)); // 计算(较慢)
console.log(memoizedFib(10)); // 从缓存读取(立即)
闭包的常见陷阱
陷阱 1:循环中的闭包(var vs let)
问题代码(使用 var):
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i); // 全部输出 4!
}, 100);
}
// 输出:4 4 4
// 原因:
// 1. var 是函数作用域,整个循环中只有一个 i
// 2. setTimeout 的回调捕获了同一个 i 的引用
// 3. 回调执行时,循环早已结束,i 已经是 4
解决方法 1:使用 let(推荐)
for (let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i); // 1 2 3
}, i * 100);
}
// 输出:1 2 3
// 原因:let 是块级作用域,每次迭代创建新的 i
解决方法 2:使用闭包(IIFE)
for (var i = 1; i <= 3; i++) {
(function(index) {
setTimeout(function() {
console.log(index); // 1 2 3
}, index * 100);
})(i); // 每次迭代创建新的作用域,捕获当时的 i 值
}
解决方法 3:使用 forEach
[1, 2, 3].forEach(function(i) {
setTimeout(function() {
console.log(i); // 1 2 3
}, i * 100);
});
陷阱 2:this 指向问题
let obj = {
count: 0,
start() {
// 这里的 this 是 obj
console.log(this.count); // 0
setTimeout(function() {
this.count++; // ❌ 这里的 this 不是 obj!
console.log(this); // 浏览器: window,Node.js: global 或 undefined(严格模式)
}, 100);
}
};
obj.start();
解决方法 1:使用箭头函数
let obj = {
count: 0,
start() {
setTimeout(() => {
this.count++; // ✅ 箭头函数继承外层 this
console.log(this.count); // 1
}, 100);
}
};
obj.start();
解决方法 2:保存 this 引用
let obj = {
count: 0,
start() {
let self = this; // 用闭包保存 this
setTimeout(function() {
self.count++; // ✅ 使用 self
console.log(self.count); // 1
}, 100);
}
};
obj.start();
陷阱 3:变量共享问题
function createFunctions() {
let result = [];
for (var i = 0; i < 3; i++) {
result.push(function() {
return i; // 所有函数共享同一个 i!
});
}
return result;
}
let funcs = createFunctions();
console.log(funcs[0]()); // 3(不是 0!)
console.log(funcs[1]()); // 3(不是 1!)
console.log(funcs[2]()); // 3(不是 2!)
解决方法:使用 let 或闭包
// 方法 1:let
function createFunctions() {
let result = [];
for (let i = 0; i < 3; i++) { // let 每次迭代创建新的 i
result.push(function() {
return i; // 每个函数捕获各自的 i
});
}
return result;
}
let funcs = createFunctions();
console.log(funcs[0]()); // 0 ✅
console.log(funcs[1]()); // 1 ✅
console.log(funcs[2]()); // 2 ✅
内存管理和性能
闭包会占用多少内存?
function createClosure() {
let small = 42; // 很小的数据
let huge = new Array(1000000).fill('*'); // 很大的数据
return function() {
console.log(small); // 只使用了 small
// 但 huge 也被闭包持有,无法释放!
};
}
let fn = createClosure();
// 即使只使用 small,huge 数组也一直占用内存
内存泄漏问题
// 危险:闭包持有大量数据
function setup() {
let hugeData = new Array(1000000).fill('*');
document.getElementById('btn').addEventListener('click', function() {
// 这个事件监听函数形成了闭包
console.log('clicked');
// 它捕获了 setup 的作用域,包括 hugeData!
});
// hugeData 永远不会被释放(只要事件监听还在)
}
// 解决方法:手动移除监听,或避免捕获不需要的数据
function setupFixed() {
let hugeData = new Array(1000000).fill('*');
let btn = document.getElementById('btn');
function handler() {
console.log('clicked');
}
btn.addEventListener('click', handler);
// 稍后移除
// btn.removeEventListener('click', handler);
// 或者:
// handler = null; // 断开引用
}
如何"释放"闭包
function create() {
let data = 'some data';
return function() {
return data;
};
}
let closure = create();
console.log(closure()); // "some data"
// 释放闭包(断开所有引用)
closure = null; // 现在闭包可以被垃圾回收
性能考虑
// 函数内部的函数定义(每次调用都创建新闭包)
function outer() {
let x = 10;
return function() { // 每次调用 outer,都创建新的闭包
return x;
};
}
// 更好的方式:避免频繁创建闭包
const inner = (function() {
let x = 10;
return function() {
return x;
};
})(); // 只创建一次
// 或者:如果不需要闭包,就不要使用
function add(a, b) {
return a + b; // 不需要闭包,更高效
}
闭包 vs 类(Class)
ES6 引入了 class,有时可以替代闭包实现封装:
// 使用闭包实现封装
function createCounter() {
let count = 0;
return {
increment() {
count++;
return count;
},
getCount() {
return count;
}
};
}
let c1 = createCounter();
console.log(c1.increment()); // 1
// count 是私有的
// 使用 class 实现(ES6)
class Counter {
#count = 0; // 私有字段(ES2022)
increment() {
this.#count++;
return this.#count;
}
getCount() {
return this.#count;
}
}
let c2 = new Counter();
console.log(c2.increment()); // 1
// c2.#count // SyntaxError(私有字段)
| 特性 | 闭包 | Class |
|---|---|---|
| 私有性 | 完全私有(闭包内) | 私有字段 #(ES2022) |
| 内存 | 每个实例独立闭包 | 方法共享原型 |
| 适合场景 | 函数式编程、工厂函数 | 面向对象编程、复杂对象 |
| 性能 | 每个实例独立闭包 | 方法在原型上共享 |
综合示例
示例 1:实现可配置的计时器
function createTimer(interval) {
let timerId = null;
let count = 0;
let callbacks = []; // 闭包捕获的回调列表
return {
start() {
if (timerId) return;
timerId = setInterval(() => {
count++;
callbacks.forEach(cb => cb(count));
}, interval);
},
stop() {
clearInterval(timerId);
timerId = null;
},
reset() {
count = 0;
},
onTick(callback) {
callbacks.push(callback);
},
getCount() {
return count;
}
};
}
let timer = createTimer(1000);
timer.onTick(count => console.log('tick:', count));
timer.start();
// 5 秒后停止
setTimeout(() => {
timer.stop();
console.log('最终计数:', timer.getCount());
}, 5000);
示例 2:实现观察者模式
function createObservable() {
let observers = []; // 闭包私有的观察者列表
return {
subscribe(callback) {
observers.push(callback);
// 返回取消订阅的函数
return () => {
observers = observers.filter(cb => cb !== callback);
};
},
notify(data) {
observers.forEach(cb => cb(data));
}
};
}
let observable = createObservable();
let unsub1 = observable.subscribe(data => console.log('观察者1:', data));
let unsub2 = observable.subscribe(data => console.log('观察者2:', data));
observable.notify('Hello!');
// 输出:
// 观察者1: Hello!
// 观察者2: Hello!
unsub1(); // 取消订阅
observable.notify('World!');
// 输出:
// 观察者2: World!(只有观察者2 收到)
示例 3:解决循环中的闭包问题(完整方案)
// 问题:循环中的闭包
function createWrong() {
let result = [];
for (var i = 0; i < 3; i++) {
result.push(() => console.log(i)); // 全部输出 3
}
return result;
}
// 方案 1:使用 let(推荐)
function createWithLet() {
let result = [];
for (let i = 0; i < 3; i++) { // let 块级作用域
result.push(() => console.log(i)); // 0, 1, 2
}
return result;
}
// 方案 2:使用闭包(IIFE)
function createWithIIFE() {
let result = [];
for (var i = 0; i < 3; i++) {
((index) => {
result.push(() => console.log(index)); // 0, 1, 2
})(i);
}
return result;
}
// 方案 3:使用 forEach
function createWithForEach() {
let result = [];
[0, 1, 2].forEach(i => {
result.push(() => console.log(i)); // 0, 1, 2
});
return result;
}
// 测试
let f1 = createWithLet();
f1.forEach(fn => fn()); // 0 1 2
let f2 = createWithIIFE();
f2.forEach(fn => fn()); // 0 1 2
示例 4:实现缓存装饰器
function cached(fn) {
let cache = new Map(); // 闭包缓存
return function(...args) {
let key = JSON.stringify(args);
if (cache.has(key)) {
console.log('缓存命中:', key);
return cache.get(key);
}
console.log('计算中:', key);
let result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// 使用
function expensiveCalculation(x, y) {
console.log('(执行耗时计算...)');
return x * y;
}
let cachedCalc = cached(expensiveCalculation);
console.log(cachedCalc(3, 4)); // 计算中: [3,4] → 12
console.log(cachedCalc(3, 4)); // 缓存命中: [3,4] → 12
console.log(cachedCalc(5, 6)); // 计算中: [5,6] → 30
总结:闭包速查表
| 概念 | 说明 |
|---|---|
| 定义 | 函数 + 其词法作用域的引用 |
| 构成 | 内部函数 + 访问外部变量 |
| 原理 | 词法作用域;变量不会被销毁 |
| 每次调用 | 产生独立的闭包(独立作用域) |
| 内存 | 闭包会持有外部变量的引用 |
| 释放 | 断开引用(赋值为 null) |
| 经典陷阱 | 循环中的闭包(用 let 解决) |
| this 问题 | 普通函数 this 动态;箭头函数继承 |
| 常见应用 | 封装、工厂、模块化、偏函数、记忆化 |
闭包检查清单
当你写代码时,问自己:
- 这个函数访问了外部变量吗?(如果是,它就是闭包)
- 外部变量会在函数执行完后还存在吗?(闭包会让它存在)
- 是否需要释放闭包?(赋值为 null)
- 循环中创建闭包时,用 let 了吗?(避免共享变量问题)
- this 指向正确吗?(考虑用箭头函数)
核心记忆:闭包就是函数可以记住它被创建时的环境,即使后来在别处执行,仍然可以访问那个环境中的变量。