skip to content
月与羽

闭包是什么

/ 12 min read

闭包

  1. 函数嵌套:一个内部函数定义在另一个外部函数中。
  2. 内部函数引用外部函数的变量
  3. 外部函数返回或传递内部函数,使其能够在外部函数执行完毕后被调用。

当一个函数能够记住并访问它在被创建时所处的词法作用域(Lexical Scope),即使该函数在当前词法作用域之外执行,它也仍然可以访问那些变量。这种现象就叫做闭包。


闭包的常见使用场景

以下是闭包在日常开发中非常常见的应用场景。

1. 模拟私有变量(数据封装)

这是闭包最经典和重要的用途之一。在 JavaScript 的早期(ES6 class 语法和私有字段 # 出现之前),闭包是实现面向对象编程中“私有成员”的唯一方式。

function createPerson(name) {
let _age = 25; // 这是一个“私有”变量,外部无法直接访问
return {
getName: function() {
return name;
},
getAge: function() {
return _age;
},
growUp: function() {
_age++;
console.log(`${name} is now ${_age} years old.`);
}
};
}
const person = createPerson("Alice");
console.log(person.getName()); // "Alice"
console.log(person.getAge()); // 25
person.growUp(); // "Alice is now 26 years old."
// 无法直接访问或修改 _age
console.log(person._age); // undefined
person._age = 100; // 这只是给 person 对象添加了一个新属性,并没有改变闭包内的 _age
console.log(person.getAge()); // 26 (仍然是闭包中安全的值)

优点: 保护了内部数据(_age),只通过暴露的特定方法 (getAge, growUp) 来进行操作,避免了外部的随意篡改,使代码更健壮。这就是封装

2. 创建特定状态的函数(函数工厂/柯里化)

闭包可以用来创建“预设”了某些参数的函数。

// 一个通用的加法器工厂
function makeAdder(x) {
// x 被保存在返回的函数闭包中
return function(y) {
return x + y;
};
}
const add5 = makeAdder(5); // 创建一个“加5”的函数
const add10 = makeAdder(10); // 创建一个“加10”的函数
console.log(add5(2)); // 7 (相当于 5 + 2)
console.log(add10(2)); // 12 (相当于 10 + 2)

这种技术在函数式编程中非常常见(称为柯里化 Currying),可以生成更专用、更灵活的函数。

3. 在异步操作中保存状态(回调函数)

setTimeoutPromise、事件监听器等异步场景中,闭包无处不在。回调函数需要访问定义它们时的上下文信息。

function waitAndSay(message, delay) {
setTimeout(function() {
// 这个回调函数是一个闭包
// 它记住了外层的 message 变量
console.log(message);
}, delay);
}
waitAndSay("Hello after 2 seconds", 2000);
waitAndSay("Hi after 1 second", 1000);

如果没有闭包,setTimeout 的回调函数在未来执行时,将无法知道当初调用 waitAndSay 时传入的 message 是什么。

4. 解决循环中的经典问题

这是一个非常经典的面试题,完美地展示了闭包的作用。

错误示例(使用 var):

for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
// 预期输出: 1, 2, 3
// 实际输出: 4, 4, 4

原因: for 循环瞬间就执行完了,此时 i 的值变成了 4。由于 var 是函数作用域,所有 setTimeout 的回调函数都共享着同一个 i。当它们在 1、2、3 秒后执行时,它们去读取 i 的值,发现都是 4

使用闭包修复(ES5 方法):

for (var i = 1; i <= 3; i++) {
(function(j) { // 使用立即执行函数(IIFE)创建新的作用域
setTimeout(function() {
console.log(j); // 回调函数引用的是 IIFE 的参数 j
}, j * 1000);
})(i); // 将每次循环的 i 的值“固定”住,传给 j
}
// 输出: 1, 2, 3

每个 IIFE 都创建了一个新的作用域,把当时 i 的值(1, 2, 3)作为副本 j 保存了下来。setTimeout 的回调形成了对 j 的闭包。

现代解决方法(ES6 let): ES6 的 let 关键字拥有块级作用域,它在 for 循环中有一个特殊的行为:为每次循环创建一个新的词法环境,从而自动地解决了这个问题。这实际上是语法层面的闭包应用。

for (let i = 1; i <= 3; i++) { // let 会为每次循环创建一个新的 i
setTimeout(function() {
console.log(i);
}, i * 1000);
}
// 输出: 1, 2, 3

有什么场景是“必须”要用闭包的?

这个问题的核心在于“必须”。在编程中,很多问题都有多种解法,但有些场景下,闭包是最自然、最直接、最符合语言设计意图的解决方案。可以说,不使用闭包(或其语法糖)就无法优雅地解决这些问题。

以下场景可以认为是“必须”使用闭包的:

1. 数据封装和私有状态(The Module Pattern)

场景: 你需要创建一个组件或模块,它有自己的内部状态,并且你希望这个状态是私有的,不能被外部直接访问,只能通过你提供的特定接口来操作。

为什么必须用闭包? 这是在 JavaScript 中实现真正信息隐藏(Information Hiding) 的基石。全局变量、对象属性都是公开的。只有函数作用域内的变量才是“私有”的。而闭包是唯一能让外部访问到这个“私有”作用域内部,同时又保持变量本身不被暴露的机制。

如果没有闭包, 你只能:

  • 使用全局变量:污染全局命名空间,数据不安全。
  • 使用普通对象属性:person.age = -100,数据可以被随意修改,无法保证其有效性。
  • 使用 ES6 class 的私有字段 (#):这其实是闭包概念的“语法糖”实现,其底层机制和思想与闭包紧密相关。所以即便你用 #,本质上还是在利用闭包的思想。

结论: 在需要创建拥有私有状态的、可复用的组件时,闭包(或其变体和语法糖)是不可或缺的。

2. 将函数与特定状态“绑定”后传递

场景: 当你需要将一个函数作为参数(例如回调函数、事件处理器)传递给另一个函数,并且这个函数在未来执行时需要访问到创建它时的特定上下文数据

为什么必须用闭包? 函数在 JavaScript 中是“一等公民”,可以像值一样传来传去。但函数本身只是代码逻辑。当你需要“代码逻辑 + 当时的数据”这个组合时,闭包就是这个组合的载体。

  • 异步回调fetch('/api/user/123').then(user => console.log(user.name)),这个箭头函数就是一个闭包,它能访问到外部的变量(比如一个 userId)。
  • 事件监听button.addEventListener('click', () => alert('Button ' + buttonId + ' was clicked!')),这个回调函数必须“记住”它被绑定时对应的 buttonId
  • 函数式编程:像 makeAdder(5) 这种,其核心目的就是创建一个“代码 + 数据”的函数包。

结论: 只要你需要在未来的某个时间点,在一个不同的执行上下文中,执行一个需要**“记忆”创建时上下文数据**的函数,你就正在使用闭包。这是 JavaScript 异步和事件驱动模型的根本。

闭包代码

✅ 1. 计数器(经典闭包例子)

function createCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

函数 counter 保留了对 count 的引用。


✅ 2. 私有变量封装

function User(name) {
let _password = 'secret';
return {
getName() {
return name;
},
checkPassword(pwd) {
return pwd === _password;
}
};
}
const user = User("Alice");
console.log(user.getName()); // "Alice"
console.log(user.checkPassword("secret")); // true
console.log(user._password); // undefined ❌ 外部访问不到

_password 是私有变量,通过闭包保持作用域。


✅ 3. 延迟执行 &

setTimeout

循环闭包问题

❌ 错误写法:

for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}
// 输出:3 3 3

✅ 正确闭包方式:

for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(() => console.log(j), 1000);
})(i);
}
// 输出:0 1 2

