1. CSS 与布局
Q1: 请简述 HTML5 新增的语义化标签及其应用场景。
解析与回答:
HTML5 新特性速查表
1. 语义化标签
| 标签 | 含义 | 示例 |
|---|---|---|
<header> | 页面或区块头部 | <header>网站标题</header> |
<nav> | 导航区域 | <nav><a href="#">首页</a></nav> |
<main> | 主内容区 | <main>主要内容</main> |
<article> | 独立文章/内容块 | <article>博客正文</article> |
<section> | 文档分区/章节 | <section>章节内容</section> |
<aside> | 侧边栏/附属信息 | <aside>广告位</aside> |
<footer> | 页脚 | <footer>版权信息</footer> |
<figure>/ <figcaption> | 图片+说明组合 | <figure><img src="a.jpg"><figcaption>图 1</figcaption></figure> |
<mark> | 高亮文本 | <mark>重点</mark> |
<time> | 时间/日期标记 | <time datetime="2026-03-04">今天</time> |
<details>/ <summary> | 可折叠详情 | <details><summary>更多</summary>隐藏内容</details> |
2. 表单增强
| 特性 | 含义 | 示例 |
|---|---|---|
type="email" | 邮箱输入验证 | <input type="email"> |
type="url" | URL 输入验证 | <input type="url"> |
type="number" | 数字输入 | <input type="number" min="1" max="10"> |
type="range" | 滑块输入 | <input type="range" min="0" max="100"> |
type="date"/ month/ week | 日期选择 | <input type="date"> |
type="search" | 搜索框 | <input type="search"> |
type="tel" | 电话输入 | <input type="tel"> |
placeholder | 占位提示文字 | <input placeholder="请输入"> |
required | 必填项 | <input required> |
autocomplete | 自动填充控制 | <form autocomplete="on"> |
pattern | 正则验证 | <input pattern="[A-Za-z]{3}"> |
multiple | 多选文件/值 | <input type="file" multiple> |
datalist | 输入建议列表 | <input list="browsers"><datalist id="browsers"><option>Chrome</option></datalist> |
3. 多媒体标签
| 标签 | 含义 | 示例 |
|---|---|---|
<video> | 视频播放 | <video src="movie.mp4" controls></video> |
<audio> | 音频播放 | <audio src="music.mp3" controls></audio> |
<source> | 多格式资源 | <source src="video.webm" type="video/webm"> |
<track> | 字幕/文本轨道 | <track kind="subtitles" src="sub.vtt"> |
<embed> | 嵌入外部内容 | <embed src="a.swf"> |
<object> | 嵌入插件内容 | <object data="a.pdf" type="application/pdf"></object> |
4. 图形与存储
| 特性 | 含义 | 示例 |
|---|---|---|
<canvas> | 2D 绘图 | <canvas id="c"></canvas> |
<svg> | 矢量图形 | <svg width="100" height="100"><circle cx="50" cy="50" r="40"/></svg> |
localStorage | 本地持久化存储 | localStorage.setItem('key','value'); |
sessionStorage | 会话级存储 | sessionStorage.getItem('key'); |
indexedDB | 客户端数据库 | let db = indexedDB.open('db'); |
Web Storage | 键值对存储 | window.sessionStorage |
Drag and Drop API | 拖放功能 | draggable="true" |
Geolocation API | 地理位置 | navigator.geolocation.getCurrentPosition(fn) |
Web Workers | 后台线程 | new Worker('worker.js') |
WebSocket | 双向通信 | new WebSocket('ws://...') |
Q2: CSS3 有哪些常用的新特性?请列举并说明其用途。
解析与回答:
CSS3 新特性速查表
1. 选择器增强
| 选择器 | 含义 | 示例 |
|---|---|---|
E:nth-child(n) | 父元素第 n 个子元素 | li:nth-child(2){color:red;} |
E:nth-of-type(n) | 同类型第 n 个元素 | p:nth-of-type(1){font-weight:bold;} |
E:first-of-type/ last-of-type | 同类型首/末元素 | p:first-of-type{} |
E:not(selector) | 排除匹配元素 | div:not(.box){} |
E::before/ E::after | 伪元素插入内容 | div::before{content:"★";} |
E[attr^=val] | 属性值以 val 开头 | a[href^="https"]{color:green;} |
E[attr$=val] | 属性值以 val 结尾 | img[src$=".png"]{border:1px solid #000;} |
E[attr*=val] | 属性值包含 val | a[href*="example"]{} |
E~F | E 之后同层 F 元素 | h1~p{} |
E+F | E 后紧邻的 F 元素 | h1+p{} |
2. 盒模型与布局
| 特性 | 含义 | 示例 |
|---|---|---|
box-sizing: border-box | 宽高含 padding 和 border | div{box-sizing:border-box;width:100px;padding:10px;} |
display: flex | 弹性布局 | display:flex;justify-content:center; |
display: grid | 网格布局 | display:grid;grid-template-columns:1fr 1fr; |
gap | 网格/弹性布局间距 | gap: 10px; |
align-items/ justify-content | 弹性布局对齐 | align-items:center; |
place-items | 简写对齐 | place-items:center; |
order | 改变元素顺序 | order:2; |
float/ clear | 浮动与清除 | float:left; clear:both; |
position: sticky | 粘性定位 | position:sticky; top:0; |
3. 动画与过渡
| 特性 | 含义 | 示例 |
|---|---|---|
transition | 平滑过渡效果 | transition: all 0.3s ease; |
@keyframes | 定义动画关键帧 | @keyframes fade{from{opacity:0;}to{opacity:1;}} |
animation | 应用动画 | animation: fade 2s infinite; |
animation-delay | 动画延迟 | animation-delay:1s; |
animation-fill-mode | 动画前后状态 | animation-fill-mode: forwards; |
4. 变形与滤镜
| 特性 | 含义 | 示例 |
|---|---|---|
transform | 旋转/缩放/位移/倾斜 | transform: rotate(45deg) scale(1.2); |
transform-origin | 变形原点 | transform-origin: top left; |
filter | 图像滤镜效果 | filter: blur(5px) grayscale(100%); |
backdrop-filter | 背景滤镜 | backdrop-filter: blur(10px); |
5. 背景与边框
| 特性 | 含义 | 示例 |
|---|---|---|
background-size | 背景图尺寸 | background-size: cover; |
background-clip | 背景绘制区域 | background-clip: text; |
background-blend-mode | 背景混合模式 | background-blend-mode: multiply; |
border-radius | 圆角 | border-radius: 10px; |
box-shadow | 盒子阴影 | box-shadow: 2px 2px 5px rgba(0,0,0,0.3); |
border-image | 图片边框 | border-image: url(border.png) 30 round; |
outline | 外轮廓线 | outline: 2px solid red; |
6. 媒体查询(响应式设计)
| 特性 | 含义 | 示例 |
|---|---|---|
@media | 根据设备条件应用样式 | @media(max-width:768px){body{font-size:14px;}} |
min-width/ max-width | 最小/最大宽度 | @media(min-width:1200px){} |
orientation | 横竖屏 | @media(orientation:portrait){} |
resolution | 屏幕分辨率 | @media(resolution:2dppx){} |
7. 其他常用特性
| 特性 | 含义 | 示例 |
|---|---|---|
calc() | 计算值 | width: calc(100% - 20px); |
var() | CSS 变量 | --main-color:#333; color: var(--main-color); |
clamp() | 限制范围值 | font-size: clamp(12px, 2vw, 24px); |
mix-blend-mode | 混合模式 | mix-blend-mode: overlay; |
isolation | 隔离合成层 | isolation: isolate; |
scroll-behavior | 平滑滚动 | scroll-behavior: smooth; |
appearance | 重置控件外观 | appearance: none; |
user-select | 禁止选中文本 | user-select:none; |
pointer-events | 禁用鼠标事件 | pointer-events:none; |
Q3: 在 CSS 中实现元素垂直居中有哪些方案?分别适用于什么场景?
解析与回答:
18. 🎨 CSS 垂直居中 (手写题)
场景:未知宽高、已知宽高、多元素。
| 方案 | 代码关键点 | 适用场景 | 优缺点 |
|---|---|---|---|
| Flex (首选) | display: flex; align-items: center; justify-content: center; | 绝大多数现代布局 | ✅ 简单强大 ❌ 极老 IE 不支持 |
| Grid (极简) | display: grid; place-items: center; | 二维布局需求 | ✅ 代码最少 ❌ 兼容性略低于 Flex |
| 绝对定位 | top: 50%; left: 50%; transform: translate(-50%, -50%); | 未知宽高元素 | ✅ 兼容性好 ❌ 脱离文档流 |
| Table-cell | display: table-cell; vertical-align: middle; | 老旧项目兼容 | ✅ 兼容极好 ❌ 语义差,影响布局流 |
Q4: 如何使用原生 HTML5 Drag and Drop API 实现列表拖拽排序?需要注意哪些性能问题?
解析与回答:
19. 🖱️ HTML5 拖拽排序 (实操题)
🧠 核心记忆模型: “搬箱子”三部曲
想象你在搬家(拖拽),只需要记住三个动作:
| 阶段 | 事件名 | 谁触发? | 你要做什么?(唯一核心动作) | 口诀 |
|---|---|---|---|---|
| 1. 抓起 | dragstart | 被拖的元素 | 存身份证 dataTransfer.setData('id', 当前索引) | 起手存 ID |
| 2. 路过 | dragover | 目标容器 | 1. 开绿灯 (preventDefault)2. 算位置 (决定插哪) | 过路必防默 |
| 3. 放下 | drop | 目标容器 | 取身份证 dataTransfer.getData('id') → 换顺序 | 落地换顺序 |
⚡ 面试回答逻辑流 (直接背这个)
如果面试官问:“如何实现拖拽排序?” 请按以下 3 步流 回答,逻辑清晰且专业:
- 第一步:存数据 (Start)
- 在
dragstart事件中,把当前拖动元素的索引/ID 存入dataTransfer对象。 - 话术:“首先,在开始拖拽时,我要告诉浏览器我拖的是谁,把它的 ID 存起来。”
- 第二步:定位置 (Over) —— ⭐最关键
- 在容器的
dragover事件中,做两件事:
- 必须调用
e.preventDefault(),否则浏览器默认禁止放置(Drop 不会触发)。 - 根据鼠标 Y 轴坐标,计算应该插入到哪个元素之前/之后(视觉反馈)。
- 话术:“其次,在拖拽经过时,我必须阻止默认行为才能允许释放,同时实时计算鼠标位置来决定插入点。”
- 第三步:更数据 (Drop)
- 在
drop事件中,取出之前存的 ID,更新数组顺序,重新渲染列表。 - 话术:“最后,在释放时,取出 ID,修改数据源中的数组顺序,Vue 会自动更新视图。另外要注意,
dragover触发太频繁,我通常会加一个节流函数,避免高频重排导致页面卡顿。”
💣 唯一的“坑” (面试加分项)
如果只说上面三点,是及格;说出下面这个,是优秀:
- 性能坑:
dragover事件触发频率极高(每秒几十次),如果在里面频繁操作 DOM 会导致卡顿。 - 解决方案:必须加 节流 (Throttle)!
- 话术:“另外要注意,
dragover触发太频繁,我通常会加一个节流函数,避免高频重排导致页面卡顿。”
2. JavaScript 与 ES6+
Q5: 请详细介绍 ES6 中常用的数组方法及其实战场景。
解析与回答:
ES6 数组方法
| 方法 | 用途 | 实战场景示例 |
|---|---|---|
filter() | 筛选元素 | 权限过滤:users.filter(u => u.role === 'admin') |
map() | 转换数据 | 接口适配:apiData.map(item => ({ label: item.name, value: item.id })) |
reduce() | 聚合计算 | 订单总价:cart.reduce((sum, item) => sum + item.price * item.qty, 0) |
find() / findIndex() | 查找元素 | 详情页匹配:list.find(item => item.id === routeId) |
some() / every() | 条件判断 | 表单校验:fields.every(f => f.isValid) |
Array.from() | 类数组转数组 | DOM 操作:Array.from(document.querySelectorAll('.item')) |
includes() | 判断存在 | 角色检查:['admin', 'editor'].includes(userRole) |
flat() / flatMap() | 扁平化嵌套 | 评论展开:posts.flatMap(p => p.comments) |
JS 中有哪些数组方法
| 方法 | 用途 | 返回值 | 实战场景示例 |
|---|---|---|---|
push() / pop() | 末尾增/删 | push: 新长度;pop: 被删元素 | 栈结构操作、动态添加表单字段 |
unshift() / shift() | 开头增/删 | unshift: 新长度;shift: 被删元素 | 队列处理、消息通知从顶部插入 |
slice(start, end) | 浅拷贝部分数组 | 新数组 | 分页截取、避免直接修改原数据 |
splice(start, delCount, ...items) | 删除/插入/替换 | 被删元素组成的数组 | 动态表格行删除、购物车商品更新 |
concat() | 合并数组 | 新数组 | 合并多个 API 分页数据 |
join(separator) | 数组转字符串 | 字符串 | 生成 CSV 行、标签拼接(如 tags.join(', ')) |
indexOf() / lastIndexOf() | 查找元素位置 | 索引(-1 表示未找到) | 判断是否已选中某项(如多选框) |
includes() | 判断是否包含 | 布尔值 | 权限校验:['admin', 'editor'].includes(role) |
reverse() | 反转数组 | 原数组(会改变) | 时间倒序展示(注意先 slice() 再 reverse() 避免副作用) |
sort(compareFn) | 排序 | 原数组(会改变) | 商品按价格排序:arr.sort((a, b) => a.price - b.price) |
forEach() | 遍历执行 | undefined | 打印日志、触发副作用(如埋点) |
map() | 映射转换 | 新数组 | 接口数据格式化:users.map(u => ({ ...u, label: u.name })) |
filter() | 筛选 | 新数组 | 搜索过滤、权限控制:list.filter(item => item.visible) |
reduce() | 累积计算 | 累积结果(任意类型) | 统计总价、分组归类:orders.reduce((acc, o) => acc + o.amount, 0) |
find() / findIndex() | 查找第一个匹配项 | 元素 / 索引 | 详情页匹配:list.find(item => item.id === routeId) |
some() / every() | 条件判断 | 布尔值 | 表单校验:fields.every(f => f.valid) |
flat(depth) | 扁平化嵌套 | 新数组 | 处理多级评论:comments.flat(2) |
flatMap() | map + flat(1) | 新数组 | 展开子列表:posts.flatMap(p => p.tags) |
Array.from() | 类数组转真数组 | 新数组 | 操作 DOM NodeList:Array.from(document.querySelectorAll('li')) |
Array.isArray() | 判断是否为数组 | 布尔值 | 工具函数入参校验 |
Q6: ES6 提供了哪些循环遍历方法?它们之间有什么区别,适用场景是什么?
解析与回答:
ES6 中循环的方法
| 方法 / 语法 | 特点 | 适用场景示例 |
|---|---|---|
for...of | 遍历可迭代对象(数组、Set、Map、字符串等),支持 break/continue | 遍历 API 返回的列表:for (const item of data) { ... } |
forEach() | 数组专用,无返回值,不能中断 | 简单遍历渲染或日志:list.forEach(item => console.log(item)) |
map() | 返回新数组,不可中断 | 数据转换:ids.map(id => ({ id, loading: false })) |
for...in | 遍历对象可枚举属性名(不推荐用于数组) | 遍历配置对象:for (const key in config) { ... } |
Array.prototype.entries() + for...of | 同时获取索引和值 | 需要 index 的高性能循环:for (const [i, v] of arr.entries()) { ... } |
break、continue、return 在循环中的作用?
| 关键字 | 作用 | 适用场景示例 |
|---|---|---|
break | 立即退出整个循环 | 找到目标后停止:for...of 中匹配到用户 ID 就 break |
continue | 跳过当前迭代,进入下一轮 | 过滤无效项:遍历时遇到 null 直接 continue |
return | 在函数内终止函数(连带退出循环) | 在 forEach/map 等回调中用 return 只结束当前回调,不能中断整个循环;但在普通函数的 for 循环里可直接退出 |
回答模板:
“
break用于立刻退出循环,比如找到匹配项就停;continue跳过当前项继续下一轮,常用于过滤;return在函数循环里能直接退出整个函数。但注意在forEach里return只结束当前回调,不能中断循环——这时候我会改用for...of。”
Q7: 请解释 ES6 中的 Promise、async/await 以及 Generator,并说明它们在异步编程中的应用。
解析与回答:
13.Promise
- Promise 是什么:处理异步操作的容器。
- 三种状态:Pending(进行中)、Resolved(成功)、Rejected(失败)。状态一旦改变不可逆转。
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true // 假设制作成功了
if (success) {
resolve('🥤 您的珍珠奶茶好了!')
} else {
reject('❌ 抱歉,珍珠卖光了!')
}
}, 2000) // 模拟耗时 2 秒
})
- 核心方法:
.then()处理成功。.catch()处理失败。.finally()无论成败都执行。
promise
.then(res => {
console.log(res)
return '喝完啦'
})
.then(res => {
console.log(res)
})
.catch(err => {
console.log(err)
})
.finally(() => {
console.log('finally')
})
- 最佳实践:使用
async/await写法,代码更清晰。
async function run() {
try {
// 看起来就像同步代码一样!
const loginData = await login();
console.log('1. 登录成功', loginData.token);
const user = await getUserInfo(loginData.token);
console.log('2. 获取到用户', user.name);
const orders = await getOrders(user.userId);
console.log('3. 获取到订单', orders);
} catch (error) {
// 捕获任何一步发生的错误
console.error('出错了:', error);
}
}
run();
- 作用:解决“回调地狱”,让异步代码逻辑清晰、易于维护。
讲讲 ES6 Generator?项目里用过吗?
“Generator 是我理解异步编程底层的钥匙,虽然日常业务多用
async/await,但在特定场景它不可替代:
- 核心机制:它能通过
yield暂停函数执行,保留上下文,再通过next()恢复。这是实现‘分步执行’的基础。- 实战场景 - 大列表分片渲染:
- 背景:之前有个后台管理系统需展示 2 万条日志,直接渲染导致主线程阻塞,页面卡顿 2 秒。
- 方案:我编写了一个 Generator 函数,每次
yield返回 500 条数据,配合requestAnimationFrame分批插入 DOM。- 结果:首屏渲染时间从 2s 降至 200ms,滚动流畅无掉帧。
- 原理认知:我也清楚
async/await本质就是Generator + Promise的自动执行器。理解 Generator 让我能更好地处理复杂的任务队列调度,甚至在某些无 Promise 环境下模拟异步流。 总结来说,它是解决长任务切片和复杂流程控制的利器。”
Q8: 请简述 JavaScript 的事件循环(Event Loop)机制,宏任务与微任务的区别是什么?
解析与回答:
23. 事件循环 (Event Loop) & 任务队列
核心机制:JS 单线程,同步代码执行完后,先清空微任务,再执行一个宏任务,循环往复。
宏任务 (MacroTask) vs 微任务 (MicroTask) 对比表
| 特性维度 | 宏任务 (MacroTask) | 微任务 (MicroTask) |
|---|---|---|
| 包含类型 | script (整体代码)setTimeout / setInterval``setImmediate (Node)I/O 操作 UI 渲染 (部分浏览器) | Promise.then / .catch / .finally``process.nextTick (Node)MutationObserver``queueMicrotaskVue nextTick |
| 执行时机 | 当前宏任务执行完后,从队列取下一个宏任务 | 当前宏任务执行完后,立即清空整个微任务队列 |
| 优先级 | 低 | 高 (插队执行) |
| UI 渲染关系 | 两次宏任务之间可能进行 UI 渲染 | 微任务执行期间不触发 UI 渲染 |
| 典型应用 | 延时执行、定时轮询、异步 I/O 回调 | 获取最新 DOM、Promise 链式调用、状态同步 |
执行流程图解 (口述逻辑)
- 执行同步代码 (作为一个宏任务)。
- 同步代码中遇到的微任务放入微任务队列,宏任务放入宏任务队列。
- 同步代码结束 → 立即执行所有微任务 (直到队列为空)。
- 尝试 UI 渲染 (浏览器有机会重绘)。
- 从宏任务队列取一个宏任务执行。
- 回到步骤 2,循环。
3. 高频考点与回答策略
Q1: 为什么 Vue 的 nextTick 要用微任务?
- 考点:理解微任务在 DOM 更新后的执行时机。
- 回答:
- “Vue 数据更新是异步的。当数据变化后,DOM 不会立即更新,而是等到微任务队列清空时才批量更新。”
- “
nextTick利用Promise.then(微任务) 确保回调函数在DOM 更新完成后立即执行,而不是等到下一个宏任务(如setTimeout),这样能拿到最新的 DOM 节点,且性能更好,减少重绘次数。” - 加分项:“在旧版本 Vue 或某些兼容性场景下,如果微任务不可用,它会降级到
setTimeout(宏任务),但优先选微任务是为了‘快’和‘准’。”
Q2: setTimeout(fn, 0) 是真的立即执行吗?
- 考点:宏任务的延迟机制。
- 回答:
- “不是立即执行。它会被放入宏任务队列末尾。”
- “必须等当前同步代码 + 所有微任务执行完,且浏览器完成一次可能的渲染后,才会执行它。”
- 场景举例:“我曾用它将耗时计算拆分,避免阻塞主线程导致页面卡顿(长任务切片),让浏览器有机会响应用户点击。”
Q3: 代码执行顺序题 (必考)
- 题目示例:
console.log('1'); // 同步
setTimeout(() => console.log('2'), 0); // 宏
Promise.resolve().then(() => console.log('3')); // 微
console.log('4'); // 同步
- 正确顺序:
1->4->3->2 - 避坑指南:
await后面的代码是微任务。new Promise构造函数内的代码是同步执行的。- 微任务队列是一次性清空,不是执行一个就切回宏任务。
Q9: JavaScript 中的垃圾回收机制(GC)是如何工作的?
解析与回答:
12.垃圾回收机制(GC)
只要没有任何变量、属性、作用域等引用它,它就会被自动回收。
Q10: 请解释 Map 和 Set 数据结构,它们与 Object 和 Array 相比有什么优势?
解析与回答:
Map 是 ES6 引入的键值对数据结构,专为解决 Object 作为字典使用时的缺陷而生
核心对比表:Map vs Object
| 特性 | Map (推荐用于字典/缓存) | Object (推荐用于配置/模型) |
|---|---|---|
| 键的类型 | 任意类型 (对象、函数、数字、NaN) | 仅限 字符串 或 Symbol (其他类型会被强转) |
| 顺序性 | 严格有序 (按插入顺序遍历) | 无序 (虽新规范有改进,但语义不明确) |
| 尺寸获取 | map.size (O(1) 复杂度) | Object.keys(obj).length (需遍历,开销大) |
| 原型安全 | 纯净 (无原型链干扰,键名可设为 "toString") | 有风险 (键名可能与 __proto__ 等冲突) |
| 性能表现 | 频繁增删场景下性能更优 | 静态读取场景下略快,但大数据量增删慢 |
| 序列化 | 需手动转换 (如 Array.from) | 原生支持 JSON.stringify |
为什么要引入 Map?
“做通用缓存工具或复杂状态管理时会优先选 Map。 核心原因是 Object 的键只能是字符串,之前遇到过把‘不同请求参数对象’当 Key 存缓存时,因为都被转成了
'[object Object]'导致数据覆盖的 Bug。 换成 Map 后,它支持对象作为键,完美区分了不同引用;而且它的 .size 属性让统计缓存数量变成了 O(1) 操作,比Object.keys().length更高效,也避免了__proto__这类原型污染的安全风险。”
Set
核心对比表:Set vs Array
| 特性 | Set (推荐用于去重/存在性检查) | Array (推荐用于有序列表/索引访问) |
|---|---|---|
| 成员唯一性 | 自动去重 (添加重复值无效) | 允许重复,需手动过滤 |
| 查找效率 | O(1) (基于哈希,大数据量极快) | O(n) (需遍历,数据量大时慢) |
| 数据类型 | 可存任意类型 (包括 NaN,且 NaN === NaN) | 可存任意类型 (但 NaN !== NaN) |
| 遍历顺序 | 按插入顺序遍历 | 按索引顺序遍历 |
| 操作 API | add, delete, has (语义清晰) | push, splice, includes (功能多但杂) |
| 索引访问 | 不支持 (不能用 set[0],需转数组) | 支持 (随机访问效率高) |
为什么要引入 Set?(解决三大痛点)
- 极简去重逻辑
- 数组痛点:去重需写
[..., new Set(arr)]或复杂的filter+indexOf循环。 - Set 优势:构造函数天然去重。
new Set([1, 2, 2, 3])直接得到{1, 2, 3},代码行数减少 50%。
- 高性能“存在性”判断
- 数组痛点:判断元素是否存在用
arr.includes(val),数据量 1 万时需遍历 1 万次 (O(n))。 - Set 优势:
set.has(val)基于哈希表,无论数据量多大,耗时几乎不变 (O(1))。适合权限列表、黑名单校验等高频查询场景。
- 数学集合运算语义化
- 数组痛点:求交集、并集需手写多重循环,逻辑易错。
- Set 优势:配合扩展运算符可一行代码实现。
- 并集:
new Set([...a, ...b]) - 交集:
new Set([...a].filter(x => b.has(x)))
回答模板
“我在处理标签系统或权限校验时会优先用 Set。 比如之前做‘用户角色权限’功能,需要判断用户是否拥有某个权限。如果用数组
includes,每次判断都要遍历整个权限列表,性能是 O(n) ; 改用 Set 存储权限后,has()方法基于哈希实现,复杂度降为 O(1) ,即使权限项上千个也能毫秒级响应。 此外,利用new Set(array)还能一行代码完成数组去重,比手写 filter 逻辑更简洁、不易出错。”
Q11: 请解释 Reflect 对象的作用及其在 Proxy 中的应用。
解析与回答:
Reflect
“
Reflect提供了一套标准化的对象操作 API,比如用Reflect.get/set替代直接访问属性。它主要用在Proxy拦截器里保持默认行为,也让delete、in这类操作变成函数式调用,代码更清晰、可测、可组合。”
3. TypeScript
Q12: TypeScript 中 interface 和 type 有什么区别?在实际开发中如何选择?
解析与回答:
interface vs type 对比表
| 维度 | interface (接口) | type (类型别名) | 策略 | ||
|---|---|---|---|---|---|
| 核心定位 | 定义对象形状 (Object Shape) | 定义任何类型 (别名/联合/元组) | “定义 API 数据模型、Vue Props、Class 结构时,首选interface;处理复杂逻辑类型时用type。” | ||
| 声明合并 | ✅ 支持同名接口自动合并属性 | ❌ 不支持同名会报错 (Duplicate identifier) | “在大型项目中,interface允许不同模块对同一类型进行扩展(如扩展全局Window对象),维护性更强。” | ||
| 扩展方式 | extends (继承) | & (交叉类型) | “两者功能 90% 重叠。我习惯:对象继承用extends UserBase,类型组合用type Admin = User & { role: 'admin' }。” | ||
| 支持类型 | 仅限对象、函数签名 | 全能:对象、联合(` | `)、元组、原始类型、映射类型 | “当需要定义 `Status = 'success' | 'error'或[number, string] 元组时,**必须用type`**。” |
| 计算属性 | ❌ 不支持 (需配合type使用) | ✅ 支持 (keyof, in, 条件类型) | “做通用组件库时,我用type配合Pick/Omit动态推导 Props 类型,这是interface做不到的。” | ||
| 性能/提示 | 略快,IDE 提示更友好 | 略慢 (复杂嵌套时),但差异可忽略 | “在 VS Code 中,interface的错误提示通常更直观。团队规范建议:对外暴露用interface,内部逻辑用type。” |
💡 示例
Q: 什么时候用 interface,什么时候用 type?
A: “大部分场景两者互通。我的原则是:描述数据结构(如 API 返回、Props)优先用
interface,因为它支持声明合并,方便后期扩展和 IDE 提示;涉及联合类型、元组或复杂类型运算(如Pick,Partial)时用type。例如,我定义用户模型用interface User,但定义用户状态type Status = 'active' | 'inactive'。”
Q13: TypeScript 中 any 和 unknown 有什么区别?为什么推荐使用 unknown?
解析与回答:
unknown vs any 对比表
| 维度 | any (任意类型) | unknown (未知类型) | 你的实战/面试策略 (13-15K) |
|---|---|---|---|
| 类型安全 | ❌ 关闭检查可随意访问属性、调用方法 | ✅ 强制检查使用前必须进行类型收窄 (Type Narrowing) | “any是 TS 的‘逃生舱’,unknown是‘安全锁’。为了线上稳定,我严禁在新代码中使用any。” |
| 赋值兼容性 | ✅ 可赋值给任何类型 | ❌ 只能赋值给any或unknown(需收窄后才能赋给具体类型) | “接收第三方库回调或JSON.parse结果时,我定义为unknown,强迫自己写if (typeof ...)判断,避免运行时崩溃。” |
| 操作权限 | ✅ 可直接 data.id 或 data() | ❌ 禁止直接操作编译报错:“Object is of type 'unknown'" | “这迫使我在业务层做防御性编程。比如解析后端动态配置时,先校验结构再使用,减少了 30% 的空指针异常。” |
| 适用场景 | 遗留 JS 代码迁移、临时调试 | 外部输入、动态数据、catch 错误参数 | “在try-catch中,错误对象e必须是unknown。我会写一个isError(e)守卫函数来安全提取错误信息。” |
| 团队规范 | 🚫 红线 (Code Review 不通过) | ✅ 推荐 (处理不确定数据的首选) | “我在项目中配置了 ESLint 规则 @typescript-eslint/no-explicit-any: 'error',从源头杜绝any的滥用。” |
💡 示例
Q: any 和 unknown 有什么区别?为什么不用 any?
A: “
any会关闭所有类型检查,相当于写回了 JavaScript,容易埋下运行时隐患;而unknown是类型安全的顶层类型,强制要求在使用前进行类型收窄(如typeof判断)。在深圳的高并发项目中,稳定性第一,我处理后端动态数据或catch异常时,一律使用unknown配合类型守卫函数,确保代码健壮性。”
Q14: TypeScript 中的泛型(Generics)是什么?如何在组件 Props 中应用泛型?
解析与回答:
TS 中的泛型是什么?什么时候用到?怎么约束?
- 泛型:让组件/函数在定义时不指定具体类型,而是在使用时传入,保持类型灵活且安全
- 常用场景:
- 封装通用工具(如
axios<T>()返回指定类型) - 处理数组/对象的函数(如
map<T>(list: T[]): U[]) - Vue 组件 props 或组合式函数(如
useFetch<T>()) - 约束方式:用
extends限制泛型范围,例如:
function getValue<T extends object, K extends keyof T>(obj: T, key: K): T[K]
泛型在组件 Props 中的应用:提升复用性
- 场景:封装一个通用的
Select下拉框或Table表格组件,需适应不同数据结构。 - 代码示例(Vue3 + TS):
// 定义泛型 Props
interface Props<T> {
options: T[]; // 选项列表
modelValue: T | null; // 双向绑定值
labelKey: keyof T; // 指定显示字段,如 'name'
}
// 使用 defineProps 接收泛型 (Vue 3.3+)
const props = withDefaults(defineProps<Props<User>>(), {
labelKey: 'name' as keyof User
});
- 核心价值:
- 类型安全:传入
User数组时,labelKey只能填'name' | 'id',填错直接报错,无需运行时检查。 - 智能提示:调用组件时,VS Code 自动联想
options里的字段,开发效率提升 30%。 - 面试高分话术:
- “我封装的
ProTable组件使用了泛型<T>,让后端返回的任意列表数据都能获得完整的类型推导。以前改一个字段要搜全局,现在改接口类型,组件内部自动报错定位,Bug 率降低了 20%。”
Q15: TypeScript 中 keyof 的作用是什么?如何提取一个类型中的部分字段?
解析与回答:
TS 中 keyof 的作用?
“keyof 能提取对象类型的键名,比如我写一个通用表单校验工具时,用 keyof FormValues 限制传入的字段名,确保类型安全,避免拼错字段导致运行时错误。”
TS 用过哪些?怎么提取一个类型中的部分字段作为新类型?
1. 常用内置工具类型(必会基础)
- 操作:
Required<T>:将泛型T中的所有可选属性(?)强制变为必填属性。Partial<T>:把所有属性变可选(用于表单更新,只传修改项)。Pick<T, K>:提取指定字段(核心考点,见下文详解)。Omit<T, K>:排除指定字段(如列表展示时去掉password)。Record<K, T>:定义键值对映射(如字典数据{ [key: string]: string })。
2.参考回答:
“在日常开发中,我重度依赖 TS 来保证代码健壮性,特别是类型复用和接口收敛。
- 常用工具:最常用的是
Partial(做表单更新)、Omit(过滤敏感字段)和Record(定义字典)。- 字段提取方案:
- 首选
Pick:比如后端返回完整的User对象,但表格只需要id和name。我会写type UserTable = Pick<User, 'id' | 'name'>。这样既复用了主类型,又明确了视图层的数据契约。- 组合拳:如果需要提取并修改某字段类型,我会用
Omit排除旧字段,再用&交叉类型补充新定义,如Omit<User, 'id'> & { id: string }。
- 原理与价值:
- 我也了解
Pick的底层是映射类型[P in K]: T[P]。- 实际收益:在之前的项目中,通过这种‘主类型 + 衍生类型’的模式,当后端接口变更时,TS 编译报错能帮我们在提交前发现 90% 的类型不匹配问题,极大减少了线上
Cannot read property of undefined的 Bug。 TS 不仅是加类型注解,更是通过类型推导来规范数据结构设计。”
Q16: 什么是 TypeScript 中的函数重载?它在什么场景下使用?
解析与回答:
TS 函数重载是什么?什么时候使用?
- 函数重载:为同一个函数提供多个类型签名,根据传参不同返回不同类型,但共用一个实现
- 使用场景:
- 参数类型/数量不同,行为一致但返回类型不同(如
createElement(tag: 'div'): HTMLDivElementvscreateElement(tag: string): HTMLElement) - 兼容多种调用方式(如配置项可传对象或字符串)
- TS 中需先写多个声明签名,再写一个兼容所有情况的实现签名
4. Vue 核心与原理
Q17: 请简述 MVVM 模式与 MVC 模式的区别,以及 Vue 是如何体现 MVVM 思想的?
解析与回答:
VUE
MVVM vs MVC
核心对比表
| 维度 | MVC (Model-View-Controller) | MVVM (Model-View-ViewModel) | 话术 |
|---|---|---|---|
| 核心流向 | 单向/双向混合 View → Controller → Model → View | 双向自动绑定 View ⇄ ViewModel ⇄ Model | “MVC 需要手动同步视图,而 MVVM 通过数据劫持+发布订阅实现自动同步,让我能专注于业务逻辑而非 DOM 操作。” |
| DOM 操作 | 频繁手动操作Controller 需直接获取 DOM 节点更新 | 声明式/无感操作开发者只改数据,VM 层自动更新 DOM | “在旧项目重构中,我将 jQuery/MVC 的手动document.getElementById替换为 Vue 的v-bind,代码量减少 40%,Bug 率显著降低。” |
| 耦合度 | 高耦合View 与 Model 强依赖,Controller 臃肿 | 低耦合View 与 Model 完全解耦,通过 VM 通信 | “MVVM 让 UI 设计师改 HTML 不影响 JS 逻辑,后端改接口字段只需调整 Model 映射,维护成本更低。” |
| 典型代表 | jQuery + Backbone, AngularJS (早期), JSP | Vue.js, React (广义), Angular (2+) | “深圳目前主流是 Vue3/React,本质都是 MVVM 思想。我擅长利用其响应式特性处理复杂状态。” |
| 适用场景 | 简单交互、SEO 要求极高且需服务端渲染的传统页 | 单页应用 (SPA)、复杂交互、数据密集型后台 | “对于咱们公司的 SaaS 后台,数据流转复杂,MVVM 的状态驱动模式比 MVC 更适合快速迭代。” |
💡 实战策略
- 简述 MVC 和 MVVM 区别?:“最大的区别在于数据到视图的同步方式。MVC 像‘推拉模式’,需要 Controller 手动更新 View;而 MVVM(如 Vue3)是‘订阅发布模式’,数据一变,视图自动更新。我在上一个项目中,利用这一特性将表单提交逻辑从 50 行 DOM 操作缩减为 5 行数据赋值。”
- 强调“解耦”带来的价值:“MVVM 让 View 层变成了‘哑终端’,只负责展示。这使得我们在做多端适配(如同时开发 H5 和小程序)时,可以复用同一套 ViewModel 逻辑,开发效率提升明显。”
Q18: Vue 2 和 Vue 3 的响应式原理有什么区别?Vue 2 是如何解决数组和对象动态增删问题的?
解析与回答:
Vue2 用 Object.defineProperty,那它怎么解决数组动态变化和对象属性增删的问题?
参考回答:
“Vue2 的响应式基于
Object.defineProperty,它有两个天然缺陷:无法监听对象属性的动态增删,以及无法监听数组索引和长度的变化。Vue2 通过两套‘补丁’方案解决:
- 数组:重写 7 个变异方法
- Vue2 拦截了
push,pop,splice等 7 个会改变原数组的方法。- 实现:在这些方法内部,先调用原生方法修改数据,然后手动触发依赖更新,并对新增的元素递归做响应式处理。
- 局限:直接通过索引赋值(
arr[0]=1)或修改长度仍无效,必须用splice替代。
- 对象:提供
$set和$deleteAPI
- 因为无法自动劫持新增属性,Vue 提供了
Vue.set(target, key, val)和Vue.delete(target, key)。- 原理:这两个 API 内部会手动调用
defineReactive为新属性添加 getter/setter,并强制触发视图更新。 总结:Vue2 的方案是‘能劫持的自动劫持,不能劫持的提供 API 手动触发’。Vue3 要全面转向Proxy,因为Proxy能原生解决这些问题。”
能具体讲讲 Vue3 依赖收集的底层流程吗?
“Vue3 的依赖收集核心是靠
track函数、全局activeEffect和一个 三层嵌套的数据结构targetMap完成的。流程分四步:
- 标记当前执行者:
- 当
effect(如组件渲染函数)执行时,Vue 会把它赋值给全局变量activeEffect,表示‘现在是谁在读数据’。
- 拦截读取操作:
- 当代码访问响应式对象的属性(如
obj.name)时,触发 Proxy 的get拦截器,内部调用track(obj, 'name')。
- 建立映射关系(核心) :
track函数会在targetMap(一个 WeakMap)中查找:先找对象obj,再找属性'name'。- 如果找不到对应的依赖集合(Set),就新建一个。
- 最后,把当前的
activeEffect添加到这个 Set 中。- (可选加分项) 同时,为了后续清理,也会把这个 Set 记录到
activeEffect自己的deps数组里,形成双向引用。
- 完成收集:
- 此后,只要
obj.name变化(触发trigger),就能从这个 Set 里找到所有依赖它的 effect 并执行更新。 关键点:这种机制是动态的。每次 effect 重新运行前,会先清理旧的依赖关系,重新执行一遍以收集新的依赖(比如处理if/else分支变化),保证了依赖的精准性。”
除了响应式,Vue2 升 Vue3 还有哪些主要变动?
“除了响应式底层从
defineProperty换成Proxy,Vue3 在开发模式、架构设计和生态上还有 5 个关键变动:
- 编程模型革新 (Composition API) :
- 引入
<script setup>语法糖,解决了 Options API 逻辑分散的问题。- 通过 Composables (
useXxx) 替代 Mixins,实现了更清晰的逻辑复用和 TypeScript 支持,彻底消除了this指向烦恼。
- 模板能力增强 (Fragments & Teleport) :
- 支持 多根节点,不再需要无意义的
<div>包裹,优化了 DOM 结构和 CSS 布局。- 新增
<Teleport>组件,轻松解决 Modal/Toast 等需要渲染到body的场景,无需手动挂载 DOM。
- 全局 API 实例化 (Tree-shaking) :
- 废弃全局
Vue对象,改为createApp()实例化。- 全局注册(组件/指令)绑定到 App 实例,未使用的 API 可被打包工具剔除,包体积更小。
- 状态管理与路由升级:
- 官方推荐 Pinia 替代 Vuex,去除了 Mutation,API 更简洁,TS 支持更好。
- Vue Router 4 改用
createRouter工厂函数,并移除了mode配置项,改用createWebHistory等明确模式。
- 生命周期与细节调整:
- 生命周期钩子更名(如
beforeDestroy→onBeforeUnmount),且需显式导入。- 移除了
$listeners和.native修饰符,事件监听和属性继承机制更统一。 在我的迁移实践中:我主导将老项目从 Options API 重构为<script setup>+ Pinia,不仅代码行数减少了 20%,还利用 Teleport 重写了全局弹窗逻辑,彻底解决了 z-index 层级冲突问题。”
Q19: Vue 中的 computed 缓存是如何实现的?
解析与回答:
VUE 中的 computer 缓存是怎么实现的
- Vue 的 computed 有缓存机制:依赖的响应式数据不变时,多次访问直接返回缓存结果,不重复执行 getter
- 基于 Watcher 和 dirty 标记:首次读取时计算并缓存;依赖更新时 dirty = true,下次读才重新计算
Q20: Vue 3 中 watch 的配置项有哪些?immediate 和 deep 的作用是什么?
解析与回答:
watch
import { watch } from 'vue'
// 监听单个源
watch(source, callback, options?)
// 监听多个源
watch([source1, source2], callback, options?)
watch 配置项(options 对象)
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
immediate | boolean | false | 是否在侦听器创建后立即执行一次回调。常用于初始化时触发逻辑(如首次加载数据)。 |
deep | boolean | false | 是否深度监听对象或数组内部的变化。仅当监听的是引用类型(如对象、数组)时需要。 |
flush | pre / post / sync | 'pre' | 控制回调的调用时机: - 'pre':在组件更新前调用(默认) - 'post':在组件更新后调用(类似 Vue 2 的 $nextTick) - 'sync':同步调用(不推荐,可能影响性能) |
onTrack | (event: DebuggerEvent) => void | — | 调试用:当响应式属性被读取时触发(需开启 devtools) |
onTrigger | (event: DebuggerEvent) => void | — | 调试用:当响应式属性被修改时触发 |
Q21: Vue 2 和 Vue 3 中的 v-model 有什么本质区别?如何实现多个 v-model?
解析与回答:
13.Vue 2 vs Vue 3 v-model 速记表
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 核心本质 | 语法糖::value + @input | 语法糖::modelValue + @update:modelValue |
| 默认 Prop | value | modelValue |
| 默认 Event | input | update:modelValue |
| 多 v-model 支持 | ❌ 不支持 (一个组件只能有一个) (变通方案:手动绑定 value/input) | ✅ 原生支持 (无限多个) (通过参数名区分,如 v-model:title) |
| 修饰符处理 | 需在组件内手动解析 event.modifiers | 自动作为 modifiers 对象传递给组件 |
| 自定义修饰符 | 较繁琐,需手动处理 | 简单,直接在 emits 中定义 update:xxx:modifier |
| 迁移关键字 | N/A | .sync 修饰符被移除,统一合并为 v-model |
💻 代码实现对比
1. 父组件调用写法
| 场景 | Vue 2 写法 | Vue 3 写法 |
|---|---|---|
| 基础用法 | <Child v-model="msg" /> | <Child v-model="msg" /> |
| 多模型 (Multi) | ❌ 不支持 <Child :title="t" @input="t=$event" /> | ✅ 支持 <Child v-model="msg" v-model:title="t" /> |
| 自定义事件名 | ❌ 不支持 (需用 .sync 或手动绑定) | ✅ 支持 <Child v-model:custom="data" /> |
| 带修饰符 | <Child v-model.trim="msg" /> | <Child v-model.trim="msg" /> |
2. 子组件内部实现 (关键差异)
Vue 2 实现 (单 v-model)
<!-- Child.vue -->
<template>
<input
:value="value"
@input="$emit('input', $event.target.value)"
/>
</template>
<script>
export default {
props: ['value'], // 1. 接收 value
model: { // 2. (可选) 如果要改事件名,需配置 model 选项
prop: 'value',
event: 'input'
}
}
</script>
Vue 3 实现 (多 v-model / 自定义名称)
<!-- Child.vue -->
<template>
<!-- 第一个模型 -->
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
<!-- 第二个模型 (title) -->
<input :value="title" @input="$emit('update:title', $event.target.value)" />
</template>
<script setup>
// 接收多个值
defineProps({
modelValue: String,
title: String
})
// 声明多个更新事件
defineEmits(['update:modelValue', 'update:title'])
</script>
Q22: Vue 3 中的插槽(Slot)有哪几种?它们的区别和使用场景是什么?
解析与回答:
问题归纳:Vue3 插槽类型、区别与实战用法
1. 三种核心插槽(场景 + 代码)
- 默认插槽 (Default Slot)
- 场景:子组件只有一个“坑”,父组件填什么渲染什么(如:通用按钮、简单卡片内容)。
- 子组件:
<slot>默认内容</slot> - 父组件:
<Card>我是填充内容</Card> - 关键点:不带
name属性,默认为default。 - 具名插槽 (Named Slot)
- 场景:子组件有多个区域需要区分填充(如:布局组件的 Header、Sidebar、Footer)。
- 子组件:
<slot name="header"></slot>+<slot name="main"></slot> - 父组件:
<Layout>
<template #header>头部内容</template> <!-- 简写语法 -->
<template #main>主体内容</template>
</Layout>
- 关键点:必须用
template #名称包裹内容,否则内容会被丢弃或放入默认插槽。 - 作用域插槽 (Scoped Slot)
- 场景:数据在子组件,但渲染逻辑由父组件决定(如:Table 组件列自定义、列表项自定义)。
- 子组件:
<slot :user="userInfo" :id="123"></slot>(把数据绑在 slot 标签上) - 父组件:
<UserList>
<template #default="{ user, id }"> <!-- 解构接收子组件数据 -->
<div>{{ user.name }} - ID: {{ id }}</div>
</template>
</UserList>
- 关键点:子传父数据,父组件控制样式/结构。
2. 核心区别一张表
| 特性 | 默认插槽 | 具名插槽 | 作用域插槽 |
|---|---|---|---|
| 数据流向 | 父 → 子 (内容) | 父 → 子 (内容) | 子 → 父 (数据) + 父 → 子 (模板) |
| 使用场景 | 单区域内容分发 | 多区域布局分发 | 动态渲染列表/表格列 |
| 语法特征 | 直接写内容 | #名称 | #默认="{ 参数 }" |
| 深圳面试高频点 | 基础组件封装 | 页面布局组件 | 中后台表格/表单自定义 |
Q23: Vue 3 中 ref 和 reactive 有什么区别?在实际开发中推荐优先使用哪个?
解析与回答:
🗣️ 面试高频追问预演 (Bonus)
- Q: Vue3 的
ref和reactive选哪个?
- A: 推荐优先用
ref。 ref通用性强,基本类型/对象都能用,解构不会丢失响应性 (toRefs)。reactive对基本类型无效,且解构会丢失响应性,替换整个对象会切断响应式链接。
Q24: 请列举 Vue 3 中常用的 v-指令及其作用。
解析与回答:
Vue 3 常用 v- 指令速记表
| 指令 | 含义 | 常用简写 | 示例 | 说明 |
|---|---|---|---|---|
v-bind | 动态绑定属性或 props | : | :src="url"``:class="{ active: isActive }" | 可绑定任何 HTML 属性、class、style、组件 prop。 |
v-model | 双向数据绑定(表单元素) | 无 | v-model="username"``v-model.number="age" | 用于 input、textarea、select 等,可加修饰符 .trim、.number、.lazy。 |
v-on | 绑定事件监听器 | @ | @click="handleClick"``@input="onInput" | 可加修饰符 .prevent、.stop、.once、.capture 等。 |
v-if | 条件渲染(真正的条件判断) | 无 | v-if="isShow" | 不满足条件时,元素不会存在于 DOM 中。 |
v-else | 与 v-if 搭配使用 | 无 | v-if="ok"``v-else | 必须紧跟在 v-if 或 v-else-if 后。 |
v-else-if | 多条件分支 | 无 | v-if="a"``v-else-if="b"``v-else | 同上,可链式使用。 |
v-show | 条件显示(切换 CSS display) | 无 | v-show="isVisible" | 元素始终存在于 DOM,只是 display 切换。 |
v-for | 列表渲染 | 无 | v-for="item in list"``v-for="(item, index) in list" | 建议始终绑定 :key,避免原地复用。 |
v-html | 渲染 HTML 字符串 | 无 | v-html="rawHtml" | 存在 XSS 风险,慎用用户输入内容。 |
v-text | 渲染文本内容 | 无 | v-text="msg" | 等价于 {{ msg }},但不会解析 HTML。 |
v-slot | 插槽(具名/作用域插槽) | # | #header="{ title }"``v-slot:default="slotProps" | Vue 2 中为 slot/slot-scope,Vue 3 统一为 v-slot。 |
v-pre | 跳过编译,原样显示 | 无 | v-pre>{{ rawText }}</v-pre> | 用于显示 Mustache 语法而不解析。 |
v-cloak | 隐藏未编译的 Mustache | 无 | [v-cloak] { display: none; } | 配合 CSS 防止页面闪烁。 |
v-once | 只渲染一次,后续不更新 | 无 | v-once>{{ staticMsg }}</v-once> | 用于静态内容优化性能。 |
v-memo (Vue 3.2+) | 缓存模板片段,避免不必要渲染 | 无 | v-memo="[dep1, dep2]" | 依赖不变时不重新渲染,性能优化利器。 |
Q25: 如何通过 JSON 配置来实现一个高度可配置的 BasicTable 组件?
解析与回答:
BasicTable 组件实现 JSON 配置的思路
- 定义 Props: 组件通过
props: basicProps(L324) 声明了它能接受的所有配置项。 - 合并配置源: 使用一个计算属性
getProps将来自父组件模板的静态props和通过setTableProps方法传入的动态props(存储在propsRef中) 合并成一个最终的配置对象。 - 暴露接口: 通过
register事件暴露setTableProps方法,允许父组件在任何时候传递一个新的 JSON 对象来动态修改表格的行为和外观。 - 统一消费: 组件内部的所有逻辑都从
getProps这个统一的配置源读取信息,确保了行为的一致性。 将组件的配置与具体实现解耦,使得BasicTable成为一个高度可复用和可配置的组件。通过一个 JSON 对象来描述整个表格的外观和行为,而无需关心其内部复杂的实现逻辑。
5. 工程化与构建工具
Q26: Vite 和 Webpack 的核心区别是什么?从 Webpack 迁移到 Vite 有哪些具体的提升?
解析与回答:
从 Webpack 转到 Vite,构建上有哪些具体的提升?
“从 Webpack 迁移到 Vite,本质是从 **‘打包优先’**转向 ‘按需编译’,带来了三个维度的显著提升:
- 启动速度质的飞跃:
- Webpack 启动需全量打包,千级模块项目常需 30 秒以上。
- Vite 利用浏览器原生 ESM +
esbuild预构建,启动不打包,同规模项目启动压缩至 1 秒内,几乎即开即用。
- HMR(热更新)性能恒定:
- Webpack 随项目运行时间变长,HMR 延迟会增加到数秒。
- Vite 基于 ESM 边界,修改文件只刷新当前模块,无论项目多大,HMR 始终保持在 50ms 以内,且组件状态不丢失,开发体验极其流畅。
- 生产构建更高效:
- Vite 生产环境采用
Rollup进行打包,Tree-shaking 比 Webpack 更彻底,通常能减少 10%-15% 的包体积,且构建速度快 30% 以上。
Vite 和 Webpack 有什么区别?白屏怎么优化?有写过插件吗?
“1. 核心区别:构建理念不同
- Webpack 是‘打包中心主义’。启动时必须递归分析所有依赖,打包成 Bundle 给浏览器。项目越大,冷启动和 HMR 越慢。
- Vite 是‘服务中心主义’。利用浏览器原生 ESM,启动时不打包,按需编译。
- 源码:即时转译。
- 依赖:用 esbuild 进行预打包(Pre-bundling),将 CommonJS 转 ESM 并合并请求,结果缓存到
node_modules/.vite。这使得 Vite 启动通常是毫秒级,且 HMR 速度与项目大小无关。 2. 白屏优化方案- 开发环境:靠 Vite 的依赖预编译和按需加载,解决大量 node_modules 导致的解析慢和网络请求多问题。
- 生产环境:
- 路由懒加载:拆分代码块,首屏只加载核心 JS。
- 按需引入:配合
unplugin自动移除 UI 库未用代码。- 压缩与 CDN:开启 Brotli 压缩,将大依赖(Vue/Element)托管到 CDN。 3. 插件/Loader 实战
- 我在项目中写过 Vite 插件 处理 Markdown 文档:
- 利用
transform钩子拦截.md文件。- 调用
markdown-it转为 HTML 字符串,直接export default。- 价值:实现了‘导入即渲染’,无需额外 API 请求,且享受 Vite 的 HMR 热更新。
- 也配置过 SVG Sprite Loader:
- 自动将 SVG 合成雪碧图并生成组件,解决了老项目图标管理混乱和请求过多的问题。”
Q27: 请简述 Vite 和 Webpack 的核心配置项及其优化作用。
解析与回答:
8. Vite 核心配置速查表 (vite.config.ts)
| 配置模块 | 关键配置项 | 作用/场景 | 核心价值 (面试话术) |
|---|---|---|---|
| 依赖预构建 | optimizeDeps.include | 强制预构建大型库 (如 element-plus, lodash) | “解决冷启动慢,将首次加载时间减少 40%。” |
| 资源压缩 | build.gzip: true build.gzipOptions.level: 9 | 生成 .gz 文件,配合 Nginx 开启 gzip_static | “传输体积减少 60%-70%,显著提升弱网加载速度。” |
| 别名映射 | resolve.alias | 设置 @ 指向 src | “统一路径规范,避免相对路径 (../../) 导致的维护灾难。” |
| 代码分割 | build.rollupOptions.output.manualChunks | 手动拆分 vendor (UI 库/工具库) 与业务代码 | “利用浏览器长缓存,更新业务代码时用户无需重新下载第三方库。” |
| 大资源处理 | assetsInclude | 将特定大文件 (如 >10kb 图片) 排除在 Base64 外 | “避免 Base64 导致 JS 包体积过大,平衡请求数与包大小。” |
| 构建分析 | build.sourcemap: false (生产) | 生产环境关闭 SourceMap | “保护源码安全,同时减少构建时间 30% 和输出体积。” |
9. Webpack 5 核心配置速查表 (webpack.config.js)
| 配置模块 | 关键配置项 | 作用/场景 | 核心价值 (面试话术) |
|---|---|---|---|
| 持久化缓存 | cache: { type: 'filesystem' } | 将构建缓存写入磁盘 (.webpack_cache) | “二次构建速度从 10s+ 降至 1s 内,大幅提升开发体验。” |
| 树摇优化 | optimization.usedExports: true optimization.sideEffects: false | 标记未使用代码并剔除 (需 package.json 配合) | “彻底移除死代码,生产包体积平均减少 20%-30%。” |
| 智能分包 | optimization.splitChunks.chunks: 'all' | 自动提取公共代码 (node_modules) | “多页面应用共享库体积减少 50%,避免重复下载。” |
| 资源模块 | module.type: 'asset' | 替代 file-loader/url-loader,自动判断转 Base64 | “简化配置,小于 8kb 自动转 Base64,减少 HTTP 请求数。” |
| 压缩插件 | TerserPlugin (默认开启) options.compress.drop_console: true | 生产环境移除 console.log 和 debugger | “净化生产代码,防止敏感信息泄露,微减体积。” |
| 范围提升 | optimization.concatenateModules: true | 将多个模块合并为一个函数 (Scope Hoisting) | “减少闭包开销,提升运行时执行效率,减小包体积。” |
10. 通用优化策略 (Vite & Webpack 均适用)
| 策略名称 | 实施动作 | 预期效果 | 面试数据支撑 |
|---|---|---|---|
| CDN 外部引入 | 配置 externals,将 Vue/React/ECharts 改为 CDN 链接 | 打包体积瞬间减少 1MB+ | “构建时间缩短 40%,利用大厂 CDN 加速。” |
| 图片懒加载 | 路由组件 () => import() + 图片 <img loading="lazy"> | 首屏加载资源减少 50% | “首屏 FCP (First Contentful Paint) 从 2.5s 降至 1.2s。” |
| Moment 替换 | 替换 moment.js 为 dayjs 或按需加载 | 减少 200KB+ 体积 | “解决 Moment 体积过大痛点,提升解析性能。” |
| 可视化分析 | 使用 rollup-plugin-visualizer 或 webpack-bundle-analyzer | 精准定位大包来源 | “基于数据驱动优化,而非盲目猜测。” |
Q28: 请介绍 Node.js 在前端工程化中的核心工具链及应用场景。
解析与回答:
Node 工具链
核心工具链全景图(按场景分类)
| 场景 | 核心工具 (2026 主流) | 实战价值 |
|---|---|---|
| 包管理 | pnpm (首选), npm, yarn | “从 npm 迁移到pnpm,利用硬链接机制,将node_modules体积减少 60%,安装速度从 45s 降至 12s。” |
| 构建打包 | Vite (标配), Webpack (旧项目) | “主导项目从 Webpack 迁移至Vite,利用 ESM 原生支持,将冷启动时间从 30s 优化至<1s,HMR 更新几乎无延迟。” |
| 代码质量 | ESLint + Prettier + Husky | “配置Husky + lint-staged,在 git commit 时自动修复格式和语法错误,阻止不合规代码入库,Code Review 效率提升 40%。” |
| 脚本任务 | npm scripts, zx, execa | “编写zx脚本自动化处理多环境部署,将原本手动的 10 步部署流程缩减为一条命令npm run deploy:prod。” |
| 服务端渲染 | Nuxt.js (Vue), Next.js (React) | “使用Nuxt 3搭建 SEO 敏感页面,通过 SSR 将首屏 FCP 从 2.8s 优化至 1.2s,显著提升搜索排名。” |
| Mock/中间层 | Mock.js, Vite Plugin Mock, BFF | “开发BFF 层(Backend for Frontend),用 Node 聚合多个微服务接口,减少前端请求次数从 15 次降至 3 次。” |
Q29: 如何配置 ESLint、Prettier、Husky 和 Commitlint 来自动化代码规范流程?
解析与回答:
11. 配置 ESLint + Prettier + Husky + Commitlint 自动化代码规范流程
总结:ESLint+Prettier 统一代码风格,利用 Husky+lint-staged 在 commit 阶段自动修复并拦截不合规代码,最后通过 Commitlint 强制规范提交信息。落地后,Code Review 效率提升了 50%,因格式问题导致的合并冲突降为 0。
1. 统一标准:ESLint + Prettier 配置(定规矩)
- 核心动作:安装
eslint,prettier,eslint-config-prettier。 - 关键配置:
- 在
.eslintrc.js中继承plugin:prettier/recommended,让 ESLint 报错 Prettier 格式问题,避免两者冲突。 - 在
.prettierrc中锁定团队风格:semi: true(分号),singleQuote: true(单引号),printWidth: 100(行宽)。 - 价值:消除“缩进用 Tab 还是空格”的无谓争论,代码风格统一度 100%。
2. 提交拦截:Husky + lint-staged(设卡点)
- 核心动作:安装
husky,lint-staged。执行npx husky install初始化。 - 关键配置:
- 创建
.husky/pre-commit钩子,执行npx lint-staged。 - 在
package.json配置lint-staged:只检查暂存区文件(git add的文件),而非全量扫描。 - 规则示例:
"*.{js,vue,ts}": ["eslint --fix", "prettier --write"]。 - 价值:开发者执行
git commit时,自动修复格式错误;若修复失败(如语法错误),直接阻断提交,杜绝脏代码入库。
3. 信息规范:Commitlint(管日志)
- 核心动作:安装
@commitlint/cli,@commitlint/config-conventional。 - 关键配置:
- 创建
.husky/commit-msg钩子,执行npx commitlint --edit $1。 - 创建
commitlint.config.js,强制格式:type(scope): subject(如feat(user): add login api)。 - 限制
type只能是feat,fix,docs,style,refactor,test,chore。 - 价值:保证 Git 日志清晰可读,自动化生成 Changelog 成为可能,便于版本回溯。
Q30: 请简述 Git 分支管理策略,特别是从 Prod 拉分支的开发流程。
解析与回答:
GIt
🚀 核心逻辑
“基准是 Prod,功能独立拉;新功能
prod分支拉取feat分支;先合 Dev 自测,再合 Test 验收;最终回合 Prod,发布即上线。”
📝 回答模板 (直接背诵)
1. 流转过程 (三步走)
- 第一步 (联调):功能开发完,发起 PR 合并到
dev分支。 - 动作:自动部署到开发环境,进行前端联调和冒烟测试。
- 第二步 (提测):
dev验证通过后,合并到test分支。 - 动作:自动部署到测试环境,QA 介入进行完整测试和 Bug 修复。
- 第三步 (发布):
test验收通过,最终合并回prod分支。 - 动作:打 Git Tag (如
v1.2.0),触发生产环境部署,完成上线。 2. 关键规范 (体现专业性) - 冲突解决:若有冲突,我在本地先
git rebase prod解决,保持历史线性整洁。 - 质量卡点:每个环节 (
dev/test/prod) 的合并都必须走 Pull Request,强制要求 Code Review 且 CI 流水线 (Lint+Build) 通过才能合并。 - 紧急修复:线上紧急 Bug 直接从
prod拉hotfix分支,修复后快速走完test验证,立即合并回prod。
💡 高分话术 (应对挑战)
Q: 为什么你们是从 Prod 拉分支,而不是从 Dev 拉?(面试官可能会挑战这个非主流流程)
A: “这是为了最大化保证‘发布基线’的稳定性。 传统的
dev->prod流程中,dev分支可能包含大量未成熟代码,长期开发容易偏离线上版本。 我们从prod拉分支,意味着每个功能都是基于‘绝对稳定’的线上版本开发的,这大大减少了因基础代码不一致导致的深层冲突。 虽然流向是prod->dev->test->prod,看起来是‘回流’,但我们通过严格的环境隔离和PR 卡点,确保了只有经过充分验证的代码才能回到prod。这种模式对团队的代码质量和测试效率要求更高,但也更安全可靠。”
6. 网络、安全与认证
Q31: 请列举常见的 HTTP 状态码及其含义。
解析与回答:
HTTP 状态码
- 200 成功
- 201 创建成功
- 301 永久重定向
- 302 临时重定向
- 304 缓存
- 400 参数错
- 401 未登录
- 403 没权限
- 404 找不到
- 415 类型不支持
- 500 服务器错误
Q32: 什么是 RESTful API?在项目实践中如何遵循 RESTful 规范?
解析与回答:
如何理解 RESTful API?在项目里怎么用的?
“RESTful 不仅仅是规范,更是前后端解耦的关键。我主要落实了三点:
- 严格遵循资源导向:URL 只用名词(如
/orders),操作全靠 HTTP Method(GET/POST/PUT/DELETE),杜绝了/getOrder这种语义混淆的接口。- 标准化状态码处理:我和后端约定,严禁‘全 200'模式。利用
401自动登出、403权限拦截、400提示参数错误,这让前端的全局拦截器逻辑非常清晰,Bug 率降低了约 20%。- 性能与版本意识:针对列表页,我推动后端支持
?fields=字段筛选,减少无效数据传输;同时采用 URL 版本号(/v1/)管理迭代,确保旧业务不受影响。
Q33: 前端如何处理跨域问题?有哪些常见的解决方案?
解析与回答:
1. 跨域 & 安全
跨域:开发用 Vite Proxy,上线靠后端配 CORS;老接口走 Nginx 反向代理。
4. 掌握跨域解决方案(CORS、Proxy)
- 开发环境(Vite/Webpack Proxy):
- 场景:本地
localhost:5173请求api.dev.com。 - 做法:配置
vite.config.ts中的server.proxy,将/api路径重写并转发至后端,利用服务端无同源限制特性解决开发期跨域。 - 细节:开启
changeOrigin: true修改 Host 头,防止后端校验 Host 失败;配置rewrite去掉/api前缀适配后端路由。 - 生产环境(Nginx + CORS):
- 场景:前端部署在
www.example.com,后端在api.example.com。 - 做法:首选 Nginx 反向代理,将前后端统一映射到同一域名(如
example.com/web和example.com/api),从根源消除跨域,避免浏览器预检(OPTIONS)请求增加延迟。 - 备选:若必须跨域,推动后端配置
Access-Control-Allow-Origin(禁止使用*,需指定具体域名),并处理Allow-Credentials: true时的 Cookie 携带问题。
Q34: 前端常见的安全问题有哪些(XSS, CSRF)?如何防御?
解析与回答:
1. 跨域 & 安全
XSS (跨站脚本攻击):禁用危险
v-html,必须用时配合DOMPurify清洗 + CSP 限制脚本来源。 CSRF (跨站请求伪造):Axios 拦截器自动带 CSRF Token,关键操作加二次验证。
5. XSS 防御(侧重“输入过滤 + 输出转义”)
- 框架层面:
- 做法:坚持使用 Vue/React 的默认插值语法(
{{ }}/{}),利用框架自带的自动转义机制,杜绝直接渲染用户输入的 HTML。 - 高危场景处理:
- 场景:富文本编辑器(如文章详情、评论)。
- 做法:严禁直接使用
v-html。必须引入DOMPurify库进行白名单过滤,只保留<p>,<img>,<b>等安全标签,剔除<script>,onerror等恶意属性。 - 数据:在过往项目中,通过接入
DOMPurify,拦截了 100% 的存储型 XSS 攻击尝试。 - HTTP 头加固:
- 推动运维配置
Content-Security-Policy (CSP)头,限制脚本只能从本站加载,禁止eval()和内联脚本。
6. CSRF 防御(侧重“令牌验证 + 同站策略”)
- 核心机制:
- 做法:采用 Double Submit Cookie 模式
- 细节:登录成功后,后端将 Token 写入
HttpOnlyCookie(防 XSS 窃取),前端在 Axios 拦截器中自动读取该 Token 并放入请求头X-CSRF-Token。后端校验 Header 与 Cookie 是否一致。
Q35: JWT 认证机制中,如何实现 Token 的无感刷新?
解析与回答:
JWT 无感刷新
面试高分话术 (直接背)
Q: 你们项目怎么做的登录认证?Token 过期怎么处理?
A: “我们采用 JWT 双 Token 机制。
- 存储:短效 Access Token 存在内存中,长效 Refresh Token 存在
HttpOnlyCookie 里,杜绝 XSS 窃取。- 无感刷新:在 Axios 拦截器中监听
401。一旦过期,暂停当前请求,自动用 Refresh Token 换取新 Access Token。成功后,重放刚才失败的请求队列。- 体验:用户在操作过程中完全无感知,只有在 Refresh Token 也过期时才会跳转登录。
- 安全:因为是 HttpOnly Cookie,前端 JS 拿不到 Refresh Token,即使有 XSS 漏洞也无法维持长期会话。”
Q: JWT 注销登录怎么做?(无状态的痛点)
A: “JWT 本身难注销。我们的方案是:
- 前端:清除内存 Token 和 Cookie,强制跳转登录。
- 后端 (可选):对于高安场景,我们将注销的 Token ID 加入 Redis 黑名单,设置剩余有效期。网关层校验时会拒绝黑名单中的 Token。虽然牺牲了一点无状态性,但保证了安全性。”
核心机制:双 Token 策略 (Access + Refresh)
| Token 类型 | 有效期 | 存储位置 | 用途 | 安全/实战策略 |
|---|---|---|---|---|
| Access Token | 短 (15-30 分钟) | Memory (推荐 内存) 或 HttpOnly Cookie | 携带在 Header 中访问业务接口 | “过期即失效。存内存可防 XSS;若存 Cookie 必须设HttpOnly防 JS 读取。” |
| Refresh Token | 长 (7-15 天) | HttpOnly Cookie (严禁 JS 访问) | 仅用于向认证服务换取新的 Access Token | “这是‘救命稻草’。必须存HttpOnly + Secure Cookie,防止 XSS 窃取,即使 XSFR 也可通过 SameSite 防护。” |
Q36: 如何实现搜索“点击两次,只请求一次”?认证失败后,如何用新 token 重试请求?
解析与回答:
如何实现搜索“点击两次,只请求一次”?认证失败后,如何用新 token 重试请求?
回答模板
“这两个问题我都通过 Axios 拦截器 封装解决: 第一,搜索去重: 我用
AbortController维护一个 pending 请求 Map。 每次新请求前,若发现同参数请求未完成,直接 abort() 取消旧请求,确保同一时间只发一次。 第二,Token 无感刷新: 我在响应拦截器捕获 401,设一个isRefreshing锁。 第一个请求触发刷新,后续并发请求推入队列等待。 刷新成功后,遍历队列 用新 Token 重发所有请求。 这样既避免了多次刷新,又保证了用户无感知。”
Q37: Cookie、Session 和 LocalStorage 有什么区别?在跨域 SSO 场景下如何处理?
解析与回答:
浏览器 Cookie、session 机制
核心机制区别
- Cookie:浏览器自动携带的“身份证”,存在客户端,适合存 Token/SessionID。
- 关键点:必须配
SameSite=None; Secure才能跨域。 - Session:服务端的“档案柜”,存用户状态。
- 关键点:依赖 Cookie 中的 SessionID 来查找,本身不跨域,是 Cookie 在跨域。
处理跨域 SSO
“处理跨域 SSO,我主要分两种场景:
- 同根域名:直接让后端设
Domain=.parent.com,前端配withCredentials: true,浏览器自动共享,最简单。- 完全跨域:采用**‘重定向换票’**模式。A 站登录后带 Ticket 跳回 B 站,B 站后端拿 Ticket 换自己的 Token 存本地 Cookie。绝不尝试在 iframe 里强搞跨域 Cookie,因为会被浏览器拦截且不安全。 关键点:
- 后端 Cookie 必须
SameSite=None; Secure。- CORS 头必须
Allow-Credentials: true且指定具体 Origin。- 防 CSRF 必须加 Token 校验。”
22. Cookie vs LocalStorage vs SessionStorage 对比表
| 特性维度 | Cookie | LocalStorage | SessionStorage |
|---|---|---|---|
| 是否自动携带 | 是 (每次 HTTP 请求自动带) | 否 (需手动 JS 读取发送) | 否 (需手动 JS 读取发送) |
| 存储容量 | 4KB (极小,影响带宽) | 5MB+ (较大) | 5MB+ (较大) |
| 生命周期 | 可设过期时间,否则关闭浏览器失效 | 永久 (除非手动清除) | 当前标签页关闭即失效 |
| 作用域 | 同源所有窗口/标签页共享 | 同源所有窗口/标签页共享 | 仅限当前标签页 (新开不共享) |
| 服务端交互 | 后端可直接读写 (Set-Cookie) | 仅前端 JS 读写 | 仅前端 JS 读写 |
| 主要安全风险 | CSRF (需配 SameSite)XSS (需配 HttpOnly) | XSS (脚本可直接读取) | XSS (脚本可直接读取) |
| 典型应用场景 | 登录 Token (HttpOnly) 用户追踪 (Track ID) | 长期配置 (主题/语言) 离线数据缓存 | 表单分步暂存临时过滤条件 |
7. 性能优化
Q38: 前端有哪些常见的导致内存泄漏和性能崩溃的原因?如何排查?
解析与回答:
前端中导致内存失控、性能恶化乃至浏览器崩溃的常见前端编码与资源管理问题
回答模板
“前端内存泄漏和崩溃,核心通常是 ‘对象不再需要却被引用’。我遇到过最典型的 3 个场景:
- 事件监听未清理:比如组件里给
window绑了resize或 ECharts 实例,销毁时没removeEventListener或dispose,导致组件整棵树无法回收。
- 对策:在
beforeUnmount严格清理所有监听和定时器。
- 大 DOM 与资源:直接渲染万级列表或未压缩的大图,导致 DOM 节点爆炸或显存溢出,直接触发浏览器 ‘Aw, Snap!’ 崩溃。
- 对策:列表必用虚拟滚动,图片必做压缩/懒加载。
- 异步/定时器泄漏:组件销毁了,但
setInterval还在跑,或 Axios 请求回来更新状态,持有旧组件引用。
- 对策:用
AbortController取消请求,销毁时清除 Timer。 排查手段:我用 Chrome DevTools 的 Heap Snapshot,对比操作前后的快照,重点看 Detached DOM tree 和引用链(Retainers),快速定位是谁持有了不该持有的对象。”
Q39: 如何实现大规模列表的虚拟滚动?如何解决虚拟滚动中的跨页多选状态丢失问题?
解析与回答:
大规模列表的虚拟滚动中,跨页多选与统一保存的实现问题
实际问题
虚拟滚动仅渲染可视区域 DOM,导致非可视区域的勾选状态因节点销毁而丢失,无法在后续统一提交时正确收集所有已勾选的数据。
回答模板
“针对虚拟列表滚动后状态丢失的问题,核心方案是 ‘状态与 DOM 分离,用 Set 存 ID’:
- 独立状态池:我在 Pinia 中维护一个
Set集合(如selectedIdSet),专门存储所有被勾选行的 唯一 ID。这个集合常驻内存,不随 DOM 销毁而消失。- 动态渲染:虚拟列表只渲染可视区 DOM。渲染时,通过
selectedIdSet.has(item.id)动态计算 Checkbox 的选中状态。
- 滚走了:DOM 销毁,但 ID 还在 Set 里。
- 滚回来:重新渲染 DOM,读取 Set 发现 ID 存在,自动恢复勾选态。
- 统一提交:用户操作时只更新 Set,不调接口。点击‘保存’时,直接将
Set转为数组一次性发给后端。 优势:既解决了万级数据 DOM 复用导致的状态丢失,又保证了 O(1) 的操作性能和最终的一致性提交。”
虚拟滚动万级列表下,跨区域(如顶部和底部)部分勾选时,高效获取全部已勾选数据的问题。
实际问题
由于 DOM 节点随滚动不断创建和销毁,无法直接通过遍历 DOM 获取所有勾选状态;需要一个与渲染解耦的高效数据管理方案来跟踪跨区域的勾选项。
回答模板
“针对万级数据的全选和获取,我采用 ‘Set 集合存储 + 一次性遍历’ 策略,避免过度优化:
- 数据结构:维护一个全局
Set存储所有选中项的 ID。- 全选实现:
- 点击‘全选’时,我直接遍历一次源数据(1 万条),将所有 ID
add进Set。- 性能:现代浏览器处理 1 万次
Set.add仅需 10~20 毫秒,用户完全无感知,无需搞复杂的‘排除法’逻辑。- 点击‘取消全选’:直接
Set.clear(),耗时 O(1)。
- 单个交互:勾选/取消只是
Set.add(id)或Set.delete(id),都是 O(1) 操作,不影响滚动性能。- 最终获取:
- 点击保存时,直接
Array.from(selectedSet)转为数组发送给后端。- 优势:不管用户滚到哪里,不管 DOM 怎么复用,数据全在内存 Set 里,获取结果是毫秒级的,且绝对准确。 总结:用空间换时间,利用
Set的高性能特性,简单粗暴地解决万级数据选中问题。”
Q40: 电商详情页或长列表场景下,有哪些具体的性能优化手段?
解析与回答:
15. 🛒 电商详情页性能优化 (场景题)
核心痛点:首屏慢、长列表卡顿、频繁请求。 回答策略:从资源、渲染、数据、网络四个维度展开。 | 优化维度 | 关键手段 | 面试加分细节 (How & Why) | | :----------- | :------------------------ | :------------------------------------------------------------------------------------------------------------------------------ | | 🖼️ 图片资源 | 压缩 + 懒加载 + WebP + CDN | • 懒加载:原生
loading="lazy"或IntersectionObserver。 • 格式:WebP 体积小 30%,需做<picture>降级兼容。 • CDN:静态资源上云,利用边缘节点减少延迟。 | | ⚡ 渲染性能 | 虚拟列表 + Keep-Alive | • 虚拟滚动:只渲染可视区 DOM (vue-virtual-scroller),解决万级数据卡顿。 • Keep-Alive:缓存组件实例,避免重复销毁/重建,配合onActivated恢复数据。 | | 📊 响应式开销 | ShallowRef + Computed | • 浅层响应:大对象/数组用shallowRef或markRaw,避免深层递归代理带来的初始化耗时。 • 计算缓存:复杂逻辑用computed替代模板表达式,利用缓存避免重复计算。 | | 🌐 网络与缓存 | 防抖节流 + LRU 缓存 | • 请求控制:搜索/滚动用debounce/throttle。 • 多级缓存:内存 Map (LRU 算法淘汰) +localStorage持久化不常变数据。 |
Q41: 前端视频加载有哪些优化策略?H.264 和 H.265 有什么区别?
解析与回答:
视屏加载
核心策略拆解 (场景/步骤/数字)
| 策略维度 | 具体做法 (Action) | 技术细节 (Tech) | 预期收益 (Result) |
|---|---|---|---|
| 首帧极速展示 | 封面图 + 预加载关键帧 | 使用高质量 WebP 作为poster;利用<link rel="preload">预加载视频前 2 秒数据。 | 用户感知“秒开”,FCP (首屏内容绘制) 从 2.5s → 0.8s。 |
| 分片加载 (HLS/DASH) | 切片传输 + 自适应码率 | 将 MP4 转为.m3u8 (TS 切片);根据网速动态切换 720P/1080P;只加载当前播放片段。 | 弱网环境下起播时间缩短 60%;流量节省 40%。 |
| 智能预加载 | 视口检测 + 静默缓冲 | 使用IntersectionObserver监听视频进入视口前 200px 时,仅缓冲前 5 秒数据 (media.preload = 'metadata')。 | 避免无效加载,页面整体加载资源减少 30%。 |
| 编码优化 | 现代格式 + 压缩 | 优先使用 H.265 (HEVC) 或 AV1 编码;配合 ffmpeg 调整 CRF 参数 (23-28)。 | 同画质下体积减小 50%,带宽成本直接减半。 |
视频编码原理
H.264 vs H.265、不同设备的动态码率调整
H.264 vs H.265 (HEVC) 核心原理对比
| 维度 | H.264 (AVC) | H.265 (HEVC) | 你的实战策略 (13-15K) |
|---|---|---|---|
| 压缩效率 | 基准 | 同画质下体积减少 50%(或同码率下画质提升一倍) | “在高清/4K 场景首选 H.265,节省 50% CDN 带宽成本;但在老旧设备需降级。” |
| 编码单元 | 宏块 (Macroblock, 16x16) | **CTU (Coding Tree Unit, 最大 64x64)**分割更灵活,预测更精准 | “H.265 的大块划分更适合高分辨率视频,减少了块效应,边缘更平滑。” |
| 预测技术 | 帧内/帧间预测较基础 | 增强型预测支持 33 种帧内模式,运动矢量精度更高 | “复杂运动场景(如体育直播),H.265 能大幅减少马赛克,但编码耗时增加 30%。” |
| 兼容性 | ✅ 全兼容(所有浏览器/手机/PC) | ❌ 部分兼容(iOS 11+, Android 5.0+, Chrome/Edge 新版) | “必须做兜底方案:检测到不支持 H.265 的设备,自动切换 H.264 流。” |
| 专利费 | 高 (收费复杂) | 更高 (但也逐渐开放) | “商业项目需评估授权成本,目前主流方案是‘H.265 为主,H.264 为辅’。” |
Q42: 当本地存储需求超过 localStorage 的 5MB 限制时,有哪些解决方案?
解析与回答:
前端持久化存储需求超过浏览器为单个域名分配的 5MB 配额时的解决方案探讨。
回答模板
“遇到超过 5MB 的本地存储需求,我的方案是 ‘弃用 localStorage,升级 IndexedDB’:
- 认知纠正:5MB 只是
localStorage的硬限制,浏览器的IndexedDB容量上限通常是硬盘的 50% ,存 6MB 甚至几百 MB 都毫无压力。- 技术选型:
- 直接使用
IndexedDB,它是浏览器内置的异步 NoSQL 数据库。- 为了开发效率,我会封装
localforage或idb库。它们提供和localStorage一样的setItem/getItemAPI,但底层自动走 IndexedDB,代码改动极小。
- 优势匹配:
- 大容量:完美解决 6MB+ 配置文件存储。
- 非阻塞:异步读写,不会像
localStorage那样阻塞主线程导致页面卡顿。- 持久性:数据永久保存,除非用户主动清除,完全满足‘半年不改’的需求。 总结:对于大文件或大量配置,前端标准答案就是 IndexedDB,绝不强行切割
localStorage。”
8. 场景题与架构设计
Q43: 如何实现基于用户角色的动态路由加载和按钮级权限控制?
解析与回答:
在用户登录后,根据其身份(如角色、权限点列表)动态添加其有权访问的路由,并注入到路由器中。
回答模板
用户登录成功后,后端会返回当前用户的权限码列表和可访问路由标识,我存在 Pinia 里并做持久化。路由方面使用 动态路由,根据权限过滤后端返回的路由表,再通过 router.addRoute 动态挂载。页面按钮我封装了一个全局自定义指令
v-permission,传入权限码就能自动控制显隐。同时在路由全局守卫里做权限校验,没有权限直接跳 403 或首页。
Q44: 如何实现跨页面、跨 Tab 的实时状态同步?
解析与回答:
跨页面、跨 Tab 实时同步状态变更
实际问题
两个独立的浏览器标签页(A 用户页,B 管理页)之间数据状态不同步。在 B 页操作后,A 页无法感知权限已被取消,其界面(编辑按钮)仍处于可操作状态。
回答模板
“跨设备场景下,本地状态无法共享,必须上实时推送。
- 我会建立 WebSocket 长连接,让用户页订阅权限变更频道。
- 管理员操作后,后端主动推送禁用指令到该频道。
- 前端收到消息,直接更新本地 Store,利用 Vue 响应式瞬间禁用按钮。
- 若连接异常,我会有轮询兜底策略,确保数据最终一致,实现无感知的实时同步。”
Q45: 同一个列表组件在多个浏览器标签页打开时,如何隔离各自的状态?
解析与回答:
同一个列表组件实例,在多个浏览器标签页中同时打开,需要隔离各自的状态。
场景总结
在同个组件(如订单列表)被多个页签/标签同时打开,且各自展示不同数据状态(如“已完成”、“已取消”)时,如何让它们彼此独立,数据、筛选、分页等状态互不干扰。
回答模板
“这个场景核心是利用
keep-alive的动态key实现多实例隔离。
- 我给
<router-view>绑定:key="$route.fullPath"(或特定的 query 参数)。- 这样 Vue 会把‘完成’和‘取消’视为两个独立的组件实例分别缓存。
- 切换时,只是暂停旧实例、激活新实例,互不覆盖。
- 我在
onActivated钩子里做数据 freshness 检查,确保用户切回来时看到最新数据,同时保留滚动位置和筛选状态。”
Q46: 前端密码哈希加盐处理时,盐(Salt)应该存储在哪里?
解析与回答:
在前端进行密码哈希加盐处理时,盐(Salt)的存储位置安全问题。
回答模板
“关于‘加盐’的存放位置,核心原则是:前端代码对客户端是不设防的,所以密钥/盐绝不能硬编码在前端。 具体分三种情况:
- 登录密码验证:
- 盐在后端。前端只负责通过 HTTPS 传输密码,绝不在前端做哈希加盐。因为前端代码可被逆向,盐一旦暴露,哈希就失效了。
- 本地数据防篡改(签名) :
- 签名由后端生成。后端用后端私钥算好签名发给前端,前端只存‘数据 + 签名’。验证时传回后端校验。前端无法伪造签名,因为密钥从未离开过后端。
- 本地数据加密(如离线隐私数据) :
- 盐存在本地(公开),密钥由用户密码派生。
- 利用 Web Crypto API,结合用户输入的‘主密码’和本地随机生成的‘盐’,动态计算出加密密钥。
- 原理:盐可以公开,但没有用户的‘主密码’,黑客拿到盐也解不开数据。 总结:凡是涉及安全校验的‘秘密’,要么在后端,要么由用户记忆,绝不要写死在前端代码或本地存储里。”
Q47: 你对 AI 开发、Skills 和 MCP 有了解吗?它们在前端领域如何落地?
解析与回答:
你对 AI 开发、Skills 和 MCP 有了解吗?
理解它们是现代 AI 应用的三大核心组件:
- 概念理解:
- AI Agent 是‘大脑’,负责自主规划任务;
- Skills 是‘专业技能包’,将领域知识和工具封装好,让通用模型变成专家(比如‘前端代码规范 Skill’);
- MCP (Model Context Protocol) 是‘通用 USB 接口’,标准化了 AI 与外部数据/工具的连接,实现了‘即插即用’,解决了以往每个工具都要写胶水代码的痛点。
- 前端落地思路:
- 提效:我会为团队定制 Frontend Skills,封装我们的 Vue3 规范、组件库用法和 Lint 规则,让 AI 生成的代码直接符合团队标准,减少 Review 成本。
- 集成:利用 MCP 快速搭建内部助手,让 AI 能直接读取公司的 Swagger 文档、Figma 设计稿或日志系统,无需重复开发检索功能。
- 创新:探索用 Agent 自动解析需求,调用 UI 库 Skills,自动生成中后台页面的原型代码。 总结:我认为未来的前端不仅是写页面,更是 ‘AI 技能的编排者’。利用 MCP 和 Skills,我们可以将重复劳动自动化,聚焦于更复杂的业务逻辑和用户体验。” 加分项 :
- “我看过 Anthropic 关于 Skills 的文档,也尝试过用 TypeScript 写一个简单的 MCP Server 来连接本地文件系统,让 AI 能直接读取项目里的 README 来回答问题。”
Q48: 在 AI 开发中,如何理解和管理“上下文”(Context)?
解析与回答:
上下文
“简单来说,上下文就是 AI 的 ‘短期记忆’。 我的理解就三点:
- 它是有限的:太长会慢且贵,还会让 AI 变笨(注意力分散)。
- 贵在精准:我不会把整个项目塞给它,而是通过检索(RAG) ,只把当前相关的代码片段喂给它。
- 重在管理:多轮对话时,我会自动丢弃旧记录或做摘要,确保它永远只关注‘当下最需要的信息’。 核心原则:用最少的 Token,提供最准的信息。”
Q49: 请简述 Virtual DOM 的原理,以及 Vue 2 和 Vue 3 在 Virtual DOM 上的核心区别。
解析与回答:
2. Virtual DOM
Virtual DOM 是用 JS 对象模拟真实 DOM 结构,在数据变化时先在内存中 diff,再最小化更新真实 DOM,减少直接操作 DOM 的性能损耗。
🆚 Vue2 vs Vue3 Virtual DOM 核心区别
| 维度 | Vue 2 | Vue 3 |
|---|---|---|
| Diff 算法 | 全量递归对比(O(n³)) | 静态提升 + Block Tree + PatchFlag(O(n)) |
| 静态节点处理 | 每次重新创建 VNode | 编译时标记静态节点,永不 diff |
| 响应式触发更新 | Object.defineProperty → 触发整个组件 re-render | Proxy + 精准依赖追踪 → 只更新用到的动态节点 |
| 实际效果 | 大列表/复杂模板更新慢 | 同场景下 更新性能提升 1.5~2 倍 |
“我在 Vue2 升级 Vue3 项目中亲测:因 Vue3 的 PatchFlag 和静态提升,一个含 50+ 表单项的配置页,点击保存后的 re-render 时间从 120ms 降到 45ms。这正是 Virtual DOM 在编译时优化的威力。”
Q50: 在 Uni-app 跨端开发中,如何解决 IM 即时通信场景下的长列表卡顿问题?
解析与回答:
7. 熟悉 Uni-app 跨端开发框架及 IM 即时通信场景
1. 核心亮点
- 多端一致性:基于 Uni-app 一套代码编译至 H5、微信小程序、App,通过条件编译处理平台差异,减少 60% 重复开发工作量,确保 IM 核心功能在多端表现一致。
2. 场景化实战案例
- 痛点:群聊历史消息超过 500 条时,长列表滚动卡顿,内存飙升。
- 做法:
- 虚拟列表:自研虚拟滚动组件,仅渲染可视区域(如 10 条)+ 上下缓冲(各 5 条),DOM 节点数恒定在 20 个以内。
- 分页加载:采用“倒序分页”策略,上拉加载更早的历史消息,利用
uni.createSelectorQuery精准维持滚动位置,避免加载后视图跳动。 - 图片优化:消息中的图片/视频默认只加载缩略图,点击才加载原图;对超长文本进行截断折叠处理。
- 结果:千条消息列表滑动帧率稳定在 55fps+,首屏渲染时间从 1.5s 降至 400ms。
Q51: 在 Vue 2 升级到 Vue 3 + TS + Vite 的过程中,遇到了哪些核心难点?是如何解决的?
解析与回答:
13. 从 Vue 2 向 Vue 3 + TypeScript + Vite 的升级重构
针对深圳 13-15K 薪资段位的 3 年经验前端,面试官更看重**“落地能力”和“解决具体问题的思路”**。以下是 Vue2 升级 Vue3+TS+Vite 的核心难点与解法:
1. 思维模式重构:Options API → Composition API
- 难点:老代码逻辑分散在
data/methods/mounted中,难以按业务逻辑抽离;TS 类型推导在 Options API 下较弱。 - 解决:
- 渐进式策略:不一次性重写,新模块用
<script setup lang="ts">,旧模块保留 Options API,通过@vue/compat构建版本过渡。 - 逻辑复用:将散落在 mixins 中的逻辑(如表单校验、列表加载)重构为Composables 函数(如
useFormValidator.ts),利用 TS 泛型增强类型提示。 - 案例:曾将某 SaaS 项目的 15 个 Mixin 重构为 8 个 Hook,代码行数减少 30%,TS 报错率降为 0。
2. 生态断裂与兼容性:第三方库与全局 API 变更
- 难点:大量 Vue2 插件(如旧版 ElementUI、VueRouter3、Vuex3)不兼容;全局 API(
Vue.use)移除;v-model语法变更(.sync废弃)。 - 解决:
- 依赖替换:
vuex→pinia(去除了 mutation,TS 支持更好);vue-router@3→@4(路由配置扁平化)。 - 语法自动化修复:使用官方_codemod_脚本批量转换
v-model和生命周期钩子(destroyed→beforeUnmount)。 - 临时方案:对于无 Vue3 版本的老旧组件库,使用
<div>包裹并手动挂载实例,或寻找替代库(如naive-ui或element-plus)。
3. 构建工具迁移:Webpack → Vite 的“水土不服”
- 难点:
- CommonJS 依赖报错:Vite 默认 ESM,老项目引用的某些 npm 包(如旧版 lodash 封装)是 CJS 格式,导致
require is not defined。 - 环境变量变化:
process.env不可用,需改为import.meta.env。 - 静态资源路径:Webpack 的
require('@/assets/img.png')在 Vite 中需改为import或新的 URL 处理方式。 - 解决:
- 配置优化:在
vite.config.ts中配置optimizeDeps.include强制预打包 CJS 依赖;使用plugin-commonjs处理特殊包。 - 全局替换:编写正则脚本,将项目中所有
process.env.VUE_APP_替换为import.meta.env.VITE_。 - 成果量化:迁移后本地冷启动从45s 降至 1.2s,HMR 热更新从3s+ 降至毫秒级,打包体积减少18%(Tree-shaking 更彻底)。
Q52: 如何制定统一的前后端接口响应规范与错误处理机制?
解析与回答:
14. 制定统一的前后端接口响应规范与错误处理机制
SaaS = 云上软件,打开浏览器就能用,不用自己装
1. 统一响应数据结构(HTTP Status vs 业务 Status)
- 规范定义:严格区分网络层错误与业务层错误
- 推动后端输出Swagger(API 文档生成与测试工具)/OpenAPI 文档,并约定
code枚举值表(如:200 成功,401 未登录,500 系统异常)。 - 前端封装泛型响应类型
interface Response<T> { code: number; data: T; msg: string },在 TS 层面强制约束数据结构,避免any。
2. 拦截器统一处理(Axios Interceptors)
- 难点:每个页面重复写
if (res.code !== 200)判断,导致代码冗余且易漏处理。 - 解决:
- 响应拦截器:在
response interceptor中统一解析: code === 200:直接返回res.data,业务层无感调用。code === 401:自动清除 Token 并跳转登录页(带 redirect 参数)。code === 403/500:统一触发全局 Message 报错,或静默记录日志。- 请求拦截器:统一注入
Authorization: Bearer ${token}和timestamp防重放签名。 - 效果:业务组件代码减少40%,只需关注
try/catch中的正常数据流。
Q53: 在核心业务模块的技术选型中,为什么选择 Node.js 18 LTS 和 Day.js?
解析与回答:
15. 负责核心业务模块的技术选型与难点攻关
1. 运行时选型:Node.js 18 LTS (而非 14/16 或 20+)
- 选型理由:
- 原生支持:内置
fetchAPI 和Web Crypto,包体积减小约 15%。 - 稳定性:LTS 版本经过生产验证,避开 20+ 版本的实验性特性风险,同时兼容主流 CI/CD 流水线。
2. 时间库选型:Day.js 1.11.x (按需加载插件)
- 选型理由:
- 轻量级:核心库仅2KB(Moment.js 的 1/30),符合首屏性能优化指标。
- 插件化:只引入业务需要的
utc、timezone、isSameOrBefore插件,避免全量引入。
Q54: 在对接地图 SDK 实现海量点可视化时,如何解决渲染性能问题?
解析与回答:
16. 对接高德/百度地图 SDK,实现站点分布可视化、附近车辆检索及路径规划功能
1. 站点分布可视化:海量点渲染与聚合
- 难点:直接渲染上千个
Marker会导致 DOM 节点爆炸,页面卡顿(FPS < 30)。 - 解决:
- 技术选型:使用
AMap.MarkerClusterer(点聚合)或CanvasLayer(自定义图层)。 - 具体实现:
- 当缩放级别低时,显示聚合气泡(如“100+"),减少渲染对象至1/50。
- 当缩放级别高时,启用
MassMarks(海量点标记),利用 WebGL 绘制,支持**10 万+**数据点流畅渲染。 - 成果:在展示 2000+ 站点时,首屏渲染时间从3s 降至 400ms,滚动帧率稳定在55FPS+。
ECharts
一、项目中你怎么封装 ECharts?
把 ECharts 封装成通用可复用组件:
- 在 Vue 组件里用 ref 获取容器 DOM。
- 在 onMounted 生命周期里初始化图表。
- 通过 setOption 渲染配置和数据。
- 监听窗口变化(window.resize())做自适应(chart.resize())。
- 组件销毁时释放实例,防止内存泄漏。
- 支持响应式、loading、数据更新。
二、ECharts 怎么做自适应?
- 监听 window 的 resize 事件。
- 使用防抖(debounce)优化性能。
- 在回调里调用 chart.resize ()。
- 保证窗口变化时图表自动适配。
三、组件销毁时必须做什么?
必须销毁 ECharts 实例:
- 调用 chart.dispose ()。
- 清空 resize 监听。
- 清空定时器。不销毁会造成内存泄漏,页面越来越卡。
四、ECharts 性能优化(高频)
- 复用实例,不要重复 init。
- 使用 setOption 增量更新,不重新创建。
- 大数据关闭动画:animation: false。
- 减少阴影、渐变、多余标签。
- 大量数据使用 dataZoom 或分段渲染。
- 避免频繁 setOption,做节流防抖。
五、图表不显示 / 白屏原因
- 容器 DOM 没有设置宽高。
- 在 DOM 挂载前就初始化。
- 数据格式错误或为空。
- 多次初始化导致实例异常。
- 路由切换没销毁,实例污染。
六、两个图表如何联动?
- 给多个图表设置相同的 group。
- 使用 echarts.connect (group) 关联。
- 可实现 tooltip、brush、数据联动。
七、海量数据怎么渲染?
- 关闭动画。
- 数据采样、简化数据。
- 使用 dataZoom 只展示可视区域。
- 使用 appendData 追加数据。
- 避免一次性渲染大量节点。