字节跳动面试记录

一面

关于项目:

  • 抽象结构树
    • 如何根据一颗 AST 渲染出组件
  • 富文本编辑器
    • 实现的难点

常规问题:

  • webpack 打包细节

    • commonjs 与 esmodule 是如何相互转换(这里不会)
  • Task 与 Microtask 的区别, 以及时序

    详细说明可以见: https://html.spec.whatwg.org/multipage/webappapis.html#queuing-tasks

    简单的说就是每次执行task之前会把microtask都处理掉。在处理过程中加入的microtask也会按照顺序处理掉

  • 闭包问题:

    1
    2
    3
    4
    5
    for (var i = 0; i < 5; i++) {
    setTimeout(() => {
    console.log(i);
    }, 1000);
    }

    结果是什么? (我回答了5个4, 其实是5个5..扶额)

    然后问如何让其输出 0 1 2 3 4
    解法一:

    1
    2
    3
    4
    5
    6
    7
    for (var i = 0; i < 5; i++) {
    (function (_i) {
    setTimeout(() => {
    console.log(_i);
    }, 1000);
    })(i);
    }

    解法二:

    1
    2
    3
    4
    5
    for (let i = 0; i < 5; i++) {
    setTimeout(() => {
    console.log(i);
    }, 1000);
    }

    我没有说出解法二, 想了半天想出了一个解法三。。

    1
    2
    3
    4
    5
    6
    7
    for (var i = 0; i < 5; i++) {
    Promise.resolve(i).then((i) => {
    setTimeout(() => {
    console.log(i);
    }, 1000);
    });
    }
  • 中文数字转阿拉伯数字

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    // 五千三百万零五百零一

    const chineseNumMap = {
    一: 1,
    二: 2,
    三: 3,
    四: 4,
    五: 5,
    六: 6,
    七: 7,
    八: 8,
    九: 9,
    };

    const unitMap = {
    千: 1000,
    百: 100,
    十: 10,
    };

    function parseChineseNumWhenLowerThan10K(chineseNum: string) {
    let res = 0;
    let hasNum = false;
    let currentNum = 0;
    for (const s of chineseNum) {
    if (hasNum === false && chineseNumMap[s]) {
    res += chineseNumMap[s];
    hasNum = true;
    currentNum = chineseNumMap[s];
    } else if (hasNum === true && unitMap[s]) {
    res -= currentNum;
    res += currentNum * unitMap[s];
    hasNum = false;
    currentNum = 0;
    }
    }

    return res;
    }

    function chineseNumToInt(input: string): number {
    const chineseNumArr = input.split("万");
    let res = 0;

    res += parseChineseNumWhenLowerThan10K(chineseNumArr[0]);

    if (typeof chineseNumArr[1] === "string") {
    res *= 10000;
    res += parseChineseNumWhenLowerThan10K(chineseNumArr[1]);
    }

    return res;
    }

    console.log(chineseNumToInt("五千三百万零五百零一"));

二面

一面过后第二天 HR 就打电话过来约二面,就效率方面来说还是非常迅速的

二面没有考察具体代码,主要是问了问项目方面的问题。

比如公司项目的架构,公司团队协作方面是如何协作的,除了平时工作之外有没有做什么其他的事情,有自己的项目么,自己的项目由什么亮点难点这种比较抽象的问题。

其中可能也有混入价值观方面的问题。总之需要注意一下。

然后我这面被刷了,第三天收到了感谢面试的信。至少有个反馈,从这点来看字节做的还是非常不错的。

再战

又被别的部门捞起来了, 之前是飞书, 现在是抖音部门, 就再战一次呗。

一面

  • 聊聊项目
  • esmodule 和 commonjs 的区别
  • webpack 打包原理
  • nodejs 的 event loop
  • nodejs 的使用 microtask 和 task 的时机

算法题

路径总和:给你二叉树的根节点 root 和一个表示目标和的整数 targetSum ,判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class TreeNode {
left: TreeNode | null = null;
right: TreeNode | null = null;
value: number;
}

