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

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
面经原文内容
📍面试公司:字节跳动
🕐面试时间:近期
💻面试岗位:前端暑期一面(中国广告交易)
⏱️面试时长:未提及
❓面试问题:
- 流式输出的方案的时候呢,服务端给的不是你要的数据格式时候,怎么处理
- Markdown 格式的话出错或者说它格式不符合你要求,你是怎么处理的?(这里详细追问了好几个问题,直到问的答不上来)
- 虚拟列表解决的性能问题,性能问题是怎么发现的,怎么排查性能问题(详细追问,直到答不上来)
- 项目中的登录鉴权是怎么做的?(追问)
- 项目完整的构建流程是怎样
- 项目中的静态资源是怎么处理的?
- 项目里的图片是怎么压缩的?
- nextTick 的作用是什么?
- 伪元素有什么作用?
- CSS 自定义变量有什么作用?
- BFC 能解决什么问题?
- 怎样可以产生一个 BFC?
- 什么是暂时性死区?
- 为什么会产生暂时性死区?
- 用什么方式声明变量会存在暂时性死区?
- 讲一下生成器(Generator)和迭代器(Iterator)
- for...in 和 for...of 的区别是什么?
- 自己写的普通对象能被 for...of 遍历吗?前提是什么?(没答上来)
- 手写:实现多个数组的全组合(笛卡尔积),如机型、颜色、存储全排列
- 手写:有效的括号(判断括号是否合法匹配)
来源:牛客网 暑期实习必拿offer
💡 木木有话说(刷前先看)
这份字节广告交易部门的一面面经,典型的"项目深挖 + 基础追问"风格。前3题围绕流式输出和虚拟列表层层追问,直到答不上来------这是大厂面试的常见策略,不是为难你,而是要看你的技术边界在哪里。后面的基础题覆盖CSS(伪元素、自定义变量、BFC)、JS(暂时性死区、迭代器、for...of)、手写算法,覆盖面很全。特别值得注意的是第18题"普通对象能被for...of遍历吗",这是一个容易被忽略的细节考点。
📝 字节广告交易前端一面·深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 面试风格 | 项目深挖型 + 基础追问型 + 边界探测型 |
| 难度评级 | ⭐⭐⭐⭐(四星,项目细节追问深,基础考察细) |
| 考察重心 | 流式数据处理、性能优化实战、CSS/JS基础、手写算法 |
| 特殊之处 | 面试官会围绕一个点连续追问直到答不上来,探测知识边界 |
🔍 逐题深度解析
一、流式输出时服务端给的数据格式不对,怎么处理
回答思路:考察对流式数据处理容错性的设计。
处理策略:
- 校验层 :在解析流式数据前,增加格式校验逻辑,判断是否符合预期格式(如SSE标准格式
data: {...}\n\n) - 降级处理:格式错误时,尝试用宽松模式解析(如按行分割后直接提取文本);如果完全无法解析,记录错误日志,展示友好提示
- 错误上报:将格式错误上报到监控系统,便于服务端排查
- 重连机制:如果是协议错误(如服务端切成了普通HTTP响应),可以主动断开重连
javascript
class StreamParser {
processChunk(rawChunk) {
try {
// 尝试按标准SSE格式解析
const lines = rawChunk.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6))
this.onMessage(data)
}
}
} catch (e) {
// 格式错误时降级:当作纯文本处理
console.warn('解析失败,降级处理', e)
this.onMessage({ content: rawChunk, raw: true })
this.reportError(e, rawChunk)
}
}
}
二、Markdown格式出错/不符合要求时的处理
回答思路:这是上一题的细化,追问Markdown渲染的容错机制。
处理策略:
- 预检:在渲染前检测Markdown结构完整性(如代码块是否闭合、表格是否完整)
- 分段渲染:将内容按段落、代码块等分割,不完整部分暂存,等完整后再渲染
- 降级显示:如果Markdown解析失败,直接显示纯文本内容,避免页面空白
- 防抖渲染:设置延迟渲染,等待可能的后续补全数据
javascript
class MarkdownStreamHandler {
constructor() {
this.buffer = ''
this.renderTimer = null
}
append(chunk) {
this.buffer += chunk
clearTimeout(this.renderTimer)
this.renderTimer = setTimeout(() => this.safeRender(), 100)
}
safeRender() {
let content = this.buffer
// 检查代码块是否完整
const backtickCount = (content.match(/```/g) || []).length
if (backtickCount % 2 === 1) {
// 不完整,截断到最后一个```之前
content = content.slice(0, content.lastIndexOf('```'))
}
try {
this.renderMarkdown(content)
} catch (e) {
// 降级:显示纯文本
this.renderPlainText(content)
console.error('Markdown渲染失败', e)
}
}
}
三、虚拟列表:性能问题发现与排查
回答思路:面试官连续追问"怎么发现、怎么排查",考察性能优化的实战经验。
怎么发现性能问题:
- 用户反馈:滚动卡顿、页面响应慢
- Chrome DevTools Performance:录制性能,查看FPS掉帧、长任务(Long Task >50ms)
- Lighthouse/Web Vitals:检测CLS(累积布局偏移)、FID(首次输入延迟)
- 内存监控:Performance Monitor查看JS堆大小、DOM节点数量
怎么排查:
- Performance面板:录制滚动操作,查看主线程任务,定位耗时函数
- React DevTools Profiler:查看组件渲染次数、渲染耗时,识别不必要的重渲染
- Memory面板:堆快照对比,查看DOM节点数量是否异常增长
- Event Listeners:检查滚动事件是否有防抖/节流,是否设置了passive
javascript
// 性能监控示例
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn('长任务:', entry.duration, entry.name)
// 上报到监控平台
}
}
})
observer.observe({ entryTypes: ['longtask'] })
虚拟列表解决的问题:DOM节点过多导致的重排重绘慢、内存占用高。虚拟列表只渲染可视区域节点,滚动时动态替换,保持DOM节点数恒定。
四、项目中的登录鉴权是怎么做的
回答思路:考察对常见鉴权方案的理解。
常见方案:
- JWT(JSON Web Token):无状态,服务端不存session,适合分布式。前端存token,每次请求带上
- Session + Cookie:传统方案,服务端存session,客户端存sessionId(cookie)
- OAuth 2.0:第三方登录(微信、GitHub)
流程:
- 用户登录,前端发送账号密码
- 服务端验证,返回token(JWT)
- 前端存储token(localStorage或cookie)
- 后续请求在
Authorization头携带token - 服务端验证token有效性
- token过期时刷新token或跳转登录
安全问题:XSS(存储token用httpOnly cookie)、CSRF(使用SameSite=Strict)
五、项目完整的构建流程
回答思路:从源码到上线的完整链路。
- 代码检出:从Git仓库拉取代码
- 依赖安装 :
npm install或pnpm install - 环境变量注入:根据环境(dev/test/prod)注入不同配置
- 代码编译:TypeScript → JS,Sass → CSS
- 打包:Webpack/Vite打包,产出静态文件
- 优化:代码压缩、图片压缩、tree-shaking
- 静态资源处理:上传到CDN,替换资源路径
- 部署:上传到服务器或对象存储
- 版本管理:生成版本号,支持回滚
六、静态资源是怎么处理的
回答思路:关注缓存、CDN、版本控制。
处理策略:
- CDN加速:静态资源(图片、字体、JS、CSS)上传到CDN,减轻源服务器压力
- 版本化:文件名加contenthash,资源更新时URL变化,解决强缓存问题
- 雪碧图:小图标合并成一张图,减少请求数
- 懒加载:非首屏图片、组件按需加载
- 预加载:关键资源(字体、关键CSS)preload
javascript
// webpack配置
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js'
}
七、图片是怎么压缩的
回答思路:构建时压缩 + 运行时优化。
构建时压缩:
- Webpack插件 :
image-webpack-loader,在打包时压缩图片 - Node脚本 :用
sharp库批量压缩
运行时优化:
- WebP格式 :使用
<picture>或<img srcset>,根据浏览器支持返回WebP或原格式 - 响应式图片:根据屏幕尺寸返回不同大小图片
- 懒加载 :
loading="lazy"
javascript
// sharp压缩示例
const sharp = require('sharp')
sharp('input.jpg')
.resize(800, 600)
.webp({ quality: 80 })
.toFile('output.webp')
八、nextTick的作用是什么
回答思路 :Vue的nextTick和Node的nextTick不同,这里指Vue。
Vue nextTick :将回调延迟到下次DOM更新循环之后执行。在修改数据后,想获取更新后的DOM,需要用它。
javascript
this.message = 'updated'
this.$nextTick(() => {
// DOM已更新,可以操作
console.log(this.$el.textContent)
})
原理 :Vue的DOM更新是异步的(批量缓冲),nextTick利用Promise/MutationObserver/setTimeout将回调放入微任务或宏任务队列,在更新后执行。
九、伪元素有什么作用
回答思路 :CSS伪元素(::before、::after等)用于在元素内容的前后插入样式化内容。
作用:
- 装饰性内容:添加图标、引号、分割线
- 清除浮动 :
clear: both - 实现特殊效果:三角形、遮罩层
- 避免额外DOM节点:不污染HTML结构
css
.clearfix::after {
content: '';
display: table;
clear: both;
}
十、CSS自定义变量有什么作用
回答思路 :CSS变量(--variable-name)用于复用值、动态主题、响应式设计。
作用:
- 主题切换:定义一组颜色变量,切换时修改变量值
- 响应式设计:在媒体查询中改变变量值
- 提高维护性:一处定义,全局使用
css
:root {
--primary-color: #007bff;
--spacing: 8px;
}
.button {
background: var(--primary-color);
margin: var(--spacing);
}
十一、BFC能解决什么问题
回答思路:BFC(块级格式化上下文)是独立的渲染区域,内部元素与外部隔离。
解决的问题:
- 清除浮动:父元素触发BFC,包含浮动子元素
- 防止margin重叠:相邻块级元素上下margin会合并,触BFC可分隔
- 自适应两栏布局:左侧浮动,右侧触发BFC,不会环绕
十二、怎样产生一个BFC
触发条件:
float不为noneposition为absolute或fixeddisplay为inline-block、flex、grid、table-celloverflow不为visible(hidden、auto、scroll)display: flow-root(最干净)
十三、什么是暂时性死区
定义 :在代码块内,使用let或const声明的变量,在声明之前访问会报错ReferenceError。这个区域称为"暂时性死区"(TDZ)。
javascript
console.log(a) // undefined(var存在提升)
var a = 1
console.log(b) // ReferenceError(TDZ)
let b = 2
十四、为什么会产生暂时性死区
原因 :ES6引入块级作用域,let和const声明的变量不会提升到块顶部。为了强制开发者"先声明后使用",引擎在进入块级作用域时,将变量放入TDZ,直到声明语句执行后才移出。
设计目的 :避免var带来的变量提升导致的意外行为,让代码更可预测。
十五、用什么方式声明变量会存在暂时性死区
letconstclass(类声明也有TDZ)
var和function没有TDZ。
十六、讲一下Generator和Iterator
Iterator :提供统一遍历接口的对象,有next()方法,返回{ value, done }。
Generator :返回Iterator对象的函数,用function*定义,内部用yield暂停。
javascript
function* generator() {
yield 1
yield 2
return 3
}
const it = generator()
it.next() // { value: 1, done: false }
it.next() // { value: 2, done: false }
it.next() // { value: 3, done: true }
十七、for...in 和 for...of 的区别
| 维度 | for...in | for...of |
|---|---|---|
| 遍历对象 | 普通对象 | 可迭代对象(Iterator) |
| 遍历内容 | 可枚举属性(包括原型链) | 迭代器的value值 |
| 适用场景 | 对象属性枚举 | 数组、Map、Set、字符串 |
javascript
const arr = ['a', 'b']
for (let i in arr) console.log(i) // '0', '1'(索引)
for (let v of arr) console.log(v) // 'a', 'b'
十八、普通对象能被 for...of 遍历吗?前提是什么?
回答 :不能直接遍历 。普通对象没有内置的Symbol.iterator方法。如果想让它被for...of遍历,需要手动添加[Symbol.iterator]方法。
javascript
const obj = { a: 1, b: 2 }
// 手动添加迭代器
obj[Symbol.iterator] = function* () {
for (let key of Object.keys(this)) {
yield this[key]
}
}
for (let v of obj) console.log(v) // 1, 2
十九、手写:多个数组的全组合(笛卡尔积)
javascript
function cartesianProduct(...arrays) {
if (arrays.length === 0) return [[]]
const result = []
const first = arrays[0]
const rest = arrays.slice(1)
for (const item of first) {
const restProducts = cartesianProduct(...rest)
for (const product of restProducts) {
result.push([item, ...product])
}
}
return result
}
// 迭代版本
function cartesianProductIterative(...arrays) {
return arrays.reduce((acc, curr) => {
const result = []
for (const a of acc) {
for (const c of curr) {
result.push([...a, c])
}
}
return result
}, [[]])
}
// 示例:机型、颜色、存储全排列
const result = cartesianProduct(
['iPhone', '小米'],
['黑色', '白色'],
['128G', '256G']
)
二十、手写:有效的括号
javascript
function isValid(s) {
const stack = []
const pairs = {
'(': ')',
'[': ']',
'{': '}'
}
for (const char of s) {
if (pairs[char]) {
// 左括号,入栈
stack.push(char)
} else {
// 右括号,检查栈顶
const last = stack.pop()
if (pairs[last] !== char) {
return false
}
}
}
return stack.length === 0
}
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| 流式数据容错 | 校验层、降级处理、错误上报 |
| Markdown容错 | 完整性检测、分段渲染、降级显示 |
| 性能排查 | Performance面板、长任务检测、内存快照 |
| 登录鉴权 | JWT、Session、OAuth、token存储 |
| 构建流程 | 依赖安装→编译→打包→CDN上传→部署 |
| 静态资源 | CDN、contenthash版本、懒加载、雪碧图 |
| 图片压缩 | webpack-loader、sharp、WebP格式 |
| nextTick | 下次DOM更新后执行回调 |
| 伪元素 | ::before/::after,装饰、清除浮动 |
| CSS变量 | 主题切换、响应式、维护性 |
| BFC | 清除浮动、防止margin重叠、自适应布局 |
| 暂时性死区 | let/const声明前不可访问 |
| Generator/Iterator | function*、yield、next() |
| for...in vs for...of | 属性枚举 vs 值遍历 |
| 对象for...of | 需实现Symbol.iterator |
| 笛卡尔积 | 递归或reduce实现 |
| 括号匹配 | 栈结构 |
📌 最后一句:
字节广告交易这场一面,从流式数据容错到虚拟列表性能排查,从CSS细节到JS底层原理,再到两道手写算法,全方位检验了前端工程师的实战能力和知识深度。面试官连续追问直到答不上来,不是为了打击你,而是为了找到你的"技术上限"。能清晰地说出自己的知识边界,同样是一种能力------这代表你清楚自己会什么、不会什么,而不是在模棱两可地猜测。