2025.11 面经
少年当心中有志,而后能行远
字节 懂车帝 一面
1 . ES6 新增了哪些数据结构
Map
WeakMap
Set
WeakSet
由于可能太紧张当时只回答了 SetMap , WeakSet , WeakMap 有了解过吗
- Map 是 ES6 新增的一种键值对集合,类似于 Object,但是值可以有各种类型
- WeakMap 与 Map 类似,但是其键只能是对象,并且是弱引用,不会阻止垃圾回收
- WeakSet 类似于 Set,但是成员只能是弱引用的对象
另外因为 WeakMap 和 WeakSet 是弱引用,随时可能会被 GC 回收,故而被设计成不可枚举
2 . 对于数据的遍历,平常采用哪些方式
while , for , for…in… ,for…of…
map() , reduce() , filter() , forEach()
Object.keys() , Object.values() , Object.entries()
假如要发 100 个请求,是串行的需要 async,这时候要用到哪种循环方法
- 采用 for 或者 for…of… 配合 await 进行遍历
- 不能采用 forEach() , map() 因为其为函数式回调,不会等待请求执行
3 . 如何判断 call , apply , bind 函数 this 指向的执行逻辑
call , apply , bind 都可以显示改变 this 指向为其执行逻辑的第一个参数,若为 null 或者 undefined , 则在非严格模式下指向全局对象 window , 严格模式下指向 undefined。另外,箭头函数没有 this ,箭头函数的 this 只与其所在的外作用域有关,没办法通过这类方法改变
apply 和 call 的区别主要在于?
- apply 和 call 的区别仅在传入参数的方式上,apply 接收一个对象对于 this 指向,而后接收一个数组作为 function 的参数列表,call 接收的是一系列参数,第一个参数依然是一个对象用于 function 的 this ,但后续的所有参数都将作为 function 的 arguments
- 而 apply 与 call 的使用上没有区别,都是基于某一个函数,改变 this 指向为需要的对象后调用这个函数
4 . 使用 React 时,主要用到了哪些 hooks
useEffect , useState , useCallback , useMemo
要定义一个不期望去影响渲染的变量时,该使用哪一种 hook
- useRef : useRef 返回的对象在组件整个生命周期里都是同一个引用,改 current 不会触发 reconcile,也不会让函数组件重新执行一遍。相比之下,useState 会触发调度器任务,造成无意义的 render,不符合‘不期望影响渲染’的题意。
- 凡是需要跨渲染、不触发 reconcile、又随时可读可写的数据——DOM 节点、定时器 id、上一次的值、昂贵实例、甚至强制刷新函数——都可以交给 useRef
5 . 在 hooks 里面能不能用一些判断语句来直接 return
可以,hooks 规则只限定了 hook 的调用规则,并没有限制 hook 的执行逻辑 ,在 hook 里调用 return 只是把其内部的普通函数逻辑段落 , 只需要保证 hook 本身不在 条件调用语句当中即可
(ps: 笔者当时可能还是太紧张了,一听到条件就回答了不行,然后把 react hook 的底层逻辑扯了一遍,以为面试官问的是能不能在条件
分支中写 hook)
除了 React 官方的 hooks ,有去了解过,或者用过其他的 hooks 吗
6 . 当发现诉求和实现时有一定的额难度时,有考虑过使用 AI 工具来解决问题吗
过去实现技术难点时,使用百度,Google 搜索,难以找到匹配的结果,在现如今还会使用搜索引擎还是 Kimi 之类的大模型沟通?
7 . 能介绍一下 koa 的洋葱模型吗
- 核心思想是:中间件像一层一层洋葱皮,请求传过去再一层一层传出来执行剩余逻辑,即外层先进入,内层执行逻辑,再由外层回溯执行
- await next() 是其执行的核心,如果中间哪一个中间件没有加,则其更内部的中间件将无法执行
- 这种模型的好处主要在于方便外层统一日志,异常捕获,权限校验,在返回阶段可以统一做响应压缩,统一响应格式的工作
8 . git 有在用吗,使用时是基于 GUI 工具还是命令行 (具体指如何处理请求冲突)
9 . 编程题 ,实现一个 maxCalls 函数,在次数用完之前返回剩余次数,用完之后返回最后一次的执行结果,执行结果示例
1 | const limitedAdd = maxCalls((a, b) => a + b, 2); // 最多调用 2 次 |
答案如示
1 | function maxCalls(func, maxTimes) { |
总结
还是太紧张了,好几次理解错面试官到底想问什么,加上闭包函数写的太少,这个应该在五分钟内出结果的闭包题写了半个多小时还没写出来,准备不够充足,没有发挥应有实例,大概率是 g 了,实习是场长征路,继续沉淀,继续刷题
七牛云 一面
(在面 七牛云 的时候,明明已经记得录音了,但是在正式面试的过程中,还是忘掉了 (> <) 555 所以仅凭记忆记下了面试官的几个问题,不过大致是这些)
1 . 你认为自己的优势在哪里,又认为自己有什么缺点
2 . 如果现在有这么一个场景,需要你去独立完成某个应用功能的开发,包括前端后端,该如何借助 AI 来提高自己的完成效率,来真正保质保量的在 DDL 之前彻底解决这一开发需求
3 . 你对 HTML 元素中 img 标签的 tag 属性有什么了解
4 . 如果我们项目中某一个应用的首屏加载很慢,你会采取什么手段来优化他
5 . 如果要让你来帮助一个对于前端 0 认知的同学来完成一个采用 React 或者 Vue 技术栈去完成项目的具体开发和实现,你会怎么做
6 . 你对除了 React , Vue 框架以外的其他前端框架有了解过吗
7 . 你平常有什么兴趣爱好
总结
面完以后,我是谁,我在哪,我要干什么,这还是前端吗????????
虽然话是这么说,但是笔者发现确实对 AI 的应用并没有那么全面,还是停留在很模糊的概念上
字节 飞书 PC 端 一面
(这位面试官超好的!!不管最后结果如何,尽我所能,前进!!)
1 . 你能介绍一下为什么选择前端吗?
2 . 你能讲一讲在学校做的那些项目吗?(这里直接说项目亮点)
这里主要讲了基于 MSE 实现的前端视频渲染 (结果发现当时写的有的问题,后续重新了解了一下,结果发现原生 video 标签已经可以实现边下载边播放了)
这里并非 MSE 无用,而是 video 标签的视频文件是基于内部黑盒自动实现基于 Range 的分片请求,如果只是简单的视频预览场景直接使用即可(即项目中的实现场景)
与原生 video 相比,这里我所采用的 MSE 可以自定义缓冲大小格式,相当于是一个内部的拓展性更强的一个实现,主要在于灵活切换清晰度,配合弹幕即可
(ps:后续笔者打算基于此技术深入研究一下,自己做一个仿 B 站的桌面端应用,这样的个人项目技术实现上会更深入一些)
3 . Mobx 和 Redux 的区别
- Redux 追求函数式编程,强制不可变性(Immutable),state 是只读对象,严格单向数据流,dispatch(action)->reducer->new state->subscribe->re-render,可预测性优先 而 Mobx 采用响应式编程 ,state 是可观察对象,允许直接修改,开发效率优先
- Redux 单一 Store 实现全局状态树,所有数据集中管理通过 combineReducers 模块化,物理层面仍是单一对象
Mobx 多 Store,官方鼓励划分多 Store 直接实例化使用 - 在代码量上 Redux 需要多样本代码,Mobx 开发效率上快的多
- Redux 依赖 useSelector 的引用比较(浅比较)决定是否 re-render,若 selector 返回新对象(如 return { …state })会触发不必要渲染,需手动优化(shallowEqual 或 memoized selector)。Mobx 则基于精确依赖追踪:组件仅对实际访问的 observable 属性做出反应,更新粒度细到字段级,且是同步触发,通常性能更优,尤其在频繁更新的场景。但 Mobx 的自动追踪也可能让开发者忽略性能陷阱
- Redux 的最大优势是 Redux DevTools:支持时间旅行调试、action 回溯、热重载保持状态,配合 RTK 的 immer 可清晰看到每个 action 的状态 diff。这在复杂 bug 排查时无可替代。Mobx 调试依赖 spy 或 trace,不如 Redux 直观,且由于直接修改状态,很难追溯”谁最后改了值”。
- 总的来说要求可控选择 Redux,要快选择 Mobx
Mobx 的变化是怎么监听的,他的变化是怎么映射到 UI 上的(这是面试官的追问,现在复盘的时候才发现答的稀碎)
- 调用 makeAutoObservable() 时,Mobx 会递归遍历对象,对属性使用 Proxy 劫持。函数式组件被 observer 包裹后,其渲染函数执行时会触发首次渲染。在此期间,任何对 observable 属性的读取(如 store.count)都会被 Mobx 运行时捕获:当前正在执行的组件渲染函数会被自动注册为该属性的依赖(保存到全局 derivation 上下文)。这个过程是隐式的,你无需手动订阅。
- 当你直接赋值(如 store.count = 2)时,Proxy setter 会拦截这次写入,标记该属性为已脏,并立即查找所有依赖它的 Reaction(即关联的组件渲染函数)。Mobx 不会直接重新渲染,而是将这些 Reaction 加入微任务队列(类似 Promise.then),批量合并更新。这意味着多次连续修改只触发一次重新渲染,性能高效。
- observer 高阶组件内部会劫持你的函数组件,使其 render 函数成为一个 Reaction。当队列调度执行时,Mobx 会强制组件重新渲染。关键在于:重渲染只发生在真正读取了变化属性的组件。若组件 A 用了 count,组件 B 只用了 name,修改 count 只会让 A 重渲染,B 完全不受影响。这种字段级精确更新是 Mobx 的核心优势,无需手动 memo 或 useSelector 优化。
- 实际开发中,你用 const todoStore = useLocalObservable(() => ({ list: [], get completedCount() { … } })) 创建 store,组件用 observer(() => { … }) 包裹。面试要强调的是:依赖追踪发生在 render 阶段,而非 useEffect。因此,在条件语句或 early return 中读取 observable 是安全的(Mobx 会动态追踪真实执行路径)。但注意,不要在异步回调后读取 observable,此时已脱离 Reaction 上下文。
总的来说 Mobx 让组件渲染函数本身成为“智能订阅者”,通过 Proxy 自动收集依赖、精确派发更新,把手动优化的活交给了框架,这是它与 Redux 主动式 dispatch 的本质区别。
Mobx 有 Store 的概念吗
在 Mobx 中,Store 通常是一个普通的 类或对象,里面包含:
- Observable state(用 makeAutoObservable 标记)
- Actions(修改状态的方法)
- Computed values(衍生状态)
与 Redux 的单一 Store 不同,Mobx 鼓励按领域划分多个 Store(如 UserStore、TodoStore),直接实例化使用,灵活性更高。面试时可以说:Redux 是”一个 Store 管全局”,Mobx 是”多个 Store 各管一块”。
4 . 从 Url 输入到 浏览器渲染的总过程是什么样的
老生常谈,八股奉上
为什么 HTTP 请求是基于 TCP 而不是 UDP ?
HTTP 作为应用层协议,要求数据必须完整、有序地交付。一个 HTML 页面或 JSON 接口,丢失或错序一个字节都会导致内容损坏、解析失败。TCP 提供可靠传输(丢包重传)、按序到达、校验和纠错,完美契合这一需求。UDP 则纯粹暴露网络层不可靠性,数据可能丢失、重复、乱序,HTTP 若直接基于 UDP,每个应用都需重写重传逻辑,不现实.
(另外说了一下 TCP 具体是如何实现可靠传输的)- 序列号与确认应答(ACK)机制: 序列号解决乱序重组问题,ACK 解决送达确认问题。
- 超时重传: 发送方每发送一个报文段都会启动定时器,若超过 RTO(重传超时时间)未收到 ACK,则自动重传。RTO 是基于 RTT(往返时延)动态计算的
- 滑动窗口 (Receive Window 即 RWND):
- 接收方维护一个接收缓冲区;
- 每接收到一些数据,并将其交给应用层处理后,就会更新 RWND
- 发送方根据 RWND 控制发送速率,不发送超过 RWND 字节的未确认数据。
- 拥塞控制
- 慢启动
- 拥塞避免
- 快重传
- 快恢复
- 超时重传
5 . 浏览器是如何实现与服务器之间的压缩算法协商的?(这里是完全不知道啊啊啊啊啊啊,只凭记忆有 Content-Encoding 和 Accept-Encoding 这俩字段来着)
浏览器的压缩算法是一个主动协商的过程
- 1 . 在请求头中自动附加 Accept-Encoding 这个字段以告知服务器所支持的压缩算法以及偏好
- 2 . 服务器收到请求以后,执行“权重+顺序”匹配。即先解析客户端算法列表 + 优先级,筛选服务器支持的算法(如 Nginx 的 gzip_types 配置)按 q(即优先级)值降序排列,同 q 值按出现顺序,匹配第一个双方都支持的算法
- 3 . 服务器压缩并标记响应
1
2Content-Encoding: gzip
Vary: Accept-Encoding // 关键!告诉缓存需区分编码版本 - 4 . 浏览器根据 Content-Encoding 头自动调用对应解压库处理内容,对用户透明。
在这个过程中服务器会动态评估:
- 小文件不压:<1KB 直接返回原内容
- 压缩率不足:压缩后体积减少<10%则放弃
- CPU 成本:高并发时可能降低压缩级别以节省算力
整个协商过程在毫秒级完成,无需人工干预,是 Web 性能优化的基础设施。
6 . 编程题,实现一个 add 函数,所有的加法都需要调用 addRemote 函数,要求最快的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 这里模拟的是网络请求
const addRemote = async (a, b) =>
new Promise((resolve) => {
const result = a + b;
setTimeout(() => resolve(result), Math.random() * 100);
});
// 请实现本地的add方法,所有的加法都需要调用addRemote
// 以最快的实现输入数字的加法
async function add(...inputs) {
// TODO
}
// 请用示例验证运行结果:
const log = (promise) => {
const start = Date.now();
promise.then((result) => {
console.log(`result is: ${result}, cost: ${Date.now() - start}ms`);
});
};
log(add(5, 6)); // 11
log(add(1, 1, 9, 1, 1)); //13
log(add(2, 3, 9, 3, 4, 1, 3, 9, 5)); //39笔者当时的实现是这样的,想着既然要快,那就一次性执行的要多,所以每次都是偶数个参数,最后一个参数单独处理,然后递归调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21async function add(...inputs) {
if (inputs.length === 1) return inputs[0];
const addArray = new Array();
let index = 0;
while (index < inputs.length) {
if (index + 1 >= inputs.length) break;
addArray.push(addRemote(inputs[index], inputs[index + 1]));
index += 2;
}
const resultArr = await Promise.allSettled(addArray);
console.log(resultArr);
let newInputs = [];
if (index === inputs.length - 1) newInputs.push(inputs[inputs.length - 1]);
index = -1;
while (++index < resultArr.length) {
if (resultArr[index].status === 'fulfilled') {
newInputs.push(resultArr[index].value);
}
}
return add(...newInputs);
}但是实际上如果是这样写的话是按轮次执行的,即单轮最快的 Promise 仍要等待单轮最慢的那一位执行完成,才能继续下一轮的执行,在面试官的提醒下,我想到了这样一种实现:
- 1 . 维护两个队列,一个为 Promise 队列,一个为结果队列,这里 Promise 队列采用集合实现
- 2 . 只要结果队列的值的长度大于等于 2 ,就从结果队列中取出两个值,调用 addRemote 函数,将返回的 Promise 加入 Promise 队列中(直到结果队列中的值的长度小于 2 )
- 3 . 只要 Promise 队列不为空,就等待 Promise 队列中的 Promise 执行完成,将结果加入结果队列中,每加入一个结果,就判定步骤 2 是否成立
- 4 . 直到 Promise 队列且结果队列只剩一个值,返回结果队列中的最后一个值
所以最快实现应该是这样的,此即笔者能够想到的最优解(在面试之后才写出来,面试过程中只讲了思路 T _ T ):
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
28async function add(...inputs) {
if (inputs.length === 0) return 0;
if (inputs.length === 1) return inputs[0];
const promiseSet = new Set();
const elements = [...inputs];
function scheduleAdd() {
while (elements.length >= 2) {
const a = elements.pop();
const b = elements.pop();
const promise = addRemote(a, b).then((res) => {
elements.push(res);
promiseSet.delete(promise);
});
promiseSet.add(promise);
}
}
while (elements.length > 1 || promiseSet.size > 0) {
scheduleAdd();
await Promise.race(promiseSet);
}
return elements[0];
}
字节 飞书 PC 端 二面
(两天了,还是没有音讯,啊啊啊啊啊啊,等待的过程好煎熬)
(还是算法题没写好,让面试官犹豫了吧,一直想着对字符串操作,脑子里没转过来想对字符本身操作)
1 . 为什么选择这个大学以及专业
2 . 学习前端的技术历程
3 . 之前的项目现在还在持续维护中吗
4 . 如果需求方需要在文件分块上传的基础上加一个断点续传的功能,你会怎么做
这里参考了 TCP 保障消息可靠所采用的序列号机制回答(ps: 我真的觉得 TCP 保障数据传输是一个很巧妙的实现)
5 . AI 在日常开发中扮演一个什么样的角色呢(后面又追问了具体在项目中,使用 AI 帮助完成了什么开发上的难点)
6 . 信息流比较多时,会引入虚拟列表的解决方案吗
7 . 后续项目中的功能还会有哪些
8 . 去除一个字符串中所有的 ‘y’ 字符 和 连着的 ‘xz’ 字符
先给出这里的测试用例1
2
3console.log(deleteChar('xxyyz'));
console.log(deleteChar('xxxyyyzzz'));
console.log(deleteChar('xyzwzyx'));对字符串本身的操作不太熟悉,面试时写了个时间复杂度巨拉跨的暴力写法,虽然能跑通但是不满意
思路是先去除 y ,每执行一次去除 xz 直接进入下一轮循环直到遍历完字符串也无法去除退出循环并输出结果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
27function deleteChar(str) {
let temp = '';
for (let i = 0; i < str.length; i++) {
if (str[i] !== 'y') {
temp += str[i];
}
}
let judge = true;
while (judge) {
let i = -1;
let record = temp.length;
if (temp.length < 2) {
judge = false;
}
console.log(temp);
for (; i < temp.length - 1; ) {
i++;
if (temp[i] === 'x' && temp[i + 1] === 'z') {
temp = temp.slice(0, i) + temp.slice(i + 2);
break;
}
}
i -= 1;
if (i == record - 2) judge = false;
}
return temp;
}于是花了一小会儿时间给出了这样的实现(面试过后)
主要思路是:- 依旧先去除 y
- 定义一个函数,用于处理只包含 xz 的子串,用栈的思想实现子串中的字符消除
- 然后遍历大字符串,一一找出子串,不是 xz 字符直接加
这样就可以只遍历一次实现目标需求
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
39function deleteChar(str) {
let temp = '';
let result = '';
for (let i = 0; i < str.length; i++) {
if (str[i] !== 'y') {
temp += str[i];
}
}
function tempDeleteFn(tempStr) {
let outputStr = '';
let stackX = [];
let recordIndex;
for (let i = 0; i < tempStr.length; i++) {
if (tempStr[i] === 'x') stackX.push(i);
else {
if (stackX.length) recordIndex = stackX.pop();
else outputStr += 'z';
}
}
if (stackX.length)
outputStr += recordIndex
? tempStr.slice(stackX[0], recordIndex)
: tempStr.slice(stackX[0]);
return outputStr;
}
let startIndex = 0;
for (let i = 0; i < temp.length; i++) {
if (temp[i] !== 'x' && temp[i] !== 'z') {
result += tempDeleteFn(temp.slice(startIndex, i));
result += temp[i];
startIndex = i + 1;
}
}
if (startIndex !== temp.length - 1)
result += tempDeleteFn(temp.slice(startIndex));
return result;
}虽然但是,我是蠢货,这道题远没有我想象的这么复杂,T_T
直接用栈操作就行的啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊
直接对每一个字符入栈出栈的判定就好了1
2
3
4
5
6
7
8
9function deleteChar(str) {
const temp = str.replace(/y/g, '');
const stack = [];
for (const ch of temp) {
if (ch === 'z' && stack.at(-1) === 'x') stack.pop();
else stack.push(ch);
}
return stack.join('');
}