1. 概念与原理
在 JavaScript 中,闭包指的是函数在创建时“记住”了它的词法作用域,从而即使外部函数已经返回,内部函数仍然能够访问外部变量。这个特性让自由变量成为闭包中的关键数据:它们是在当前作用域无法直接声明,但在外部作用域有定义的变量。
理解词法作用域和环境记录是掌握闭包的基础。JavaScript 的运行时会维持一个作用域链,其中包含外部变量及其绑定。当内部函数被调用时,它会沿着这条链向上查找变量的值,即使创建该函数的外部函数已经执行结束。

下面给出一个简单的示例,展示如何创建一个闭包以及它如何捕获自由变量。通过这段代码你可以看到,变量 a 被内部函数继续访问,即便外部函数的执行已经结束。
function makeCounter() {let count = 0;return function() {count++;return count;};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
1.1 闭包与自由变量的定义
闭包是一种函数及其创建时所在环境的综合体,它使得内部函数能够访问外部函数的变量。对于开发者来说,理解这一点就是掌握“如何让变量在未来调用中依然可用”的关键。
在上述例子中,变量 count 是一个自由变量,它并未在内部函数中声明,但内部函数通过闭包绑定了它的引用。当你多次调用内部函数时,变量 count 的值会持续增长。
1.2 作用域与环境记录的关系
JavaScript 的作用域链由几个环境记录组成:当前作用域的变量对象、可访问的外部函数的作用域,以及全局对象。闭包保留了对这些环境记录的引用,因此外部变量在闭包生命周期内不会被垃圾回收。
这也意味着滥用闭包可能导致内存占用持续增加,尤其当闭包引用了大量的外部变量时。理解内存管理与垃圾回收的关系有助于写出更健壮的代码。
2. 闭包捕获自由变量的原理
2.1 作用域链与环境记录的工作机制
当一个函数被创建时,解释器会创建一个与之关联的环境记录,其中包含了该函数在创建时能访问的所有变量。这个环境记录会被闭包引用,从而实现对外部变量的持久访问。
如果一个外部变量在后续引用中被修改,闭包也会看到该修改,因为它访问的是同一个环境记录中的绑定。这个机制是实现对状态持续访问的核心。环境记录的持有是闭包功能的实际基础。
function outer() {let x = 10;function inner() {return x;}return inner;
}
const fn = outer();
console.log(fn()); // 10
2.2 自由变量的绑定与垃圾回收
闭包通过对外部变量的引用来保持绑定,因此即使外部函数结束执行,变量绑定仍然存在。引用不会马上释放,直到不再有闭包引用或变量不再需要时,才会被垃圾回收机制清理。
这也是为什么在某些场景下要小心使用闭包,避免产生不必要的内存泄漏。在实际开发中,理解引用关系和生命周期有助于优化内存占用。
3. JavaScript 实现闭包的常见模式
3.1 简单闭包的定义与使用
最直观的闭包场景是一个函数返回另一个函数,内部函数能够继续访问外部作用域的变量。通过这种模式,你可以实现私有状态、需要的局部变量保护等需求。合法的闭包使用方式是在需要时返回函数,而不是只在全局暴露变量。
下面演示一个常见的计数器模式:每次调用返回一个递增后的数值。这个模式非常适合用来演示闭包对自由变量的捕获与封装能力。
function makeCounter() {let count = 0;return function() {count += 1;return count;};
}
const next = makeCounter();
console.log(next()); // 1
console.log(next()); // 2
在这个示例中,外部变量 count 被内部函数持续绑定,每次调用都基于同一个状态进行更新,这是闭包持久化的典型体现。
3.2 IIFE 与私有变量的实现方式
立即执行函数表达式(IIFE)常用于创建一个独立的执行环境,并返回一个带有私有变量的函数。通过这种模式,外部无法直接访问私有变量,只能通过闭包暴露的接口来交互。模块化与封装的理念正是靠此实现。
const CounterModule = (function() {let privateCount = 0;function increment() {privateCount++;return privateCount;}return {next: increment};
})();
console.log(CounterModule.next()); // 1
console.log(CounterModule.next()); // 2
上述例子中,变量 privateCount 的直接访问被封装在 IIFE 的作用域内部,外部只通过暴露的 next 方法来操作,从而实现了私有变量的保护。
3.3 模块化模式中的闭包应用
在模块化设计中,闭包经常用于维持模块内部的状态并向外部暴露接口。这使得模块可以具备清晰的边界和可测试性,同时减少全局污染。闭包的封装能力是实现高内聚、低耦合模块的关键。
const LoggerModule = (function() {let logs = [];function log(message) {const entry = `${new Date().toISOString()} - ${message}`;logs.push(entry);}function getAll() {return logs.slice();}return {info: (m) => { log(`INFO: ${m}`); },get: getAll};
})();
LoggerModule.info("应用启动");
console.log(LoggerModule.get());
在这里,logs 是一个私有变量,通过闭包暴露的接口进行读写,外部无法直接污染内部状态,从而实现更可靠的模块化设计。
4. 实战演练:常见场景中的闭包应用
4.1 私有变量与访问控制
闭包提供了一个天然的访问控制机制:只有通过指定的接口才能读取或修改内部状态。私有状态的保护对于数据一致性和代码可维护性非常重要。
下面是一个简单的私有变量封装示例,展示如何通过闭包隐藏实现细节,同时提供受控的访问入口。
function createAccount(initial) {let balance = initial;return {deposit: (n) => { balance += n; return balance; },withdraw: (n) => { balance = Math.max(0, balance - n); return balance; },getBalance: () => balance};
}
const acc = createAccount(100);
console.log(acc.deposit(50)); // 150
console.log(acc.withdraw(30)); // 120
console.log(acc.getBalance()); // 120
4.2 防抖/节流实现中的闭包
在前端性能优化中,防抖与节流常借助闭包保留计时信息与上一次执行的状态。通过闭包,可以保存一个定时器或时间戳,使得回调函数在未来某个时刻再执行,从而避免过于频繁的触发。
下面给出一个简单的防抖实现,展示如何利用闭包保存定时器引用以及参数缓存,以实现“最后一次调用在等待时间后执行”的行为。
function debounce(fn, wait) {let timer = null;return function(...args) {clearTimeout(timer);timer = setTimeout(() => fn.apply(this, args), wait);};
}
const log = (msg) => console.log(msg);
const debouncedLog = debounce(log, 200);
debouncedLog("事件1");
debouncedLog("事件2");
// 只有最后一个事件在 200ms 后输出
4.3 柯里化与闭包的组合应用
柯里化将一个函数分解成多个参数部分,并返回一个新函数。这一过程中,外部参数通过闭包被持续绑定,内部则可以继续接收后续参数完成计算。
以下示例展示了如何通过柯里化实现将固定前缀参数绑定到一个日志输出函数上。
function prefixLogger(prefix) {return function(message) {console.log(`[${prefix}] ${message}`);};
}
const errorLogger = prefixLogger("ERROR");
errorLogger("Something went wrong");


