经典面试题 - LAZYMAN (HARDMAN / 懒汉)

主要考察JavaScript异步编程、事件循环、链式调用和任务调度的综合能力。题目要求实现一个hardMan函数,支持链式调用study()rest()restFirst()方法,并满足特定时序逻辑。


一、题目要求与行为分析

hardMan('潘潘') 
// 输出: Hi! I am 潘潘.

hardMan('潘潘').study('敲码') 
// 输出: Hi! I am 潘潘. → I am studying 敲码.

hardMan('潘潘').rest(3).study('敲码')
// 输出: Hi! I am 潘潘. → 等待3秒 → Wait 3 seconds. → I am studying 敲码.

hardMan('潘潘').restFirst(3).study('敲码')
// 输出: 等待3秒 → Wait 3 seconds. → Hi! I am 潘潘. → I am studying 敲码.

关键难点

  1. restFirst(3)需让整个调用链延迟执行,而非仅延迟后续调用。
  2. 链式调用需支持任意顺序组合(如rest可能在study前或后)。
  3. 阻塞执行rest需同步阻塞后续调用,但restFirst需阻塞整个链。

二、两种核心实现方案

方案1:利用宏任务调度(setTimeout)(不推荐)

核心思路:将非restFirst的方法转为宏任务,确保restFirst先同步执行。

function hardMan(name) {
  setTimeout(() => console.log(`Hi! I am ${name}.`), 0);

  const obj = {
    study(project) {
      setTimeout(() => console.log(`I am studying ${project}.`), 0);
      return this;
    },
    rest(duration) {
      setTimeout(() => {
        const start = Date.now();
        while (Date.now() - start < duration * 1000) {} // 同步阻塞
        console.log(`Wait ${duration} seconds.`);
      }, 0);
      return this;
    },
    restFirst(duration) {
      const start = Date.now();
      while (Date.now() - start < duration * 1000) {} // 同步阻塞整个链
      console.log(`Wait ${duration} seconds.`);
      return this;
    }
  };
  return obj;
}

问题

  • restFirst的同步阻塞会卡死页面,实际面试中需优化为异步非阻塞。
  • 还有一个问题,就是 wait 等待的时间如果使用 setTimeout 的话,需要考虑时间间隔,并且不通过尾递归也会导致执行时间和预期不符,再有就是放入任务队列也会导致时间不准确的问题。
方案2:任务队列+递归调度(推荐)

核心思路:构建队列管理任务,通过next()递归驱动执行。

function hardMan(name) {
  const queue = [];
  const initTask = () => setTimeout(() => console.log(`Hi! I am ${name}.`), 0);
  queue.push(initTask);

  const obj = {
    study(project) {
      queue.push(() => console.log(`I am studying ${project}.`));
      return this;
    },
    rest(duration) {
      queue.push(() => new Promise(resolve => {
        setTimeout(() => {
          console.log(`Wait ${duration} seconds.`);
          resolve();
        }, duration * 1000);
      }));
      return this;
    },
    restFirst(duration) {
      queue.unshift(() => new Promise(resolve => { // 插入队首
        setTimeout(() => {
          console.log(`Wait ${duration} seconds.`);
          resolve();
        }, duration * 1000);
      }));
      return this;
    }
  };

  // 启动队列执行
  setTimeout(async () => {
    for (const task of queue) await task();
  }, 0);
  return obj;
}

关键设计

  1. 队列管理queue数组按顺序存储任务函数。
  2. restFirst 优先级:通过unshift将其插入队首,确保最先执行。
  3. 异步阻塞rest/restFirst返回Promise,用await实现异步阻塞。
  4. 启动机制setTimeout确保所有任务注册完成后才执行队列。

三、考察点与面试意图

  1. 事件循环理解:区分同步/异步、宏任务/微任务,解释为何setTimeout能调整时序。
  2. 链式调用实现:返回this维持上下文连贯性。
  3. 任务调度设计:队列管理体现工程化思维,避免回调地狱。
  4. 性能与体验:避免同步阻塞(如while循环),改用异步方案。
  5. JavaScript底层机制:如闭包(维护queue)、Promise控制流。

四、实际应用场景

此类问题常见于异步流程控制需求,如:

  • 批量任务按序执行(含延迟)。
  • 优先级调度(如restFirst插队)。
  • 面试官可能追问:如何扩展支持rest(5).study('A').restFirst(2)等复杂组合? :方案2的队列模型天然支持任意组合,只需按规则入队即可。

实际工作中的场景:比如之前我们公司有一个场景是在后端接口按某种顺序调用,然后依次按照某顺序拼接响应结果,当时解决方案和上述问题类似,本质上都是优先队列的使用。

你可能感兴趣的:(小轮子,javascript)