Skip to content

最近前端面试的一些代码题

2024年1月31日  16分钟

最近面了一些大小厂,虽然结果不尽人意,但还是想写篇总结发出来回馈一下社区,毕竟之前也看了很多大佬的面经分享,对我还是很有帮助的。

某哈游 - 协同文档

深度克隆

function cloneDeep(value) {
  const map = new Map();

  function _cloneDeep(value) {
    const isObject = typeof value === 'object' && value !== null;

    if (!isObject) return value;

    if (map.has(value)) {
      return map.get(value);
    }

    const clone = Array.isArray(value) ? [] : {};
    for (const [key, v] of value.entries()) {
      clone[key] = _cloneDeep(v);
    }
    map.set(value, clone);
    return clone;
  }

  return _cloneDeep(value);
}

点评

直接用 structuredClone

JSON.parse(JSON.stringify(value)) 的问题:

structuredClone 会比 JSON.parse(JSON.stringify(value)) 快吗?

实测:

对于比较小的对象还是用 JSON 更快:jsonstringify-vs-structuredclone

对于中等大小的对象,目前不同 JS Engine 结果不一样:jsonparsejsonstringify-vs-structuredclone

对于超级大的对象还是 structuredClone 更快:jsonparsejsonstringify-vs-structuredclone1

字节跳动 - 恰饭平台

实现 transform 函数:

function transform(obj) {
  return; //....
}

transform({
  'A': 1,
  'B.C': 2,
  'B.D.E': 3,
  'CC.DD.EE': 4,
});

得到:

const result = {
  A: 1,
  B: {
    C: 2,
    D: {
      E: 3,
    },
  },
  CC: {
    DD: {
      EE: 4,
    },
  },
};

实现:

function set(obj, keyPath, value) {
  let i = 0;
  while (i < keyPath.length - 1) {
    const key = keyPath[i];
    const current = obj[key];
    if (current === undefined) {
      obj[key] = {};
    }
    obj = obj[key];
    i++;
  }

  obj[keyPath[i]] = value;
}

function transform(obj) {
  const result = {};
  for (const [key, value] of Object.entries(obj)) {
    set(result, key.split('.'), value);
  }
  return result;
}

点评

估计本意是想考察实现 lodash.set

我写的时候有点紧张,那个在线平台第一次用,不太熟悉,不知道咋跑测试用例,写完代码后,面试官说讲下思路就行,就说了下思路,他说没问题。

一面挂了,我发现面试官面完后还 follow 了我 github,本来印象还挺好的。挂了就挂了,本来没放心上,结果也不知道这面试官面评写了啥,导致别的部门 hr 直接不给面试机会了。3 年前应届面试的时候,最后 leader 面面完之后也是没下文,感觉当时表现也还行。以后就算还会面字节也不会考虑这个部门了。

北京 XX 设计

爬楼梯

爬楼梯,拿羽毛球,青蛙跳,以及斐波那契数列,这几个题的思路和代码都长一样。

点评

但凡你刷过 leetcode,就会做。

面试官一开始说最后做到动态规划题吧,当时我心头一惊,草,我最怕的就是动态规划了,结果还好是到简单题。

这家公司我也挺无语的,hr 在 boss 上主动约我面试。一面面完我问面试结果,它和我说过了,下周会安排二面,结果过了两周也没安排二面。也不知道是过年太忙了,还是没 hc 了,还是说有没有可能他被裁了?

某电商大厂

实现 Array.prototype.reduce

Array.prototype.reduce = function (callback, init) {
  let array = this;
  let acc = init;
  if (init === undefined) {
    acc = array[0];
    array = array.slice(1);
  }

  for (const [index, item] of array.entries()) {
    acc = callback(acc, item, init === undefined ? index + 1 : index);
  }
  return acc;
};

点评

这种简单题我感觉更多是考察一个开发的代码风格怎么样。