function getTreePathSum(node: TreeNode, targetSum: number): boolean {
if (!node) {
return false;
}

if (targetSum - node.value === 0) {
if (node.left === null && node.right === null) {
// 如果为叶子节点: 即没有子节点
return true;
}

// 否则继续往下走。
}

return (
getTreePathSum(node.left, targetSum - node.value) ||
getTreePathSum(node.right, targetSum - node.value)
);
}

const root = new TreeNode();
root.value = 1;
const left = new TreeNode();
left.value = 2;
const right = new TreeNode();
right.value = 3;

root.left = left;
root.right = right;

console.log(getTreePathSum(root, 5));

二面

  • js有哪些类型
  • js中基本类型和引用类型分别存在哪里
    • 基本类型存在栈中,引用类型的数据存在堆中
  • 如何获得地址输入 - 缓存 - 加载模块 - 渲染 - 加载请求 - 渲染可交互 这个过程中各个区域的时延
  • 如何设计一个全链路的监控体系
    我的回答:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    1. 监听所有的用户事件
    并区分重点事件,和普通事件。
    2. 全局捕获错误信息。并将错误信息上报,以及错误信息所在的堆栈信息。
    如果可能的话,同时发送相关的dom接口与网络请求,以及截屏。
    如果是无法复现的一些线上问题,内置一个web录屏工具,在开启一个flag后录屏并将录屏信息上传(rrweb)
    3. 对用户的操作,比如RUM,等关键信息,以及用户的请求。划分不同的measure标准。并通过这些信息来衡量用户的痛苦程度。
    资源是有限的,我们应当优先处理用户最痛苦的部分——即使用频率最高,相对痛苦程度最高的部分。
    4. 增加有效的反馈机制,比如聊天机器人,比如报出异常时弹出反馈错误的模态框。或者引导用户到相关的社区。来尽可能让用户知晓我们会尽快解决问题。
    同时能够收集来自用户的直接反馈,而这些是看log很难看到的信息。
    5. 对线上服务进行监控。使用监控工具来检测各个关键服务,以及相对独立的服务的可访问性。以及相关的报警措施(比如钉钉消息)
  • React Native 的热更新 / 部署
    • 拆解
    • 怎么推送
  • 长列表 VirturalList
    • 如何解决闪屏
  • React Native的事件推送

算法题

二面算法题难度急速升高, 反正我都没做过。

1
2
3
4
5
用 Javascript 对象模拟 DOM 树,并实现它的 render 方法,通过调用该方法可以转成真正的 DOM 节点,例如我们已经实现了 element.js,通过 require('element.js'),我们可以定义 ul, 如下:

function el(tag, props, children) {
return ...
}

我的算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function el(tag, props, children) {
const el = document.createElement(tag);
for(let key in props) {
el.setAttrible(key, props[key])
}

const childrenEls = children.map(item => {
if(typeof item === 'string') {
return item;
}

return el(item.tag, item.props, item.children);
})
el.append(...childrenEls)

return el;
}

1
2
3
4
5
6
7
8
9
给你一副类似于如下的地图:
0000000000
0010001011
000010E001
00S0000100
0000001000
1000000000

初始时你在S的位置, 你可以向上下左右四个方向发射一枚子弹; 子弹碰到1时, 会顺时针变向90度, 然后继续前进; 问你在S向哪个方向射击, 能够最快将子弹打入E内; 假设一定有一个方向可以成功, 如上图中向上射击;

我的算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const land = [
'0000000000',
'0010001011',
'000010E001',
'00S0000100',
'0000001000',
'1000000000'
].map((str) => {
return str.split('')
});

function findS(target: string): [number, number] {
for(let i = 0; i < land.length; i++) {
for(let j = 0; j < land[i].length; j++) {
if(land[i][j] === target) {
return [i, j]
}
}
}
}

function getLandSymbol(y: number, x: number): '0' | '1' | 'S' | 'E' | undefined {
return land[y][x] as any;
}

const size = [land.length, land[0].length];
const sPos = findS('S');

