MST

星途 面试题库

面试题:JavaScript 闭包在复杂异步场景下的优化应用

假设你正在开发一个具有多个异步操作且依赖闭包的复杂前端应用,其中涉及大量的事件监听和回调函数。描述如何利用闭包优化异步操作的逻辑,避免回调地狱,并阐述在这种场景下闭包可能带来的潜在风险以及应对策略。
44.2万 热度难度
编程语言JavaScript

知识考点

AI 面试

面试题答案

一键面试

利用闭包优化异步操作逻辑避免回调地狱

  1. 模块化与封装: 将每个异步操作封装成独立的函数,这些函数内部使用闭包来管理状态和数据。例如,对于一个获取用户信息并根据信息加载用户偏好设置的场景:
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();

这里通过闭包,getUserInfoloadUserPreferences 函数可以独立管理自身的异步逻辑,并且 main 函数通过 async/await 语法(基于Promise,而Promise内部利用闭包管理状态),使得异步操作逻辑更加清晰,避免了层层嵌套的回调地狱。

  1. 函数柯里化: 柯里化是一种利用闭包的技术,它允许我们将一个多参数函数转化为一系列单参数函数。例如,对于一个需要多次根据不同条件过滤数组的操作:
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,使得代码更加模块化,异步场景下可以更方便地组合不同的过滤逻辑。

闭包带来的潜在风险及应对策略

  1. 内存泄漏风险
    • 风险:当闭包引用的外部变量在不再需要时无法被垃圾回收机制回收,就会导致内存泄漏。例如,在事件监听中,如果闭包引用了一个大对象,即使该对象在其他地方不再使用,但由于闭包的存在,垃圾回收器无法回收它。
    • 应对策略
      • 手动解除引用:在事件监听回调函数执行完毕后,手动将闭包中对不再需要的外部变量的引用设为 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);
}
  1. 变量作用域混淆风险
    • 风险:闭包内部访问外部变量时,可能会因为变量作用域链的复杂性导致意外的变量取值。例如,在循环中创建闭包,闭包可能会获取到循环结束后的最终变量值,而不是每次循环时的当前值。
    • 应对策略
      • 使用块级作用域:在ES6中,可以使用 letconst 声明变量,它们具有块级作用域。例如:
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 值传入新作用域,避免了闭包获取到错误的变量值。