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

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
面经原文内容
📍面试公司:蓝色光标
🕐面试时间:近期,用户上传于2026-03-23
💻面试岗位:前端(已OC)
⏱️面试时长:未提及(项目拷打20分钟)
📝面试体验:面试官温柔引导型,上午面完下午OC
❓面试问题:
- 拷打项目20min ing...
- 一般用TS里哪些东西,了解泛型吗
- 平时用哪些AI呢,在你平时的工作中大概占比是多少
- 了解margin的重叠问题吗,这个问题怎么解决,原理是什么
- image标签是行内元素还是块级元素
- 讲两个实现div水平垂直居中的方法
- 如何实现一个抽奖圆盘?
- 如何实现一个文本点开收起展开的那种效果,OK,你说的transform会触发重排吗?那position absolute绝对定位之后会触发吗?
- 判断object为空的方法有哪些
- setInterval和setTimeout区别
- JS数组方法里边有哪些改变原数组的方法
- sessionStorage和localStorage和cookie的区别,cookie什么时候过期
- v-if和v-show的区别,如果现在有一个tab切换,你会选择用v-if还是v-show
- 了解keep-alive吗?
- Vue里面key的作用
- Vue组件传值的方法有哪些
- 手撕:数组转树
面试官建议:思路挺清楚的,需要扎实前端基础
💡 木木有话说(刷前先看)
这是一份非常典型的中小厂/业务型公司前端一面面经,整体难度适中偏基础,但覆盖面广。面试官是引导型风格,会针对你的回答追问细节(比如第8题追问重排问题)。用户上午面完下午就OC,说明只要基础扎实、思路清晰,通过率很高。这份面经非常适合校招或实习同学查漏补缺,检验自己的前端基础是否牢固。
📝 蓝色光标前端一面·深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 面试风格 | 项目驱动 + 基础扎实 + 引导追问 |
| 难度评级 | ⭐⭐⭐(三星,基础为主,偶有深度追问) |
| 考察重心 | CSS布局与渲染、JS基础、Vue核心概念、手写算法 |
| 特殊之处 | 面试官会追问细节(如transform是否重排),考察理解深度而非背诵 |
🔍 逐题深度解析
一、项目拷打20分钟
回答思路:项目介绍不是背流水账,而是展示你的技术决策和解决问题的能力。面试官想通过项目了解你的实际能力。
建议准备结构:
- 项目背景:做什么的?用户是谁?解决了什么问题?
- 技术选型:为什么选这个框架/库?有没有对比过其他方案?
- 核心难点:遇到的最难的技术问题是什么?怎么解决的?
- 你的贡献:你具体负责了哪些模块?代码量?效果?
- 优化成果:性能提升了多少?用户体验有哪些改进?
常见追问点:
- "这个功能你是怎么实现的?" → 准备核心代码逻辑
- "为什么不用XX方案?" → 准备技术选型对比
- "如果用户量暴增,怎么优化?" → 准备扩展性思考
二、TypeScript使用与泛型
回答思路:从实际使用角度回答,展现你对TS的理解深度而非API背诵。
常用TS特性:
- 类型注解 :
const name: string = 'hello' - 接口/类型别名 :
interface User { name: string; age: number } - 枚举 :
enum Status { Pending, Success, Error } - 泛型:让组件/函数可以适用多种类型,保持类型安全
泛型核心理解:
typescript
// 泛型是什么?------ 类型的"参数"
function identity<T>(arg: T): T {
return arg
}
// 使用时不指定类型,TS自动推断
identity('hello') // 推断为 string
identity(123) // 推断为 number
// 实际场景1:API响应封装
interface ApiResponse<T> {
code: number
data: T
message: string
}
// 使用时指定具体类型
type UserResponse = ApiResponse<{ id: number; name: string }>
// 实际场景2:数组操作
function getFirst<T>(arr: T[]): T | undefined {
return arr[0]
}
const first = getFirst([1, 2, 3]) // 类型推断为 number | undefined
// 实际场景3:约束泛型(确保有特定属性)
interface HasLength {
length: number
}
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length)
return arg
}
logLength('hello') // 字符串有length ✅
logLength([1, 2, 3]) // 数组有length ✅
// logLength(123) // 数字无length ❌ 编译报错
回答要点:泛型的本质是"类型的参数化",让代码在保持类型安全的同时提高复用性。
三、AI工具使用情况
回答思路:诚实回答,同时展示你如何利用AI提效而非依赖AI。
常见AI工具:
- Cursor / Copilot:代码补全、生成样板代码
- ChatGPT / Claude:调试问题、学习新知识、代码审查
- 通义灵码 / CodeGeeX:国内替代方案
使用占比:建议回答"20%-30%",并强调"AI是辅助工具,核心逻辑和架构还是自己把控"。
加分回答:"我会用AI快速生成重复性代码(如CRUD),但复杂业务逻辑和架构设计会自己写,同时会审查AI生成的代码是否符合项目规范。"
四、margin重叠问题
回答思路:先解释什么是margin重叠,再说解决方案,最后说原理。
问题定义:相邻块级元素的上下margin会合并,取较大值,而非相加。
css
.box1 { margin-bottom: 20px; }
.box2 { margin-top: 30px; }
/* 实际间距为30px,而非50px */
解决方案:
- 触发BFC (Block Formatting Context):给其中一个元素添加
overflow: hidden或display: flow-root - 添加边框或内边距 :
border: 1px solid transparent或padding: 1px - 使用flex/grid布局:现代布局方式天然避免margin重叠
原理 :BFC内部的元素与外部的margin不重叠。触发BFC的条件包括overflow: hidden/auto、float、position: absolute/fixed、display: inline-block/flex/grid等。
五、image标签的display类型
回答 :<img>是行内元素 ,但它有行内元素不具备的宽度高度属性,属于可替换行内元素。
关键点:
- 默认display: inline
- 可以设置宽高(普通行内元素不行)
- 受line-height影响会有底部留白,常通过
display: block解决 - 多个img之间会有空格(换行符产生的空格)
六、div水平垂直居中方法
回答两个常用方法:
方法1:flex布局(推荐)
css
.parent {
display: flex;
justify-content: center;
align-items: center;
}
方法2:绝对定位+transform
css
.parent {
position: relative;
}
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
补充:grid布局、table-cell等也可实现,但flex和绝对定位最常用。
七、抽奖圆盘实现
回答思路:核心是圆盘绘制 + 抽奖逻辑 + 旋转动画。
实现方案:
- Canvas绘制:动态绘制扇形,性能好,适合复杂奖品
- CSS/SVG:适合固定奖品数量
javascript
// 核心思路
class LuckyWheel {
constructor(prizes) {
this.prizes = prizes // 奖品数组
this.angle = 0 // 当前角度
this.anglePerPrize = 360 / prizes.length
}
// 抽奖
spin(callback) {
const targetPrize = this.getRandomPrize()
const targetAngle = this.getTargetAngle(targetPrize)
const rotateAngle = 360 * 5 + targetAngle // 多转几圈
// 执行旋转动画
this.animate(rotateAngle, () => {
callback(targetPrize)
})
}
// 根据抽中的奖品计算停止角度
getTargetAngle(prize) {
const prizeIndex = this.prizes.indexOf(prize)
// 让奖品指针指向抽中的区域
return 360 - (prizeIndex * this.anglePerPrize + this.anglePerPrize / 2)
}
}
关键点 :随机算法要公平;动画使用transform + transition实现;考虑服务端验证防止作弊。
八、文本收起展开效果 + 重排问题追问
回答思路:先说实现方式,再回答重排/重绘的追问。
文本收起展开实现:
css
.text {
display: -webkit-box;
-webkit-line-clamp: 3; /* 最多显示3行 */
-webkit-box-orient: vertical;
overflow: hidden;
}
.expand {
/* 展开时移除line-clamp限制 */
-webkit-line-clamp: unset;
}
transform会触发重排吗?
- 不会 。
transform触发复合(composite) 阶段,跳过重排(layout)和重绘(paint),直接在GPU层合成,性能最好。 - 会触发重排的属性:width、height、margin、padding、top/left等几何属性。
- 会触发重绘的属性:color、background-color等视觉属性。
position: absolute会触发重排吗?
- 会 。绝对定位元素脱离文档流,改变其位置会影响其他元素吗?不会直接影响,但设置top/left等属性时仍会触发该元素自身的重排,只是不会影响父级及兄弟元素的重排范围更小。
回答要点:重排一定导致重绘,重绘不一定导致重排;尽量用transform/opacity做动画。
九、判断对象为空的方法
javascript
// 方法1:Object.keys() 最常用
function isEmpty(obj) {
return Object.keys(obj).length === 0
}
// 方法2:JSON.stringify()
JSON.stringify(obj) === '{}'
// 方法3:for...in 遍历
function isEmpty(obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) return false
}
return true
}
// 方法4:Object.getOwnPropertyNames()
Object.getOwnPropertyNames(obj).length === 0
// 注意事项:以上方法都不能判断null/undefined,需先判空
function isEmpty(obj) {
if (obj == null) return true
return Object.keys(obj).length === 0
}
十、setInterval与setTimeout区别
| 维度 | setTimeout | setInterval |
|---|---|---|
| 执行次数 | 执行一次 | 重复执行 |
| 时间精度 | 执行前等待 | 固定间隔触发 |
| 风险 | 无 | 可能任务堆积(前一次未完成,后一次又触发) |
关键点:
- 两者都受事件循环影响,不保证精确时间
- setInterval可能导致回调堆积,生产环境常用
setTimeout递归实现轮询 - 都要及时
clear避免内存泄漏
十一、改变原数组的数组方法
会改变原数组的:
push()、pop()、shift()、unshift()splice()、sort()、reverse()fill()、copyWithin()
不会改变原数组的:
concat()、slice()、map()、filter()、reduce()
十二、存储方式区别与cookie过期
| 维度 | cookie | localStorage | sessionStorage |
|---|---|---|---|
| 容量 | 4KB | 5-10MB | 5-10MB |
| 生命周期 | 可设置过期时间 | 永久(手动清除) | 标签页关闭即失效 |
| 作用域 | 同源 + 可设置path | 同源 | 同源 + 同标签页 |
| 自动携带 | 是(每次请求) | 否 | 否 |
cookie过期时间 :通过Expires或Max-Age设置。如果不设置,是会话级cookie,浏览器关闭即失效。
十三、v-if vs v-show
| 维度 | v-if | v-show |
|---|---|---|
| 原理 | 条件渲染(真实销毁/创建) | CSS display切换 |
| 初始渲染开销 | 条件为false时不渲染 | 无论条件都渲染 |
| 切换开销 | 高(销毁重建) | 低(切换CSS) |
| 适用场景 | 切换频率低 | 切换频率高 |
tab切换选择 :v-show 。因为tab切换频率高,用v-show避免频繁创建销毁DOM,性能更好。但如果tab内容非常复杂且初始化开销大,可以结合keep-alive使用。
十四、keep-alive
作用:缓存组件实例,避免重复渲染。常用于列表详情切换、tab切换等场景。
vue
<keep-alive>
<component :is="currentTab" />
</keep-alive>
关键属性:
include:只有匹配的组件会被缓存exclude:匹配的组件不会被缓存max:最多缓存实例数
生命周期 :被缓存组件会多出activated和deactivated钩子。
十五、Vue中key的作用
核心作用:帮助Vue的虚拟DOM diff算法识别节点,实现高效的列表更新。
具体作用:
- 唯一标识节点,让Vue知道哪些节点是稳定的
- 避免就地复用导致的状态错乱(如带状态的表单输入项)
- 优化性能,减少不必要的DOM操作
常见错误 :用index作为key,当列表顺序变化时会导致错误的节点复用。
十六、Vue组件传值方法
| 方式 | 方向 | 适用场景 |
|---|---|---|
| props / emit | 父→子 / 子→父 | 父子组件通信 |
| provide / inject | 祖先→后代 | 跨多级组件 |
| eventBus | 任意 | 简单跨组件(易维护难) |
| Vuex / Pinia | 任意 | 复杂状态管理 |
| $refs | 父→子实例 | 调用子组件方法 |
| parent / children | 任意 | 不推荐,耦合高 |
| slot | 父→子模板 | 内容分发 |
十七、手撕:数组转树
题目:将扁平数组转换为树形结构(常见面试手写题)
javascript
// 输入
const arr = [
{ id: 1, name: '部门A', parentId: null },
{ id: 2, name: '部门B', parentId: 1 },
{ id: 3, name: '部门C', parentId: 1 },
{ id: 4, name: '部门D', parentId: 2 }
]
// 输出
const tree = {
id: 1,
name: '部门A',
children: [
{ id: 2, name: '部门B', children: [{ id: 4, name: '部门D' }] },
{ id: 3, name: '部门C' }
]
}
// 解法:O(n) 利用Map存储节点引用
function arrayToTree(arr, parentId = null) {
const map = new Map()
const tree = []
// 第一次遍历:建立id到节点的映射
for (const item of arr) {
map.set(item.id, { ...item, children: [] })
}
// 第二次遍历:构建父子关系
for (const item of arr) {
const node = map.get(item.id)
if (item.parentId === parentId) {
tree.push(node)
} else {
const parent = map.get(item.parentId)
if (parent) {
parent.children.push(node)
}
}
}
return tree
}
时间复杂度 :O(n)
空间复杂度:O(n)
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| TypeScript | 接口、枚举、泛型(类型参数化) |
| margin重叠 | BFC解决方案、flex/grid布局 |
| 水平垂直居中 | flex / 绝对定位+transform |
| transform重排 | 不触发,跳过layout/paint直接composite |
| 对象判空 | Object.keys()、JSON.stringify() |
| setInterval | 可能堆积,常用递归setTimeout替代 |
| 数组方法 | push/pop/splice/sort/reverse 改变原数组 |
| 存储方式 | cookie(4KB/自动携带) vs storage(5MB+) |
| v-if vs v-show | 频率高用v-show,频率低用v-if |
| keep-alive | 缓存组件,activated/deactivated |
| key作用 | 标识节点,优化diff,避免状态错乱 |
| 组件传值 | props/emit、provide/inject、Vuex |
| 数组转树 | Map映射,O(n)时间 |
📌 最后一句:
蓝色光标这场一面,考察的是前端工程师的基本功厚度 。没有偏题怪题,但每一道题都可能在追问中检验你的理解深度。用户能当天OC,说明"扎实的基础 + 清晰的思路"永远是前端面试最硬的通货。技术广度决定你走多宽,基础深度决定你走多稳。