复习手册

56 阅读52分钟

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]属性值包含 vala[href*="example"]{}
E~FE 之后同层 F 元素h1~p{}
E+FE 后紧邻的 F 元素h1+p{}

2. 盒模型与布局

特性含义示例
box-sizing: border-box宽高含 padding 和 borderdiv{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-celldisplay: 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 步流 回答,逻辑清晰且专业:

  1. 第一步:存数据 (Start)
  • dragstart 事件中,把当前拖动元素的索引/ID 存入 dataTransfer 对象。
  • 话术:“首先,在开始拖拽时,我要告诉浏览器我拖的是谁,把它的 ID 存起来。”
  1. 第二步:定位置 (Over) —— ⭐最关键
  • 在容器的 dragover 事件中,做两件事:
  1. 必须调用 e.preventDefault(),否则浏览器默认禁止放置(Drop 不会触发)。
  2. 根据鼠标 Y 轴坐标,计算应该插入到哪个元素之前/之后(视觉反馈)。
  • 话术:“其次,在拖拽经过时,我必须阻止默认行为才能允许释放,同时实时计算鼠标位置来决定插入点。”
  1. 第三步:更数据 (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 在函数循环里能直接退出整个函数。但注意在 forEachreturn 只结束当前回调,不能中断循环——这时候我会改用 for...of。”


Q7: 请解释 ES6 中的 Promise、async/await 以及 Generator,并说明它们在异步编程中的应用。

解析与回答:

13.Promise

  1. Promise 是什么:处理异步操作的容器。
  2. 三种状态:Pending(进行中)、Resolved(成功)、Rejected(失败)。状态一旦改变不可逆转。
const promise = new Promise((resolve, reject) => {
 setTimeout(() => {
 const success = true // 假设制作成功了
if (success) {
 resolve('🥤 您的珍珠奶茶好了!')
 } else {
 reject('❌ 抱歉,珍珠卖光了!')
 }
 }, 2000) // 模拟耗时 2 秒
})
  1. 核心方法
  • .then() 处理成功。
  • .catch() 处理失败。
  • .finally() 无论成败都执行。
promise
 .then(res => {
 console.log(res)
 return '喝完啦'
 })
 .then(res => {
 console.log(res)
 })
 .catch(err => {
 console.log(err)
 })
 .finally(() => {
 console.log('finally')
 })
  1. 最佳实践:使用 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();
  1. 作用:解决“回调地狱”,让异步代码逻辑清晰、易于维护。

讲讲 ES6 Generator?项目里用过吗?

“Generator 是我理解异步编程底层的钥匙,虽然日常业务多用 async/await,但在特定场景它不可替代:

  1. 核心机制:它能通过 yield 暂停函数执行,保留上下文,再通过 next() 恢复。这是实现‘分步执行’的基础。
  2. 实战场景 - 大列表分片渲染
  • 背景:之前有个后台管理系统需展示 2 万条日志,直接渲染导致主线程阻塞,页面卡顿 2 秒。
  • 方案:我编写了一个 Generator 函数,每次 yield 返回 500 条数据,配合 requestAnimationFrame 分批插入 DOM。
  • 结果:首屏渲染时间从 2s 降至 200ms,滚动流畅无掉帧。
  1. 原理认知:我也清楚 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 链式调用、状态同步
执行流程图解 (口述逻辑)
  1. 执行同步代码 (作为一个宏任务)。
  2. 同步代码中遇到的微任务放入微任务队列,宏任务放入宏任务队列
  3. 同步代码结束 → 立即执行所有微任务 (直到队列为空)。
  4. 尝试 UI 渲染 (浏览器有机会重绘)。
  5. 从宏任务队列取一个宏任务执行。
  6. 回到步骤 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)
遍历顺序按插入顺序遍历按索引顺序遍历
操作 APIadd, delete, has (语义清晰)push, splice, includes (功能多但杂)
索引访问不支持 (不能用 set[0],需转数组)支持 (随机访问效率高)

为什么要引入 Set?(解决三大痛点)

  1. 极简去重逻辑
  • 数组痛点:去重需写 [..., new Set(arr)] 或复杂的 filter + indexOf 循环。
  • Set 优势:构造函数天然去重。new Set([1, 2, 2, 3]) 直接得到 {1, 2, 3},代码行数减少 50%。
  1. 高性能“存在性”判断
  • 数组痛点:判断元素是否存在用 arr.includes(val),数据量 1 万时需遍历 1 万次 (O(n))。
  • Set 优势set.has(val) 基于哈希表,无论数据量多大,耗时几乎不变 (O(1))。适合权限列表、黑名单校验等高频查询场景
  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 拦截器里保持默认行为,也让 deletein 这类操作变成函数式调用,代码更清晰、可测、可组合。”


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。”
赋值兼容性✅ 可赋值给任何类型只能赋值给anyunknown(需收窄后才能赋给具体类型)“接收第三方库回调或JSON.parse结果时,我定义为unknown,强迫自己写if (typeof ...)判断,避免运行时崩溃。”
操作权限✅ 可直接 data.iddata()禁止直接操作编译报错:“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: anyunknown 有什么区别?为什么不用 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 来保证代码健壮性,特别是类型复用接口收敛

  1. 常用工具:最常用的是 Partial(做表单更新)、Omit(过滤敏感字段)和 Record(定义字典)。
  2. 字段提取方案
  • 首选 Pick:比如后端返回完整的 User 对象,但表格只需要 idname。我会写 type UserTable = Pick<User, 'id' | 'name'>。这样既复用了主类型,又明确了视图层的数据契约。
  • 组合拳:如果需要提取并修改某字段类型,我会用 Omit 排除旧字段,再用 & 交叉类型补充新定义,如 Omit<User, 'id'> & { id: string }
  1. 原理与价值
  • 我也了解 Pick 的底层是映射类型 [P in K]: T[P]
  • 实际收益:在之前的项目中,通过这种‘主类型 + 衍生类型’的模式,当后端接口变更时,TS 编译报错能帮我们在提交前发现 90% 的类型不匹配问题,极大减少了线上 Cannot read property of undefined 的 Bug。 TS 不仅是加类型注解,更是通过类型推导来规范数据结构设计。”

Q16: 什么是 TypeScript 中的函数重载?它在什么场景下使用?

解析与回答:

TS 函数重载是什么?什么时候使用?

  • 函数重载:为同一个函数提供多个类型签名,根据传参不同返回不同类型,但共用一个实现
  • 使用场景
  • 参数类型/数量不同,行为一致但返回类型不同(如 createElement(tag: 'div'): HTMLDivElement vs createElement(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 (早期), JSPVue.js, React (广义), Angular (2+)“深圳目前主流是 Vue3/React,本质都是 MVVM 思想。我擅长利用其响应式特性处理复杂状态。”
适用场景简单交互、SEO 要求极高且需服务端渲染的传统页单页应用 (SPA)、复杂交互、数据密集型后台“对于咱们公司的 SaaS 后台,数据流转复杂,MVVM 的状态驱动模式比 MVC 更适合快速迭代。”

💡 实战策略

  1. 简述 MVC 和 MVVM 区别?:“最大的区别在于数据到视图的同步方式。MVC 像‘推拉模式’,需要 Controller 手动更新 View;而 MVVM(如 Vue3)是‘订阅发布模式’,数据一变,视图自动更新。我在上一个项目中,利用这一特性将表单提交逻辑从 50 行 DOM 操作缩减为 5 行数据赋值。”
  2. 强调“解耦”带来的价值:“MVVM 让 View 层变成了‘哑终端’,只负责展示。这使得我们在做多端适配(如同时开发 H5 和小程序)时,可以复用同一套 ViewModel 逻辑,开发效率提升明显。”

Q18: Vue 2 和 Vue 3 的响应式原理有什么区别?Vue 2 是如何解决数组和对象动态增删问题的?

解析与回答:

Vue2 用 Object.defineProperty,那它怎么解决数组动态变化和对象属性增删的问题?

参考回答:

“Vue2 的响应式基于 Object.defineProperty,它有两个天然缺陷:无法监听对象属性的动态增删,以及无法监听数组索引和长度的变化。Vue2 通过两套‘补丁’方案解决:

  1. 数组:重写 7 个变异方法
  • Vue2 拦截了 push, pop, splice 等 7 个会改变原数组的方法。
  • 实现:在这些方法内部,先调用原生方法修改数据,然后手动触发依赖更新,并对新增的元素递归做响应式处理。
  • 局限:直接通过索引赋值(arr[0]=1)或修改长度仍无效,必须用 splice 替代。
  1. 对象:提供 $set$delete API
  • 因为无法自动劫持新增属性,Vue 提供了 Vue.set(target, key, val)Vue.delete(target, key)
  • 原理:这两个 API 内部会手动调用 defineReactive 为新属性添加 getter/setter,并强制触发视图更新。 总结:Vue2 的方案是‘能劫持的自动劫持,不能劫持的提供 API 手动触发’。Vue3 要全面转向 Proxy,因为 Proxy 能原生解决这些问题。”

能具体讲讲 Vue3 依赖收集的底层流程吗?

“Vue3 的依赖收集核心是靠 track 函数全局 activeEffect 和一个 三层嵌套的数据结构 targetMap 完成的。流程分四步:

  1. 标记当前执行者
  • effect(如组件渲染函数)执行时,Vue 会把它赋值给全局变量 activeEffect,表示‘现在是谁在读数据’。
  1. 拦截读取操作
  • 当代码访问响应式对象的属性(如 obj.name)时,触发 Proxy 的 get 拦截器,内部调用 track(obj, 'name')
  1. 建立映射关系(核心)
  • track 函数会在 targetMap(一个 WeakMap)中查找:先找对象 obj,再找属性 'name'
  • 如果找不到对应的依赖集合(Set),就新建一个。
  • 最后,把当前的 activeEffect 添加到这个 Set 中。
  • (可选加分项) 同时,为了后续清理,也会把这个 Set 记录到 activeEffect 自己的 deps 数组里,形成双向引用。
  1. 完成收集
  • 此后,只要 obj.name 变化(触发 trigger),就能从这个 Set 里找到所有依赖它的 effect 并执行更新。 关键点:这种机制是动态的。每次 effect 重新运行前,会先清理旧的依赖关系,重新执行一遍以收集新的依赖(比如处理 if/else 分支变化),保证了依赖的精准性。”

除了响应式,Vue2 升 Vue3 还有哪些主要变动?

“除了响应式底层从 defineProperty 换成 Proxy,Vue3 在开发模式、架构设计和生态上还有 5 个关键变动:

  1. 编程模型革新 (Composition API)
  • 引入 <script setup> 语法糖,解决了 Options API 逻辑分散的问题。
  • 通过 Composables (useXxx) 替代 Mixins,实现了更清晰的逻辑复用和 TypeScript 支持,彻底消除了 this 指向烦恼。
  1. 模板能力增强 (Fragments & Teleport)
  • 支持 多根节点,不再需要无意义的 <div> 包裹,优化了 DOM 结构和 CSS 布局。
  • 新增 <Teleport> 组件,轻松解决 Modal/Toast 等需要渲染到 body 的场景,无需手动挂载 DOM。
  1. 全局 API 实例化 (Tree-shaking)
  • 废弃全局 Vue 对象,改为 createApp() 实例化。
  • 全局注册(组件/指令)绑定到 App 实例,未使用的 API 可被打包工具剔除,包体积更小
  1. 状态管理与路由升级
  • 官方推荐 Pinia 替代 Vuex,去除了 Mutation,API 更简洁,TS 支持更好。
  • Vue Router 4 改用 createRouter 工厂函数,并移除了 mode 配置项,改用 createWebHistory 等明确模式。
  1. 生命周期与细节调整
  • 生命周期钩子更名(如 beforeDestroyonBeforeUnmount),且需显式导入。
  • 移除了 $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 对象)

配置项类型默认值说明
immediatebooleanfalse是否在侦听器创建后立即执行一次回调。常用于初始化时触发逻辑(如首次加载数据)。
deepbooleanfalse是否深度监听对象或数组内部的变化。仅当监听的是引用类型(如对象、数组)时需要。
flushpre / 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 2Vue 3
核心本质语法糖::value + @input语法糖::modelValue + @update:modelValue
默认 PropvaluemodelValue
默认 Eventinputupdate: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)

  1. Q: Vue3 的 refreactive 选哪个?
  • 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-elsev-if 搭配使用v-if="ok"``v-else必须紧跟在 v-ifv-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 配置的思路

  1. 定义 Props: 组件通过 props: basicProps (L324) 声明了它能接受的所有配置项。
  2. 合并配置源: 使用一个计算属性 getProps 将来自父组件模板的静态 props 和通过 setTableProps 方法传入的动态 props (存储在 propsRef 中) 合并成一个最终的配置对象。
  3. 暴露接口: 通过 register 事件暴露 setTableProps 方法,允许父组件在任何时候传递一个新的 JSON 对象来动态修改表格的行为和外观。
  4. 统一消费: 组件内部的所有逻辑都从 getProps 这个统一的配置源读取信息,确保了行为的一致性。 将组件的配置与具体实现解耦,使得 BasicTable 成为一个高度可复用和可配置的组件。通过一个 JSON 对象来描述整个表格的外观和行为,而无需关心其内部复杂的实现逻辑。

5. 工程化与构建工具

Q26: Vite 和 Webpack 的核心区别是什么?从 Webpack 迁移到 Vite 有哪些具体的提升?

解析与回答:

从 Webpack 转到 Vite,构建上有哪些具体的提升?

“从 Webpack 迁移到 Vite,本质是从 **‘打包优先’**转向 ‘按需编译’,带来了三个维度的显著提升:

  1. 启动速度质的飞跃
  • Webpack 启动需全量打包,千级模块项目常需 30 秒以上。
  • Vite 利用浏览器原生 ESM + esbuild 预构建,启动不打包,同规模项目启动压缩至 1 秒内,几乎即开即用。
  1. HMR(热更新)性能恒定
  • Webpack 随项目运行时间变长,HMR 延迟会增加到数秒。
  • Vite 基于 ESM 边界,修改文件只刷新当前模块,无论项目多大,HMR 始终保持在 50ms 以内,且组件状态不丢失,开发体验极其流畅。
  1. 生产构建更高效
  • 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.jsdayjs 或按需加载减少 200KB+ 体积“解决 Moment 体积过大痛点,提升解析性能。”
可视化分析使用 rollup-plugin-visualizerwebpack-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 ReviewCI 流水线 (Lint+Build) 通过才能合并。
  • 紧急修复:线上紧急 Bug 直接从 prodhotfix 分支,修复后快速走完 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 不仅仅是规范,更是前后端解耦的关键。我主要落实了三点:

  1. 严格遵循资源导向:URL 只用名词(如/orders),操作全靠 HTTP Method(GET/POST/PUT/DELETE),杜绝了/getOrder这种语义混淆的接口。
  2. 标准化状态码处理:我和后端约定,严禁‘全 200'模式。利用401自动登出、403权限拦截、400提示参数错误,这让前端的全局拦截器逻辑非常清晰,Bug 率降低了约 20%。
  3. 性能与版本意识:针对列表页,我推动后端支持?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/webexample.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 写入 HttpOnly Cookie(防 XSS 窃取),前端在 Axios 拦截器中自动读取该 Token 并放入请求头 X-CSRF-Token。后端校验 Header 与 Cookie 是否一致。

Q35: JWT 认证机制中,如何实现 Token 的无感刷新?

解析与回答:

JWT 无感刷新

面试高分话术 (直接背)

Q: 你们项目怎么做的登录认证?Token 过期怎么处理?

A: “我们采用 JWT 双 Token 机制

  1. 存储:短效 Access Token 存在内存中,长效 Refresh Token 存在HttpOnly Cookie 里,杜绝 XSS 窃取。
  2. 无感刷新:在 Axios 拦截器中监听401。一旦过期,暂停当前请求,自动用 Refresh Token 换取新 Access Token。成功后,重放刚才失败的请求队列。
  3. 体验:用户在操作过程中完全无感知,只有在 Refresh Token 也过期时才会跳转登录。
  4. 安全:因为是 HttpOnly Cookie,前端 JS 拿不到 Refresh Token,即使有 XSS 漏洞也无法维持长期会话。”

Q: JWT 注销登录怎么做?(无状态的痛点)

A: “JWT 本身难注销。我们的方案是:

  1. 前端:清除内存 Token 和 Cookie,强制跳转登录。
  2. 后端 (可选):对于高安场景,我们将注销的 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,我主要分两种场景:

  1. 同根域名:直接让后端设 Domain=.parent.com,前端配 withCredentials: true,浏览器自动共享,最简单。
  2. 完全跨域:采用**‘重定向换票’**模式。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 对比表

特性维度CookieLocalStorageSessionStorage
是否自动携带 (每次 HTTP 请求自动带) (需手动 JS 读取发送) (需手动 JS 读取发送)
存储容量4KB (极小,影响带宽)5MB+ (较大)5MB+ (较大)
生命周期可设过期时间,否则关闭浏览器失效永久 (除非手动清除)当前标签页关闭即失效
作用域同源所有窗口/标签页共享同源所有窗口/标签页共享仅限当前标签页 (新开不共享)
服务端交互后端可直接读写 (Set-Cookie)仅前端 JS 读写仅前端 JS 读写
主要安全风险CSRF (需配 SameSite)XSS (需配 HttpOnly)XSS (脚本可直接读取)XSS (脚本可直接读取)
典型应用场景登录 Token (HttpOnly) 用户追踪 (Track ID)长期配置 (主题/语言) 离线数据缓存表单分步暂存临时过滤条件

7. 性能优化

Q38: 前端有哪些常见的导致内存泄漏和性能崩溃的原因?如何排查?

解析与回答:

前端中导致内存失控、性能恶化乃至浏览器崩溃的常见前端编码与资源管理问题

回答模板

“前端内存泄漏和崩溃,核心通常是 ‘对象不再需要却被引用’。我遇到过最典型的 3 个场景:

  1. 事件监听未清理:比如组件里给 window 绑了 resize 或 ECharts 实例,销毁时没 removeEventListenerdispose,导致组件整棵树无法回收。
  • 对策:在 beforeUnmount 严格清理所有监听和定时器。
  1. 大 DOM 与资源:直接渲染万级列表或未压缩的大图,导致 DOM 节点爆炸或显存溢出,直接触发浏览器 ‘Aw, Snap!’ 崩溃。
  • 对策:列表必用虚拟滚动,图片必做压缩/懒加载
  1. 异步/定时器泄漏:组件销毁了,但 setInterval 还在跑,或 Axios 请求回来更新状态,持有旧组件引用。
  • 对策:用 AbortController 取消请求,销毁时清除 Timer。 排查手段:我用 Chrome DevTools 的 Heap Snapshot,对比操作前后的快照,重点看 Detached DOM tree 和引用链(Retainers),快速定位是谁持有了不该持有的对象。”

Q39: 如何实现大规模列表的虚拟滚动?如何解决虚拟滚动中的跨页多选状态丢失问题?

解析与回答:

大规模列表的虚拟滚动中,跨页多选与统一保存的实现问题

实际问题

虚拟滚动仅渲染可视区域 DOM,导致非可视区域的勾选状态因节点销毁而丢失,无法在后续统一提交时正确收集所有已勾选的数据。

回答模板

“针对虚拟列表滚动后状态丢失的问题,核心方案是 ‘状态与 DOM 分离,用 Set 存 ID’

  1. 独立状态池:我在 Pinia 中维护一个 Set 集合(如 selectedIdSet),专门存储所有被勾选行的 唯一 ID。这个集合常驻内存,不随 DOM 销毁而消失。
  2. 动态渲染:虚拟列表只渲染可视区 DOM。渲染时,通过 selectedIdSet.has(item.id) 动态计算 Checkbox 的选中状态。
  • 滚走了:DOM 销毁,但 ID 还在 Set 里。
  • 滚回来:重新渲染 DOM,读取 Set 发现 ID 存在,自动恢复勾选态
  1. 统一提交:用户操作时只更新 Set,不调接口。点击‘保存’时,直接将 Set 转为数组一次性发给后端。 优势:既解决了万级数据 DOM 复用导致的状态丢失,又保证了 O(1) 的操作性能和最终的一致性提交。”

虚拟滚动万级列表下,跨区域(如顶部和底部)部分勾选时,高效获取全部已勾选数据的问题。

实际问题

由于 DOM 节点随滚动不断创建和销毁,无法直接通过遍历 DOM 获取所有勾选状态;需要一个与渲染解耦的高效数据管理方案来跟踪跨区域的勾选项。

回答模板

“针对万级数据的全选和获取,我采用 ‘Set 集合存储 + 一次性遍历’ 策略,避免过度优化:

  1. 数据结构:维护一个全局 Set 存储所有选中项的 ID
  2. 全选实现
  • 点击‘全选’时,我直接遍历一次源数据(1 万条),将所有 ID addSet
  • 性能:现代浏览器处理 1 万次 Set.add 仅需 10~20 毫秒,用户完全无感知,无需搞复杂的‘排除法’逻辑。
  • 点击‘取消全选’:直接 Set.clear(),耗时 O(1)。
  1. 单个交互:勾选/取消只是 Set.add(id)Set.delete(id),都是 O(1) 操作,不影响滚动性能。
  2. 最终获取
  • 点击保存时,直接 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 | • 浅层响应:大对象/数组用 shallowRefmarkRaw,避免深层递归代理带来的初始化耗时。 • 计算缓存:复杂逻辑用 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’

  1. 认知纠正:5MB 只是 localStorage 的硬限制,浏览器的 IndexedDB 容量上限通常是硬盘的 50% ,存 6MB 甚至几百 MB 都毫无压力。
  2. 技术选型
  • 直接使用 IndexedDB,它是浏览器内置的异步 NoSQL 数据库。
  • 为了开发效率,我会封装 localforageidb 库。它们提供和 localStorage 一样的 setItem/getItem API,但底层自动走 IndexedDB,代码改动极小
  1. 优势匹配
  • 大容量:完美解决 6MB+ 配置文件存储。
  • 非阻塞:异步读写,不会像 localStorage 那样阻塞主线程导致页面卡顿。
  • 持久性:数据永久保存,除非用户主动清除,完全满足‘半年不改’的需求。 总结:对于大文件或大量配置,前端标准答案就是 IndexedDB,绝不强行切割 localStorage。”

8. 场景题与架构设计

Q43: 如何实现基于用户角色的动态路由加载和按钮级权限控制?

解析与回答:

在用户登录后,根据其身份(如角色、权限点列表)动态添加其有权访问的路由,并注入到路由器中。

回答模板

用户登录成功后,后端会返回当前用户的权限码列表和可访问路由标识,我存在 Pinia 里并做持久化。路由方面使用 动态路由,根据权限过滤后端返回的路由表,再通过 router.addRoute 动态挂载。页面按钮我封装了一个全局自定义指令 v-permission,传入权限码就能自动控制显隐。同时在路由全局守卫里做权限校验,没有权限直接跳 403 或首页。


Q44: 如何实现跨页面、跨 Tab 的实时状态同步?

解析与回答:

跨页面、跨 Tab 实时同步状态变更

实际问题

两个独立的浏览器标签页(A 用户页,B 管理页)之间数据状态不同步。在 B 页操作后,A 页无法感知权限已被取消,其界面(编辑按钮)仍处于可操作状态。

回答模板

“跨设备场景下,本地状态无法共享,必须上实时推送

  1. 我会建立 WebSocket 长连接,让用户页订阅权限变更频道。
  2. 管理员操作后,后端主动推送禁用指令到该频道。
  3. 前端收到消息,直接更新本地 Store,利用 Vue 响应式瞬间禁用按钮
  4. 若连接异常,我会有轮询兜底策略,确保数据最终一致,实现无感知的实时同步。”

Q45: 同一个列表组件在多个浏览器标签页打开时,如何隔离各自的状态?

解析与回答:

同一个列表组件实例,在多个浏览器标签页中同时打开,需要隔离各自的状态。

场景总结

在同个组件(如订单列表)被多个页签/标签同时打开,且各自展示不同数据状态(如“已完成”、“已取消”)时,如何让它们彼此独立,数据、筛选、分页等状态互不干扰。

回答模板

“这个场景核心是利用 keep-alive 的动态 key 实现多实例隔离

  1. 我给 <router-view> 绑定 :key="$route.fullPath" (或特定的 query 参数)。
  2. 这样 Vue 会把‘完成’和‘取消’视为两个独立的组件实例分别缓存。
  3. 切换时,只是暂停旧实例、激活新实例,互不覆盖。
  4. 我在 onActivated 钩子里做数据 freshness 检查,确保用户切回来时看到最新数据,同时保留滚动位置和筛选状态。”

Q46: 前端密码哈希加盐处理时,盐(Salt)应该存储在哪里?

解析与回答:

在前端进行密码哈希加盐处理时,盐(Salt)的存储位置安全问题。

回答模板

“关于‘加盐’的存放位置,核心原则是:前端代码对客户端是不设防的,所以密钥/盐绝不能硬编码在前端。 具体分三种情况:

  1. 登录密码验证
  • 盐在后端。前端只负责通过 HTTPS 传输密码,绝不在前端做哈希加盐。因为前端代码可被逆向,盐一旦暴露,哈希就失效了。
  1. 本地数据防篡改(签名)
  • 签名由后端生成。后端用后端私钥算好签名发给前端,前端只存‘数据 + 签名’。验证时传回后端校验。前端无法伪造签名,因为密钥从未离开过后端。
  1. 本地数据加密(如离线隐私数据)
  • 盐存在本地(公开),密钥由用户密码派生
  • 利用 Web Crypto API,结合用户输入的‘主密码’和本地随机生成的‘盐’,动态计算出加密密钥。
  • 原理:盐可以公开,但没有用户的‘主密码’,黑客拿到盐也解不开数据。 总结:凡是涉及安全校验的‘秘密’,要么在后端,要么由用户记忆绝不要写死在前端代码或本地存储里。”

Q47: 你对 AI 开发、Skills 和 MCP 有了解吗?它们在前端领域如何落地?

解析与回答:

你对 AI 开发、Skills 和 MCP 有了解吗?

理解它们是现代 AI 应用的三大核心组件

  1. 概念理解
  • AI Agent 是‘大脑’,负责自主规划任务;
  • Skills 是‘专业技能包’,将领域知识和工具封装好,让通用模型变成专家(比如‘前端代码规范 Skill’);
  • MCP (Model Context Protocol) 是‘通用 USB 接口’,标准化了 AI 与外部数据/工具的连接,实现了‘即插即用’,解决了以往每个工具都要写胶水代码的痛点。
  1. 前端落地思路
  • 提效:我会为团队定制 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 的 ‘短期记忆’。 我的理解就三点:

  1. 它是有限的:太长会慢且贵,还会让 AI 变笨(注意力分散)。
  2. 贵在精准:我不会把整个项目塞给它,而是通过检索(RAG) ,只把当前相关的代码片段喂给它。
  3. 重在管理:多轮对话时,我会自动丢弃旧记录做摘要,确保它永远只关注‘当下最需要的信息’。 核心原则:用最少的 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 2Vue 3
Diff 算法全量递归对比(O(n³))静态提升 + Block Tree + PatchFlag(O(n))
静态节点处理每次重新创建 VNode编译时标记静态节点,永不 diff
响应式触发更新Object.defineProperty → 触发整个组件 re-renderProxy + 精准依赖追踪 → 只更新用到的动态节点
实际效果大列表/复杂模板更新慢同场景下 更新性能提升 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废弃)。
  • 解决
  • 依赖替换vuexpinia(去除了 mutation,TS 支持更好);vue-router@3@4(路由配置扁平化)。
  • 语法自动化修复:使用官方_codemod_脚本批量转换v-model和生命周期钩子(destroyedbeforeUnmount)。
  • 临时方案:对于无 Vue3 版本的老旧组件库,使用<div>包裹并手动挂载实例,或寻找替代库(如naive-uielement-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+)
  • 选型理由
  • 原生支持:内置fetch API 和Web Crypto,包体积减小约 15%。
  • 稳定性:LTS 版本经过生产验证,避开 20+ 版本的实验性特性风险,同时兼容主流 CI/CD 流水线。
