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

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
面经原文内容
📍面试公司:字节跳动
🕐面试时间:近期
💻面试岗位:暑期前端一面
❓面试问题:
- Sdk干什么,你为什么会在公司接触这个业务
- 插件是什么个逻辑
- sdk的treeshaking前后的大小有没有对比过
- treeshaking的逻辑
- treeshaking是在哪个阶段进行
- treeshaking在cjs有什么限制
- monorepo底层逻辑架构是什么样
- md里面的ai相关是什么逻辑
- md的优化性能优化逻辑
- 有没有其他的性能优化
- react周期
- 虚拟diff对比
- 为什么不能写在条件里面,如果一定要写怎么办
- git多个commit怎么合并一个
- sass, less, tailwind css区别
手撕
- bind
- 数字转汉字整数
来源:牛客网123456789___
💡 木木有话说(刷前先看)
字节的实习面质量都比较高。这一篇面经,面试官显然对构建优化和工程化细节 非常关注,问到了Tree Shaking的阶段、CJS的限制等底层问题。适合有一定打包工具和工程化经验的同学参考。
📝 字节暑期前端一面·深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 面试风格 | 工程化深挖型 + 构建原理型 + 实战手撕型 |
| 难度评级 | ⭐⭐⭐⭐(四星,Tree Shaking细节、Monorepo架构较深) |
| 考察重心 | SDK/插件设计、Tree Shaking原理、Monorepo、React核心、Git、CSS工具 |
| 特殊之处 | 对Tree Shaking的阶段和CJS限制追问很细,考察构建工具理解深度 |
🔍 逐题深度解析
一、SDK干什么,为什么会在公司接触这个业务
回答思路:SDK(Software Development Kit)是为第三方开发者提供的工具包。
SDK的作用:
- 封装复杂逻辑,提供简洁API
- 跨项目复用业务能力(如埋点、登录、支付)
- 版本独立演进,业务方无感知升级
接触原因:公司需要将核心能力(如监控、AI能力)输出给内部其他业务线或外部客户,通过SDK方式接入。
二、插件是什么逻辑
回答思路:插件是一种扩展机制,在不修改核心代码的情况下增加功能。
插件设计模式:
- 定义接口:规定插件的生命周期(install、init、destroy)
- 注册机制:插件向核心注册,核心维护插件列表
- 钩子(Hook):核心在特定时机调用插件的方法
javascript
// 插件架构示例
class PluginManager {
constructor() {
this.plugins = []
}
use(plugin) {
this.plugins.push(plugin)
plugin.install?.(this)
}
async callHook(hookName, ...args) {
for (const plugin of this.plugins) {
await plugin[hookName]?.(...args)
}
}
}
三、SDK的Tree Shaking前后大小对比
回答思路:Tree Shaking能移除未使用的代码,显著减小打包体积。
对比维度:
- 未优化:全量打包,包含所有API和依赖
- 优化后:只打包被引用的代码,体积可减少30%-70%
衡量方式 :使用webpack-bundle-analyzer或rollup-plugin-visualizer分析。
四、Tree Shaking的逻辑
回答思路 :Tree Shaking是静态分析过程,标记未使用的代码并在打包时删除。
核心逻辑:
- ES Module静态结构 :
import/export在编译时确定,无法动态修改 - 标记未使用:从入口开始,追踪所有被引用的导出
- 删除死代码:未被标记的导出在打包时被移除
javascript
// 被标记为未使用的函数
export function used() { console.log('used') }
export function unused() { console.log('unused') } // 会被移除
// 副作用标记:package.json中的"sideEffects": false
五、Tree Shaking在哪个阶段进行
答案 :打包阶段(Bundle Time),具体在模块解析和代码生成之间。
流程:
- 解析阶段:构建依赖图,标记每个导出是否被使用
- 优化阶段:删除未使用的导出和模块
- 生成阶段:只输出被使用的代码
工具:Webpack、Rollup、Vite(生产环境使用Rollup)都在打包阶段执行。
六、Tree Shaking在CJS有什么限制
回答思路 :CommonJS是动态模块系统,无法静态分析。
限制:
- 动态导入 :
require(condition ? 'a' : 'b')无法确定依赖 - 导出可变 :
module.exports可以在运行时修改,无法静态分析 - 无法确定引用 :
require的返回值可以像普通对象一样被动态访问
javascript
// CJS无法Tree Shaking的例子
const utils = require('./utils')
const methodName = Math.random() > 0.5 ? 'foo' : 'bar'
utils[methodName]() // 无法确定调用了哪个方法
// ESM静态结构,可分析
import { foo, bar } from './utils' // 明确引用了哪些
Webpack处理 :需要配置optimization.usedExports,但效果有限。
七、Monorepo底层逻辑架构
回答思路 :Monorepo用单一仓库管理多个项目/包,核心是依赖管理和构建优化。
架构逻辑:
- 依赖提升:公共依赖提升到根目录,减少重复安装
- 软链接:本地包之间通过软链接引用,无需发布
- 增量构建:只构建变更的包及其依赖
- 版本管理:统一版本号或独立版本(如Lerna)
工具对比:
| 工具 | 特点 |
|---|---|
| pnpm workspace | 硬链接,节省磁盘空间,严格依赖隔离 |
| Turborepo | 智能缓存,增量构建 |
| Nx | 依赖图可视化,支持多种框架 |
| Lerna | 版本管理成熟,发布流程完善 |
yaml
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
八、md里面的AI相关是什么逻辑
回答思路:可能指Markdown文件中的AI辅助功能(如智能补全、摘要生成)。
逻辑:
- 内容解析:解析Markdown AST,提取标题、段落、代码块
- AI调用:将内容发送给LLM,生成摘要/标签/翻译
- 结果注入:将AI生成的内容写入Frontmatter或特定位置
javascript
// 示例:为Markdown生成AI摘要
async function generateSummary(mdContent) {
const response = await fetch('/api/ai/summary', {
body: JSON.stringify({ content: mdContent })
})
const { summary } = await response.json()
return summary
}
九、MD的性能优化逻辑
回答思路:Markdown内容渲染和编辑的性能优化。
优化策略:
- 懒加载:分页加载长文档,按需渲染
- 增量解析:只重新解析编辑的部分
- 虚拟滚动:文档过长时,只渲染可视区域
- 防抖渲染:编辑时延迟渲染,避免频繁重绘
- Web Worker解析:将Markdown解析移到Worker线程
javascript
// 增量解析示例
let timer
editor.on('input', () => {
clearTimeout(timer)
timer = setTimeout(() => {
// 只重新解析变化的部分
incrementalParse(changedRange)
}, 300)
})
十、有没有其他的性能优化
回答思路:列举Web常见性能优化手段。
分类:
- 加载优化:代码分割、图片懒加载、预加载关键资源
- 渲染优化:虚拟列表、transform动画、减少重排
- 网络优化:HTTP/2、缓存策略(强缓存+协商缓存)、CDN
- 运行时优化:防抖节流、Web Worker计算、内存管理
十一、React生命周期
回答思路:类组件生命周期 vs 函数组件Hook。
类组件:
- 挂载:
constructor→render→componentDidMount - 更新:
shouldComponentUpdate→render→componentDidUpdate - 卸载:
componentWillUnmount
函数组件(Hook替代):
useEffect(() => {}, [])→componentDidMountuseEffect(() => () => {})→componentWillUnmountuseEffect(() => {}, [deps])→componentDidUpdate
十二、虚拟Diff对比
回答思路:虚拟DOM的diff算法是O(n)复杂度的启发式算法。
核心规则:
- 同层比较:不跨层级比较
- 类型不同:直接销毁重建
- 类型相同:保留DOM,更新属性
- 子节点列表:使用key优化移动、插入、删除
javascript
// 简化diff逻辑
function diff(oldNode, newNode) {
if (oldNode.type !== newNode.type) {
return { type: 'REPLACE', newNode }
}
if (oldNode.type === 'text') {
if (oldNode.content !== newNode.content) {
return { type: 'TEXT', content: newNode.content }
}
return null
}
// 比较属性...
}
十三、为什么不能写在条件里面,如果一定要写怎么办
回答思路:React Hooks必须在组件顶层调用,不能写在条件/循环中。
原因:
- React依赖调用顺序来关联Hook与状态
- 条件语句会改变调用顺序,导致状态错乱
如果一定要条件执行:
- 将条件逻辑移到Hook内部
- 使用自定义Hook封装条件逻辑
javascript
// ❌ 错误
if (condition) {
useEffect(() => { ... }, []) // 违反规则
}
// ✅ 正确:条件在Hook内部
useEffect(() => {
if (condition) {
// 执行逻辑
}
}, [condition])
十四、Git多个commit怎么合并一个
回答思路 :使用git rebase -i进行交互式变基。
步骤:
bash
# 合并最近3个commit
git rebase -i HEAD~3
# 在编辑器中,将想要合并的commit前面的pick改为squash(或s)
# pick abc123 commit 1
# squash def456 commit 2
# squash ghi789 commit 3
# 保存退出,编辑合并后的commit message
其他方法:
git reset --soft HEAD~3+git commit -m "new message"(更简单,但会丢失中间commit信息)git merge --squash(合并分支时使用)
十五、Sass、Less、Tailwind CSS区别
| 维度 | Sass/SCSS | Less | Tailwind CSS |
|---|---|---|---|
| 类型 | CSS预处理器 | CSS预处理器 | CSS框架(原子类) |
| 变量 | $ |
@ |
配置文件 |
| 嵌套 | ✅ | ✅ | ❌ |
| 混入(Mixin) | @mixin / @include |
函数 | 无 |
| 继承 | @extend |
无 | @apply |
| 原子类 | 需自定义 | 需自定义 | 内置(flex、p-4) |
| 学习曲线 | 中等 | 低 | 中等 |
| 最终体积 | 按需打包 | 按需打包 | 需PurgeCSS优化 |
选择建议:
- Sass:功能最强大,适合复杂项目
- Less:简单易学,Node.js生态
- Tailwind:快速开发,避免命名困扰,适合组件化项目
手撕一:实现bind
题目 :手写Function.prototype.bind
javascript
Function.prototype.myBind = function(context, ...args) {
if (typeof this !== 'function') {
throw new TypeError('Bind must be called on a function')
}
const fn = this
return function bound(...newArgs) {
// 判断是否作为构造函数调用
if (this instanceof bound) {
// 构造函数调用:原函数作为构造函数,忽略绑定的context
return new fn(...args, ...newArgs)
}
// 普通调用:绑定this
return fn.apply(context, [...args, ...newArgs])
}
}
// 使用示例
const obj = { name: 'Tom' }
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`
}
const boundGreet = greet.myBind(obj, 'Hello')
console.log(boundGreet('!')) // Hello, Tom!
手撕二:数字转汉字整数
题目:将数字(0-99999)转换为中文汉字。
javascript
function numberToChinese(num) {
if (num === 0) return '零'
const digits = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']
const units = ['', '十', '百', '千']
const bigUnits = ['', '万']
function convertSection(n) {
if (n === 0) return ''
let result = ''
let zeroFlag = false
for (let i = 3; i >= 0; i--) {
const divisor = Math.pow(10, i)
const digit = Math.floor(n / divisor) % 10
if (digit === 0) {
zeroFlag = true
} else {
if (zeroFlag) {
result += '零'
zeroFlag = false
}
result += digits[digit] + units[i]
}
}
return result
}
let result = ''
let zeroSection = false
for (let i = 1; i >= 0; i--) {
const divisor = Math.pow(10000, i)
const section = Math.floor(num / divisor) % 10000
if (section === 0) {
zeroSection = true
} else {
if (zeroSection && result !== '') {
result += '零'
zeroSection = false
}
result += convertSection(section) + bigUnits[i]
}
}
// 处理边界情况:十
if (result.startsWith('一十')) result = result.slice(1)
return result
}
// 测试
console.log(numberToChinese(12345)) // 一万二千三百四十五
console.log(numberToChinese(10001)) // 一万零一
console.log(numberToChinese(10)) // 十
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| SDK | 封装能力、跨项目复用、独立演进 |
| 插件 | 接口定义、注册机制、钩子调用 |
| Tree Shaking | 静态分析ESM、打包阶段、移除未使用代码 |
| CJS限制 | 动态导入/导出,无法静态分析 |
| Monorepo | 依赖提升、软链接、增量构建 |
| 虚拟Diff | 同层比较、key优化、类型决定策略 |
| Hook规则 | 顶层调用,原因:依赖调用顺序 |
| Git合并commit | git rebase -i + squash |
| CSS工具 | Sass/Less预处理器,Tailwind原子类 |
| bind实现 | 绑定this,注意构造函数调用 |
| 数字转汉字 | 分段处理、零的处理、万级单位 |
📌 最后一句:
字节这场一面,面试官明显是工程化背景深厚 的技术专家。从SDK设计、Tree Shaking底层原理、Monorepo架构,到CJS限制、Hook规则,每一题都在考察你是否理解工具背后的原理 而非表面用法。能答好这套题,说明你不仅会用Webpack/Vite,更知道它们为什么这样设计、有什么局限。这种深度,正是字节面试的典型风格。