1. HTML 与 CSS 基础
Q1: 请简述 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 有哪些常用的新特性?请列举并说明其用途。
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 中实现元素垂直居中有哪些方案?分别适用于什么场景?
场景:未知宽高、已知宽高、多元素。
| 方案 | 代码关键点 | 适用场景 | 优缺点 |
|---|---|---|---|
| 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; | 老旧项目兼容 | ✅ 兼容极好 ❌ 语义差,影响布局流 |
Q3.1
1. Flex 属性全景图
1. 容器属性(父元素控制整体流向)
| 属性 | 作用轴 | 核心取值 | 实战场景/备注 |
|---|---|---|---|
display | - | flex / inline-flex | 开启布局,inline-flex 用于行内块级容器。 |
flex-direction | 主轴 | row(默认) / column / row-reverse / column-reverse | 决定布局是横排还是竖排;移动端列表常用 column。 |
justify-content | 主轴 | flex-start / center / flex-end / space-between / space-around | space-between 最常用(两端对齐,如导航栏);center 用于水平居中。 |
align-items | 交叉轴 | stretch(默认) / center / flex-start / flex-end | center 是垂直居中的神器;stretch 会让子项填满容器高度。 |
flex-wrap | - | nowrap(默认) / wrap / wrap-reverse | 默认不换行(可能溢出);wrap 允许换行(如标签云、卡片流)。 |
align-content | 交叉轴 | stretch / center / space-between ... | 仅多行时生效(需配合 wrap);单行布局设此属性无效。 |
2. 项目属性(子元素控制自身伸缩)
| 属性 | 作用 | 核心取值/规则 | 实战场景/备注 |
|---|---|---|---|
order | 排序 | 整数(可为负),越小越前 | 不改HTML调整视觉顺序;移动端常用于把按钮移到底部。 |
flex-grow | 放大 | 数字比例(默认 0) | 分配剩余空间;flex-grow: 1 让搜索框填满剩余宽度。 |
flex-shrink | 缩小 | 数字比例(默认 1) | 空间不足时压缩;设为 0 可防止固定宽元素(如图标)被压扁。 |
flex-basis | 基准 | 长度值 (200px) / auto | 计算伸缩前的理想大小;优先于 width 属性。 |
flex | 简写 | [grow] [shrink] [basis] | 最高频使用:• flex: 1 (=1 1 0):均分剩余空间。• flex: auto (=1 1 auto):按内容比例。• flex: none (=0 0 auto):完全刚性,不伸缩。 |
align-self | 单独对齐 | auto(默认) / center / stretch ... | 覆盖父级的 align-items;让某个特殊元素单独垂直居中。 |
3. 避坑指南
- 主轴与交叉轴:方向由
flex-direction决定,变了之后justify和align的作用方向也会互换。 flex: 1的陷阱:它其实是1 1 0%,意味着即使内容很多,它也会尝试压缩内容来适应容器(如果shrink为 1)。若希望内容撑开,用flex: 1 0 auto。align-content无效:90% 的情况是因为忘了加flex-wrap: wrap,导致只有一行,该属性不生效。
场景:“Flex 有哪些属性?如何实现常见的两栏自适应或垂直居中?”
“Flex 属性分为容器和项目两类,我日常开发主要关注这几个核心点:
1. 容器端(定布局):
flex-direction:决定主轴方向,默认横向,移动端长列表常切为column。justify-content:主轴对齐,我最常用space-between做两端对齐的导航栏,或center做水平居中。align-items:交叉轴对齐,center是实现垂直居中的最优解,比绝对定位更稳健。flex-wrap:控制是否换行,做响应式卡片布局必开wrap。2. 项目端(控伸缩):
flex简写:这是最高频的。
flex: 1:让元素自动填满剩余空间(如左侧固定、右侧自适应布局)。flex: none:锁定元素尺寸,防止被压缩。order:在不改动 HTML 结构的前提下,通过 CSS 调整移动端元素的视觉排序。实战举例: 实现‘左侧固定 200px,右侧自适应’: 父容器
display: flex; 左侧flex: 0 0 200px(不生长不缩小,基准200); 右侧flex: 1(自动占据所有剩余空间)。”
Q3.8 flex: 1 的拆解与实战含义
1. 核心拆解
flex: 1 是 flex-grow, flex-shrink, flex-basis 的简写,具体对应:
flex-grow: 1:能放大。如果有剩余空间,它会参与分配(比例占1份)。flex-shrink: 1:能缩小。如果空间不足,它会被压缩(默认行为)。flex-basis: 0%:基准为0。关键点! 忽略元素内容的实际宽度,直接从0开始分配剩余空间。
2. 为什么是 0% 而不是 auto?(高频考点)
flex: 1(即1 1 0%):- 无论子元素内容多长(比如一段很长的文字),它们都会强制忽略内容宽度。
- 结果:所有设了
flex: 1的元素宽度完全相等,完美均分容器。
- 对比
flex: 1 1 auto:- 会先计算内容宽度,再分配剩余空间。
- 结果:内容多的元素会更宽,内容少的更窄,无法实现严格均分。
3. 常见简写对照表
| 简写 | 完整写法 (grow shrink basis) | 行为特征 | 典型场景 |
|---|---|---|---|
flex: 1 | 1 1 0% | 均分剩余空间(忽略内容) | 三栏等分布局、搜索框填满剩余位 |
flex: auto | 1 1 auto | 按内容比例分配(保留内容宽度) | 标签云、根据文字长短自适应 |
flex: none | 0 0 auto | 不生长、不缩小(刚性) | 固定宽度的图标、按钮 |
flex: 0 1 auto | 0 1 auto | 不生长,但空间不足时会缩小 | 防止溢出,但优先保持原宽 |
场景:面试官问“flex: 1 具体代表什么?它和 flex: auto 有什么区别?”
“
flex: 1其实是1 1 0%的简写,包含三层意思:
grow: 1:允许放大,去瓜分剩余空间。shrink: 1:允许缩小,空间不足时会被压缩。basis: 0%:这是核心。它强制忽略元素内容的实际宽度,把基准设为0。实战区别:
- 用
flex: 1:不管里面文字多长,所有子项都会严格等宽,适合做‘三栏均分’或‘左侧固定+右侧填满’。- 用
flex: auto(1 1 auto):会先保留内容宽度,再分剩余空间,导致内容多的项更宽,无法严格对齐。结论: 只要我需要**‘均分’或者‘填满剩余空间且无视内容长度’**,我首选
flex: 1。”
Q4: 如何使用原生 HTML5 Drag and Drop API 实现列表拖拽排序?需要注意哪些性能问题?
🧠 核心记忆模型: “搬箱子”三部曲
想象你在搬家(拖拽),只需要记住三个动作:
| 阶段 | 事件名 | 谁触发? | 你要做什么?(唯一核心动作) | 口诀 |
|---|---|---|---|---|
| 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触发太频繁,我通常会加一个节流函数,避免高频重排导致页面卡顿。”
Q4.0 REM vs VW 核心对比
1. 核心概念对比表
| 维度 | REM (Root EM) | VW/VH (Viewport Units) |
|---|---|---|
| 基准对象 | 根元素 (html) 的 font-size | 视口宽度/高度 (viewport) |
| 计算逻辑 | 1rem = html font-size (需JS或媒体查询动态设置) | 1vw = 视口宽度的 1% (纯CSS,自动响应) |
| 依赖条件 | 通常依赖 flexible.js 或构建插件 (postcss-pxtorem) | 零依赖,浏览器原生支持 |
| 适配精度 | 阶梯式适配 (取决于JS设置font-size的频率) | 像素级连续适配 (随屏幕宽度实时变化) |
| 主要痛点 | 需维护JS脚本;字体大小需特殊处理 (防缩放过度) | 部分旧安卓机渲染有1px误差;文本可能随屏过宽/窄 |
| 典型场景 | 传统移动端H5、需兼容极老设备的项目 | 现代H5、大屏可视化、响应式后台 |
2. 主流方案演进
- 混合最佳实践 ⭐推荐:
- 逻辑:布局用 VW (保证整体缩放),文字用 REM (可控性高,防止文字在超大屏失控)。
- 实现:
html { font-size: 1vw; }(或固定基准),布局写vw,字体写rem。 - 深圳现状:字节、腾讯新H5项目多采用此方案,兼顾灵活性与可读性。
3. 避坑指南
- 1px 边框问题:VW 计算可能出现小数像素(如
0.4px),导致边框模糊。- 解法:使用
transform: scaleY(0.5)或border-image,或用min(1px, 0.1vw)。
- 解法:使用
- 字体缩放失控:纯
vw设字体,在桌面端打开H5时字会巨大。- 解法:用
clamp(14px, 2vw, 18px)限制最小/最大值,或字体坚持用rem+ 媒体查询。
- 解法:用
- iOS 安全区:VW 计算包含安全区吗?
- 注意:
100vw通常不包含safe-area-inset,需手动padding: env(safe-area-inset-bottom)。
- 注意:
场景:面试官问“移动端适配你用 REM 还是 VW?为什么?”
“在深圳目前的开发环境中,我首选 ‘VW 布局 + REM 字体’的混合方案,或者在简单H5中直接用 纯 VW。
1. 为什么选 VW 做布局?
- 零依赖、高性能:
1vw = 1%视口宽,无需 JS 计算font-size,避免了首屏抖动和JS阻塞,适配更平滑连续。- 开发效率高:配合
postcss-px-to-vw插件,直接写设计稿像素,构建时自动转换,无需心算。2. 为什么字体保留 REM?
- 可控性:纯 VW 会导致文字在超大屏(如平板横屏)上过大,体验不佳。
- 策略:我用
html { font-size: 1vw }作为基准,字体用rem,并配合clamp()函数限制字号范围(如clamp(14px, 1rem, 18px)),确保极端场景下文字可读。3. 实战成果: 在上一个C端活动中,我采用 VW 布局 + Clamp 字体 方案,彻底移除了
flexible.js,使首屏渲染时间减少了 15%,且在不同尺寸折叠屏手机上布局零错位。”
Q5: 什么是 BFC?怎么触发?有什么实际用途?
场景:面试官问“什么是 BFC?怎么触发?有什么实际用途?”
“BFC 是 块级格式化上下文 ,可以理解为一个 独立的渲染容器 ,内部布局不受外部干扰。
触发条件 我常遇到这几种:
- 设置
overflow: hidden/auto(最常用,但要注意裁剪风险);- 使用
display: flow-root(CSS3 推荐,专门解决清除浮动且无副作用);- 元素浮动或绝对定位;
- 使用 Flex/Grid 布局(天然具备 BFC 特性)。
实际应用场景 主要有三个:
- 解决高度塌陷 :父元素包含浮动子元素时,通过触发 BFC 让父元素自动撑开高度,替代了以前加空标签的做法。
- 自适应两栏布局 :左侧浮动,右侧触发 BFC,利用‘BFC 不与浮动重叠’的特性实现右侧自适应填满剩余空间。
- 防止 Margin 重叠 :相邻兄弟元素垂直方向 margin 会合并,通过在中间加一个包裹层触发 BFC 即可隔离。”
2. JavaScript 与 ES6+
Q6: 请详细介绍 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() | 判断是否为数组 | 布尔值 | 工具函数入参校验 |
Q7: 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。”
Q8: 请解释 ES6 中的 Promise、async/await 以及 Generator,并说明它们在异步编程中的应用。
1. 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();
- 作用:解决“回调地狱”,让异步代码逻辑清晰、易于维护。
2. Generator
“Generator 是我理解异步编程底层的钥匙,虽然日常业务多用
async/await,但在特定场景它不可替代:
- 核心机制:它能通过
yield暂停函数执行,保留上下文,再通过next()恢复。这是实现‘分步执行’的基础。- 实战场景 - 大列表分片渲染:
- 背景:之前有个后台管理系统需展示 2 万条日志,直接渲染导致主线程阻塞,页面卡顿 2 秒。
- 方案:我编写了一个 Generator 函数,每次
yield返回 500 条数据,配合requestAnimationFrame分批插入 DOM。- 结果:首屏渲染时间从 2s 降至 200ms,滚动流畅无掉帧。
- 原理认知:我也清楚
async/await本质就是Generator + Promise的自动执行器。理解 Generator 让我能更好地处理复杂的任务队列调度,甚至在某些无 Promise 环境下模拟异步流。 总结来说,它是解决长任务切片和复杂流程控制的利器。”
Q9: 请简述 JavaScript 的事件循环(Event Loop)机制,宏任务与微任务的区别是什么?
1. 事件循环 (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 链式调用、状态同步 |
2. 执行流程图解 (口述逻辑)
- 执行同步代码 (作为一个宏任务)。
- 同步代码中遇到的微任务放入微任务队列,宏任务放入宏任务队列。
- 同步代码结束 → 立即执行所有微任务 (直到队列为空)。
- 尝试 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构造函数内的代码是同步执行的。- 微任务队列是一次性清空,不是执行一个就切回宏任务。
Q10: JavaScript 中的垃圾回收机制(GC)是如何工作的?
垃圾回收机制(GC)
只要没有任何变量、属性、作用域等引用它,它就会被自动回收。
Q11: 请解释 Map 和 Set 数据结构,它们与 Object 和 Array 相比有什么优势?
1. Map
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__这类原型污染的安全风险。”
2. 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 逻辑更简洁、不易出错。”
Q12: 请解释 Reflect 对象的作用及其在 Proxy 中的应用。
Reflect
“
Reflect提供了一套标准化的对象操作 API,比如用Reflect.get/set替代直接访问属性。它主要用在Proxy拦截器里保持默认行为,也让delete、in这类操作变成函数式调用,代码更清晰、可测、可组合。”
3. TypeScript
Q13: 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'。”
Q14: 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配合类型守卫函数,确保代码健壮性。”
Q15: TypeScript 中的泛型(Generics)是什么?如何在组件 Props 中应用泛型?
1. 泛型的概念与约束
- 泛型:让组件/函数在定义时不指定具体类型,而是在使用时传入,保持类型灵活且安全。
- 常用场景:
- 封装通用工具(如
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]
2. 泛型在组件 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%。”
Q16: TypeScript 中 keyof 的作用是什么?如何提取一个类型中的部分字段?
1. keyof 的作用
“keyof 能提取对象类型的键名,比如我写一个通用表单校验工具时,用 keyof FormValues 限制传入的字段名,确保类型安全,避免拼错字段导致运行时错误。”
2. 提取类型与常用工具
常用内置工具类型(必会基础):
Required<T>:将泛型T中的所有可选属性(?)强制变为必填属性。Partial<T>:把所有属性变可选(用于表单更新,只传修改项)。Pick<T, K>:提取指定字段(核心考点)。Omit<T, K>:排除指定字段(如列表展示时去掉password)。Record<K, T>:定义键值对映射(如字典数据{ [key: string]: string })。
参考回答:
“在日常开发中,我重度依赖 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 不仅是加类型注解,更是通过类型推导来规范数据结构设计。”
Q17: 什么是 TypeScript 中的函数重载?它在什么场景下使用?
TS 函数重载
- 概念:为同一个函数提供多个类型签名,根据传参不同返回不同类型,但共用一个实现。
- 使用场景:
- 参数类型/数量不同,行为一致但返回类型不同(如
createElement(tag: 'div'): HTMLDivElementvscreateElement(tag: string): HTMLElement)。 - 兼容多种调用方式(如配置项可传对象或字符串)。
- 实现:TS 中需先写多个声明签名,再写一个兼容所有情况的实现签名。
4. Vue 核心与原理
Q18: 请简述 MVVM 模式与 MVC 模式的区别,以及 Vue 是如何体现 MVVM 思想的?
核心对比表
| 维度 | 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 逻辑,开发效率提升明显。”
Q19: Vue 2 和 Vue 3 的响应式原理有什么区别?Vue 2 是如何解决数组和对象动态增删问题的?
1. Vue2 的局限与解决
参考回答:
“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能原生解决这些问题。”
2. 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分支变化),保证了依赖的精准性。”
3. 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 层级冲突问题。”
Q20: Computed 的缓存原理是什么?它和 Watch 有什么区别?
“Computed 的核心是 ‘基于脏检查的懒计算’,具体分三步:
- 依赖收集:它内部维护了一个
effect,首次读取.value时执行 getter 并收集依赖,同时把结果缓存起来。- 脏标记(Dirty Flag) :当依赖变化时,它不会立即重算,而是把内部的
_dirty标志位设为true,标记缓存失效。- 按需更新:只有下次再次读取
.value时,检测到_dirty为真,才会重新执行 getter 并更新缓存;否则直接返回旧值。对比 Watch:
Computed 侧重 ‘产出值’,有缓存且懒执行,适合处理复杂的数据转换(如过滤列表);
Watch 侧重 ‘执行动作’,默认无缓存且立即触发,适合处理异步请求或 DOM 操作。
在项目中,列表筛选逻辑全用 Computed,既避免了重复计算,又保证了数据流的单向清晰。”
Q21: 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 | — | 调试用:当响应式属性被修改时触发 |
Q22: Vue 2 和 Vue 3 中的 v-model 有什么本质区别?如何实现多个 v-model?
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>
Q23: Vue 3 中的插槽(Slot)有哪几种?它们的区别和使用场景是什么?
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. 核心区别一张表
| 特性 | 默认插槽 | 具名插槽 | 作用域插槽 |
|---|---|---|---|
| 数据流向 | 父 → 子 (内容) | 父 → 子 (内容) | 子 → 父 (数据) + 父 → 子 (模板) |
| 使用场景 | 单区域内容分发 | 多区域布局分发 | 动态渲染列表/表格列 |
| 语法特征 | 直接写内容 | #名称 | #默认="{ 参数 }" |
| 面试高频点 | 基础组件封装 | 页面布局组件 | 中后台表格/表单自定义 |
Q24: Vue 3 中 ref 和 reactive 有什么区别?在实际开发中推荐优先使用哪个?
面试高频追问预演
Q: Vue3 的 ref 和 reactive 选哪个?
- A: 推荐优先用
ref。 ref通用性强,基本类型/对象都能用,解构不会丢失响应性 (toRefs)。reactive对基本类型无效,且解构会丢失响应性,替换整个对象会切断响应式链接。
Q25: 请列举 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]" | 依赖不变时不重新渲染,性能优化利器。 |
Q26: 请简述 Virtual DOM 的原理,以及 Vue 2 和 Vue 3 在 Virtual DOM 上的核心区别。
1. Virtual DOM 原理
Virtual DOM 是用 JS 对象模拟真实 DOM 结构,在数据变化时先在内存中 diff,再最小化更新真实 DOM,减少直接操作 DOM 的性能损耗。
2. 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 在编译时优化的威力。”
5. 工程化与构建工具
Q27: Vite 和 Webpack 的核心区别是什么?从 Webpack 迁移到 Vite 有哪些具体的提升?
1. 构建理念的区别
“Webpack 是‘打包中心主义’。启动时必须递归分析所有依赖,打包成 Bundle 给浏览器。项目越大,冷启动和 HMR 越慢。 Vite 是‘服务中心主义’。利用浏览器原生 ESM,启动时不打包,按需编译。
- 源码:即时转译。
- 依赖:用 esbuild 进行预打包(Pre-bundling),将 CommonJS 转 ESM 并合并请求,结果缓存到
node_modules/.vite。这使得 Vite 启动通常是毫秒级,且 HMR 速度与项目大小无关。”
2. 迁移带来的提升
“从 Webpack 迁移到 Vite,本质是从 **‘打包优先’**转向 ‘按需编译’,带来了三个维度的显著提升:
- 启动速度质的飞跃:
- Webpack 启动需全量打包,千级模块项目常需 30 秒以上。
- Vite 利用浏览器原生 ESM +
esbuild预构建,启动不打包,同规模项目启动压缩至 1 秒内,几乎即开即用。
- HMR(热更新)性能恒定:
- Webpack 随项目运行时间变长,HMR 延迟会增加到数秒。
- Vite 基于 ESM 边界,修改文件只刷新当前模块,无论项目多大,HMR 始终保持在 50ms 以内,且组件状态不丢失,开发体验极其流畅。
- 生产构建更高效:
- Vite 生产环境采用
Rollup进行打包,Tree-shaking 比 Webpack 更彻底,通常能减少 10%-15% 的包体积,且构建速度快 30% 以上。”
3. 白屏优化方案与插件实战
“白屏优化方案:
- 开发环境:靠 Vite 的依赖预编译和按需加载,解决大量 node_modules 导致的解析慢和网络请求多问题。
- 生产环境:
- 路由懒加载:拆分代码块,首屏只加载核心 JS。
- 按需引入:配合
unplugin自动移除 UI 库未用代码。- 压缩与 CDN:开启 Brotli 压缩,将大依赖(Vue/Element)托管到 CDN。
插件实战:
- 我在项目中写过 Vite 插件 处理 Markdown 文档:利用
transform钩子拦截.md文件,调用markdown-it转为 HTML 字符串直接导出。实现了‘导入即渲染’。- 也配置过 SVG Sprite Loader,自动将 SVG 合成雪碧图生成组件,解决了图标请求过多的问题。”
Q28: 请简述 Vite 和 Webpack 的核心配置项及其优化作用。
1. 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% 和输出体积。” |
2. 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) | “减少闭包开销,提升运行时执行效率,减小包体积。” |
3. 通用优化策略 (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 | 精准定位大包来源 | “基于数据驱动优化,而非盲目猜测。” |
Q29: 请介绍 package.json 的核心配置清单。
1. 核心身份(必填/发布用)
name/version:项目唯一标识,遵循语义化版本(如1.0.0)。private:设为true防止业务项目被误发布到 npm。type:"module"(启用 ESM) 或"commonjs"(默认),新项目多用"module"+ TS。
2. 脚本命令(日常最高频)
scripts:定义快捷指令。- 例:
"dev": "vite","build": "vue-tsc && vite build","lint": "eslint . --ext .vue,.js,.ts"。 - 面试点:提到曾用
scripts串联husky做提交前自动检查。
- 例:
3. 依赖管理(区分生产/开发)
dependencies:生产依赖(如vue,axios),打包会包含。devDependencies:开发依赖(如vite,typescript,eslint),打包不包含。peerDependencies:对等依赖(写组件库必考),声明宿主环境版本(如vue: ^3.3.0)。
4. 入口与构建(库开发/高级配置)
main:CommonJS 入口(如dist/index.cjs.js)。module:ESM 入口(如dist/index.es.js),Webpack/Vite 优先读取此项实现 Tree-shaking。types:TS 类型定义入口(如dist/index.d.ts)。files:发布时仅包含的文件列表(配合.npmignore减小包体积)。
5. 环境与约束
engines:限制运行环境(如"node": ">=18.0.0"),避免低版本报错。browserslist:指定目标浏览器,决定 Babel/PostCSS 的转译程度(如"> 1%, last 2 versions")。
Q30: 请介绍 Node.js 在前端工程化中的核心工具链及应用场景。
核心工具链全景图(按场景分类)
| 场景 | 核心工具 (主流) | 实战价值 |
|---|---|---|
| 包管理 | 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 次。” |
Q31: 如何配置 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 成为可能,便于版本回溯。
Q32: 请简述 Git 分支管理策略,特别是从 Prod 拉分支的开发流程。
🚀 核心逻辑
“基准是 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. 网络、安全与认证
Q33: 请列举常见的 HTTP 状态码及其含义。
HTTP 状态码
- 200 成功
- 201 创建成功
- 301 永久重定向
- 302 临时重定向
- 304 缓存(未修改)
- 400 参数错误/请求错误
- 401 未登录/未授权
- 403 没权限/禁止访问
- 404 找不到资源
- 415 类型不支持
- 500 服务器内部错误
Q34: 什么是 RESTful API?在项目实践中如何遵循 RESTful 规范?
“RESTful 不仅仅是规范,更是前后端解耦的关键。我主要落实了三点:
- 严格遵循资源导向:URL 只用名词(如
/orders),操作全靠 HTTP Method(GET/POST/PUT/DELETE),杜绝了/getOrder这种语义混淆的接口。- 标准化状态码处理:我和后端约定,严禁‘全 200'模式。利用
401自动登出、403权限拦截、400提示参数错误,这让前端的全局拦截器逻辑非常清晰,Bug 率降低了约 20%。- 性能与版本意识:针对列表页,我推动后端支持
?fields=字段筛选,减少无效数据传输;同时采用 URL 版本号(/v1/)管理迭代,确保旧业务不受影响。”
Q35: 前端如何处理跨域问题?有哪些常见的解决方案?
跨域解决方案(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 携带问题。
Q36: 前端常见的安全问题有哪些(XSS, CSRF)?如何防御?
1. XSS 防御(侧重“输入过滤 + 输出转义”)
- 框架层面:坚持使用 Vue/React 的默认插值语法(
{{ }}/{}),利用框架自带的自动转义机制,杜绝直接渲染用户输入的 HTML。 - 高危场景处理:富文本编辑器(如文章详情、评论)。严禁直接使用
v-html。必须引入DOMPurify库进行白名单过滤,只保留<p>,<img>,<b>等安全标签,剔除<script>,onerror等恶意属性。 - HTTP 头加固:推动运维配置
Content-Security-Policy (CSP)头,限制脚本只能从本站加载,禁止eval()和内联脚本。
2. CSRF 防御(侧重“令牌验证 + 同站策略”)
- 核心机制:采用 Double Submit Cookie 模式。
- 细节:登录成功后,后端将 Token 写入
HttpOnlyCookie(防 XSS 窃取),前端在 Axios 拦截器中自动读取该 Token 并放入请求头X-CSRF-Token。后端校验 Header 与 Cookie 是否一致。
Q37: JWT 认证机制中,如何实现 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 防护。” |
面试高分话术
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。虽然牺牲了一点无状态性,但保证了安全性。”
Q38: 如何实现搜索“点击两次,只请求一次”?认证失败后,如何用新 token 重试请求?
“这两个问题我都通过 Axios 拦截器 封装解决: 第一,搜索去重: 我用
AbortController维护一个 pending 请求 Map。 每次新请求前,若发现同参数请求未完成,直接 abort() 取消旧请求,确保同一时间只发一次。 第二,Token 无感刷新: 我在响应拦截器捕获 401,设一个isRefreshing锁。 第一个请求触发刷新,后续并发请求推入队列等待。 刷新成功后,遍历队列 用新 Token 重发所有请求。 这样既避免了多次刷新,又保证了用户无感知。”
Q39: Cookie、Session 和 LocalStorage 有什么区别?在跨域 SSO 场景下如何处理?
1. 核心机制区别
- Cookie:浏览器自动携带的“身份证”,存在客户端,适合存 Token/SessionID。必须配
SameSite=None; Secure才能跨域。 - Session:服务端的“档案柜”,存用户状态。依赖 Cookie 中的 SessionID 来查找,本身不跨域,是 Cookie 在跨域。
2. 存储方案对比表
| 特性维度 | Cookie | LocalStorage | SessionStorage |
|---|---|---|---|
| 是否自动携带 | 是 (每次 HTTP 请求自动带) | 否 (需手动 JS 读取发送) | 否 (需手动 JS 读取发送) |
| 存储容量 | 4KB (极小,影响带宽) | 5MB+ (较大) | 5MB+ (较大) |
| 生命周期 | 可设过期时间,否则关闭浏览器失效 | 永久 (除非手动清除) | 当前标签页关闭即失效 |
| 作用域 | 同源所有窗口/标签页共享 | 同源所有窗口/标签页共享 | 仅限当前标签页 (新开不共享) |
| 服务端交互 | 后端可直接读写 (Set-Cookie) | 仅前端 JS 读写 | 仅前端 JS 读写 |
| 主要安全风险 | CSRF (需配 SameSite)XSS (需配 HttpOnly) | XSS (脚本可直接读取) | XSS (脚本可直接读取) |
| 典型应用场景 | 登录 Token (HttpOnly) 用户追踪 (Track ID) | 长期配置 (主题/语言) 离线数据缓存 | 表单分步暂存临时过滤条件 |
3. 处理跨域 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 校验。”
Q40: 前端密码哈希加盐处理时,盐(Salt)应该存储在哪里?
“关于‘加盐’的存放位置,核心原则是:前端代码对客户端是不设防的,所以密钥/盐绝不能硬编码在前端。 具体分三种情况:
- 登录密码验证:
- 盐在后端。前端只负责通过 HTTPS 传输密码,绝不在前端做哈希加盐。因为前端代码可被逆向,盐一旦暴露,哈希就失效了。
- 本地数据防篡改(签名) :
- 签名由后端生成。后端用后端私钥算好签名发给前端,前端只存‘数据 + 签名’。验证时传回后端校验。前端无法伪造签名,因为密钥从未离开过后端。
- 本地数据加密(如离线隐私数据) :
- 盐存在本地(公开),密钥由用户密码派生。
- 利用 Web Crypto API,结合用户输入的‘主密码’和本地随机生成的‘盐’,动态计算出加密密钥。
- 原理:盐可以公开,但没有用户的‘主密码’,拿到盐也解不开数据。 总结:凡是涉及安全校验的‘秘密’,要么在后端,要么由用户记忆,绝不要写死在前端代码或本地存储里。”
Q41: 如何制定统一的前后端接口响应规范与错误处理机制?
1. 统一响应数据结构
- 严格区分网络层错误与业务层错误。
- 推动后端输出 Swagger/OpenAPI 文档,并约定
code枚举值表(如:200 成功,401 未登录,500 系统异常)。 - 前端封装泛型响应类型
interface Response<T> { code: number; data: T; msg: string },在 TS 层面强制约束数据结构,避免any。
2. 拦截器统一处理(Axios Interceptors)
- 难点:每个页面重复写判断,代码冗余且易漏处理。
- 解决:
- 响应拦截器:统一解析。
code === 200直接返回res.data;code === 401自动清除 Token 并跳转登录;其他错误统一触发全局 Message 报错。 - 请求拦截器:统一注入
Authorization和防重放签名等。 - 效果:业务组件代码减少40%,只需关注
try/catch中的正常数据流。
7. 性能优化
Q42: 前端有哪些常见的导致内存泄漏和性能崩溃的原因?如何排查?
“前端内存泄漏和崩溃,核心通常是 ‘对象不再需要却被引用’。我遇到过最典型的 3 个场景:
- 事件监听未清理:比如组件里给
window绑了resize或 ECharts 实例,销毁时没removeEventListener或dispose,导致组件整棵树无法回收。
- 对策:在
beforeUnmount严格清理所有监听和定时器。
- 大 DOM 与资源:直接渲染万级列表或未压缩的大图,导致 DOM 节点爆炸或显存溢出,直接触发浏览器 ‘Aw, Snap!’ 崩溃。
- 对策:列表必用虚拟滚动,图片必做压缩/懒加载。
- 异步/定时器泄漏:组件销毁了,但
setInterval还在跑,或 Axios 请求回来更新状态,持有旧组件引用。
- 对策:用
AbortController取消请求,销毁时清除 Timer。排查手段:我用 Chrome DevTools 的 Heap Snapshot,对比操作前后的快照,重点看 Detached DOM tree 和引用链(Retainers),快速定位是谁持有了不该持有的对象。”
Q43: 如何实现大规模列表的虚拟滚动?如何解决虚拟滚动中的跨页多选状态丢失问题?
1. 跨区域多选状态丢失问题
实际问题:虚拟滚动仅渲染可视区域 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) 的操作性能和最终的一致性提交。”
2. 高效获取全部已勾选数据
实际问题:由于 DOM 节点随滚动不断创建和销毁,无法直接通过遍历 DOM 获取所有勾选状态。
回答模板: “针对万级数据的全选和获取,我采用 ‘Set 集合存储 + 一次性遍历’ 策略,避免过度优化:
- 数据结构:维护一个全局
Set存储所有选中项的 ID。- 全选实现:
- 点击‘全选’时,我直接遍历一次源数据(1 万条),将所有 ID
add进Set。现代浏览器处理 1 万次Set.add仅需 10~20 毫秒。- 点击‘取消全选’:直接
Set.clear(),耗时 O(1)。
- 最终获取:点击保存时,直接
Array.from(selectedSet)转为数组发送给后端。不管用户滚到哪里,数据全在内存 Set 里,获取结果是毫秒级的。 总结:用空间换时间,利用Set的高性能特性,简单粗暴地解决万级数据选中问题。”
Q44: 电商详情页或长列表场景下,有哪些具体的性能优化手段?
核心痛点:首屏慢、长列表卡顿、频繁请求。 回答策略:从资源、渲染、数据、网络四个维度展开。
| 优化维度 | 关键手段 | 面试加分细节 (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 持久化不常变数据。 |
Q45: 前端视频加载有哪些优化策略?H.264 和 H.265 有什么区别?
1. 视频加载核心策略
| 策略维度 | 具体做法 (Action) | 技术细节 (Tech) | 预期收益 (Result) |
|---|---|---|---|
| 首帧极速展示 | 封面图 + 预加载关键帧 | 使用高质量 WebP 作为poster;利用<link rel="preload">预加载视频前 2 秒数据。 | 用户感知“秒开”,FCP 从 2.5s → 0.8s。 |
| 分片加载 | 切片传输 + 自适应码率 | 将 MP4 转为.m3u8 (TS 切片);根据网速动态切换分辨率;只加载当前播放片段。 | 弱网环境下起播时间缩短 60%;流量节省 40%。 |
| 智能预加载 | 视口检测 + 静默缓冲 | 使用IntersectionObserver监听视频进入视口前 200px 时,仅缓冲前 5 秒数据 (media.preload = 'metadata')。 | 避免无效加载,页面整体加载资源减少 30%。 |
| 编码优化 | 现代格式 + 压缩 | 优先使用 H.265 (HEVC) 或 AV1 编码;配合 ffmpeg 调整 CRF 参数。 | 同画质下体积减小 50%,带宽成本直接减半。 |
2. H.264 vs H.265 (HEVC) 核心原理对比
| 维度 | H.264 (AVC) | H.265 (HEVC) | 实战策略 |
|---|---|---|---|
| 压缩效率 | 基准 | 同画质下体积减少 50%(或同码率下画质提升一倍) | “在高清/4K 场景首选 H.265,节省 50% CDN 带宽成本;但在老旧设备需降级。” |
| 编码单元 | 宏块 (16x16) | **CTU (最大 64x64)**分割更灵活,预测更精准 | “H.265 的大块划分更适合高分辨率视频,减少了块效应,边缘更平滑。” |
| 预测技术 | 较基础 | 增强型预测,运动矢量精度更高 | “复杂运动场景(如体育直播),H.265 能大幅减少马赛克,但编码耗时增加 30%。” |
| 兼容性 | ✅ 全兼容 | ❌ 部分兼容(iOS 11+, Android 5.0+, 现代浏览器) | “必须做兜底方案:检测到不支持 H.265 的设备,自动切换 H.264 流。” |
Q46: 当本地存储需求超过 localStorage 的 5MB 限制时,有哪些解决方案?
“遇到超过 5MB 的本地存储需求,我的方案是 ‘弃用 localStorage,升级 IndexedDB’:
- 认知纠正:5MB 只是
localStorage的硬限制,浏览器的IndexedDB容量上限通常是硬盘的 50% ,存几百 MB 都毫无压力。- 技术选型:
- 直接使用
IndexedDB,它是浏览器内置的异步 NoSQL 数据库。- 为了开发效率,我会封装
localforage或idb库。它们提供和localStorage一样的setItem/getItemAPI,但底层自动走 IndexedDB,代码改动极小。
- 优势匹配:
- 大容量:完美解决大型配置文件存储。
- 非阻塞:异步读写,不会像
localStorage那样阻塞主线程导致页面卡顿。- 持久性:数据永久保存,除非用户主动清除。 总结:对于大文件或大量配置,前端标准答案就是 IndexedDB,绝不强行切割
localStorage。”
8. 场景题与架构设计
Q47: 通用 ECharts 组件如何保证性能和实例不冲突?
场景:首页有多个图表,封装的组件如何隔离和优化。
“我主要从实例生命周期和渲染策略两方面处理:
- 严格隔离:每个组件实例通过
ref独占 DOM,并在onUnmounted中调用dispose()彻底销毁实例,防止内存泄漏;同时在v-for中绑定唯一key,杜绝 Vue 复用导致的配置串味。- 按需与防抖:采用 ECharts 按需引入,只打包用到的图表模块,减少主包体积约 80%。
- 智能重绘:弃用全局
resize事件,改用ResizeObserver监听容器变化并加 200ms 防抖,解决拖拽布局时的卡顿问题。- 增量更新:数据刷新时利用
setOption的合并模式,仅更新变化数据而非重绘整个图表,确保多个图表同时刷新时帧率稳定。”
Q55: 如何通过 JSON 配置来实现一个高度可配置的 BasicTable 组件?
BasicTable 组件实现 JSON 配置的思路
- 定义 Props: 组件声明它能接受的所有配置项。
- 合并配置源: 使用一个计算属性
getProps将来自父组件模板的静态props和通过setTableProps方法传入的动态props合并成一个最终的配置对象。 - 暴露接口: 暴露
setTableProps方法,允许父组件在任何时候传递一个新的 JSON 对象来动态修改表格的行为和外观。 - 统一消费: 组件内部的所有逻辑都从
getProps这个统一的配置源读取信息,确保了行为的一致性。 这种方式将组件的配置与具体实现解耦,使得通过一个 JSON 对象就能描述整个表格的外观和行为,极大提升了组件的复用性。
Q56: 如何实现基于用户角色的动态路由加载和按钮级权限控制?
“用户登录成功后,后端会返回当前用户的权限码列表和可访问路由标识,我存在 Pinia 里并做持久化。路由方面使用 动态路由,根据权限过滤后端返回的路由表,再通过
router.addRoute动态挂载。页面按钮我封装了一个全局自定义指令v-permission,传入权限码就能自动控制显隐。同时在路由全局守卫里做权限校验,没有权限直接跳 403 或首页。”
Q57: 如何实现跨页面、跨 Tab 的实时状态同步?
场景:两个独立的浏览器标签页(A 用户页,B 管理页)之间数据状态不同步。
“跨设备或跨 Tab 场景下,本地状态无法简单共享,必须上实时推送。
- 我会建立 WebSocket 长连接,让用户页订阅权限变更频道。
- 管理员操作后,后端主动推送变更指令到该频道。
- 前端收到消息,直接更新本地 Store,利用 Vue 响应式瞬间刷新视图。
- 若连接异常,有轮询兜底策略,确保数据最终一致,实现无感知的实时同步。或者设置心跳保活机制,维持连接状态,监听断联重连。
心跳保活
“WebSocket 心跳保活很简单:
- 客户端每 30 秒发一个
'ping'消息;- 服务端收到后立即回
'pong';- 如果连续 2 次没收到 pong,就认为断连,触发重连。
前端用
setInterval+ws.send('ping')实现,服务端 echo 即可。关键是要让心跳间隔小于 NAT 超时(一般设 30s),这样就能有效维持长连接。”
Q58: 同一个列表组件在多个浏览器标签页打开时,如何隔离各自的状态?
场景:同个组件(如订单列表)被多个页签同时打开,且展示不同数据状态(如“已完成”、“已取消”),如何互不干扰。
“这个场景核心是利用
keep-alive的动态key实现多实例隔离。
- 我给
<router-view>绑定:key="$route.fullPath"(或特定的 query 参数)。- 这样 Vue 会把‘完成’和‘取消’视为两个独立的组件实例分别缓存。
- 切换时,只是暂停旧实例、激活新实例,互不覆盖。
- 我在
onActivated钩子里做数据 freshness 检查,确保用户切回来时看到最新数据,同时保留滚动位置和筛选状态。”
Q59.1:IM 通信
IM 通信主要基于 WebSocket 长连接实现,搭配第三方 IM SDK 做消息收发。 项目里做了 心跳保活 维持连接稳定,同时监听断连状态,网络异常时自动重连,重连成功后拉取离线消息,保证消息不丢失、会话不断。
Q59: 在 Uni-app 跨端开发中,如何解决 IM 即时通信场景下的长列表卡顿问题?
痛点:群聊历史消息超过 500 条时,长列表滚动卡顿,内存飙升。 做法:
- 虚拟列表:自研虚拟滚动组件,仅渲染可视区域(如 10 条)+ 上下缓冲(各 5 条),DOM 节点数恒定在 20 个以内。
- 分页加载:采用“倒序分页”策略,上拉加载更早的历史消息,利用
uni.createSelectorQuery精准维持滚动位置,避免加载后视图跳动。- 图片优化:消息中的图片/视频默认只加载缩略图,点击才加载原图;对超长文本进行截断折叠处理。 结果:千条消息列表滑动帧率稳定在 55fps+,首屏渲染时间从 1.5s 降至 400ms。
Q60: 在 Vue 2 升级到 Vue 3 + TS + Vite 的过程中,遇到了哪些核心难点?是如何解决的?
1. 思维模式重构:Options API → Composition API
- 难点:老代码逻辑分散,难以按业务逻辑抽离;TS 类型推导在 Options API 下较弱。
- 解决:
- 渐进式策略:新模块用
<script setup lang="ts">,旧模块保留 Options API,通过@vue/compat构建版本过渡。 - 逻辑复用:将散落在 mixins 中的逻辑重构为Composables 函数(如
useFormValidator.ts),利用 TS 泛型增强类型提示。
2. 生态断裂与兼容性:第三方库与全局 API 变更
- 难点:大量 Vue2 插件不兼容;全局 API 移除;
v-model语法变更。 - 解决:
- 依赖替换:
vuex→pinia;vue-router@3→@4。 - 语法修复:使用官方 codemod 脚本批量转换
v-model和生命周期钩子。
3. 构建工具迁移:Webpack → Vite 的“水土不服”
- 难点:CommonJS 依赖报错;环境变量变化(
process.env);静态资源路径变化(require)。 - 解决:
- 配置优化:在
vite.config.ts中配置optimizeDeps.include强制预打包 CJS 依赖;使用plugin-commonjs处理特殊包。 - 全局替换:编写正则脚本,批量替换环境变量和资源引入方式。
- 成果:本地冷启动从45s 降至 1.2s,HMR 热更新降至毫秒级,打包体积减少18%。
Q61: 在核心业务模块的技术选型中,为什么选择 Node.js 18 LTS 和 Day.js?
1. 运行时选型:Node.js 18 LTS
- 原生支持:内置
fetchAPI 和Web Crypto,包体积减小约 15%。 - 稳定性:LTS 版本经过生产验证,避开新版本的实验性特性风险,同时兼容主流 CI/CD 流水线。
2. 时间库选型:Day.js 1.11.x (按需加载插件)
- 轻量级:核心库仅2KB(Moment.js 的 1/30),符合首屏性能优化指标。
- 插件化:只引入业务需要的
utc、timezone等插件,避免全量引入。
9. AI 与前沿探索
Q62: 你对 AI 开发、Skills 和 MCP 有了解吗?它们在前端领域如何落地?
理解它们是现代 AI 应用的三大核心组件:
- 概念理解:
AI Agent 是‘大脑’,负责自主规划任务;通过GitLab Webhook + n8n + Diff 构建的自动化流程,本质上就是一个“轻量级AI Agent”——它能感知代码变更、触发AI分析、生成报告、通知相关人员。与“通用AI Agent”的区别在于,更关注“在特定业务场景下,AI如何解决具体问题”,而不是“构建一个能做所有事的AI”。
Skills 是‘专业技能包’,将领域知识和工具封装好,让通用模型变成专家;正在将常用的业务逻辑(如“支付通道切换”“汇率计算”“对账逻辑”)封装成“AI可调用的Skills”,让AI在生成代码时,能直接调用这些“业务Skill”,而不是从零生成代码。
**MCP ** 是‘通用 USB 接口’,标准化了 AI 与外部数据/工具的连接,实现了‘即插即用’。利用 MCP 快速搭建内部助手,让 AI 能直接读取公司的 Swagger 文档或 Figma 设计稿。 总结:未来的前端不仅是写页面,更是 ‘AI 技能的编排者’。利用 MCP 和 Skills,我们可以将重复劳动自动化,聚焦于更复杂的业务逻辑。”
Q63: 在 AI 开发中,如何理解和管理“上下文”(Context)?
“简单来说,上下文就是 AI 的 ‘短期记忆’。 我的理解就三点:
- 它是有限的:太长会慢且贵,还会让 AI 变笨(注意力分散)。
- 贵在精准:我不会把整个项目塞给它,而是通过检索(RAG) ,只把当前相关的代码片段喂给它。
- 重在管理:多轮对话时,我会自动丢弃旧记录或做摘要,确保它永远只关注‘当下最需要的信息’。 核心原则:用最少的 Token,提供最准的信息。”
10. ECharts
Q64: 项目中你怎么封装 ECharts?
- 在 Vue 组件里用 ref 获取容器 DOM。
- 在 onMounted 生命周期里初始化图表。
- 通过 setOption 渲染配置和数据。
- 监听窗口变化(window.resize())做自适应(chart.resize())。
- 组件销毁时释放实例,防止内存泄漏。
- 支持响应式、loading、数据更新。
Q65: ECharts 怎么做自适应?
- 监听 window 的 resize 事件。
- 使用防抖(debounce)优化性能。
- 在回调里调用 chart.resize ()。
- 保证窗口变化时图表自动适配。
Q66: 组件销毁时必须做什么?
必须销毁 ECharts 实例:
- 调用 chart.dispose ()。
- 清空 resize 监听。
- 清空定时器。不销毁会造成内存泄漏,页面越来越卡。
Q67: ECharts 性能优化(高频)
- 复用实例,不要重复 init。
- 使用 setOption 增量更新,不重新创建。
- 大数据关闭动画:animation: false。
- 减少阴影、渐变、多余标签。
- 大量数据使用 dataZoom 或分段渲染。
- 避免频繁 setOption,做节流防抖。
Q68: 图表不显示 / 白屏原因
- 容器 DOM 没有设置宽高。
- 在 DOM 挂载前就初始化。
- 数据格式错误或为空。
- 多次初始化导致实例异常。
- 路由切换没销毁,实例污染。
Q69: 两个图表如何联动?
- 给多个图表设置相同的 group。
- 使用 echarts.connect (group) 关联。
- 可实现 tooltip、brush、数据联动。
Q70: 海量数据怎么渲染?
- 关闭动画。
- 数据采样、简化数据。
- 使用 dataZoom 只展示可视区域。
- 使用 appendData 追加数据。
- 避免一次性渲染大量节点。
描述从输入URL到页面加载完成的详细过程
“从输入 URL 到页面加载完成,我分为三个阶段:
网络阶段:先 DNS 解析拿到 IP,再 TCP 三次握手建立连接。如果是 HTTPS,还要加一次 TLS 握手(TLS 1.3 只需 1 RTT)。并发项目中,我们会用 HTTP/2 和长连接减少握手开销。
解析阶段:浏览器下载 HTML 后边解析边构建 DOM。遇到 CSS 会阻塞渲染树,遇到同步 JS 会暂停 HTML 解析。所以我们强制所有业务 JS 加
defer,关键 CSS 内联。渲染阶段:DOM + CSSOM 合成 Render Tree,然后 Layout → Paint → Composite。我们用
transform触发 GPU 加速,避免频繁回流。最终等所有图片加载完,onload事件触发,才算完全加载。”
描述从输入URL到页面加载完成的详细过程,在此阶段,说明前端性能优化的关键机会点。
“前端性能优化的关键机会点贯穿整个加载链路:
网络层:我用
dns-prefetch和preconnect提前建立连接,静态资源上 CDN 并启用 Brotli 压缩,在深圳项目实测首包时间从 300ms 降到 180ms。解析层:强制所有业务 JS 加
defer,关键 CSS 内联。Vue3 项目用 Vite 自动拆分,确保 HTML 解析不被阻塞。渲染层:动画只用
transform和opacity触发 GPU 合成;图片和非首屏组件全部懒加载。最后通过 Performance API 监控 LCP,确保稳定在 2 秒内。”
TCP连接建立与释放机制(简单说一下TCP的四次挥手和三次握手)
关键过程(按时间线 + 状态动作)
▶ TCP三次握手(建立连接)
- Client → Server:发送
SYN=1, seq=x,进入SYN_SENT状态。 - Server → Client:回复
SYN=1, ACK=1, seq=y, ack=x+1,进入SYN_RCVD状态。 - Client → Server:发送
ACK=1, seq=x+1, ack=y+1,双方进入ESTABLISHED状态。 - 关键点:防止历史重复连接初始化(通过 seq 防旧包干扰)。
▶ TCP四次挥手(关闭连接)
- 主动方(如 Client)→ 被动方:发送
FIN=1, seq=u,进入FIN_WAIT_1。 - 被动方 → 主动方:回复
ACK=1, ack=u+1, seq=v,进入CLOSE_WAIT;主动方进入FIN_WAIT_2。 - 被动方处理完数据后 → 主动方:发送
FIN=1, ACK=1, seq=w, ack=u+1,进入LAST_ACK。 - 主动方 → 被动方:回复
ACK=1, seq=u+1, ack=w+1,进入TIME_WAIT(等待 2MSL 后关闭);被动方收到后关闭。 - 关键点:被动方可能还有数据要发,所以 ACK 和 FIN 不能合并(区别于握手)。
回答模板
“三次握手是建立连接:客户端先发 SYN,服务端回 SYN+ACK,客户端再回 ACK,双方进入 ESTABLISHED。目的是同步初始序列号并防止旧连接请求突然到达。
四次挥手是关闭连接:一方发 FIN 表示不再发数据,对方先回 ACK 确认(但可能还有数据要发),等它自己 ready 了再发 FIN,最后发起方回 ACK 并等待 2MSL 后关闭。之所以四次,是因为 TCP 是全双工,两边要独立关闭。”
对比 HTTP/1.1 与 HTTP/2
| 特性 | HTTP/1.1 | HTTP/2 |
|---|---|---|
| 连接模型 | 每个 TCP 连接只能处理一个请求(队头阻塞) | 单个 TCP 连接支持多路复用(Multiplexing),多个请求并行 |
| 头部压缩 | 无压缩,重复 Header 浪费带宽 | 使用 HPACK 压缩算法,减少头部体积 |
| 服务器推送 | 不支持 | 支持(Server Push),可主动推送资源 |
| 二进制 vs 文本 | 基于文本协议,解析易出错、效率低 | 二进制分帧(Frame),更高效可靠 |
| 请求优先级 | 无原生支持 | 可设置流优先级(Stream Priority) |
| 典型并发限制 | 浏览器通常限制 6 个并发连接/域名 | 单连接即可处理数十甚至上百个并发流 |
关键影响举例:
- 在 Vue3 项目中,HTTP/2 下首屏加载时间平均减少 30%(实测数据,基于 Webpack 分包 + 静态资源托管于支持 HTTP/2 的 CDN)。
- HTTP/2 的多路复用避免了 HTTP/1.1 中为绕过队头阻塞而采用的域名分片(domain sharding)等 hack 手段。
回答模板
“HTTP/2 相比 HTTP/1.1 主要有四点提升:
- 多路复用:单连接并行传输多个请求,彻底解决队头阻塞,深圳项目实测首屏快 35%。
- 二进制分帧 + HPACK 压缩:头部体积减少 70%,解析更高效。
- 服务器推送:可提前下发关键资源(如 CSS/JS),但需配合缓存策略避免浪费。
- 强制 HTTPS:安全升级,但要求 TLS 1.2 以上。
我们在 Vue3 项目中全站启用 HTTP/2,配合 CDN 和 Brotli,LCP 从 2.8s 优化到 1.9s。”
GitLab做代码状态管理,搭建了一套自动化代码规范与AI审核流程:
- 本地提交阶段:用ESLint+Prettier统一代码风格,通过Husky做提交拦截与自动修复,搭配Commitlint规范commit格式,要求必须以 feat: 修改内容(需求ID) 形式提交。
- 合并监听阶段:在GitLab配置Webhook监听代码合并事件,并将该Webhook地址接入N8N,由N8N监听合并请求。
- AI审核阶段:N8N监听到合并后,将代码按500上下文切割为多个片段,循环请求Dify进行AI审核;Dify内置提示词与扣分规则,如v-for未写key扣50分、空catch块扣5分、存在魔法值等均对应扣分与问题判定。
- 通知整改阶段:通知整改阶段:汇总所有审核分数、代码问题及整改建议,通过N8N自动推送至企业微信群,通知相关开发人员及时修改。
设计一个前端“购物车”状态管理模块,需要考虑多页面共享、数据持久化、优惠计算和库存同步。简述你的设计思路和关键技术选型。
“我的购物车模块设计围绕四个核心需求:
- 多页面共享:用
BroadcastChannel+localStorage实现跨 Tab 实时同步,避免数据分裂。- 持久化:本地存结构化数据(带版本号),启动时自动迁移或重置,保证兼容性。
- 优惠计算:后端下发规则,前端用响应式计算(如 Vue3 computed)自动重算总额,支持满减/折扣叠加逻辑。
- 库存同步:添加时预拉库存做前端限制,提交前二次校验,结合 AbortController 防止请求堆积。
技术上选 Pinia/Zustand 管理状态,自研存储 Hook 控制持久化,整套方案在深圳电商项目中支撑日均 50 万 UV,购物车放弃率下降 12%。”
前端项目中有10个独立的API请求需要并发发送,要求最多容忍3个失败,且在全部请求完成后(无论成功失败)统一处理结果。请给出你的实现方案核心代码逻辑
“我会用 Promise.allSettled 并发跑完全部 10 个请求,它能保证所有请求都完成后再汇总结果。然后遍历结果数组,统计 rejected 的数量——如果 ≤3 就认为整体成功,提取 fulfilled 的数据做业务处理;否则走降级逻辑。整个过程不用第三方库,靠原生 Promise 就能精准控制容错阈值和结果聚合。”
函数式编程
“函数式编程是用纯函数和不可变数据来构建逻辑,避免副作用,比如用
map、filter链式处理数据而不是操作原始数组。”高阶函数:我们封装
debounce用于搜索防抖,或者写useLocalStorage这样的组合式函数,把通用逻辑抽离出来,业务组件只关注‘做什么’。
watch 重载
watch 重载’通常指未正确清理监听器导致重复触发。我在项目中通过精准监听路径(如
() => obj.a.b)和依赖组件生命周期自动清理,避免了内存泄漏和无效回调。
如何实现一个高效的前端“图片懒加载"组件?请说明其核心原理、性能考量以及与“虚拟列表”技术的异同。
“高效图片懒加载我用 Intersection Observer 实现:
- 原理:监听图片是否进入视口(支持 rootMargin 预加载),进入后才设置
src触发真实请求。- 性能点:
- 固定宽高防布局抖动
- 加载后 disconnect 观察器
- 失败时替换默认图
- 与虚拟列表区别:
- 懒加载只省带宽,DOM 还是全量的;
- 虚拟列表直接少建 DOM,解决万级列表卡顿。
优化 SPA 首屏加载速度
“针对 SPA 首屏慢,我从三层优化:
网络层:主包启用 Brotli 压缩(体积降 20%),静态资源走腾讯云 CDN,深圳 TTFB 从 320ms 压到 90ms。
构建层:路由级代码分割 + 第三方库按需引入,首屏 JS 从 1.8MB 降到 700KB;关键 CSS 内联,消除渲染阻塞。
运行时层:非核心逻辑用
requestIdleCallback延迟执行,首屏组件预加载,配合骨架屏让 FCP 提前 300ms。
面对一个遗留的、技术栈陈旧(如jQuery)、模块混乱、难以维护的大型前端项目如果由你主导进行渐进式重构或现代化改造,你的总体思路和关键步是什么?
“渐进式重构分四步走:
先评估再动手:用 Lighthouse 和 Bundle 分析锁定性能瓶颈,选低风险页面试点,CI 禁止新代码用 jQuery 反模式。
搭现代基建:引入 Vite 多入口构建,旧页面不动,新页面走独立路由;必要时用 Module Federation 嵌入新组件。
组件级替换:从展示型组件开始,用 Vue3 重写,通过 jQuery 插件 API 供旧代码调用,状态靠 CustomEvent 同步。
自动化兜底:Cypress 覆盖核心流程,新组件强测,旧页面 UV 低于阈值自动下线。
在金融后台项目中,6 个月将 jQuery 代码占比从 92% 降到 18%,零 P0 故障。”
前端架构组职能价值
具体做三件事:
- 工程提效:用 n8n 自动审代码、Mock 提前联调、Sentry 自动上报问题;
- 质量保障:靠 ESLint+规范手册强制统一代码,避免各搞一套;
- 规范统一:收敛技术栈(统一 Vue/React 版本、构建工具(Vite/Webpack)、测试框架,减少兼容性问题)、管控包体积,防止项目越跑越重。
公司需要,当团队人数多时,没有统一基建就会陷入‘重复造轮子、问题难追溯、新人难上手’的泥潭——架构组就是解决这些问题。”
ODP 数据中台 主要是用来做什么的?我的主要开发工作?
主要服务于B端用户,如运营、财务和风控团队。核心作用: 支撑跨境支付的全链路业务流转,为跨境支付业务提供统一的订单、资金与结算数据中枢。比如,当C端用户在商户端完成一笔消费后,我们的运营人员就需要在这个ERP平台上进行订单审核、资金对账、以及处理与Visa或八达通等渠道的结算。
在这个项目中,前端的作用是让核心业务得以落地、执行、监控的技术载体,我主要负责基建提效。我基于Ant Design进行了二次封装,构建了适配金融业务的通用组件库。技术上,我利用
defaultProps透传原生属性,同时对外暴露JSON配置接口。通过这种**‘配置化驱动’的模式,我们实现了搜索列表、弹窗等模块的快速搭建,直接减少了70%的重复HTML代码**。统一了系统的交互规范,提升了团队的交付效率。”
数据清洗的核心逻辑
“数据清洗体系,核心是为了确保金融数据的绝对准确与高效展示。
输入侧,我通过封装自定义指令和正则校验,对金额、账号等关键字段进行实时拦截,从源头防止脏数据录入,这是防御性编程。
展示侧,我建立了一个统一的数据适配层,将后端晦涩的时间戳、状态码,自动转换为带时区的可读时间和可视化的状态标签,提升运营效率。
兜底策略,对异常数据进行高亮预警而非直接报错,确保系统的高可用性。我认为,最好的清洗其实是用交互设计从源头规避错误。”
你的工作对业务有什么价值?
负责前端工程化建设,包括引入AI提效工具、搭建镜像监听系统、完善代码审核流程。这些工作看似是内部‘基建’,但它们共同作用,提升团队的开发效率,降低线上Bug率。 技术指标的提升,直接转化为业务价值:能更快地响应市场需求(如快速接入外贸),更稳定地保障交易进行,更安全地守护资金流转(杜绝安全漏洞)。技术不是目的,而是支撑业务高速增长的引擎。”
职业规划
“我有三年 Vue3 前端经验的工程师。大学时系统学过 Python,最近在研究开源框架 OpenClaw。后续的工作中先高质量完成本职的前端开发任务,再主动思考如何用 AI 提升用户体验。比如,用户是不是还得翻十张报表才能汇总出来结果?能不能改成对话式交互?系统可以自动调用 Python 脚本聚合数据,通过 OpenClaw 执行查询和分析,最终在前端直接展示图表和摘要。
常用 Git 命令(按工作流顺序 + 核心作用)
▶ 本地开发阶段
git clone <url>:克隆远程仓库到本地git status:查看文件状态(已修改/未跟踪/已暂存)git add <file>或git add .:将变更加入暂存区git commit -m "feat: add login button":提交暂存区到本地仓库(带语义化 message)
▶ 协作与同步阶段
git pull origin main:拉取远程最新代码并合并(=fetch+merge)git push origin feature/login:推送本地分支到远程git branch:列出所有本地分支git checkout -b new-feature:创建并切换到新分支
▶ 问题修复与回退
git log --oneline:查看简洁提交历史git diff:查看工作区与暂存区差异git reset --hard HEAD~1:危险! 回退到上一个提交(丢弃最近一次提交及修改)git revert <commit-id>:安全回滚(生成新提交取消某次变更,保留历史)
▶ 代码审查准备
git rebase -i main:交互式变基,整理提交记录(合并/重命名/删除 commit)git stash/git stash pop:临时保存/恢复工作区修改(用于紧急切分支)
Nuxt 核心特点 + 项目示例
Nuxt 是什么?
- 基于 Vue 3(Nuxt 3)的全栈框架,开箱支持:
- 服务端渲染(SSR):首屏 HTML 由 Node.js 生成,提升 SEO 和 LCP。
- 静态站点生成(SSG):
nuxt generate输出纯静态文件,适合内容型网站。- 自动路由:
pages/目录结构 → 自动生成 Vue Router 配置。- 集成生态:内置 Nitro(轻量服务层)、TypeScript、Vite、Pinia。
回答模板
“Nuxt 是 Vue 的全栈框架,我用它主要解决两个问题:
- SEO 和首屏性能:比如在深圳做的跨境商品页,用 SSR 让 Google 能爬内容,LCP 从 3.2s 优化到 1.4s;
- 开发效率:自动路由、内置 Pinia、TypeScript 开箱即用,不用自己搭脚手架。
Node.js 使用
基于本地部署的 Claude Code 模型(官方未开放 API),通过 Node.js 封装了一层内部服务接口,对外提供标准化的代码审核能力。该服务接收 CI 系统提交的代码片段,调用本地 Claude Code 进行语义分析与潜在问题识别(如安全漏洞、规范违反等),并将结构化结果返回,集成到研发流程中,实现自动化代码审核。
Playwright
前端自动化测试时,选择Playwright的原因: 一方面:它体积小,支持JSON语法,对前端同学学习成本更低、更容易上手。 另一方面:它自带浏览器环境,还支持屏幕录制直接生成测试用例,效率很高。 不过录制出来的用例也有缺点:它默认是根据文本内容定位,不是用ID和class,而且不会自动加等待逻辑,容易不稳定。 所以我们实际使用时,会手动改成ID、class定位,再补充等待逻辑,这样用例复用性更强,就算页面结构改动,只要标识不变,用例依然可以正常运行。
大文件上传
大文件上传我用的是前端文件切片+断点续传+秒传方案。 主要用 HTML5 的 File.slice 做切片, SparkMD5 生成文件唯一标识,配合 FormData 和接口并发上传。 先拿 MD5 问后端文件是否存在,存在就直接秒传;不存在就按 2MB 左右分片,只传没上传过的切片,全部传完通知后端合并。 同时做进度展示、失败重试和断点续传,网络断了恢复后可以接着传,不用从头开始。
网络代理相关知识?
前端主要用代理解决开发跨域问题。
本地开发用 Proxy,把 /api 开头的请求转发到后端地址;生产环境使用 CORS(需要后端配置),老接口走 Nginx 反向代理。调试时也会用Mock。
核心是让浏览器认为请求是同源的,实际由代理服务器转发。
移动端联调,PC浏览器远程手机webView调试?
移动端联调是把 H5 页面嵌入 App WebView 后,在真机上验证功能和兼容性。
常用 Chrome 的 chrome://inspect 调试 Android WebView,前提是 Native 开启了调试开关;iOS 则通过 Safari 的开发菜单连接真机调试。
关键是要确保调试开关打开,并且注意 iOS 16.4 之后需要加特定 meta 标签才能被识别。
手机代理到PC本地开发服务器联调?
手机联调本地服务,首先让 PC 和手机连同一 Wi-Fi,查 PC 局域网 IP。配置本地 dev server 监听 0.0.0.0,手机浏览器访问 http://PC_IP:端口即可。
如果需要代理 API 请求,就在 PC 装 Charles / Whistle,手机 Wi-Fi 设置代理指向 PC IP 和 Whistle 端口,再配转发规则。注意防火墙和 Android/iOS 的网络限制。
终面问题
1. 您对这个前端岗位,未来半年到一年最核心的期望是什么?
2. 咱们团队目前在业务推进中,面临的主要挑战有哪些?