他们那个在线代码平台在我的 chrome 上打开老是崩溃,浪费挺多时间,应该是和某个 chrome 插件有关系,关掉大部分插件后就不崩溃了。

当时我写的时候没处理不传初始值的情况,因为我平时写代码一直都是写初始值的,当然面试官也问了这个问题,我就说了下没传初始值就是用第一个元素当初始值。

Monkey

实现 Monkey 函数,运行后得到下面的输出。

Monkey('Alan').eat('Banana').sleep(4).eat('Apple').sleep(5).eat('Pear');
// my name is Alan
// I eat Banana
// 等待 4s
// I eat Apple
// 等待 5s
// I eat Pear

好像是腾讯校招最近特别喜欢考的题,怀疑面试官是腾讯跳槽过去的。这道题我之前在牛客上看过,但是记不清了。

临场发挥写的有点问题:

function Monkey(name) {
  console.log(`my name is ${name}`);
  let waiting = 0;

  const result = {
    eat(fruit) {
      if (waiting === 0) {
        console.log(`I eat ${fruit}`);
      } else {
        setTimeout(() => {
          console.log(`I eat ${fruit}`);
        }, waiting);
      }

      return result;
    },

    sleep(seconds) {
      console.log(`等待 ${seconds}s`);
      waiting += seconds;
      return result;
    },
  };
  return result;
}
// my name is Alan
// I eat Banana
// 等待 4s
// 等待 5s
// I eat Apple
// I eat Pear

面试完想了下:

function Monkey(name) {
  console.log(`my name is ${name}`);
  const queue = [];
  let waiting = 0;
  let running = false;

  const result = {
    eat(fruit) {
      const sleepTime = waiting;
      if (waiting !== 0) {
        waiting = 0;
      }

      const task = () => {
        running = true;
        if (sleepTime !== 0) {
          console.log(`等待 ${sleepTime}s`);
        }

        setTimeout(() => {
          console.log(`I eat ${fruit}`);
          running = false;

          if (queue.length > 0) {
            const frontTask = queue.shift();
            frontTask();
          }
        }, sleepTime * 1000);
      };

      if (running === false) {
        task();
      } else {
        queue.push(task);
      }
      return result;
    },

    sleep(seconds) {
      waiting += seconds;
      return result;
    },
  };
  return result;
}

插曲

面试官:vue2 data 中的数组是怎样监听它的修改? 我:覆写数组方法 面试官:怎么覆写的呢? 我:由于刚做过 Array.prototype.reduce,没想太多,回答:直接改原型上的方法呗

其实回答的时候我就感觉不太对劲,我刚想改说应该是直接增加实例属性,并且用 Object.defineProperty 修改了 enumerationfalse,但是主要面试官也没反问说这有问题就问下一个问题了。

面完研究了下,应该是在原型链上增加一个中间原型来实现的:

// 获取数组的原型
const arrayProto = Array.prototype;
// 基于数组的原型创建一个新的对象
const middleProto = Object.create(arrayProto);

// 一个要被重写的方法列表
const overrideMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];

for (const method of overrideMethods) {
  // 缓存原始方法
  const original = arrayProto[method];
  // 定义重写的方法
  Object.defineProperty(middleProto, method, {
    value: function mutator(...args) {
      // 调用原始方法
      const result = original.apply(this, args);
      // 获取数组对象的 ob 对象,它是一个 Observer 实例
      const ob = this.__ob__;
      // 通知变更
      ob.dep.notify();
      return result;
    },
    enumerable: false,
    writable: true,
    configurable: true,
  });
}

// 假设 arr 是 Vue 管理的数组
// 将 arr 的原型指向重写了方法的对象
Reflect.setPrototypeOf(arr, middleProto);

原型链也就是 dataArray -> middleProto -> Array.prototype-> Object.prototype -> null

上海某电商

TypeScript 体操

实现一个函数,返回值的类型和参数类型相同:

const identity = <T>(a: T): T => {
  // ...
};

点评