2. 时间库选型:Day.js 1.11.x (按需加载插件)
  • 选型理由
  • 轻量级:核心库仅2KB(Moment.js 的 1/30),符合首屏性能优化指标。
  • 插件化:只引入业务需要的utctimezoneisSameOrBefore插件,避免全量引入。

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 封装成通用可复用组件

  1. 在 Vue 组件里用 ref 获取容器 DOM。
  2. 在 onMounted 生命周期里初始化图表。
  3. 通过 setOption 渲染配置和数据。
  4. 监听窗口变化(window.resize())做自适应(chart.resize())。
  5. 组件销毁时释放实例,防止内存泄漏。
  6. 支持响应式、loading、数据更新。

二、ECharts 怎么做自适应?

  1. 监听 window 的 resize 事件。
  2. 使用防抖(debounce)优化性能。
  3. 在回调里调用 chart.resize ()。
  4. 保证窗口变化时图表自动适配。

三、组件销毁时必须做什么?

必须销毁 ECharts 实例

  1. 调用 chart.dispose ()。
  2. 清空 resize 监听。
  3. 清空定时器。不销毁会造成内存泄漏,页面越来越卡。

四、ECharts 性能优化(高频)

  1. 复用实例,不要重复 init。
  2. 使用 setOption 增量更新,不重新创建。
  3. 大数据关闭动画:animation: false。
  4. 减少阴影、渐变、多余标签。
  5. 大量数据使用 dataZoom 或分段渲染。
  6. 避免频繁 setOption,做节流防抖。

五、图表不显示 / 白屏原因

  1. 容器 DOM 没有设置宽高
  2. 在 DOM 挂载前就初始化。
  3. 数据格式错误或为空。
  4. 多次初始化导致实例异常。
  5. 路由切换没销毁,实例污染。

六、两个图表如何联动?

  1. 给多个图表设置相同的 group。
  2. 使用 echarts.connect (group) 关联。
  3. 可实现 tooltip、brush、数据联动。

七、海量数据怎么渲染?

  1. 关闭动画。
  2. 数据采样、简化数据。
  3. 使用 dataZoom 只展示可视区域。
  4. 使用 appendData 追加数据。
  5. 避免一次性渲染大量节点。