function tryPath(step: number, currentPos: [number,number], vector: [number, number]): number {
step = step + 1;
const symbol = getLandSymbol(currentPos[0], currentPos[1]);
if(symbol === '1') {
// [-1. 0] => [0, 1]
// [0, 1] => [1, 0]
// [1, 0] => [0, -1]
// [-1, 0] => [0, 1]
vector = [vector[1], -vector[0]] // 顺时针旋转90deg
}else if(symbol === 'E') {
return step;
}else if(symbol === undefined) {
return 999999;
}

return tryPath(step, [currentPos[0] + vector[0], currentPos[1] + vector[1]], vector);
}
console.log(tryPath(0, sPos, [-1, 0])); // 向上

三面

面试官上来就说,之前面了很多技术问题,我们来聊一聊项目吧。

大概问了问项目经历,工作经历,职业规划,如何协作,如何codereview,最近在学什么新技术等等看上去很随意的问题。

回头跟朋友们复盘了一下,其实是一道情商题,只有情商高的人才能答对,而我情商。。不高,就是一个憨憨。

如何面对这种软刀子题:

记住以下几点:

  • 面试不是跟你谈心,作为候选人得揣测对方想听啥。
  • 可以先说下自己有啥优点,经验。能为公司,部门,要做的事情带来什么转机。然后再夸一把公司,部门。说来虚心学习。
  • 学会包装自己,技术是为业务服务的

三战

又换了个团队继续,直接从二面开始

二面

  • 项目经历。项目是如何打包,如何发布的。
  • 有使用过一些自动化工具来确保项目质量么。
  • 有了解过webpack打包原理么?你们的项目是用什么打包的
  • 了解过lock文件么,升级单个依赖时会遇到什么坑
  • 看你的项目用到过cypress, 你是如何解决cypress的下载问题的

题:

1
2
3
4
5
6
7
8
设计一个组件,用 React 编写,写出伪代码,有三个要求

Input 组件
非受控
Validate:
1. 只接受英文字母

2. 如果出现不合法的字符,就删掉

我的解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, {useState, useCallback} from 'react';

interface Props {
initVal: string;
onChange: (newVal: string) => void;
}
const MyInput: React.FC<Props> = React.memo((props) => {
const [val, setVal] = useState(props.initVal ?? '');
const handleChange = useCallback((e) => {
const text = e.target.value;
const newVal = '';
for(let c of text) {
if(/[a-zA-Z]/.test(c)) {
// 这里忘记怎么取c的ascii码的方法了,直接用正则表达式先对付着
newVal += c
}
}

if(typeof props.onChange === 'function') {
props.onChange(newVal);
}

setVal(newVal);
}, [props.onChange])

return <input value={val} onChange={handleChange} />
})
MyInput.displayName = 'MyInput'

其实还有就是使用jquery时代的代码来进行处理,然后外面包一层react代码
但是我觉得不够react因此没有选择这个方案。

三面

杂七杂八的聊了聊,大部分都忘了,就记录一下记得的

  • 讲讲你的开源项目
  • 你的开源项目是怎么宣传的,取得了什么样的milestone
  • 你的开源项目未来的发展是怎么样的
  • 你公司的产品的基础架构是怎么样的
  • 你对未来三年的职业规划是怎么样的
  • 你希望做什么样的产品,是to b的还是to develop的
  • 你如何实现一个需求,在用户不知道自己想要什么样的功能的情况下
  • 你现在是965, 可以接受996么

一道题:

1
2
3
4
5
6
7
8
9
实现 tom().eat('apple').play('football').sleep(5).eat('apple').play('football')

输出:
// tom
// eat apple
// play football
// 停顿 5 s
// eat apple
// play football

我的写法, 其实不对。主要是一个sleep函数不太会写。求大佬给出正解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function tom() {
return new Tom()
}

function Tom() {
this.p = Promise.resolve()
console.log('tom')
}

Tom.prototype.eat = function(name: string) {
this.p.then(() => {
console.log('eat ' + name);
})

return this;
}

