最近面了一些大小厂,虽然结果不尽人意,但还是想写篇总结发出来回馈一下社区,毕竟之前也看了很多大佬的面经分享,对我还是很有帮助的。
某哈游 - 协同文档
深度克隆
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))
的问题:
JSON.stringify
不支持循环引用,字符串没法表示循环引用JSON.stringify
不能处理JSON
支持的类型以外的类型,JSON
只支持object
,array
,number
,string
,boolean
,null
。一些 JavaScript 的数据类型就不支持例如:Date
,Set
,Map
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
修改了 enumeration
为 false
,但是主要面试官也没反问说这有问题就问下一个问题了。
面完研究了下,应该是在原型链上增加一个中间原型来实现的:
// 获取数组的原型
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,第一次用有些地方体验不是很好:
- 不知道咋格式化代码,试了试
monaco
的alt + shift + f
没用 tsconfig.json
的target
不高,没有Object.entries
的类型hover
到变量上没有类型信息
总结
题目都不难,做到:
- 不要太紧张
- 多刷刷题保持手感
就没太大问题。
最近一个月开始面试,总共也就面了 6 家吧,有些面试官给我的感觉就挺专业,有些面试官给我的感觉就是我上我也行,我还能比他更不尊重人。
就拿做代码题来说,可能你写的代码有点小问题,但总体思路没太大问题,这个时候给我感觉良好的面试官就会指出你代码问题在哪,让你去改,你很有参与感。而有些面试官就是说你的代码有问题,没告诉你具体哪块有问题,然后我自己又觉得没啥问题,一直尬在那。换平时我直接 debug 一下就知道问题在哪了,面试的时候又不方便 debug 和运行代码(可能会被判作弊),而且谁能保证自己写出来的代码没 bug,浪费彼此时间。
问问题的过程中对你有些答的不对的地方,感觉良好的面试官会指出哪里有问题,让你去解决问题,就很多参与感,但是有些面试官就是像一开始就不打算要你,问完一个问题就直接问下一个问题(不排除面试官自己也没发现有问题)。
目前还在找工作,如果你对我感兴趣,这是我的在线简历,也欢迎私信或者邮件互加微信交流找工作的事情。