闭包(Closure)面试详解
本文覆盖概念、本质、示例、常见陷阱、答题模板与练习题,便于背记和变形作答。
0) 一句话高分答案
闭包是函数与其定义时的词法环境(Lexical Environment)的组合;即函数可以"记住并访问"定义时的外层作用域变量,即使外层作用域已结束。常用于封装私有变量、延长变量生命周期、实现工厂函数/模块等。
1) 概念详解
- 词法作用域:函数定义的位置决定了它能访问哪些外层变量(不是调用位置)。
- 闭包:当一个函数引用了外部函数的变量并被外部返回或使用时,该函数形成闭包。闭包保存的是"对外层环境的引用",因此被引用的变量不会被回收。
2) 为什么会有闭包(实现角度)
- 引擎为每个执行上下文维护词法环境,包含变量绑定及指向外层环境的引用。
- 若某变量被仍可访问的内部函数引用,则其绑定必须留在内存中;闭包就是"函数 + 可访问的词法环境"。
3) 常见用途(面试要点)
- 模拟私有变量 / 信息隐藏
- 工厂函数 / 生成器模式(返回预配置的函数)
- 函数记忆(memoization)
- 保持某些状态(如计数器)
- 部分应用 / 柯里化(保存部分参数)
- 在事件/回调中携带上下文状态
4) 经典示例(逐行解释)
js
function createCounter() {
let count = 0; // 外层变量
return function () { // 内部函数引用了 count
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
解释:createCounter 执行完后,其执行上下文应被释放。但返回的内部函数仍引用 count,因此 count 继续存在,counter 与其词法环境形成闭包。
5) 面试常见陷阱与解析
题 A:for 循环 + var(最常见)
js
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function () {
console.log(i);
});
}
funcs[0](); // 3
funcs[1](); // 3
funcs[2](); // 3
原因:var i 是函数作用域,三个函数引用的是同一个 i,循环结束后 i === 3。
修复:
- 使用
let i(块级作用域):
js
for (let i = 0; i < 3; i++) {
funcs.push(() => console.log(i));
}
- 或使用 IIFE 捕获当前值:
js
for (var i = 0; i < 3; i++) {
(function (j) {
funcs.push(function () { console.log(j); });
})(i);
}
题 B:闭包与内存占用
闭包会延长被引用变量的生命周期。如果大量闭包保留巨大数据结构且长时间不释放,可能导致内存高占用。需避免无谓保留大对象(例如把大量 DOM 节点放入长期闭包)。
题 C:闭包与 this 的误区
闭包与 this 是两回事:闭包是词法环境,this 是运行时绑定。闭包不会改变 this 的行为,但闭包中的函数仍受 this 规则影响(call/apply/bind)。
6) 追问点与回答思路
- 为什么闭包不会被回收?因为内部函数持有对外层变量的引用,引用存在即不回收。
- 闭包的性能问题?延长变量生命周期、增加内存占用;避免在全局长期引用大量数据,必要时主动解除引用(如置
null)。 - 和模块模式关系?模块常利用闭包隐藏私有状态,只暴露必要 API(IIFE + 返回对象)。
7) 标准答题结构(简洁完整)
- 一句定义(函数 + 词法环境)
- 为什么产生(词法作用域 + 引擎词法环境)
- 常见用途(私有变量、工厂函数等)
- 简单举例(计数器)
- 陷阱/性能补充(如 for/var 示例)
8) 练习题
练习 1:预测输出
js
function makeAdd(x) {
return function(y) {
return x + y;
}
}
const add5 = makeAdd(5);
console.log(add5(2)); // 7
练习 2:修复 for 闭包
js
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function() { console.log(i); });
}
// 改为:
for (let i = 0; i < 3; i++) {
funcs.push(() => console.log(i));
}
funcs.forEach(f => f());
练习 3:实现带 reset 的计数器
js
function createCounter() {
let count = 0;
return {
inc() { count++; return count; },
value() { return count; },
reset() { count = 0; }
};
}
9) 常见变形题(加分项)
- 闭包在异步场景(
setTimeout/Promise)的工作机制 - 用闭包实现
once(fn)(只执行一次) - 用闭包实现
memoize(fn)(结果缓存) - 闭包在模块化(CommonJS/ESM)下的角色
10) 答题小技巧
- 举例时说明"为什么 GC 不会回收该变量"。
- 对 for+var 陷阱:先说结果,再给两种修复(
let与 IIFE)。 - 若被追问性能:说明闭包会延长变量生命周期,应及时解除不必要的引用。
闭包(Closure)超精简速记卡片
专门用于快速复习、面试前突击、临场背诵。内容极度浓缩,但覆盖所有核心点,读一遍就能记住。
1. 闭包是什么?(最标准定义)
闭包 = 函数 + 其定义时的词法环境(Lexical Environment)。
即:函数能访问它外层作用域的变量,即使外层作用域已执行结束。
2. 为什么会有闭包?(一句话解释)
因为 JS 是词法作用域,并且内部函数对外层变量形成引用,导致外层变量不会被回收。
3. 闭包的作用(全部面试高频点)
- 模拟私有变量 / 数据隐藏
- 延长变量生命周期
- 工厂函数(返回预配置函数)
- 函数记忆(memoize)
- 柯里化/部分应用(curry)
- 异步回调中保持状态
4. 最典型的闭包例子(必背)
javascript
function createCounter() {
let count = 0;
return function () {
return ++count;
};
}
5. for + var 经典面试题(必考)
问题
javascript
var arr = [];
for (var i = 0; i < 3; i++) {
arr.push(() => console.log(i));
}
arr[0](); arr[1](); arr[2]();
输出: 3 3 3
原因: 三个函数共享同一个 var i,循环结束后 i = 3。
修复方法(两种都要会)
-
方法一: 换成
letjavascriptvar arr = []; for (let i = 0; i < 3; i++) { arr.push(() => console.log(i)); } -
方法二: IIFE 捕获当前值
javascriptvar arr = []; for (var i = 0; i < 3; i++) { (function(j) { arr.push(() => console.log(j)); })(i); }
6. 闭包常见问题(两句就够)
- 闭包会让外层变量常驻内存 → 可能造成高内存占用
- 及时释放:把引用设为
null或让闭包脱离作用域
7. 面试标准作答模板(背下来就能稳过)
"闭包是函数与其词法环境的组合,它允许函数在外层作用域结束后仍能访问外层变量。
闭包常用于私有变量、工厂函数、柯里化和在异步回调中保持状态。
本质是内部函数对外层变量的引用未释放,因而外层变量的生命周期被延长。"
8. 面试进阶加分句(一句即可)
闭包的根本来源是 JS 的词法作用域规则 + 引擎对词法环境的持久化保存,而不是函数调用位置决定的。