Tom.prototype.play = function(name: string) {
this.p.then(() => {
console.log('play ' + name);
})

return this;
}

Tom.prototype.sleep = function(interval: number) {
this.p.then(() => {
return new Promise(resolve => {
setTimeout(() => {
resolve(1);
}, interval * 1000)
})
})

return this;
}

四战

直接三面

  • 聊了聊项目
  • 怎么做工程化
  • 如何监控前端的性能
  • 我们会做很多页面,如何提升开发体验
  • 了解无头浏览器么?它内部的引擎是如何去计算代码的

题:
实现一个LRU:

我的回答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class LRU {
list: string[] = [];
map: Record<string, unknown> = {};

get(key: string): unknown {
const index = this.list.findIndex((item) => key === item);
if (index >= 0) {
this.list.splice(index, 1);
}
this.list.unshift(key);

return this.map[key];
}

set(key: string, value: unknown): void {
const index = this.list.findIndex((item) => key === item);
if(index >= 0) {
this.list.splice(index, 1);
}
this.list.unshift(key);

if(this.list.length > 1000) {
const _key = this.list.pop();
delete this.map[_key];
}

this.map[key] = value;
}
}

我的不是一个优解。
建议正确的答案参考:

  • Map 实现
  • 双向链表实现

五战

又被教育部门捞起来了,反正我来者不拒继续面呗。

一面

  • 一个空数组的原型链是怎么样的
  • 用过flex么? 讲讲flex, 有那些熟悉,干嘛用的
  • webpack_require esmodule commonjs, 他们的区别是什么。esmodule可以通过拼字符串来动态加载么
  • function的构造函数与class的构造函数有什么区别
  • 讲讲HTTP2解决了什么问题
    • 二进制传输
    • Header 压缩
    • 多路复用
    • server push
    • TLS(虽然HTTP2不强制, 但是chrome, firefox 只支持tls的HTTP2)
  • 讲讲箭头函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
inner = 'window';

function say() {
console.log(inner);
console.log(this.inner);
}

var obj1 = (function() {
var inner = '1-1';
return {
inner: '1-2',
say: function() {
console.log(inner);
console.log(this.inner);
}
}
})();

var obj2 = (function() {
var inner = '2-1';
return {
inner: '2-2',
say: () => {
console.log(inner);
console.log(this.inner);
}
}
})();


say();// window window
obj1.say(); // 1-1 1-2
obj2.say(); // 2-1 window
obj1.say = say;
obj1.say(); // window 1-2
obj1.say = obj2.say;
obj1.say(); // 2-1 window

做算法题:
实现16进制加法,不能将两数直接转成十进制相加再转回十六进制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
var map = {
a: 10,
b: 11,
c: 12,
d: 13,
e: 14,
f: 15
}

function parseToNumber(hex) {
if(hex === undefined) {
return 0;
}

if(Object.keys(map).includes(hex)) {
return map[hex]
}else {
return Number(hex)
}
}

function hexAdd(hex1, hex2) {
const arr1 = hex1.split('')
const arr2 = hex2.split('')

let addon = 0;
const result = [];
for(let i = 0; i < Math.max(arr1.length, arr2.length) + 1; i++) {
const num1 = parseToNumber(arr1[arr1.length - 1 - i])
const num2 = parseToNumber(arr2[arr2.length - 1 - i])

let sum = num1 + num2 + addon;
addon = Math.floor(sum / 16);
let rest = sum % 16;
let r = String(rest)
if(rest >= 10) {
const [hex] = Object.entries(map).find(([key, val]) => {
return val === rest
})

r = hex
}

result.unshift(r)
}

return result.join('')
}

console.log(hexAdd('ff', '1'))

结果

最后还是没有成功拿到offer, 很遗憾,原因是【不太适合团队协作】(当然也有可能只是单纯的敷衍)。面试者除了提升自己的硬实力,还应当提升一下自己在职场的软实力,比如朋友推荐的这本书 软技能

六战

被飞书部门又拉起来了,继续

