什么才是好的前端面试题?

1,550 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情

我正在参加 码上掘金体验活动,详情:show出你的创意代码块

什么是一道好的面试题呢?

我认为其中最重要的一个点就是:有区分度。对于初级开发可以看到一层,而对于高级开发可以看到第十层。

这样的面试题在前端领域很多,比如最常见的两个问题:

  1. 当你做性能优化时,你会考虑什么
  2. 当页面输入 URL 时,发生了什么

在前端中也有一道代码题目,出自字节跳动区分度很大,其中考察的元素过多,promise 串行,并行,二分,并发控制,层层递进,不失为一道好的面试题。

请实现以下 sum 函数,只能调用 add 进行实现,而 add 为异步方法

/*
  请实现一个 sum 函数,接收一个数组 arr 进行累加,并且只能使用add异步方法
  
  add 函数已实现,模拟异步请求后端返回一个相加后的值
*/
function add(a, b) {
  return Promise.resolve(a + b);
}

function sum(arr) {
  
}

本代码中代码示例均放置在掘金的码上掘金,可在 Script 面板查看代码,在 Console 面板查看输出结果,然而在掘金文章中不显示 Console 面板,可在代码片段右上角点击链接前往码上掘金查看。

初级实现: 串行方式

遇到这种题目,第一反应就是如同同步 sum 的实现一样,一个一个进行累加。

以下是一个借助于 Array.prototype.reduce 实现的 Promise 版本。

这里一个注意的点是:不能将 0 作为累加器的初始值,因此 add 为异步的,不能保证 add(x, 0) === x

function sum(arr) {
  if (arr.length === 1) return arr[0];
  return arr.reduce((x, y) => Promise.resolve(x).then((x) => add(x, y)));
}

借助于 async/await 的异步 sum 的实现,逻辑更为清楚,同样我们把初始值设置为数组中第一个数,而非 0。

async function sum(arr) {
  let s = arr[0]
  for (let i = 1; i < arr.length; i++) {
    s = await add(s, arr[i])
  }
  return s
}

不管是基于 promise 还是 async 实现,这应该是大部分人的第一反应,然而还有很多同学,直接最简单的实现无法通过,很可能无法通过面试。

但是它有一个问题,在异步sum函数中,其中最为耗时的是 add(),因为他是一个异步 IO 操作,模拟的是服务器数据请求,假设 add 延时一秒,此时需要 N-1 秒,延时太长。

中级实现: 并行方式

关于上边的同步实现,有可能就会筛了一部分同学。面试官到了这里,就会继续增加难度。

接下来是并行的写法: 我们实现一个 chunk 函数,将数组两两分组,每两个计算一次,使用 chunk 二分,此时延时变为 logN 秒

关于 chunk 的 API 可以参考 lodash.chunk: _.concat(array, [values]),在平常工作中也会用到。

function chunk(list, size) {
  const l = [];
  for (let i = 0; i < list.length; i++) {
    const index = Math.floor(i / size);
    l[index] ??= [];
    l[index].push(list[i]);
  }
  return l;
}

在通过 chunk 进行两两分组时,有可能最后一项为单数,此时直接返回数值即可,在最终得到结果后,迭代该函数继续二分,直到最后只有一个数值。

async function sum(arr) {
  if (arr.length === 1) return arr[0];
  const promises = chunk(arr, 2).map(([x, y]) =>
    // 注意此时单数的情况
    y === undefined ? x : add(x, y)
  );
  return Promise.all(promises).then((list) => sum(list));
}

写到这里,感觉难度还不是很大,注意,此时使用的是 Promise.all,意味着不管 Promise.all 所接收的数组中有多少元素,将会同时进行处理。

此时面试官会进行扩展: 比如有10000个数据,那第一次就会发送5000个请求,网络拥堵了,我想控制成只能同时发送10个请求怎么办?

更进一步: 控制并行数

如果需要控制并行数,则可以先实现一个 promise.map 用以控制并发,这也是在面试中经常考察的一个点。使用 promise.map 来代替上一步的 promise.all

关于 promise.map 的 API 可以参考 bluebird: new Promise(function(function resolve, function reject) resolver) -> Promise

与上一步相同,使用 sum 迭代该函数继续二分,直到最后只有一个数值。

关于 promise.map 还有一个更简单的实现,可参考 async-pool

image.png

思考

在面试中,如果以代码为主进行考核,那这一道题就已经足够了,刚好与一场面试的时间差不多,这也是我在面试过程中面试候选人最喜欢的一个问题。

如果是你,你这个问题能通关到哪一步?