在JavaScript前端面试中,闭包(Closure)几乎是必考题。它不仅是理解JS作用域链的关键,更是掌握高阶函数、模块化和异步编程的基石。本页面将从底层机制到实战应用,全方位拆解这一核心概念。
在MDN(Mozilla Developer Network)中,闭包被定义为:“函数和其周围状态(词法环境)的引用捆绑在一起形成闭包。可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员。”
通俗来说,闭包就是一个“有记忆”的函数。当一个函数能够记住并访问它被创建时所处的作用域,即使该函数在其原始作用域之外执行,这种行为就称为闭包。
function outer() {
let count = 0; // 外部函数变量
// 内部函数,访问了 count
function inner() {
count++;
console.log(count);
}
return inner; // 返回内部函数引用
}
const counter = outer();
counter(); // 输出: 1
counter(); // 输出: 2
// 此时 outer 的作用域并未被销毁,因为 inner 引用了它
理解闭包的关键在于理解JavaScript的作用域链(Scope Chain)和垃圾回收机制(Garbage Collection)。
当JavaScript引擎执行代码时,会创建一个执行上下文栈(Call Stack)。每个函数执行时,都会创建一个包含其局部变量、参数和外部函数引用的对象。如果内部函数引用了外部函数的变量,这些变量就不会被垃圾回收器回收,而是被保存在一个特殊的作用域链中,供内部函数随时访问。
由于闭包会持有外部函数的变量,如果不当使用,可能导致内存占用过高。例如,在循环中创建大量闭包,或者在不需要时未及时解除引用,都会导致内存无法释放。
函数执行完毕,局部变量若无引用,则被GC回收。内存释放。
内部函数引用外部变量,即使外部函数执行结束,变量仍驻留内存。
面试官不仅考察你是否知道闭包的定义,更考察你能否在实际开发中运用它。以下是几个经典场景:
在ES6模块出现之前,闭包是实现“私有变量”的主要方式。通过返回一个包含公共方法的对象,可以隐藏内部实现细节,防止外部直接修改状态。
function createBankAccount(initialBalance) {
let balance = initialBalance; // 私有变量
return {
deposit: function(amount) {
balance += amount;
return balance;
},
withdraw: function(amount) {
if (amount > balance) return 'Insufficient funds';
balance -= amount;
return balance;
},
getBalance: function() {
return balance;
}
};
}
const myAccount = createBankAccount(100);
console.log(myAccount.getBalance()); // 100
// myAccount.balance 是 undefined,外部无法直接访问
柯里化是将接受多个参数的函数转变为一系列接受单个参数的函数的技术。闭包在这里起到了“记忆”之前参数的作用。
function curryAdd(x) {
return function(y) {
return function(z) {
return x + y + z;
};
};
}
// 或者更常见的链式调用形式
function add(x) {
return function(y) {
return x + y;
};
}
const add5 = add(5);
console.log(add5(3)); // 8
console.log(add5(10)); // 15
这是面试中最经典的题目之一。在ES6之前,使用 `var` 定义的循环变量在 `setTimeout` 中会输出相同的结果,因为所有定时器共享同一个变量作用域。使用闭包(立即执行函数 IIFE)可以捕获每次循环的变量值。
// ❌ 错误示范:输出 5, 5, 5, 5, 5
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// ✅ 正确示范:使用闭包捕获 i
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
虽然闭包功能强大,但滥用会导致性能问题和逻辑错误。
如果在循环中定义函数但未正确捕获变量,所有函数将引用循环结束后的最后一个值。
| 问题现象 | 原因分析 | 解决方案 |
|---|---|---|
| 循环内 setTimeout 输出错误值 | var 声明的变量具有函数作用域,所有闭包共享同一个变量 | 使用 let 块级作用域 或 IIFE 闭包捕获 |
| 内存占用持续增长 | 大量闭包持有大对象引用,GC 无法回收 | 及时将变量置为 null,减少不必要的闭包 |
| 意外修改外部状态 | 内部函数直接修改了外部可变对象 | 使用深拷贝或不可变数据结构 |
每次调用创建闭包都会分配新的内存空间。在高性能要求的场景(如游戏循环、高频动画)中,应避免在内部函数中创建不必要的闭包。
为了在保证功能的同时优化性能,可以采取以下策略:
null,帮助垃圾回收器工作。let 关键字简化作用域管理。WeakMap 可以避免内存泄漏,因为它允许键对象被垃圾回收。const privateData = new WeakMap();
class Counter {
constructor() {
privateData.set(this, { count: 0 });
}
increment() {
const data = privateData.get(this);
data.count++;
console.log(data.count);
}
}
// 当 Counter 实例被销毁时,WeakMap 中的键值对也会被自动清理