面试题答案
一键面试利用闭包优化异步操作逻辑避免回调地狱
- 模块化与封装: 将每个异步操作封装成独立的函数,这些函数内部使用闭包来管理状态和数据。例如,对于一个获取用户信息并根据信息加载用户偏好设置的场景:
function getUserInfo() {
return new Promise((resolve, reject) => {
// 模拟异步操作,比如从API获取数据
setTimeout(() => {
resolve({ name: 'John', age: 30 });
}, 1000);
});
}
function loadUserPreferences(user) {
return new Promise((resolve, reject) => {
// 基于用户信息获取偏好设置
setTimeout(() => {
resolve({ theme: 'dark', language: 'en' });
}, 1000);
});
}
async function main() {
const user = await getUserInfo();
const preferences = await loadUserPreferences(user);
console.log('User:', user, 'Preferences:', preferences);
}
main();
这里通过闭包,getUserInfo
和 loadUserPreferences
函数可以独立管理自身的异步逻辑,并且 main
函数通过 async/await
语法(基于Promise,而Promise内部利用闭包管理状态),使得异步操作逻辑更加清晰,避免了层层嵌套的回调地狱。
- 函数柯里化: 柯里化是一种利用闭包的技术,它允许我们将一个多参数函数转化为一系列单参数函数。例如,对于一个需要多次根据不同条件过滤数组的操作:
function filterArray(callback) {
return function (array) {
return array.filter(callback);
};
}
const isEven = num => num % 2 === 0;
const filterEven = filterArray(isEven);
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = filterEven(numbers);
console.log(evenNumbers);
通过柯里化,filterArray
函数返回的新函数形成了闭包,记住了传入的 callback
,使得代码更加模块化,异步场景下可以更方便地组合不同的过滤逻辑。
闭包带来的潜在风险及应对策略
- 内存泄漏风险:
- 风险:当闭包引用的外部变量在不再需要时无法被垃圾回收机制回收,就会导致内存泄漏。例如,在事件监听中,如果闭包引用了一个大对象,即使该对象在其他地方不再使用,但由于闭包的存在,垃圾回收器无法回收它。
- 应对策略:
- 手动解除引用:在事件监听回调函数执行完毕后,手动将闭包中对不再需要的外部变量的引用设为
null
。例如:
- 手动解除引用:在事件监听回调函数执行完毕后,手动将闭包中对不再需要的外部变量的引用设为
let largeObject = { /* 包含大量数据 */ };
document.addEventListener('click', function () {
// 使用 largeObject
console.log(largeObject);
// 操作完成后解除引用
largeObject = null;
});
- **使用WeakMap或WeakSet**:如果闭包中需要存储一些关联关系,可以使用 `WeakMap` 或 `WeakSet`。它们不会阻止对象被垃圾回收,因为它们对对象的引用是弱引用。例如:
const weakMap = new WeakMap();
function addToWeakMap(key, value) {
weakMap.set(key, value);
}
function getFromWeakMap(key) {
return weakMap.get(key);
}
- 变量作用域混淆风险:
- 风险:闭包内部访问外部变量时,可能会因为变量作用域链的复杂性导致意外的变量取值。例如,在循环中创建闭包,闭包可能会获取到循环结束后的最终变量值,而不是每次循环时的当前值。
- 应对策略:
- 使用块级作用域:在ES6中,可以使用
let
或const
声明变量,它们具有块级作用域。例如:
- 使用块级作用域:在ES6中,可以使用
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000 * i);
}
这里 let
声明的 i
在每次循环迭代时都会创建一个新的块级作用域,闭包中获取到的就是每次循环时的 i
值。
- 立即执行函数表达式(IIFE):在ES5及更早版本中,可以使用IIFE来创建新的作用域。例如:
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(() => {
console.log(j);
}, 1000 * j);
})(i);
}
这里IIFE创建了新的作用域,将每次循环的 i
值传入新作用域,避免了闭包获取到错误的变量值。