或者用 let(块作用域)也可以:

for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}

✅ 4. 工厂函数

function multiplier(factor) {
return function(x) {
return x * factor;
};
}
const double = multiplier(2);
console.log(double(5)); // 10

double 保留了 factor=2 的闭包。


✅ 5. 缓存函数(记忆函数)

function memoize(fn) {
const cache = {};
return function(n) {
if (n in cache) {
return cache[n];
} else {
const result = fn(n);
cache[n] = result;
return result;
}
};
}
const square = memoize(n => n * n);
console.log(square(5)); // 25(计算)
console.log(square(5)); // 25(读取缓存)

✅ 6. 模拟私有方法(模块模式)

const Counter = (function() {
let count = 0;
return {
increment() {
count++;
},
getCount() {
return count;
}
};
})();
Counter.increment();
Counter.increment();
console.log(Counter.getCount()); // 2

✅ 7. DOM 事件处理中的闭包

function setupButton(id) {
let clicked = false;
document.getElementById(id).addEventListener("click", function () {
if (!clicked) {
console.log("Button clicked!");
clicked = true;
} else {
console.log("Already clicked.");
}
});
}

clicked 被闭包捕获,记住每个按钮是否点过。


✅ 8. 实现 once 函数(只执行一次)

function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
result = fn.apply(this, args);
called = true;
}
return result;
};
}
const init = once(() => {
console.log("初始化执行!");
return 42;
});
init(); // 输出 "初始化执行!"
init(); // 不再执行,只返回结果