考察泛型的理解和使用。

应该是看我简历上写了熟悉类型体操,本来还想说终于可以在面试的时候表演一波体操,但这完全体现不出我的实力呀。

异步加法

要求:

// 已知异步方法
function add(a, b, callback) {
  return setTimeout(() => {
    callback(a + b);
  });
}

// 实现...
async function sum(...args) {}

// 效果
sum(1, 2, 3).then((value) => console.log(value)); // => 6

答案:

const _add = (a, b) => {
  return new Promise((resolve) => {
    add(a, b, (result) => resolve(result));
  });
};

async function sum(...args) {
  let result = 0;
  for (const arg of args) {
    result = await _add(result, arg);
  }
  return result;
}

sum(1, 2, 3).then((value) => console.log(value)); // => 6

点评

考察对 JavaScript 异步的理解和处理。

使用栈实现队列

这是二面的题目,和 leetcode 原题不一样的是多了个时间复杂度要求为常数级printMin 方法:

class Queue {
  put() {}
  take() {}
  size() {}
}

class Stack {
  constructor() {
    this.queue = new Queue();
    this.min = Infinity;
  }

  push(value) {
    this.queue.put(value);
    if (value < this.min) {
      this.min = value;
    }
  }

  pop() {
    let top;
    let min = Infinity;
    for (let i = 0, len = this.queue.size(), last = len - 1; i < len; i++) {
      const front = this.queue.take();
      if (i < last) {
        if (front < min) {
          min = front;
        }
        this.queue.put(front);
      } else {
        top = front;
      }
    }
    this.min = min;
    return top;
  }

  // 要求常数级
  printMin() {
    if (this.queue.size() === 0) {
      throw new Error('Stack is empty!');
    }
    return this.min;
  }
}

点评

这家公司的面试官和 hr 给我感觉都还蛮专业和友好的,尤其是对接的 hr,每次面试开始前还会微信提醒面试马上要开始。但是由于第一份工作薪资太低,给不到我预期的薪资,再加上我目前顾虑也挺多,就拒了,只能说目前不合适。

深圳某创业公司

对这家公司 hr 印象还蛮好,沟通的时候非常耐心。分享下今天刚做的两道笔试题吧。

parseHtml

const input = '<div><div>6</div><h1> Title </h1><p>Some description. </p></div>';
function parseHtml(html: string): any {
  // 实现...
}
console.log(JSON.stringify(parseHtml(input), null, 4));
// {
//     "tagName": "div",
//     "children": [
//         {
//             "tagName": "div",
//             "children": [
//                 "6"
//             ]
//         },
//         {
//             "tagName": "h1",
//             "children": [
//                 "Title"
//             ]
//         },
//         {
//             "tagName": "p",
//             "children": [
//                 "Some description."
//             ]
//         }
//     ]
// }

我的答案:

function findCloseTagIndex(html: string, openTag: string, closeTag: string) {
    const tagRegexp = new RegExp(`${openTag}|${closeTag}`, 'g');
    let match = tagRegexp.exec(html);
    let openTagCount = 0;
    let closeTagCount = 0;
    while (match) {
        if (match[0].includes('/')) {
            closeTagCount++;
        } else {
            openTagCount++;
        }

        if (closeTagCount === openTagCount) {
            return match.index;
        }
        match = tagRegexp.exec(html);
    }
    return -1;
}

const input = '<div><div>6</div><h1> Title </h1><p>Some description. </p></div>';

