前言
大家好,我是木斯佳。
相信很多人都感受到了,在AI浪潮的席卷之下,前端领域的门槛在变高,纯粹的"增删改查"岗位正在肉眼可见地减少。曾经热闹非凡的面经分享,如今也沉寂了许多。但我们都知道,市场的潮水退去,留下的才是真正在踏实准备、努力沉淀的人。学习的需求,从未消失,只是变得更加务实和深入。
这个专栏的初衷很简单:拒绝过时的、流水线式的PDF引流贴,专注于收集和整理当下最新、最真实的前端面试资料。我会在每一份面经和八股文的基础上,尝试从面试官的角度去拆解问题背后的逻辑,而不仅仅是提供一份静态的背诵答案。无论你是校招还是社招,目标是中大厂还是新兴团队,只要是真实发生、有价值的面试经历,我都会在这个专栏里为你沉淀下来。

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
面经原文内容
📍面试公司:字节跳动
🕐面试时间:01/09~01/13
💻面试岗位:前端开发
❓面试问题(二面前端部分):
- 你对前端掌握到什么程度?前端需要掌握哪些东西?
- 什么是 JS 事件循环机制?
- 什么是协商缓存?强制缓存和协商缓存有什么区别?
- 缓存过期机制是怎么实现的?
- React useState 是什么,特性和优势是什么?
- 调用 setState 之后 React 内部是怎么处理的?
- 使用 setState 有没有发现过渲染失败的问题?
- 开发中有没有遇到改了 state 但视图不更新的情况?
(后端/AI部分略过,9-15不作答)
- Git merge 和 rebase 区别是什么?为什么说 rebase 危险?
- 开发时多次 commit,如何合并成一个 commit?
- 算法题:模块依赖编译顺序(拓扑排序)
🙌面试感想: 作为一名后端选手,前端的部分被拷打死了,后端的部分全部答出来也挂了二面。面试官前端后端测试AI全都懂,非常强。
📝 字节跳动前端二面·深度解析(后端同学视角)
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 面试岗位 | 前端开发(候选人是后端背景) |
| 面试风格 | 全栈式拷打 + 原理深入型 |
| 难度评级 | ⭐⭐⭐⭐(四星,全栈涉及知识比较广,但前端部分只能打到三星) |
| 考察重心 | 前端基础、React原理、缓存机制、Git操作、拓扑排序 |
木木有话说:收录这篇是因为看到up是后端选手,被前端岗位捞起来了,面试里一些题目也比较有代表性,在实习面算比较经典的内容了,所以剔除了后端的部分,以及AI的部分(与其他面经高度重复。有需要的可以专门看AI岗面经)
🔍 逐题深度解析(前端部分)
一、前端掌握程度
问题:你对前端掌握到什么程度?前端需要掌握哪些东西?
javascript
// 前端知识体系(后端同学面试前要恶补的)
// 1. 基础三件套
- HTML:语义化标签、SEO、Canvas
- CSS:布局(flex/grid)、响应式、动画、预处理器
- JavaScript:ES6+、原型链、闭包、事件循环、异步编程
// 2. 框架
- React/Vue:核心原理、生命周期、Hooks、状态管理
- 至少一个框架要深入
// 3. 工程化
- Webpack/Vite:配置、插件、热更新
- Babel:编译原理
- Git:分支管理、协作流程
// 4. 浏览器原理
- 渲染机制:重排重绘
- 缓存策略:强缓存、协商缓存
- 安全:XSS、CSRF
// 5. 性能优化
- 加载优化:懒加载、代码分割
- 运行时优化:虚拟列表、防抖节流
- 监控:性能指标、错误上报
// 6. 网络
- HTTP/HTTPS、WebSocket
- 跨域解决方案
二、JS事件循环机制
问题:什么是 JS 事件循环机制?
javascript
// 事件循环(Event Loop)是JS处理异步的机制
// 1. 执行顺序
console.log('1') // 同步
setTimeout(() => {
console.log('2') // 宏任务
}, 0)
Promise.resolve().then(() => {
console.log('3') // 微任务
})
console.log('4') // 同步
// 输出:1, 4, 3, 2
// 2. 宏任务 vs 微任务
// 宏任务:setTimeout、setInterval、I/O、UI渲染
// 微任务:Promise.then、MutationObserver、queueMicrotask
// 3. 详细流程
// - 执行同步代码
// - 遇到异步任务,交给Web API
// - 同步代码执行完,执行栈清空
// - 检查微任务队列,执行所有微任务
// - 从宏任务队列取一个任务执行
// - 重复以上步骤
// 4. async/await
async function test() {
console.log('1')
await console.log('2') // await后面的代码相当于Promise.then
console.log('3')
}
console.log('4')
test()
console.log('5')
// 输出:4, 1, 2, 5, 3
三、协商缓存与强制缓存
问题:什么是协商缓存?强制缓存和协商缓存有什么区别?
javascript
// 1. 强制缓存(不发请求)
// 响应头
Cache-Control: max-age=3600 // 缓存1小时
Expires: Wed, 21 Oct 2025 07:28:00 GMT
// 适用:静态资源(带hash的文件)
// 2. 协商缓存(发请求,服务器判断)
// 第一次响应
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
ETag: "33a64df551..."
// 后续请求
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
If-None-Match: "33a64df551..."
// 服务器判断:
// - 未修改 → 304(不返回body)
// - 已修改 → 200 + 新资源
// 3. 区别对比
| 维度 | 强制缓存 | 协商缓存 |
|------|---------|---------|
| 是否发请求 | 否 | 是 |
| 状态码 | 200 (from cache) | 304 |
| 控制头 | Cache-Control | Last-Modified/ETag |
| 适用场景 | 长期不变的资源 | HTML、API数据 |
// 4. 最佳实践
// HTML:协商缓存
Cache-Control: no-cache
// JS/CSS/图片:强缓存 + hash文件名
// app-8f3c9d.js → 内容变化时hash变化
Cache-Control: max-age=31536000
四、缓存过期机制实现
问题:缓存过期机制是怎么实现的?
javascript
// 1. 服务端实现
// Redis示例
// 设置过期时间
await redis.setex('key', 3600, 'value') // 1小时后过期
// 主动删除
await redis.del('key')
// 2. 浏览器端实现
// 基于时间的过期
localStorage.setItem('timestamp', Date.now())
const isExpired = Date.now() - timestamp > 3600000
// 3. CDN实现
// CDN节点根据Cache-Control判断
// 过期后回源站拉取新资源
// 4. 具体算法
class Cache {
constructor(maxAge = 3600000) {
this.cache = new Map()
this.maxAge = maxAge
}
set(key, value) {
this.cache.set(key, {
value,
timestamp: Date.now()
})
}
get(key) {
const item = this.cache.get(key)
if (!item) return null
if (Date.now() - item.timestamp > this.maxAge) {
this.cache.delete(key) // 过期删除
return null
}
return item.value
}
}
五、React useState
问题:React useState 是什么,特性和优势是什么?
javascript
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>点击了 {count} 次</p>
<button onClick={() => setCount(count + 1)}>
增加
</button>
</div>
)
}
// 1. useState特性
// - 返回值:[当前状态, 更新函数]
// - 初始值只在首次渲染生效
// - 更新函数触发组件重新渲染
// 2. 优势
// - 函数组件有了自己的状态
// - 逻辑复用更简单(自定义Hook)
// - 相比class组件,代码更简洁
// 3. 注意事项
// - 更新是异步的
console.log(count) // 旧值
setCount(count + 1)
console.log(count) // 还是旧值
// - 可以使用函数式更新
setCount(prev => prev + 1) // 基于上一次的值
// - 对象状态要合并
const [user, setUser] = useState({ name: '', age: 0 })
setUser(prev => ({ ...prev, name: 'Tom' })) // 需要手动合并
六、setState内部处理
问题:调用 setState 之后 React 内部是怎么处理的?
javascript
// setState工作流程
// 1. 调用setState
setCount(count + 1)
// 2. React内部流程
// - 将更新加入队列
// - 调度更新(批量处理)
// - 协调(Reconciliation)
// - 计算差异(Diff)
// - 提交更新(Commit)
// 3. 简化版实现
class Component {
constructor() {
this.state = {}
this.pendingState = []
}
setState(partialState) {
this.pendingState.push(partialState)
if (!this.isBatching) {
this.performUpdate()
}
}
performUpdate() {
// 合并所有pendingState
const nextState = this.pendingState.reduce(
(state, update) => ({
...state,
...(typeof update === 'function' ? update(state) : update)
}),
this.state
)
this.state = nextState
this.pendingState = []
// 触发重新渲染
this.render()
}
}
// 4. React 18自动批处理
function handleClick() {
setCount(c => c + 1) // 不会立即渲染
setFlag(f => !f) // 不会立即渲染
// React会在事件处理完后批量更新
}
七、setState渲染失败问题
问题:使用 setState 有没有发现过渲染失败的问题?
javascript
// 常见渲染失败原因
// 1. 对象引用相同
const [user, setUser] = useState({ name: 'Tom' })
// ❌ 错误:直接修改对象
user.name = 'Jerry'
setUser(user) // 引用相同,React认为没变化,不渲染
// ✅ 正确:创建新对象
setUser({ ...user, name: 'Jerry' })
// 2. 数组操作错误
const [list, setList] = useState([1, 2, 3])
// ❌ 错误:直接修改数组
list.push(4)
setList(list)
// ✅ 正确:返回新数组
setList([...list, 4])
// 3. 闭包陷阱
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1) // 闭包捕获了初始count
}, 1000)
return () => clearInterval(timer)
}, []) // ❌ count是闭包里的旧值
// ✅ 正确:使用函数式更新
setCount(prev => prev + 1)
}
八、state改了但视图不更新
问题:开发中有没有遇到改了 state 但视图不更新的情况?
javascript
// 1. 直接修改state(最常见)
const [user, setUser] = useState({ name: 'Tom' })
// ❌
user.name = 'Jerry'
setUser(user)
// ✅
setUser({ ...user, name: 'Jerry' })
// 2. 嵌套对象
const [state, setState] = useState({
user: { name: 'Tom', address: { city: '北京' } }
})
// ❌
state.user.address.city = '上海'
setState(state)
// ✅
setState({
...state,
user: {
...state.user,
address: {
...state.user.address,
city: '上海'
}
}
})
// 3. key不变化
{items.map((item, index) => <Item key={index} data={item} />)}
// ❌ 用index做key,列表变化时可能不更新
// ✅ 用唯一id
{items.map(item => <Item key={item.id} data={item} />)}
九、Git merge vs rebase
问题:Git merge 和 rebase 区别是什么?为什么说 rebase 危险?
bash
# 1. git merge
# 特点:保留分支历史,生成merge commit
git checkout main
git merge feature
# 提交历史:
# * merge commit
# |\
# | * feature commit
# * | main commit
# |/
# * base
# 2. git rebase
# 特点:线性历史,无merge commit
git checkout feature
git rebase main # 将feature的提交"移动"到main之后
git checkout main
git merge feature # 快进合并
# 提交历史:
# * feature commit
# * main commit
# * base
# 3. 为什么rebase危险?
# - 改写历史:commit的hash会变
# - 如果rebase已经推送到远程的分支:
# * 别人基于旧历史开发,会混乱
# * 需要force push,可能覆盖他人代码
# 4. 使用原则
# - 公共分支(main/develop):用merge
# - 个人分支(feature):用rebase整理后合并
# - 永远不要rebase已经推送到公共仓库的分支
十、合并多个commit
问题:开发时多次commit,如何合并成一个commit?
bash
# 1. git rebase -i(交互式变基)
git rebase -i HEAD~3 # 合并最近3个commit
# 进入编辑器后,将pick改为squash
pick 123abc 第一次提交
squash 234bcd 第二次提交 # 合并到上一个
squash 345cde 第三次提交 # 合并到上一个
# 保存退出,然后编辑新的commit message
# 2. git reset + 重新提交
git reset --soft HEAD~3 # 撤销最近3次commit,保留修改
git commit -m "新的合并提交"
# 3. 合并已推送到远程的commit
git rebase -i HEAD~3
# 修改后需要force push
git push --force-with-lease # 更安全的force push
# 4. 只合并最后一个commit到上一个
git commit --amend # 将暂存区修改合并到上一个commit
# 5. 使用场景
# - 提交PR/MR前整理commit
# - 修复bug的多个小commit合并
# - 保持提交历史清晰
十一、拓扑排序
问题:模块依赖编译顺序(拓扑排序)
javascript
// 拓扑排序:对有向无环图进行排序,每个节点出现在它的所有依赖节点之后
// 场景:编译时确定模块加载顺序
// 模块A依赖B和C,模块B依赖D,模块C依赖D
// 1. 数据结构
const graph = {
A: ['B', 'C'],
B: ['D'],
C: ['D'],
D: []
}
// 2. 拓扑排序实现(Kahn算法)
function topologicalSort(graph) {
const inDegree = {} // 入度表
const result = []
const queue = []
// 初始化入度
for (let node in graph) {
inDegree[node] = 0
}
for (let node in graph) {
for (let dep of graph[node]) {
inDegree[dep] = (inDegree[dep] || 0) + 1
}
}
// 将入度为0的节点入队
for (let node in inDegree) {
if (inDegree[node] === 0) {
queue.push(node)
}
}
// BFS
while (queue.length) {
const node = queue.shift()
result.push(node)
for (let dep of graph[node] || []) {
inDegree[dep]--
if (inDegree[dep] === 0) {
queue.push(dep)
}
}
}
// 检查是否有环
if (result.length !== Object.keys(graph).length) {
throw new Error('图中存在环')
}
return result
}
// 3. DFS实现
function topologicalSortDFS(graph) {
const visited = new Set()
const stack = []
function dfs(node) {
visited.add(node)
for (let dep of graph[node] || []) {
if (!visited.has(dep)) {
dfs(dep)
}
}
stack.push(node)
}
for (let node in graph) {
if (!visited.has(node)) {
dfs(node)
}
}
return stack.reverse()
}
// 4. 使用示例
const order = topologicalSort(graph)
console.log(order) // ['D', 'B', 'C', 'A'] 或 ['D', 'C', 'B', 'A']
// 5. 应用场景
// - 模块打包(Webpack确定构建顺序)
// - 任务调度
// - 依赖安装
📚 知识点速查表(前端部分)
| 知识点 | 核心要点 |
|---|---|
| 前端体系 | 基础三件套、框架、工程化、浏览器原理、性能优化 |
| 事件循环 | 宏任务/微任务、执行顺序、async/await |
| 缓存 | 强缓存(不发请求)、协商缓存(发请求)、304 |
| 缓存过期 | 服务端过期、本地过期、CDN回源 |
| useState | 状态管理、更新触发渲染、函数式更新 |
| setState流程 | 入队→调度→协调→diff→提交 |
| 渲染失败 | 对象引用相同、闭包陷阱、Hook规则 |
| 视图不更新 | 直接修改、嵌套对象、key问题 |
| git merge/rebase | merge保留历史、rebase线性历史、rebase危险原因 |
| 合并commit | rebase -i、reset、amend、force push |
| 拓扑排序 | Kahn算法、DFS、入度表、环检测 |
📌 最后一句:
字节的这场二面,后端同学面前端,本身就是一次挑战。能答出后端部分说明底子很好,前端部分补上,在AI 时代还是很吃香的。