文章中英模式
布鲁斯前端JS面试题目 - JavaScript 闭包(Closure)
从基础理解 JavaScript 闭包(Closure)与 Lexical Scope,并搭配 useState 模型理解 React 如何记住 state,搭配内存泄漏案例与常见面试题,全面掌握闭包原理与陷阱。
文章中英模式
懒得看文章?那就来看视频吧
什么是闭包?JavaScript 为什么需要它?
闭包(Closure)是指一个函数和它定义时的外部变量的组合。即使函数被传递到不同的作用域执行,它仍然可以存取当时建立时的变量。
- •保存函数中的中间状态(例如计数器)
- •封装数据,实现私有变量
- •提供记忆性(如 memoization)
- •处理异步流程与事件回调
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
词法作用域(Lexical Scope)是什么?和闭包有什么关系?
JavaScript 采用词法作用域(Lexical Scope),也就是「函数可以存取的是它定义当下的变量环境,而不是它执行当下的环境」。
闭包就是这个机制的延伸,让函数在未来仍然能记得定义当下的变量。
function outer() {
const name = 'Bruce';
function greet() {
console.log('Hello, ' + name);
}
return greet;
}
const sayHello = outer();
sayHello(); // Hello, Bruce
内存泄漏的真相:闭包用不好就变成地雷
闭包延长变量生命周期,若引用大型数据或DOM元素,又与长寿命结构绑定,会导致内存无法释放。
范例 1:全局变量与 DOM 结合
let cache = {};
function bindElement(elementId) {
const element = document.getElementById(elementId);
cache[elementId] = () => {
console.log(element.innerText);
};
}
bindElement('myDiv');
DOM元素被闭包引用并存在全局cache中,即使元素被移除,GC也无法回收。解决:不需要时设置 cache[elementId] = null。
范例 2:setTimeout 长期引用变量
function setupLogger() {
let bigData = new Array(1000000).fill('*');
setTimeout(() => {
console.log(bigData[0]);
}, 10000);
}
计时器回调引用大型数组,即使函数执行完毕,bigData仍保留在内存10秒。问题在于闭包让setTimeout持续引用bigData,阻止垃圾回收。解决:清除计时器或只保留必要数据。
React 的 useState 为什么能记住值?多次调用 Hook 为什么需要闭包?
React 在每次 render 时会重新执行整个组件函数。但 useState 返回的 getter/setter 是闭包,能记住对应状态。
function simpleUseState(initialValue) {
let value = initialValue;
return [
() => value,
(newVal) => { value = newVal; }
];
}
const [getA, setA] = simpleUseState(1);
const [getB, setB] = simpleUseState(100);
setA(2);
console.log(getA()); // 2
console.log(getB()); // 100
🔥 常见面试题目
(一)什么是闭包?JavaScript 为什么需要它?
闭包是函数与其变量环境的组合,让函数记住定义时的变量。JavaScript 用它来保存状态、封装数据和处理回调。
function createCounter() {
let count = 0; // 这个变量被闭包保存
return function() {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
(二)词法作用域是什么?与闭包的关系?
词法作用域指变量范围由定义位置决定,非执行位置。闭包基于此特性,让函数带着「出生环境」到处执行。
let name = 'Global';
function outer() {
let name = 'Outer';
function inner() {
console.log(name); // 使用的是 outer 的 name
}
return inner;
}
const innerFn = outer();
innerFn(); // 'Outer' - 即使在全局执行,仍记得定义时的环境
(三)如何用闭包实现私有变量?
闭包可创建私有变量,外部只能通过指定方法存取,实现类似面向对象的封装概念。
function createSecret() {
let secret = '1234';
return {
get() { return secret; },
set(val) { secret = val; }
};
}
const user = createSecret();
console.log(user.get()); // '1234'
user.set('abcd');
console.log(user.get()); // 'abcd'
// console.log(secret); // 错误!外部无法直接存取
(四)React 的 useState 是怎么利用闭包来记住值?多次调用 Hook 为什么需要闭包?
每个 useState 创建独立闭包,为每个状态提供专属「保险箱」,使状态在重新渲染时保持独立且不互相干扰。
function simpleUseState(initialValue) {
let value = initialValue;
return [
() => value,
(newVal) => { value = newVal; }
];
}
const [getA, setA] = simpleUseState(1);
const [getB, setB] = simpleUseState(100);
setA(2);
console.log(getA()); // 2
console.log(getB()); // 100
(五)闭包可能造成内存泄漏的情境有哪些?
闭包持续引用大型数据或DOM元素且未释放时,会阻止垃圾回收,像忘关水龙头般消耗资源。
- •全局变量绑定 DOM(造成元素无法被 GC)
- •setTimeout 或事件监听绑定变量未清除
// 内存泄漏范例
function setupEventHandler() {
const largeData = new Array(10000).fill('data');
document.getElementById('button').addEventListener('click', function() {
console.log(largeData.length); // 持续引用大型数据
});
// 正确做法:保存引用并在适当时机移除
// const handler = function() { console.log(largeData.length); };
// document.getElementById('button').addEventListener('click', handler);
// 之后可以用 removeEventListener('click', handler) 移除
}
(六)工厂函数与闭包的关联是什么?
工厂函数利用闭包批量生产功能相似但参数不同的函数,每个函数都记住自己的独立环境。
function createMultiplier(factor) {
return function(num) {
return num * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15