function parseHtml(html: string): any {
    const elements = [];
    const openTagRegexp = /^<(\w+)>/;

    while (html.length) {
        const match = html.match(openTagRegexp);
        if (match) {
            const openTag = match[0];
            const tagName = match[1];
            const closeTag = `</${tagName}>`;
            // 需要考虑子节点标签和父标签相同的情况,所以不能 closeTagIndex = html.indexOf(closeTag)
            const closeTagIndex = findCloseTagIndex(html, openTag, closeTag);
            const childrenHtml = html.slice(openTag.length, closeTagIndex);
            // console.log({
            //     openTag,
            //     closeTag,
            //     childrenHtml,
            // });
            html = html.slice(closeTagIndex + closeTag.length);
            const children = parseHtml(childrenHtml);
            const element = {
                tagName,
                children: Array.isArray(children) ? children : [children],
            };
            elements.push(element);
        } else {
            const trimmed = html.trim();
            if (trimmed.length !== 0) {
                elements.push(trimmed);
                break;
            }
        }
    }

    if (elements.length === 1) {
        return elements[0];
    }

    if (elements.length > 1) {
        return elements;
    }

    return null;
}

查找循环依赖

依赖树:

const tree: Record<string, string[]> = {
    A: ['B', 'C'],
    B: ['D', 'E'],
    C: ['A'],
    D: ['A'],
};

输出依赖树中的循环依赖:

[
  ['A', 'B', 'D'],
  ['A', 'C'],
];

注意要去重的,有些人输出可能会是下面这样:

[
  ['A', 'B', 'D'],
  ['A', 'C'],
  ['B', 'D', 'A'],
  ['B', 'D', 'A', 'C'],
  ['C', 'A', 'B', 'D'],
  ['C', 'A'],
  ['D', 'A', 'B'],
  ['D', 'A', 'C'],
];

答案:

function findCirclesInDependencies() {
    const circles: string[][] = [];
    const depsInCircle = new Set<string>();
    const dfs = (parentPath: Set<string>, pkg: string) => {
        if (parentPath.has(pkg)) {
            const newCircle = [...parentPath];
            circles.push(newCircle);
            for (const dep of newCircle) {
                depsInCircle.add(dep);
            }
        } else {
            for (const dep of tree[pkg] ?? []) {
                dfs(new Set([...parentPath, pkg]), dep);
            }
        }
    };

    for (const pkg of Object.keys(tree)) {
      // 避免重复
      if (!depsInCircle.has(pkg)) {
          dfs(new Set(), pkg);
        }
    }

    return circles;
}

点评

两道笔试题貌似以前都看过,但是没印象,都是临时发挥。还好时间充裕,都写出来了。

虽然公司小,不得不说这两题确实出的非常好。我都怀疑出题的人是看过我简历的,因为我简历上写了一些前端工程化相关的。

第一题考察到正则的使用,字符串处理,html 结构理解。

第二题我知道可以用回溯去优化空间复杂度,但是还是采用了更保守的复制,优化可以留到面试的时候拖拖时间。

初看感觉有点麻烦,但是笔试给了俩小时,就不慌了,找到思路后慢慢用代码实现它就好了。

这家公司用的笔试平台是 show me bug,第一次用有些地方体验不是很好:

总结

题目都不难,做到:

就没太大问题。

最近一个月开始面试,总共也就面了 6 家吧,有些面试官给我的感觉就挺专业,有些面试官给我的感觉就是我上我也行,我还能比他更不尊重人。

就拿做代码题来说,可能你写的代码有点小问题,但总体思路没太大问题,这个时候给我感觉良好的面试官就会指出你代码问题在哪,让你去改,你很有参与感。而有些面试官就是说你的代码有问题,没告诉你具体哪块有问题,然后我自己又觉得没啥问题,一直尬在那。换平时我直接 debug 一下就知道问题在哪了,面试的时候又不方便 debug 和运行代码(可能会被判作弊),而且谁能保证自己写出来的代码没 bug,浪费彼此时间。

问问题的过程中对你有些答的不对的地方,感觉良好的面试官会指出哪里有问题,让你去解决问题,就很多参与感,但是有些面试官就是像一开始就不打算要你,问完一个问题就直接问下一个问题(不排除面试官自己也没发现有问题)。

目前还在找工作,如果你对我感兴趣,这是我的在线简历,也欢迎私信或者邮件互加微信交流找工作的事情。