一面

  • 聊一下项目
  • 聊一下你对前端的优化的实践,达成了那些目的
    • 如何判断前端用户体验
  • 聊一下在前端体验方面做的工作
  • 聊聊vue和react的区别
  • 聊聊如何处理中文输入法在输入过程中定时保存状态的问题

算法题

  1. 用css实现一个布局, 来实现一个3x3的布局,每个窗口都要实现16比9的比例

没做出来,关键词 padding-bottom百分比

实现: https://blog.csdn.net/weixin_39357177/article/details/81186498

  1. 实现一个方法,通过栈来存储,实现O(1)的复杂度下获取栈的最小值。

没做出来

框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MiniStack {
store = [];

push(val: number) {
//
}

pop() {
//
}

getMini(): number {
// 实现它
}
}

原题: https://leetcode-cn.com/problems/min-stack/

二面

  • 聊聊公司的项目实现
  • 了解http3么
  • 聊聊react hooks
  • 聊聊公司的业务规范与组件设计规范
  • 聊聊业务代码
  • 聊聊你是如何进行项目优化的
  • 聊聊项目上线的流程
  • 聊聊网络安全性方面的问题,做过xss用户输入防护么
  • 聊聊你是如何关注用户的体验的,主要关心那些指标
  • 让你设计一个倒计时组件,你会怎么设计

做题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 消除字符串中全部的y和连续的xz.
// 用例:
// 'xxyyz' -> 'x'
// 'xxxyyyzzz' -> ''
// 'xyzwzyx' -> 'wzx'

function fixStr(str) {
let result = str;

function loop(str) {
const len = str.length
const fixedStr1 = str.replace('y', '');
if(len !== fixedStr1.length) {
// 变了
loop(fixedStr1)
return;
}

const fixedStr2 = str.replace('xz', '');
if(len !== fixedStr2.length) {
loop(fixedStr2);
return
}


result = str;
}

loop(str)

return result
}
console.log(fixStr('xxyyz'))
console.log(fixStr('xxxyyyzzz'))
console.log(fixStr('xyzwzyx'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 实现一个功能, 发送5个请求,当请求数
// >=4 报错,则渲染错误页面
// 否则渲染正确页面

function renderErrorPage() {
//
}

function renderSuccessPage(resultMap) {
//
}

const arr = ['a', 'b', 'c', 'd', 'e'];
const successedResultMap = {};

let errorCount = 0;

Promise.all(
arr.map((url, i) =>
fetch(url)
.then((res) => {
successedResultMap[url] = res;
})
.catch(() => {
errorCount++;
})
)
).finally(() => {
if (errorCount >= 4) {
renderErrorPage();
} else {
renderSuccessPage(successedResultMap);
}
});

该问题可以使用Promise.allSettled进行简化

三面

问的和二面差不多,只是更加细一点

算法题在面试官的要求下不做公开

HR面

反正基本不挂人,主要是谈薪的问题。有一个比较难受的点就是字节的薪资是基于你上家薪资来的,或者说大部分大厂的薪资都是基于上家来的。所以比较好的做法是可以先去别的厂然后再来字节996。我就是基础薪资比较低的那种,最后也没感觉有太大的竞争力,比较菜。

结论

万万没想到,我最终还是拿到了Offer。虽然经历比较坎坷,但总不算完全浪费时间。总得来说跳槽这事三分靠运气七分靠实力,但绝对不要被眼前的利益限制住了自己的发展,主要还是要看这次跳槽在自己的职业生涯中能收获什么,而不要为跳而跳。

文章目录
  1. 1. 一面
  2. 2. 二面
  • 再战
    1. 1. 一面
      1. 1.1. 算法题
    2. 2. 二面
      1. 2.1. 算法题
    3. 3. 三面
  • 三战
    1. 1. 二面
    2. 2. 三面
  • 四战
    1. 1. 直接三面
  • 五战
    1. 1. 一面
  • 结果
  • 六战
    1. 1. 一面
    2. 2. 二面
    3. 3. 三面
    4. 4. HR面
    5. 5. 结论