前端面试复习资料包
适合:3~5 年 Vue 全栈前端工程师
总篇幅:约 5 万字 / 5 个文档
复习周期:建议 4 周冲刺
📚 文档结构
前端面试复习/
├── README.md ← 本文件(导航)
├── 01-基础篇.md ← JS/TS/CSS/HTTP/浏览器/算法/手写题
├── 02-框架篇.md ← Vue3/Pinia/Vite/uni-app/微前端/SSR/测试
├── 03-项目篇.md ← 实时通信/可视化/地图/工程化/项目话术
├── 04-软实力篇.md ← 自我介绍/离职/薪资/反向提问/AI 工具
└── 05-自测题库.md ← 100 道高频题 + 参考答案
🗓 4 周复习路径
第 1 周:基础夯实
主攻:01-基础篇
第 1-2 章(JS / TS)
第 8 章(手写题专项)
第 9 章(算法)
每天刷 2 道 LeetCode(题号见 9.5)
每周末:做 05-自测题库 第一/二部分(25 道)
第 2 周:框架深入
主攻:02-框架篇
第 1-7 章(Vue3 / Pinia / Router / Vite / uni-app)
第 8-12 章(组件 / 微前端 / SSR / 测试 / 设计模式)
每周末:做 05-自测题库 第三/四部分(28 道)
第 3 周:项目实战
主攻:03-项目篇
第 1-5 章(实时通信 / 可视化 / 地图 / 工程化 / 低代码)
第 6 章(Node.js)
重点:第 7-8 章(项目话术 + 5 个项目预设追问)
每周末:做 05-自测题库 第五-八部分(35 道)
第 4 周:软实力 + 模拟
主攻:04-软实力篇
全篇研读
自我介绍背诵到流利(30s/1min/3min 三版本)
离职原因 / 期望薪资话术练习
找朋友模拟面试 3 次
每天:做 05-自测题库 第九部分(12 道)+ 复习薄弱点
📊 自测评分标准
| 答对题数 | 评估 |
|---|---|
| 90+ | 大厂候选,可挑战 P6+ |
| 70-90 | 中厂稳过,大厂有戏 |
| 50-70 | 还需补漏 |
| <50 | 系统复习 |
💡 使用建议
不要全背:理解原理 > 背答案
项目深挖:每个项目至少 3 个难点细节 + 数字
诚信底线:不熟的不写"精通"
手写题练习:必练 10 道(见 01-基础篇 第 8 章)
大厂额外:算法(leetcode hot 100)+ 计算机基础
📦 推荐资源
| 类型 | 资源 |
|---|---|
| JS 进阶 | 《JavaScript 高级程序设计》第 4 版 |
| Vue 源码 | 霍春阳《Vue.js 设计与实现》 |
| 算法 | 《代码随想录》 |
| 网络 | 《图解 HTTP》《网络是怎样连接的》 |
| 浏览器 | 极客时间《浏览器工作原理与实践》 |
| 面试题库 | 牛客网、力扣、前端九部 |
✅ 面试前一天 checklist
简历最新版(PDF + 纸质 3 份)
自我介绍背到流利
5 个项目的 STAR 故事
期望薪资 + 谈判区间
反向提问 3-5 个
公司调研(业务、产品、新闻)
装备:笔、本子、充电宝、耳机(视频面)
提前确认面试地址 / 视频链接
早睡,第二天提前 15 分钟到
🎯 终极心态
面试是技能,可以训练
失败不丢人,每次都是练习
保持谦逊但自信
复盘比刷题更重要
祝你早日拿到心仪 offer!💪
前端面试复习|基础篇
涵盖:JavaScript 核心 / TypeScript / CSS / HTTP / 浏览器原理 / 网络协议 / 性能优化 / 手写题 / 算法
复习时长建议:1 周
📑 目录
一、JavaScript 核心
1.1 数据类型与判断
8 种数据类型:Number / String / Boolean / null / undefined / Symbol / BigInt / Object
| 方法 | 优点 | 缺点 |
|---|---|---|
typeof |
快、判断基本类型准 |
null 返回 'object',无法区分对象/数组 |
instanceof |
判断引用类型 | 跨 iframe 失效,不能判断基本类型 |
Object.prototype.toString.call() |
最准 | 写法繁琐 |
Array.isArray() |
专门判数组 | 仅判数组 |
Object.prototype.toString.call(null) // [object Null]
Object.prototype.toString.call([]) // [object Array]
Object.prototype.toString.call(new Date) // [object Date]
1.2 闭包
是什么:函数 + 它能访问到的外部变量 = 闭包。
function counter() {
let count = 0;
return () => ++count;
}
const add = counter();
add(); // 1
add(); // 2
用途:模块封装、防抖节流、柯里化、私有变量
坑点:内存泄漏、循环里 var 闭包陷阱(用 let 解决)
1.3 原型与原型链
核心三句话:
- 每个对象都有
__proto__,指向它的"父对象" - 每个函数都有
prototype - 查找属性时,沿
__proto__向上查找直到 null
arr.__proto__ === Array.prototype
Array.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
1.4 this 指向(5 种规则)
-
new→ this 指向新对象 -
bind / call / apply→ this 指向第一个参数 -
obj.fn()→ this 指向 obj - 普通
fn()→ 严格模式 undefined,非严格 window - 箭头函数没有自己的 this
1.5 事件循环(Event Loop)
同步代码 → 清空所有微任务 → 取一个宏任务 → 清空微任务 → 渲染 → 下一轮
| 微任务 | 宏任务 |
|---|---|
| Promise.then | setTimeout / setInterval |
| MutationObserver | I/O、UI 渲染 |
| queueMicrotask | setImmediate (Node) |
console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => console.log(3));
console.log(4);
// 输出:1 4 3 2
1.6 Promise / async-await
三状态:pending → fulfilled / rejected(不可逆)
async/await 本质:Generator + 自动执行器的语法糖。
1.7 深浅拷贝
| 方式 | 深/浅 | 缺点 |
|---|---|---|
Object.assign / 扩展运算符 |
浅 | 嵌套对象仍是引用 |
JSON.parse(JSON.stringify()) |
深 | 丢失函数、Symbol、循环引用报错 |
structuredClone() |
深 | 现代浏览器最佳选择 |
| 手写递归 + WeakMap | 深 | 能处理循环引用 |
function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (map.has(obj)) return map.get(obj);
const clone = Array.isArray(obj) ? [] : {};
map.set(obj, clone);
for (const key in obj) {
if (obj.hasOwnProperty(key)) clone[key] = deepClone(obj[key], map);
}
return clone;
}
二、TypeScript 进阶
2.1 基础类型
let n: number = 1;
let arr: string[] = ['a'];
let tuple: [string, number] = ['a', 1];
type Status = 'success' | 'error';
type Both = A & B;
2.2 interface vs type
| interface | type | |
|---|---|---|
| 同名合并 | ✅ | ❌ |
| 联合/交叉 | ❌ | ✅ |
| 描述对象 | ✅ | ✅ |
经验:定义对象用 interface,定义类型组合用 type。
2.3 泛型
function identity<T>(arg: T): T { return arg; }
interface ApiResponse<T> {
code: number;
data: T;
}
function getLen<T extends { length: number }>(arg: T) {
return arg.length;
}
2.4 内置工具类型(必背)
Partial<T> // 所有属性变可选
Required<T> // 所有属性变必填
Readonly<T> // 所有属性只读
Pick<T, K> // 挑选指定属性
Omit<T, K> // 排除指定属性
Record<K, V> // 构造键值对类型
ReturnType<T> // 取函数返回值类型
Parameters<T> // 取函数参数类型
2.5 类型守卫
function pad(value: string | number) {
if (typeof value === 'string') return value.padStart(2, '0');
}
function isString(x: unknown): x is string {
return typeof x === 'string';
}
三、CSS 与样式工程化
3.1 盒模型
* { box-sizing: border-box; }
3.2 BFC(块级格式化上下文)
触发:overflow: hidden / display: flex/grid / position: absolute / float
用途:清除浮动、防止 margin 重叠、自适应两栏布局
3.3 Flex 必备
.parent {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.child { flex: 1; }
3.4 Grid 必备
.parent {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
3.5 居中 6 种方式
.parent { display: flex; justify-content: center; align-items: center; }
.parent { display: grid; place-items: center; }
.child { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); }
.child { position: absolute; inset: 0; margin: auto; }
3.6 移动端适配
| 方案 | 原理 |
|---|---|
| rem | 根据 html 字体大小动态计算 |
| vw/vh | 视口百分比 |
| postcss-px-to-viewport | px 自动转 vw |
3.7 重排 vs 重绘
| 重排 | 重绘 | |
|---|---|---|
| 触发 | 几何属性变化 | 颜色等非几何属性 |
| 开销 | 大 | 小 |
优化:使用 transform/opacity 触发 GPU 加速
四、HTTP 与浏览器原理
4.1 输入 URL 到页面渲染
1. URL 解析 → 2. DNS → 3. TCP 三次握手 → 4. TLS 握手
→ 5. HTTP 请求 → 6. 返回 HTML → 7. 解析 DOM → 8. 解析 CSS
→ 9. Render Tree → 10. Layout → 11. Paint → 12. Composite
4.2 HTTP 缓存
| 类型 | 字段 | 状态码 |
|---|---|---|
| 强缓存 | Cache-Control / Expires | 200 (from cache) |
| 协商缓存 | ETag / Last-Modified | 304 |
4.3 跨域
-
CORS(推荐):后端加
Access-Control-Allow-Origin - 代理:开发用 Vite proxy,生产用 Nginx
- postMessage:iframe 间通信
4.4 HTTP/1.1 vs HTTP/2 vs HTTP/3
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| 传输 | 文本 | 二进制 | 二进制 |
| 多路复用 | ❌ | ✅ | ✅ |
| 队头阻塞 | 有 | TCP 层有 | 解决(QUIC) |
4.5 浏览器存储
| cookie | localStorage | sessionStorage | IndexedDB | |
|---|---|---|---|---|
| 容量 | 4KB | ~5MB | ~5MB | 无限 |
| 生命周期 | 可设置 | 永久 | 标签页 | 永久 |
| 随请求发送 | ✅ | ❌ | ❌ | ❌ |
4.6 HTTPS 握手
1. Client Hello(加密套件、随机数1)
2. Server Hello(证书、随机数2)
3. 客户端验证证书 → pre-master-secret 用公钥加密发送
4. 双方生成对称密钥
5. 后续用对称密钥加密通信
五、浏览器原理深挖
5.1 V8 引擎
源代码 → AST → 字节码(Ignition)→ 监控热点 → TurboFan 优化 → 机器码
优化:隐藏类、内联缓存、逃逸分析
给开发者的建议:
- 对象保持相同的属性顺序
- 不要中途增删属性
- 数组保持同一类型
5.2 垃圾回收
| 引用计数 | 标记清除 | |
|---|---|---|
| 原理 | 引用为 0 即垃圾 | 从根遍历,未到达即垃圾 |
| 缺点 | 循环引用 | STW |
V8 分代回收:
- 新生代(Scavenge 复制算法)
- 老生代(Mark-Sweep + Mark-Compact)
内存泄漏排查:
- 全局变量
- 未清理的定时器/事件监听
- 闭包持有大对象
- DOM 引用未释放
5.3 浏览器多进程架构
浏览器主进程
├── 网络进程
├── GPU 进程
├── 渲染进程(每 Tab 一个,沙箱隔离)
└── 插件进程
5.4 渲染流水线
HTML → DOM 树
CSS → CSSOM 树
DOM + CSSOM → Render Tree
→ Layout(布局)
→ Paint(绘制)
→ Composite(合成)
触发 GPU 加速:transform / opacity / filter / will-change
5.5 Service Worker / PWA
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
// sw.js
self.addEventListener('fetch', (e) => {
e.respondWith(caches.match(e.request).then(r => r || fetch(e.request)));
});
PWA 三大特性:可安装 / 离线可用 / 推送通知
六、网络协议补全
6.1 TCP 三次握手
客户端 ─── SYN(seq=x) ──→ 服务端
客户端 ←── SYN+ACK ── 服务端
客户端 ─── ACK ──→ 服务端
为什么不是两次:防止历史连接干扰新连接。
6.2 TCP 四次挥手
客户端 ─── FIN ──→ 服务端
客户端 ←── ACK ── 服务端
客户端 ←── FIN ── 服务端
客户端 ─── ACK ──→ 服务端
TIME_WAIT (2MSL) → 关闭
TIME_WAIT 作用:等最后 ACK 到达 / 让旧报文消失。
6.3 DNS 解析
浏览器缓存 → 系统缓存 → 本地 DNS → 根 DNS → 顶级 DNS → 权威 DNS
优化:
- DNS 预解析:
<link rel="dns-prefetch" href="//cdn.com"> - HTTP DNS(绕过运营商劫持)
6.4 WebSocket 握手报文
请求:
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...
响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Accept: ...
6.5 HTTP 状态码
| 类别 | 含义 | 常见 |
|---|---|---|
| 1xx | 信息 | 100 / 101 |
| 2xx | 成功 | 200 / 201 / 204 |
| 3xx | 重定向 | 301 / 302 / 304 |
| 4xx | 客户端错误 | 400 / 401 / 403 / 404 / 429 |
| 5xx | 服务端错误 | 500 / 502 / 504 |
七、性能优化
7.1 首屏优化清单
| 阶段 | 优化点 |
|---|---|
| 请求 | DNS 预解析、CDN、HTTP/2、Gzip、缓存 |
| 加载 | 路由懒加载、组件异步、图片懒加载、骨架屏 |
| 渲染 | SSR、关键 CSS 内联、虚拟滚动 |
| 构建 | Tree Shaking、代码分割、按需引入 |
7.2 长列表优化
虚拟滚动:根据 scrollTop 计算应该渲染哪些行,其他用占位。
7.3 图片优化
- WebP / AVIF 格式
-
loading="lazy"原生懒加载 -
srcset+sizes响应式 - tinypng、imagemin 压缩
7.4 Web Vitals
| 指标 | 含义 | 目标 |
|---|---|---|
| FCP | 首次内容绘制 | < 1.8s |
| LCP | 最大内容绘制 | < 2.5s |
| FID/INP | 首次输入延迟 | < 100ms |
| CLS | 累积布局偏移 | < 0.1 |
| TTFB | 首字节时间 | < 600ms |
7.5 防抖节流
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
function throttle(fn, delay) {
let last = 0;
return function(...args) {
const now = Date.now();
if (now - last >= delay) {
fn.apply(this, args);
last = now;
}
};
}
八、手写题专项
8.1 手写 new
function myNew(Constructor, ...args) {
const obj = Object.create(Constructor.prototype);
const result = Constructor.apply(obj, args);
return result instanceof Object ? result : obj;
}
8.2 手写 instanceof
function myInstanceof(left, right) {
let proto = Object.getPrototypeOf(left);
while (proto) {
if (proto === right.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
8.3 手写 call / apply / bind
Function.prototype.myCall = function (context, ...args) {
context = context || window;
const fn = Symbol('fn');
context[fn] = this;
const result = context[fn](...args);
delete context[fn];
return result;
};
Function.prototype.myApply = function (context, args = []) {
context = context || window;
const fn = Symbol('fn');
context[fn] = this;
const result = context[fn](...args);
delete context[fn];
return result;
};
Function.prototype.myBind = function (context, ...args) {
const fn = this;
return function F(...newArgs) {
if (this instanceof F) return new fn(...args, ...newArgs);
return fn.apply(context, [...args, ...newArgs]);
};
};
8.4 手写 Object.create
function myCreate(proto) {
function F() {}
F.prototype = proto;
return new F();
}
8.5 手写 Promise.all / race / allSettled
Promise.myAll = function (promises) {
return new Promise((resolve, reject) => {
const result = [];
let count = 0;
promises.forEach((p, i) => {
Promise.resolve(p).then(
(val) => {
result[i] = val;
if (++count === promises.length) resolve(result);
},
reject
);
});
});
};
Promise.myRace = function (promises) {
return new Promise((resolve, reject) => {
promises.forEach((p) => Promise.resolve(p).then(resolve, reject));
});
};
Promise.myAllSettled = function (promises) {
return Promise.all(
promises.map((p) =>
Promise.resolve(p)
.then((value) => ({ status: 'fulfilled', value }))
.catch((reason) => ({ status: 'rejected', reason }))
)
);
};
8.6 手写 EventEmitter
class EventEmitter {
constructor() {
this.events = {};
}
on(event, fn) {
(this.events[event] ||= []).push(fn);
}
off(event, fn) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter((f) => f !== fn);
}
emit(event, ...args) {
this.events[event]?.forEach((fn) => fn(...args));
}
once(event, fn) {
const wrapper = (...args) => {
fn(...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}
8.7 手写 flat / compose / pipe
function myFlat(arr, depth = 1) {
return depth > 0
? arr.reduce((a, b) => a.concat(Array.isArray(b) ? myFlat(b, depth - 1) : b), [])
: arr.slice();
}
const compose = (...fns) => fns.reduce((a, b) => (...args) => a(b(...args)));
const pipe = (...fns) => fns.reduce((a, b) => (...args) => b(a(...args)));
8.8 数组去重 5 种
[...new Set(arr)] // 1
arr.filter((item, i) => arr.indexOf(item) === i) // 2
arr.reduce((acc, cur) => acc.includes(cur) ? acc : [...acc, cur], []) // 3
// 4. Map + filter(对象去重)
// 5. 双层循环(最差)
8.9 手写 LRU 缓存
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return -1;
const val = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, val);
return val;
}
put(key, val) {
if (this.cache.has(key)) this.cache.delete(key);
else if (this.cache.size >= this.capacity) {
this.cache.delete(this.cache.keys().next().value);
}
this.cache.set(key, val);
}
}
8.10 手写 Ajax
function ajax({ url, method = 'GET', data, headers = {} }) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
Object.keys(headers).forEach(k => xhr.setRequestHeader(k, headers[k]));
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) return;
xhr.status >= 200 && xhr.status < 300
? resolve(JSON.parse(xhr.responseText))
: reject(xhr.statusText);
};
xhr.send(data ? JSON.stringify(data) : null);
});
}
九、算法与数据结构
9.1 双指针 / 滑动窗口
// 两数之和(有序)
function twoSum(nums, target) {
let l = 0, r = nums.length - 1;
while (l < r) {
const sum = nums[l] + nums[r];
if (sum === target) return [l, r];
sum < target ? l++ : r--;
}
}
// 无重复字符最长子串
function lengthOfLongestSubstring(s) {
const map = new Map();
let max = 0, l = 0;
for (let r = 0; r < s.length; r++) {
if (map.has(s[r])) l = Math.max(l, map.get(s[r]) + 1);
map.set(s[r], r);
max = Math.max(max, r - l + 1);
}
return max;
}
9.2 链表
// 反转链表
function reverseList(head) {
let prev = null, curr = head;
while (curr) {
const next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
// 链表是否有环
function hasCycle(head) {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
}
9.3 二叉树遍历
function preorder(root) {
if (!root) return [];
return [root.val, ...preorder(root.left), ...preorder(root.right)];
}
function levelOrder(root) {
if (!root) return [];
const res = [], queue = [root];
while (queue.length) {
const level = [], len = queue.length;
for (let i = 0; i < len; i++) {
const node = queue.shift();
level.push(node.val);
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
res.push(level);
}
return res;
}
9.4 动态规划
// 爬楼梯
function climbStairs(n) {
const dp = [1, 1];
for (let i = 2; i <= n; i++) dp[i] = dp[i - 1] + dp[i - 2];
return dp[n];
}
// 最大子数组和
function maxSubArray(nums) {
let max = nums[0], curr = nums[0];
for (let i = 1; i < nums.length; i++) {
curr = Math.max(nums[i], curr + nums[i]);
max = Math.max(max, curr);
}
return max;
}
9.5 LeetCode Hot 100 题单(前端必刷 16 道)
| 难度 | 题号 | 题目 |
|---|---|---|
| 易 | 1 | 两数之和 |
| 易 | 20 | 有效的括号 |
| 易 | 21 | 合并两个有序链表 |
| 易 | 70 | 爬楼梯 |
| 易 | 121 | 买卖股票最佳时机 |
| 易 | 136 | 只出现一次的数字 |
| 易 | 206 | 反转链表 |
| 中 | 3 | 无重复字符最长子串 |
| 中 | 5 | 最长回文子串 |
| 中 | 15 | 三数之和 |
| 中 | 53 | 最大子数组和 |
| 中 | 200 | 岛屿数量 |
| 中 | 215 | 数组中第K大 |
| 中 | 300 | 最长递增子序列 |
| 难 | 42 | 接雨水 |
| 难 | 76 | 最小覆盖子串 |
本篇完。下一篇:02-框架篇
前端面试复习|框架篇
涵盖:Vue3 / Vue2 对比 / Pinia / Vue Router / Vite / uni-app / Element-Plus / 微前端 / SSR / 测试
复习时长建议:1 周
📑 目录
- Vue3 深入
- Vue2 vs Vue3 对比
- 状态管理
- Vue Router
- Vite & 构建工具
- uni-app 与小程序
- 小程序进阶
- Element-Plus 与组件设计
- 微前端
- 服务端渲染(SSR)
- 测试
- 设计模式
一、Vue3 深入
1.1 响应式原理(核心考点)
Vue2 用 Object.defineProperty:
- 缺点:只能监听已存在属性、数组下标修改不响应、需要递归遍历
Vue3 用 Proxy:
- 优点:拦截 13 种操作、支持新增属性、支持数组、性能更好(懒代理)
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key);
const val = Reflect.get(target, key);
return typeof val === 'object' ? reactive(val) : val;
},
set(target, key, value) {
Reflect.set(target, key, value);
trigger(target, key);
return true;
}
});
}
ref vs reactive:
-
ref:基本类型,需要.value -
reactive:对象/数组,解构会失去响应性(要用toRefs)
1.2 Composition API vs Options API
<script setup>
import { ref, onMounted } from 'vue';
const count = ref(0);
const add = () => count.value++;
onMounted(() => {});
</script>
优势:逻辑复用更好 / 类型推导友好 / 相关逻辑聚合 / Tree-shaking 友好
1.3 生命周期对照
| Vue2 | Vue3 (Composition) | 时机 |
|---|---|---|
| beforeCreate | (setup 本身) | 实例初始化 |
| created | (setup 本身) | data/methods 已注入 |
| beforeMount | onBeforeMount | 挂载前 |
| mounted | onMounted | 挂载完成 |
| beforeUpdate | onBeforeUpdate | 数据更新前 |
| updated | onUpdated | DOM 更新完 |
| beforeDestroy | onBeforeUnmount | 销毁前 |
| destroyed | onUnmounted | 销毁后 |
1.4 computed vs watch vs watchEffect
| computed | watch | watchEffect | |
|---|---|---|---|
| 有返回值 | ✅ | ❌ | ❌ |
| 缓存 | ✅ | ❌ | ❌ |
| 立即执行 | 调用时 | 默认否 | ✅ |
| 知道新旧值 | ❌ | ✅ | ❌ |
| 依赖收集 | 自动 | 显式 | 自动 |
1.5 nextTick
Vue 更新 DOM 是异步批量的。nextTick 把回调推到当前 tick 的微任务队列。
this.count++;
console.log(dom.innerText); // 旧值
nextTick(() => console.log(dom.innerText)); // 新值
1.6 diff 算法(Vue3 优化)
- 静态提升(hoistStatic):静态节点提升到 render 外
- PatchFlag:编译时给动态节点打标记
- 块(Block):缩小 diff 范围
- 最长递增子序列:减少 DOM 移动
1.7 v-if vs v-show
| v-if | v-show | |
|---|---|---|
| 原理 | 删/建 DOM | 切换 display |
| 初始开销 | 低 | 高 |
| 切换开销 | 高 | 低 |
| 场景 | 切换频率低 | 频繁切换 |
1.8 自定义 Hook
export function useCounter(initial = 0) {
const count = ref(initial);
const inc = () => count.value++;
return { count, inc };
}
1.9 Vue3 新特性
<Teleport to="body">
<div class="modal">弹窗</div>
</Teleport>
<Suspense>
<template #default><AsyncComp /></template>
<template #fallback>加载中...</template>
</Suspense>
<UserForm v-model:name="name" v-model:age="age" />
二、Vue2 vs Vue3 对比
| 维度 | Vue2 | Vue3 |
|---|---|---|
| 响应式 | Object.defineProperty | Proxy |
| API | Options API | Composition API |
| 模板根节点 | 必须单根 | Fragment(多根) |
| 生命周期名 | created/mounted | onMounted... |
| TypeScript | 支持有限 | 原生设计 |
| 包体积 | ~30KB | ~10KB |
| 性能 | baseline | +30~50% |
| 新特性 | - | Teleport / Suspense |
三、状态管理
3.1 Pinia
export const useUserStore = defineStore('user', {
state: () => ({ name: '', token: '' }),
getters: {
isLogin: (state) => !!state.token,
},
actions: {
async login(payload) {
const res = await api.login(payload);
this.token = res.token;
},
},
});
3.2 Pinia vs Vuex
| Vuex | Pinia | |
|---|---|---|
| mutation | 必须 | 取消,直接改 state |
| TypeScript | 体验差 | 完美 |
| 模块化 | 嵌套 modules | 多 store 平级 |
| 体积 | 大 | ~1KB |
3.3 持久化
const useStore = defineStore('app', {
state: () => ({ token: '' }),
persist: true, // pinia-plugin-persistedstate
});
四、Vue Router
4.1 hash vs history
| hash | history | |
|---|---|---|
| URL | /#/home | /home |
| 原理 | onhashchange | history.pushState |
| 服务器配置 | 不需要 | 需要 |
| SEO | 差 | 好 |
4.2 路由守卫
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token');
if (to.meta.requireAuth && !token) next('/login');
else next();
});
4.3 动态路由
const menus = await api.getMenus();
menus.forEach(m => router.addRoute({
path: m.path,
component: () => import(`@/views/${m.component}.vue`),
}));
4.4 路由懒加载
{ path: '/home', component: () => import('@/views/Home.vue') }
五、Vite & 构建工具
5.1 Vite 为什么快
Webpack:先打包整个项目 → 再启动 dev server
Vite:
- 开发:浏览器原生 ESM,不打包,按需编译
- 预构建:用 esbuild 把第三方依赖打成 ESM
- HMR:精准热更新
- 生产:Rollup 打包
| 冷启动 | HMR | |
|---|---|---|
| Webpack | 10~30s | 1~3s |
| Vite | <1s | <100ms |
5.2 Vite 配置
export default defineConfig({
plugins: [vue()],
resolve: { alias: { '@': path.resolve(__dirname, 'src') } },
server: {
port: 3000,
proxy: { '/api': { target: 'http://backend.com', changeOrigin: true } },
},
build: {
rollupOptions: {
output: {
manualChunks: {
vue: ['vue', 'vue-router', 'pinia'],
element: ['element-plus'],
},
},
},
},
});
5.3 Webpack 核心概念
- Entry / Output
- Loader(vue-loader、css-loader、babel-loader)
- Plugin(HtmlWebpackPlugin、MiniCssExtractPlugin)
- Tree Shaking
- Code Splitting
六、uni-app 与小程序
6.1 uni-app 原理
一份代码 → 多端编译,编译时通过条件编译 #ifdef MP-WEIXIN ... #endif 区分平台。
6.2 小程序双线程模型
┌─────────────┐ setData ┌─────────────┐
│ 逻辑层 JS │ ───────→ │ 渲染层 WXML │
│ (JsCore/V8) │ ←─────── │ (WebView) │
└─────────────┘ 事件 └─────────────┘
为什么这样设计:安全(不能直接操作 DOM)、性能(双线程并发)
性能要点:
-
setData不要传太大对象、不要频繁调用 - 用
data-而不是闭包传参 - 长列表用
recycle-view
6.3 uni-app vs Taro
| uni-app | Taro | |
|---|---|---|
| 语法 | Vue | React / Vue |
| 学习成本 | Vue 0 成本 | 需熟悉 React |
6.4 分包
{
"subpackages": [
{ "root": "pkgA", "pages": ["page1"] }
],
"preloadRule": {
"pages/index/index": { "packages": ["pkgA"] }
}
}
主包 2MB,单分包 2MB,总包 20MB。
6.5 条件编译
<!-- #ifdef MP-WEIXIN -->
<view>仅微信小程序</view>
<!-- #endif -->
<!-- #ifndef H5 -->
<view>除 H5 外都显示</view>
<!-- #endif -->
七、小程序进阶
7.1 原生 vs uni-app 对比
| 原生 | uni-app | |
|---|---|---|
| 启动速度 | 最快 | 略慢 |
| 包体积 | 最小 | 略大(runtime) |
| 多端支持 | ❌ | ✅ |
| 学习成本 | 高 | 低 |
7.2 自定义组件
Component({
properties: { title: { type: String, value: '' } },
data: { count: 0 },
methods: {
onTap() {
this.setData({ count: this.data.count + 1 });
this.triggerEvent('change', { count: this.data.count });
},
},
});
7.3 跨页面通信
// 1. globalData
getApp().globalData.user = user;
// 2. EventBus
uni.$emit('userUpdate', user);
uni.$on('userUpdate', (user) => {});
// 3. 页面栈通信
const pages = getCurrentPages();
const prev = pages[pages.length - 2];
prev.setData({ result: 'hello' });
7.4 setData 优化
// ❌ 错误
this.setData({ list: largeList });
// ✅ 正确
this.setData({ 'list[0].name': 'new' });
// 分片
function chunkSetData(list) {
const size = 50;
for (let i = 0; i < list.length; i += size) {
setTimeout(() => {
this.setData({ [`list[${i}]`]: list.slice(i, i + size) });
}, 0);
}
}
八、Element-Plus 与组件设计
8.1 二次封装
<template>
<el-table :data="data" v-bind="$attrs">
<el-table-column
v-for="col in columns"
:key="col.prop"
v-bind="col"
>
<template #default="scope" v-if="col.slot">
<slot :name="col.slot" v-bind="scope" />
</template>
</el-table-column>
</el-table>
</template>
封装原则:
- 透传属性
v-bind="$attrs" - 提供具名插槽 + 作用域插槽
- 配置驱动(columns 配置化)
8.2 表单生成器(Schema)
const schema = [
{ type: 'input', prop: 'name', label: '姓名', rules: [...] },
{ type: 'select', prop: 'gender', label: '性别', options: [...] },
];
8.3 按需引入
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
plugins: [
AutoImport({ resolvers: [ElementPlusResolver()] }),
Components({ resolvers: [ElementPlusResolver()] }),
]
体积可从 ~3MB 降到 ~200KB。
九、微前端
9.1 qiankun 原理
核心三件事:
- HTML Entry:以 HTML 为入口,解析 JS/CSS
-
JS 沙箱:
- Proxy 沙箱(默认)
- Snapshot 沙箱(不支持 Proxy 时)
- CSS 隔离:Shadow DOM / Scoped CSS
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'app1',
entry: '//localhost:8081',
container: '#container',
activeRule: '/app1',
},
]);
start();
9.2 Module Federation
// 提供方
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: { './Button': './src/Button' },
});
// 消费方
new ModuleFederationPlugin({
name: 'host',
remotes: { app1: 'app1@http://localhost:8081/remoteEntry.js' },
});
9.3 iframe vs 微前端
| iframe | 微前端 | |
|---|---|---|
| 隔离 | 完美 | 需要框架支持 |
| 通信 | postMessage | 多种 |
| URL 同步 | 麻烦 | 友好 |
| 性能 | 重 | 轻 |
十、服务端渲染(SSR)
10.1 CSR / SSR / SSG 对比
| CSR | SSR | SSG | |
|---|---|---|---|
| 渲染时机 | 浏览器 | 每次请求服务端 | 构建时 |
| 首屏 | 慢 | 快 | 极快 |
| SEO | 差 | 好 | 好 |
| 服务器压力 | 低 | 高 | 极低 |
| 适用 | SPA 后台 | 内容站 | 博客、文档 |
10.2 Nuxt 3 基础
<script setup>
const route = useRoute();
const { data: user } = await useFetch(`/api/users/${route.params.id}`);
</script>
10.3 Hydration(同构)
服务端渲染 HTML → 浏览器显示静态 HTML(首屏快)
↓
下载 JS Bundle
↓
hydrate:附加事件监听器
↓
变成可交互应用
水合不匹配:服务端和客户端结果不一致 → 报错。常见原因:使用 Date.now()、Math.random()、浏览器 API。
十一、测试
11.1 测试金字塔
/\
/E2E\ 少而慢
/─────\
/集成测试\
/─────────\
/ 单元测试 \ 多而快
/─────────────\
11.2 Vitest
import { describe, it, expect } from 'vitest';
import { sum } from './sum';
describe('sum', () => {
it('1 + 2 = 3', () => {
expect(sum(1, 2)).toBe(3);
});
});
11.3 Vue Test Utils
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';
it('点击 +1', async () => {
const wrapper = mount(Counter);
await wrapper.find('button').trigger('click');
expect(wrapper.text()).toContain('1');
});
11.4 Playwright (E2E)
test('登录流程', async ({ page }) => {
await page.goto('/login');
await page.fill('#username', 'admin');
await page.fill('#password', '123456');
await page.click('button[type=submit]');
await expect(page).toHaveURL('/dashboard');
});
11.5 覆盖率
vitest run --coverage
四种覆盖率:行 / 分支 / 函数 / 语句,团队一般 70%+。
十二、设计模式
12.1 观察者 vs 发布订阅
| 观察者 | 发布订阅 | |
|---|---|---|
| 是否有中介 | ❌ | ✅(事件中心) |
| 例子 | Vue 响应式 | EventBus |
12.2 单例
class Singleton {
static instance = null;
static getInstance() {
if (!this.instance) this.instance = new Singleton();
return this.instance;
}
}
12.3 工厂
class UserFactory {
static create(type) {
switch (type) {
case 'admin': return new AdminUser();
case 'guest': return new GuestUser();
}
}
}
12.4 装饰器
function log(target, key, descriptor) {
const original = descriptor.value;
descriptor.value = function (...args) {
console.log(`Call ${key}`);
return original.apply(this, args);
};
}
class Service {
@log
fetch() {}
}
12.5 策略模式
const strategies = {
vip: (p) => p * 0.8,
svip: (p) => p * 0.6,
normal: (p) => p,
};
const calc = (type, price) => strategies[type](price);
本篇完。下一篇:03-项目篇
前端面试复习|项目篇
涵盖:实时通信 / ECharts / 地图 SDK / 工程化 / 项目难点话术 / 简历项目 Q&A
复习时长建议:1 周
📑 目录
- 实时通信(WebSocket / WebRTC / MQTT)
- ECharts 数据可视化
- 地图 SDK 与电子围栏
- 前端工程化
- 低代码 / 中后台脚手架
- Node.js / BFF
- 项目难点话术(STAR)
- 简历项目延伸 Q&A
一、实时通信
1.1 WebSocket
为什么需要:HTTP 是请求-响应模式,服务器无法主动推送。WebSocket 是全双工长连接。
const ws = new WebSocket('wss://api.com');
ws.onopen = () => ws.send('hello');
ws.onmessage = (e) => console.log(e.data);
ws.onclose = () => reconnect();
ws.onerror = () => {};
生产级要点:
- 心跳:定时发 ping,30s 没收到 pong 就重连
- 断线重连:指数退避(1s、2s、4s、8s...)
- 消息队列:断线期间消息暂存,重连后补发
- 鉴权:URL 带 token 或第一条消息发 auth
完整封装:
class SocketClient {
constructor(url) {
this.url = url;
this.reconnectTimes = 0;
this.heartTimer = null;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.reconnectTimes = 0;
this.startHeartbeat();
};
this.ws.onclose = () => this.reconnect();
}
startHeartbeat() {
this.heartTimer = setInterval(() => {
this.ws.send(JSON.stringify({ type: 'ping' }));
}, 30000);
}
reconnect() {
clearInterval(this.heartTimer);
const delay = Math.min(1000 * 2 ** this.reconnectTimes, 30000);
setTimeout(() => {
this.reconnectTimes++;
this.connect();
}, delay);
}
}
1.2 WebRTC
用途:浏览器间 P2P 音视频传输,1V1 通话、屏幕共享、实时直播。
三大核心 API:
| API | 作用 |
|---|---|
getUserMedia() |
获取摄像头/麦克风流 |
RTCPeerConnection |
建立 P2P 连接 |
RTCDataChannel |
任意数据通道 |
信令流程:
A → 创建 Offer → 信令服务器 → B
B → 创建 Answer → 信令服务器 → A
A、B 互换 ICE Candidate → P2P 通道建立
多路视频卡顿优化:
- SFU 架构(服务器转发不合流)
- 自适应码率
- 关键帧请求(PLI)
- 硬件解码
1.3 MQTT
为什么用 MQTT 而不用 WebSocket:
- 极轻量(最小 2 字节头)
- 发布订阅模型,适合海量设备
- 支持 QoS(0/1/2 三级)
- 遗嘱消息
核心概念:
| 概念 | 解释 |
|---|---|
| Broker | 消息中转服务器(EMQX、Mosquitto) |
| Topic | 主题(如 farm/cow1/temperature) |
| QoS 0 | 最多一次(可能丢) |
| QoS 1 | 至少一次(可能重复) |
| QoS 2 | 仅一次(最可靠最慢) |
import mqtt from 'mqtt';
const client = mqtt.connect('wss://broker.com');
client.subscribe('farm/+/temperature'); // + 单层通配
client.on('message', (topic, payload) => {});
client.publish('farm/cow1/cmd', 'feed');
二、ECharts 数据可视化
2.1 基础使用
import * as echarts from 'echarts';
const chart = echarts.init(document.getElementById('main'));
chart.setOption({
xAxis: { type: 'category', data: ['周一', '周二'] },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: [120, 200] }],
});
2.2 性能优化
| 场景 | 方案 |
|---|---|
| 大数据(>1万) |
appendData 增量加载 |
| 多图表 | 共享主题 + 懒加载 |
| 体积大 | 按需引入 |
| resize 不灵 | 监听 resize + chart.resize() |
按需引入:
import * as echarts from 'echarts/core';
import { BarChart } from 'echarts/charts';
import { GridComponent, TooltipComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([BarChart, GridComponent, TooltipComponent, CanvasRenderer]);
体积从 ~1MB 降到 ~200KB。
2.3 常踩坑
-
setOption默认合并,要清空用chart.clear() - 数据切换时 series 数量变化要
setOption(option, true) - v-if 销毁后要重新 init
- 容器宽高为 0 时初始化失败
2.4 Vue3 封装
<template>
<div ref="chartRef" :style="{ width, height }"></div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
import * as echarts from 'echarts';
const props = defineProps(['option', 'width', 'height']);
const chartRef = ref();
let chart = null;
onMounted(() => {
chart = echarts.init(chartRef.value);
chart.setOption(props.option);
window.addEventListener('resize', () => chart.resize());
});
watch(() => props.option, (val) => chart?.setOption(val), { deep: true });
onBeforeUnmount(() => {
chart?.dispose();
});
</script>
2.5 大屏适配
.bigscreen {
width: 1920px;
height: 1080px;
transform: scale(var(--scale));
transform-origin: left top;
}
function setScale() {
const scale = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
document.documentElement.style.setProperty('--scale', scale);
}
window.addEventListener('resize', setScale);
2.6 主题定制
echarts.registerTheme('myDark', {
color: ['#5470c6', '#91cc75'],
backgroundColor: '#1a1a2e',
textStyle: { color: '#fff' },
});
echarts.init(dom, 'myDark');
三、地图 SDK 与电子围栏
3.1 高德 vs 腾讯
| 高德 | 腾讯 | |
|---|---|---|
| 国内数据 | ★★★★★ | ★★★★ |
| 海外 | ★★ | ★ |
| 文档 | 优秀 | 一般 |
3.2 核心能力
const map = new AMap.Map('container', { zoom: 11, center: [116.4, 39.9] });
// 标记
new AMap.Marker({ position: [lng, lat], map });
// 路径规划
new AMap.Driving().search([from, to], (status, result) => {});
// 地理编码
new AMap.Geocoder().getLocation(address, callback);
// 电子围栏判定
AMap.GeometryUtil.isPointInRing([lng, lat], polygonPath);
3.3 电子围栏
是什么:在地图上画一个虚拟"圈",判断设备/用户进入或离开 → 触发动作。
业务流程:
- 商家用
MouseTool.polygon()绘制多边形 - 后端持久化围栏数据
- 用户下单 → 取地址坐标 → 用
isPointInRing判断
完整代码:
// 1. 商家绘制
const mouseTool = new AMap.MouseTool(map);
mouseTool.polygon({
fillColor: '#1791fc',
strokeColor: '#1791fc',
});
mouseTool.on('draw', (event) => {
const path = event.obj.getPath();
const coords = path.map(p => [p.lng, p.lat]);
saveToBackend(coords);
});
// 2. 用户下单判定
const isInside = AMap.GeometryUtil.isPointInRing(
[userLng, userLat],
polygonPath
);
if (!isInside) alert('超出服务范围');
坐标系坑:
| 坐标系 | 用于 |
|---|---|
| WGS84 | GPS 原始 |
| GCJ02 | 高德、腾讯(火星坐标) |
| BD09 | 百度 |
后端拿到 GPS 坐标要先转 GCJ02 再画到高德地图,否则偏移几百米。
应用场景:
- 外卖配送范围
- 共享单车运营区/禁停区
- 门店服务范围(上门取件)
- 车辆调度
- 儿童老人定位手表
四、前端工程化
4.1 模块化历史
CommonJS(Node)→ AMD/CMD(已弃)→ ES Module
// CommonJS
const fs = require('fs');
module.exports = {};
// ESM
import fs from 'fs';
export default {};
4.2 代码规范
# ESLint:代码错误
# Prettier:代码格式
# Husky:Git hooks
# lint-staged:只检查暂存区
# Commitlint:commit 规范
.husky/pre-commit:
npx lint-staged
4.3 Git 协作
| 命令 | 用途 |
|---|---|
git rebase -i |
合并/修改历史 commit |
git cherry-pick |
挑某个 commit |
git stash |
暂存当前修改 |
git reset --soft/--hard |
软/硬重置 |
git revert |
反向 commit |
Git Flow:
master ←─ release ←─ develop ←─ feature/xxx
←─ hotfix/xxx
4.4 Monorepo
工具:pnpm workspace、Turborepo、Nx、Lerna
好处:多包共享依赖、统一版本、原子提交
4.5 CI/CD 流程
name: Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: pnpm install
- run: pnpm lint
- run: pnpm test
- run: pnpm build
- name: Deploy
run: rsync -avz dist/ user@server:/var/www/
五、低代码 / 中后台脚手架
5.1 表单生成器(Schema 驱动)
const schema = [
{
type: 'input',
field: 'username',
label: '用户名',
rules: [{ required: true, message: '必填' }],
},
{
type: 'select',
field: 'role',
label: '角色',
options: [{ label: '管理员', value: 'admin' }],
},
];
function renderForm(schema) {
return schema.map(item => {
switch (item.type) {
case 'input': return h(ElInput, { ... });
case 'select': return h(ElSelect, { ... });
}
});
}
5.2 RBAC 权限设计
用户 ─── 多对多 ─── 角色 ─── 多对多 ─── 权限
前端三层权限:
- 路由级:动态注册可访问路由
- 菜单级:根据权限渲染菜单
-
按钮级:自定义指令
v-permission="'user:delete'"
const permission = {
mounted(el, binding) {
const { value } = binding;
const userPerms = useUserStore().perms;
if (!userPerms.includes(value)) {
el.parentNode?.removeChild(el);
}
},
};
5.3 多租户
| 模式 | 隔离 | 复杂度 |
|---|---|---|
| 独立数据库 | 完全 | 高 |
| 共享库独立 schema | 中 | 中 |
| 共享库共享表(tenant_id) | 低 | 低 |
前端要点:登录确定租户 + 请求带 X-Tenant-Id 头 + 子域名区分。
六、Node.js / BFF
6.1 事件循环(与浏览器差异)
Node.js Event Loop 6 个阶段:
timers → pending → idle → poll → check → close
与浏览器差异:
- Node 11.x+ 微任务穿插在每个阶段(和浏览器一致)
- 多了
setImmediate、process.nextTick -
process.nextTick优先级最高
6.2 Buffer / Stream
const buf = Buffer.from('hello');
console.log(buf.toString('hex'));
fs.createReadStream('big.txt').pipe(fs.createWriteStream('copy.txt'));
4 种流:Readable / Writable / Duplex / Transform
6.3 Express 中间件
const app = express();
app.use((req, res, next) => {
console.log('日志');
next();
});
app.get('/users', (req, res) => res.json([]));
app.listen(3000);
Koa 洋葱模型:请求 → mw1 前 → mw2 前 → 处理 → mw2 后 → mw1 后 → 响应
6.4 BFF(Backend For Frontend)
app.get('/api/dashboard', async (req, res) => {
const [user, orders, stats] = await Promise.all([
fetch('/user-service/info').then(r => r.json()),
fetch('/order-service/recent').then(r => r.json()),
fetch('/stat-service/today').then(r => r.json()),
]);
res.json({ user, orders, stats });
});
6.5 npm 包发布
npm init
npm login
npm publish
npm version patch # 1.0.0 → 1.0.1
npm version minor # 1.0.0 → 1.1.0
npm version major # 1.0.0 → 2.0.0
七、项目难点话术(STAR)
7.1 STAR 法则
- S Situation:业务背景
- T Task:你要解决什么
- A Action:你做了什么(技术细节)
- R Result:结果(有数字)
7.2 难点 1:多端架构统一
场景:云护项目要同时支持 Web 后台 + 小程序 + 公众号 H5
问题:三端代码大量重复
方案:抽象业务层(API、Store、工具函数)成 npm 包,UI 层用 uni-app + 条件编译适配差异
结果:跨端复用率 70%+,新功能开发效率提升 40%
7.3 难点 2:长列表性能优化
场景:监控页面要展示 1000+ 设备实时状态
问题:直接渲染浏览器卡死
方案:虚拟滚动(el-table-v2)+ requestAnimationFrame 节流 + Web Worker 处理数据
结果:FPS 从 25 提升到稳定 60,内存占用降低 60%
7.4 难点 3:首屏加载优化
场景:长慈系统首屏 4.2s 用户投诉
排查:Lighthouse + Performance 面板定位,发现 ECharts 全量引入 + 图表组件同步加载
方案:路由懒加载 + ECharts 按需引入 + Gzip + 关键 CSS 内联
结果:首屏 4.2s → 1.3s,Lighthouse 58 → 92
7.5 难点 4:WebRTC 多路视频卡顿
场景:云护值守端要同屏看 16 路 1080P 视频
问题:客户端 CPU 跑满
方案:协商时降低非焦点画面分辨率(动态码率)+ 启用硬件解码 + 滚动可视区外暂停解码
结果:CPU 占用降低 50%,16 路同屏稳定运行
7.6 难点 5:MQTT 长连接稳定性
场景:牛轻松 App 在弱网环境下 MQTT 频繁断连
方案:心跳间隔动态调整(弱网延长)+ 指数退避重连 + 断线期间消息本地暂存 + QoS 1
结果:弱网下指令到达率从 78% 提升到 99.5%
八、简历项目延伸 Q&A
8.1 通用追问 10 个角度
每个项目准备这 10 个角度:
- 业务背景:to B 还是 to C?多少用户?
- 你的角色:独立做的?团队几个人?
- 技术选型理由:为什么用 Vue3 不用 React?
- 架构设计:项目目录结构?模块划分?
- 难点:最大技术挑战?
- 数据:提升数据怎么测的?工具?
- 协作:和后端/UI 怎么协作?
- 上线:怎么发布?灰度?回滚?
- 复盘:如果重做你会怎么改?
- 延伸:最自豪的一行代码/设计?
8.2 高频问题
Q:为什么用 Vue3 不用 React?
三个角度:
- 团队成本:团队 Vue 经验更多,React Hooks 学习曲线对部分成员陡峭
- 生态匹配:项目用了大量 Element-Plus 组件,Vue 生态更顺
- 维护成本:Vue 双向绑定和模板语法对业务开发者更直观
高交互复杂应用(Figma 这种),React + 类型系统可能更适合。
Q:为什么用 uni-app 不用 Taro?
团队是 Vue 技术栈,uni-app 0 学习成本。Taro 虽然 React 写法更现代,但需要培训。
项目主要面向小程序和 App,uni-app 微信生态适配更成熟。
Q:WebRTC 你做到什么深度?
- 业务层:完整对接信令、SDP 协商、ICE 流程
- 优化层:处理过多路视频卡顿
- 底层原理:理解 STUN/TURN/ICE
- 不擅长:底层协议修改、自研信令服务器(团队有专人)
诚实地标记自己的边界。
Q:项目的并发上限?
前端:1000+ 表格行 + 10 路视频流 + 实时数据推送时仍能保持 60FPS
后端:单接口 QPS 在 2000 左右(团队压测报告)
Q:项目的代码量?
- 我主导的项目代码量约 X 万行(cloc 统计)
- X 个核心模块,X 个通用组件
- 我个人贡献者占 70%+
8.3 5 个项目预设追问
云护值守
- WebRTC 信令服务器是谁搭的?
- 16 路视频同屏的硬件配置?
- 损盗率 0.003% 怎么测出来的?
长慈系统
- ECharts 按需引入加了哪些模块?
- 一对一客服 WebSocket 怎么解决断线重连?
- Lighthouse 92 分怎么测的?
牛轻松
- MQTT QoS 用的几级?为什么?
- iOS WebView 内存泄漏怎么定位的?
- App 上架审核遇到过什么坑?
犀团
- Vite 重构具体迁移了哪些 Webpack 配置?
- SKU 五维规格怎么管理 UI 复杂度?
- Web Worker 处理 Excel 用什么库?
通洗
- Vue-cli 升级 Vite 怎么保证零中断?
- 电子围栏多边形坐标怎么持久化?
- 商家端模板复用怎么实现?
本篇完。下一篇:04-软实力篇
前端面试复习|软实力篇
涵盖:自我介绍 / 离职原因 / 期望薪资 / 反向提问 / AI 工具 / 简历优化 / 面试礼仪
复习时长建议:2~3 天
📑 目录
一、自我介绍三版本
1.1 30 秒版(开场)
您好,我是 XXX,X 年前端开发经验,主要技术栈是 Vue3 + TypeScript + Vite。最近一段在 X 公司,主要负责多端业务(Web + 小程序 + App)的开发与架构。期待加入贵司从事更有挑战的工作。
1.2 1 分钟版(标准)
您好,我是 XXX,X 年前端经验。
技术栈:精通 Vue3/Vue2 + TS + Vite,熟悉 uni-app 跨端开发、ECharts 数据可视化、WebSocket/MQTT 实时通信。
项目亮点:在云护项目中,独立负责三端架构,跨端复用率达 70%,并集成 WebRTC 解决了 16 路视频同屏卡顿问题;在长慈项目中,将首屏从 4.2s 优化到 1.3s。
个人特点:喜欢深挖原理(最近在啃 Vue3 源码)、关注 AI 编程趋势(Cursor 重度用户)。期待加入贵司。
1.3 3 分钟版(深聊)
在 1 分钟基础上加:
- 1 个最有成就感的项目(用 STAR 详细讲)
- 团队协作经历
- 未来 3 年规划
3 年规划话术:
短期 1 年:在贵司平台快速上手核心业务,承担 1-2 个完整项目
中期 2 年:往技术架构方向发展,能独立设计中型系统
长期 3 年:成为团队技术骨干,能带团队完成大型项目
二、离职原因话术
2.1 ✅ 正确版本(积极向上)
- "上家公司业务方向调整,前端团队规模变小,可成长空间有限"
- "想从业务前端往架构方向发展,贵司平台更适合"
- "希望加入更有技术深度的团队,做更有挑战的事"
- "上家公司主要做后台系统,想在 C 端产品上有更多积累"
- "希望加入业务和技术双轮驱动的团队"
2.2 ❌ 避免
- 抱怨领导/同事/加班
- 钱少(即使是真的,1 面也不要说)
- 上家公司不行(贬低前公司 = 贬低自己的选择)
- "公司不重视前端" → 暴露负面情绪
- "项目没意思" → 显得挑剔
2.3 不同情境的应对
| 情境 | 话术 |
|---|---|
| 公司倒闭/裁员 | 实话实说,强调主动找下家、行业内常见 |
| 跳槽频繁 | 提前准备解释,强调每次跳槽的成长曲线 |
| 空窗期长 | 解释做了什么(学习、自由职业、家庭原因) |
| 主动离职 | 强调正向追求(成长、平台、方向) |
三、期望薪资谈判
3.1 三步话术
- 先反问:贵司这个岗位的预算范围是多少?
- 如果对方坚持你先说:根据我的经验和市场情况,期望是 X-Y 区间,最终看 offer 综合(base、月数、股票、五险一金、福利)
- 抛锚:报价比目标高 20%,留出谈判空间
3.2 报价依据
- 上一份薪资 +20~30%
- 同岗位市场价(拉钩、Boss 直聘均值)
- 自身核心竞争力溢价
- 行业景气度(互联网寒冬要保守)
3.3 综合 Offer 评估
| 维度 | 权重 | 备注 |
|---|---|---|
| Base 月薪 | 40% | 最基础 |
| 月数(13/14/16) | 15% | 年终影响大 |
| 股票/期权 | 15% | 上市公司硬通货 |
| 五险一金基数 | 10% | 长期影响大 |
| 福利(餐补/通勤/医疗) | 10% | 隐性收入 |
| 调薪/晋升机制 | 10% | 未来发展 |
3.4 谈判禁区
- ❌ 不要在第一轮面试就谈钱
- ❌ 不要给出"死"的数字("我必须 30K")
- ❌ 不要拿 offer 互相比价(除非真有)
- ❌ 不要威胁 HR("不到 X 我就走")
四、"你最大的缺点"
4.1 ✅ 正确策略
说一个真缺点,但不致命,并展示改进意识:
我有时会过于追求代码完美,比如一个组件会反复重构 2-3 次。我意识到这会影响交付节奏,最近开始主动设定"够用就好"的边界,先交付再迭代。
4.2 备选回答
版本 A(执行偏完美):
写代码时容易陷入细节,会反复打磨一个组件直到自己满意。最近在学着平衡——MVP 优先,迭代优化。
版本 B(沟通偏含蓄):
面对意见冲突时容易先妥协,事后才发现自己的方案更合适。最近在练习用文档/数据来表达观点,让讨论更高效。
版本 C(视野偏技术):
之前过于关注技术实现,对业务理解不够深。最近主动参与产品评审,带着业务视角去做技术决策。
4.3 ❌ 避免
- "我没缺点" → 自大
- "我太追求完美了" → 套话/装
- "我执行力太强容易加班" → 装
- "我太热爱学习" → 假
- 真致命缺点(沟通差、不会团队协作)
五、团队冲突处理
5.1 STAR 模板答法
S:上次我和后端在接口设计上有分歧——他想用 RESTful,我觉得 GraphQL 更适合复杂查询场景
T:需要在 1 周内对齐方案
A:我没强推自己的观点,而是写了一份对比文档(性能、学习成本、生态、迁移成本),约了一次评审会,邀请技术负责人一起决策
R:最终采纳了 RESTful(团队学习成本低 + 现有工具链支持),我也认可了这个决定。事后我发现合作关键不是"谁对谁错",而是"谁能让团队对齐"
5.2 关键原则
- 对事不对人:避免用"他怎么样",用"这个方案怎么样"
- 数据说话:拿出文档、benchmark、用户数据
- 承认对方合理性:先肯定对方观点中的合理部分
- 以团队目标为锚:不是为了赢,而是为了项目好
5.3 与不同角色冲突的应对
| 角色 | 常见冲突 | 应对要点 |
|---|---|---|
| 后端 | 接口设计 / 字段命名 | 提前对齐文档,用 OpenAPI |
| UI | 还原度 / 交互细节 | 早期介入设计阶段 |
| 产品 | 需求变更 | 评审时锁定,变更走流程 |
| 测试 | Bug 定级 / 边界情况 | 复现步骤共建,规则透明 |
| 老板 | 工期 / 优先级 | 提供选项 + 风险评估,让老板决策 |
六、反向提问(加分)
6.1 技术维度
- 团队的技术栈和最近一年的演进方向?
- 团队的代码 review 流程是怎样的?
- 团队怎么平衡技术债和业务交付?
- 团队最近一次技术升级 / 重构是什么?
- 团队的测试覆盖率要求?
6.2 团队维度
- 团队规模和构成?前后端比例?
- 团队 onboarding 流程?
- 团队氛围怎么样?加班情况?
- 团队最近一年人员流动如何?
- 团队的技术分享 / 学习机制?
6.3 岗位维度
- 这个岗位 3-6 个月希望我做出什么成果?
- 这个岗位最大的挑战是什么?
- 您觉得我哪些经历最匹配这个岗位?
- 团队对这个岗位的预期是什么?
- 入职后第一个项目是什么?
6.4 公司维度(HR 面)
- 公司的晋升机制是怎样的?
- 调薪频率和依据?
- 公司的福利体系?
- 公司未来 1-2 年的方向?
- 公司前端团队整体规模?
6.5 ❌ 避免的提问
- "贵司加班吗?"(直接显得怕加班)
- "工资是多少?"(应等到 HR 主动谈)
- "面我的人是谁?"(面试官自己会介绍)
- 任何能从官网搜到的问题
七、AI 编程工具加分项
7.1 工具对比
| 工具 | 强项 | 适合 |
|---|---|---|
| Cursor | IDE 集成、Agent 能力 | 日常开发 |
| Claude Code | 终端 / CLI、长上下文 | 重构、文档生成 |
| Codex | 命令行 | 一次性脚本 |
| GitHub Copilot | 行内补全 | 代码补全 |
7.2 实践方向
- 代码补全:日常 60% 代码 AI 生成,自己负责架构和审核
- 代码重构:选中复杂函数让 AI 提取 hook
- Bug 排查:把报错和上下文丢给 AI
- 单元测试:组件 + AI = 完整测试用例
- 技术调研:让 AI 对比方案 + 输出代码示例
- 文档生成:组件 props / 接口文档自动化
7.3 加分话术
我在日常工作中已经把 Cursor + Claude Code 深度集成到工作流。比如最近做的一个数据看板项目,我用 Claude Code 一次性生成了 8 个 ECharts 配置 + 对应的 TS 类型,省掉了大量重复劳动。但我始终保持代码 review 的习惯——AI 生成的代码我会逐行 review,确保符合团队规范和性能要求。我的理解是:AI 是杠杆,但杠杆的支点是工程师本身的技术判断力。
7.4 注意事项
- 不要说"我所有代码都让 AI 写"(显得自己没价值)
- 不要说"AI 还不够好我不用"(显得保守)
- 强调 AI + 人 的协同模式
- 准备一个具体案例(比如某个组件用 AI 生成后如何 review/调整)
八、面试前准备清单
8.1 简历准备
- 简历最新版(PDF + Word 备用)
- 5 个项目的 STAR 故事(每个 3-5 个难点)
- 自我介绍 30s/1min/3min 三版本
- 期望薪资 + 谈判区间
- 离职原因话术
8.2 公司调研
- 公司主营业务、近期新闻
- 公司产品(亲自体验)
- 公司技术博客 / GitHub
- 招聘岗位 JD 逐条对应
- 面试官背景(脉脉、领英)
8.3 技术准备
- 复习手写题(10 道核心)
- 算法题保持手感(每天 1-2 道)
- 项目代码自己再过一遍
- 面试当天的高频题
8.4 装备准备
- 西装/正装 / 商务休闲(按公司氛围)
- 简历纸质版 3 份
- 笔 + 笔记本
- 提前到达,预留交通时间
- 手机充满电
- 喝水、上厕所
九、面试当天注意事项
9.1 时间
- 提前 15 分钟到,不要太早(半小时显得焦虑)
- 视频面试提前 10 分钟开测试
- 面试中查看时间不要太频繁
9.2 形象
- 整洁干净 > 多贵
- 互联网公司:商务休闲(衬衫 + 西裤即可)
- 国企/银行:正装西装
- 视频面试:注意背景、灯光、收音
9.3 沟通
- 听完问题再答(不要打断)
- 不会就承认"这块我不熟,但我理解是..."(思考过程很重要)
- 答题用"总分总"结构
- 答不上来时承认 + 反问 ("这块您能给我点提示吗?")
9.4 心态
- 面试是双向选择
- 失败不丢人,是面试官也在被你评估
- 准备好"今天可能会失败"的心理预期
- 一面后立刻复盘,准备下一面
9.5 视频面试加分项
- 提前测试网络、麦克风、摄像头
- 用耳机(避免回音)
- 屏幕分享前清理桌面
- 准备一个干净的物理背景
- 笔记本电源插好(防断电)
十、Offer 谈判技巧
10.1 谈判时机
- ✅ HR 主动给出口头 offer 后
- ❌ 不要在面试中谈
- ❌ 不要在 offer letter 发出后才提
10.2 谈判筹码
- 多家 offer 在手(最有力)
- 当前公司即将调薪(加薪通知截图)
- 关键技能稀缺度(你刚好补这个空缺)
- 行业平均水平数据
10.3 谈判话术模板
软性争取:
非常感谢贵司的 offer!整体方案我很满意,但 base 部分能否再争取一下?我看了一下市场同岗位的水平,希望能在 X-Y 之间。其他方面我都没问题。
有竞争 offer:
我目前还有另一家公司给到 X 的 offer,但贵司的业务方向和团队我更感兴趣。如果能在薪资上对齐或接近,我会优先选择贵司。
正面拒绝:
很抱歉,目前的 offer 比我预期低了一些,需要再考虑一下。如果还有调整空间,期待您的回复。
10.4 谈判禁忌
- ❌ 不要威胁("不到 X 我就走")
- ❌ 不要假装有 offer(HR 可能会查)
- ❌ 不要谈到一半态度变差
- ❌ 不要漫天要价(开出超出市场太多的数字)
- ❌ 接了 offer 又反悔(行业很小,名声会传)
10.5 综合权衡(不只看薪资)
| 优先级 | 考虑维度 |
|---|---|
| 高 | 团队/老板(直接决定 1-2 年体验) |
| 高 | 业务前景(决定 2-3 年发展) |
| 高 | 技术成长(决定 3-5 年深度) |
| 中 | 薪资福利 |
| 中 | 通勤/工作时间 |
| 低 | 公司大小/品牌 |
🎯 终极心态建议
- 面试是技能,是可以训练的:不要害怕,多面才能进步
- 失败 ≠ 你不行:可能是 JD 不匹配、面试官风格、或者纯运气
- 保持谦逊:对每个面试官都尊重,行业很小
- 保持自信:不卑不亢,你在选公司公司也在选你
- 复盘比刷题更重要:每次面完写复盘,哪里没答好下次准备
📦 附:高频问题速查
| 问题 | 重点 |
|---|---|
| 你最近的项目 | 准备 1 个详细 STAR |
| 项目难点 | 至少 3 个技术深度点 |
| 为什么离职 | 正向、不抱怨 |
| 期望薪资 | 反问 → 区间 → 综合 |
| 你的优点 | 真实 + 与岗位匹配 |
| 你的缺点 | 不致命 + 改进 |
| 5 年规划 | 有方向,不要太具体 |
| 你的不足 | 在学什么补 |
| 业余时间做什么 | 体现持续学习 |
| 怎么了解技术 | 体现信息渠道 |
本系列复习文档完。
配套:05-自测题库(100 道高频题)
前端面试自测题库(100 道)
适合:3~5 年 Vue 全栈前端工程师
使用方法:先看题不看答案,自己答,然后对照参考答案
评分:80 分以上可以投大厂 / 60-80 中厂 / 60 以下补漏
📑 题目分布
| 模块 | 题数 |
|---|---|
| JavaScript 核心 | 20 |
| TypeScript | 5 |
| Vue3 / Vue2 | 20 |
| Vite / 工程化 | 8 |
| CSS | 7 |
| HTTP / 浏览器 | 12 |
| 性能优化 | 8 |
| 实时通信 / 可视化 / 地图 | 8 |
| 项目 + 软实力 | 12 |
| 合计 | 100 |
第一部分:JavaScript 核心(20 题)
1. 说说 JS 的数据类型
<details>
<summary>参考答案</summary>
8 种:Number / String / Boolean / null / undefined / Symbol / BigInt / Object
- 7 种基本类型 + 1 种引用类型
- Symbol 用于唯一标识,BigInt 用于大整数
- typeof null === 'object' 是历史遗留 Bug
</details>
2. typeof 和 instanceof 的区别
<details>
<summary>参考答案</summary>
-
typeof:判断基本类型,对引用类型只能区分 function 和 object -
instanceof:判断对象是否在某构造函数原型链上 - 最准确:
Object.prototype.toString.call(obj)返回[object Type]
</details>
3. 闭包是什么?有什么用?有什么坑?
<details>
<summary>参考答案</summary>
是什么:函数 + 它能访问到的外部变量
用途:模块封装、防抖节流、柯里化、私有变量
坑:
- 内存泄漏(DOM 引用未释放)
- 循环 var 陷阱
</details>
4. 解释 this 的 5 种指向规则
<details>
<summary>参考答案</summary>
优先级从高到低:
-
new→ 新对象 -
bind/call/apply→ 第一个参数 -
obj.fn()→ obj - 普通
fn()→ 严格模式 undefined / 非严格 window - 箭头函数 → 没有自己的 this,用外层作用域
</details>
5. 事件循环输出顺序
console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => {
console.log(3);
setTimeout(() => console.log(4));
});
console.log(5);
<details>
<summary>参考答案</summary>
1 → 5 → 3 → 2 → 4
执行流程:
- 同步:1, 5
- 微任务:3(执行时往宏队列 push 4)
- 宏任务:2
- 宏任务:4
</details>
6. 原型链是什么?
<details>
<summary>参考答案</summary>
每个对象都有 __proto__,指向构造函数的 prototype。
访问属性时,沿 __proto__ 一路向上查找,直到 Object.prototype.__proto__ === null。
</details>
7. 深浅拷贝的实现
<details>
<summary>参考答案</summary>
浅:Object.assign、扩展运算符
深:
-
JSON.parse(JSON.stringify())(有缺陷) -
structuredClone()(现代浏览器) - 手写递归 + WeakMap (能处理循环引用)
</details>
8. Promise 三状态?为什么不可逆?
<details>
<summary>参考答案</summary>
pending → fulfilled / rejected
不可逆是为了保证 Promise 的可预测性——一旦确定结果,所有 then 回调都能拿到一致的值。
</details>
9. 手写 Promise.all
<details>
<summary>参考答案</summary>
Promise.myAll = (promises) => new Promise((resolve, reject) => {
const result = [];
let count = 0;
promises.forEach((p, i) => {
Promise.resolve(p).then(val => {
result[i] = val;
if (++count === promises.length) resolve(result);
}, reject);
});
});
</details>
10. async/await 本质?
<details>
<summary>参考答案</summary>
Generator + 自动执行器的语法糖。
await 后面的代码相当于放进 .then 回调中。
</details>
11. 手写防抖和节流
<details>
<summary>参考答案</summary>
const debounce = (fn, delay) => {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
};
const throttle = (fn, delay) => {
let last = 0;
return function(...args) {
const now = Date.now();
if (now - last >= delay) {
fn.apply(this, args);
last = now;
}
};
};
</details>
12. 手写 new
<details>
<summary>参考答案</summary>
function myNew(C, ...args) {
const obj = Object.create(C.prototype);
const result = C.apply(obj, args);
return result instanceof Object ? result : obj;
}
</details>
13. 手写 call / apply / bind
<details>
<summary>参考答案</summary>
Function.prototype.myCall = function(ctx, ...args) {
ctx = ctx || window;
const fn = Symbol('fn');
ctx[fn] = this;
const r = ctx[fn](...args);
delete ctx[fn];
return r;
};
Function.prototype.myBind = function(ctx, ...args) {
const fn = this;
return function F(...newArgs) {
if (this instanceof F) return new fn(...args, ...newArgs);
return fn.apply(ctx, [...args, ...newArgs]);
};
};
</details>
14. 数组去重 5 种方式
<details>
<summary>参考答案</summary>
[...new Set(arr)]
arr.filter((v, i) => arr.indexOf(v) === i)
arr.reduce((acc, cur) => acc.includes(cur) ? acc : [...acc, cur], [])
// Map 对象去重 / 双层循环
</details>
15. 手写数组扁平 flat
<details>
<summary>参考答案</summary>
function flat(arr, depth = 1) {
return depth > 0
? arr.reduce((a, b) => a.concat(Array.isArray(b) ? flat(b, depth-1) : b), [])
: arr.slice();
}
</details>
16. let、const、var 区别
<details>
<summary>参考答案</summary>
| var | let | const | |
|---|---|---|---|
| 作用域 | 函数 | 块 | 块 |
| 提升 | ✅(值为 undefined) | ✅(暂时性死区) | ✅(暂时性死区) |
| 重复声明 | ✅ | ❌ | ❌ |
| 重新赋值 | ✅ | ✅ | ❌ |
</details>
17. == 和 === 的区别
<details>
<summary>参考答案</summary>
-
==:会进行类型转换(容易踩坑) -
===:严格相等,类型不同直接 false
推荐永远用 ===,除了判 null/undefined:x == null
</details>
18. 解释事件委托
<details>
<summary>参考答案</summary>
利用事件冒泡,把事件监听器绑在父元素上,通过 e.target 判断实际触发的元素。
好处:少绑定监听器,动态添加的子元素也能响应
</details>
19. 手写 EventEmitter
<details>
<summary>参考答案</summary>
class EventEmitter {
events = {};
on(e, fn) { (this.events[e] ||= []).push(fn); }
off(e, fn) { this.events[e] = this.events[e]?.filter(f => f !== fn); }
emit(e, ...args) { this.events[e]?.forEach(fn => fn(...args)); }
once(e, fn) {
const w = (...args) => { fn(...args); this.off(e, w); };
this.on(e, w);
}
}
</details>
20. 内存泄漏的常见场景
<details>
<summary>参考答案</summary>
- 全局变量
- 未清理的定时器/事件监听
- 闭包持有大对象
- DOM 引用(变量持有已删除 DOM)
- console.log 持有大对象
</details>
第二部分:TypeScript(5 题)
21. interface 和 type 的区别
<details>
<summary>参考答案</summary>
- interface 可同名合并,type 不行
- type 支持联合/交叉,interface 不擅长
- 经验:对象用 interface,组合类型用 type
</details>
22. 写出 5 个常用工具类型
<details>
<summary>参考答案</summary>
Partial<T> // 所有可选
Required<T> // 所有必填
Pick<T, K> // 挑选
Omit<T, K> // 排除
Record<K, V> // 键值对
</details>
23. 泛型怎么写?
<details>
<summary>参考答案</summary>
function identity<T>(x: T): T { return x; }
function getLen<T extends { length: number }>(x: T) { return x.length; }
interface ApiRes<T> { code: number; data: T; }
</details>
24. 手写 Pick
<details>
<summary>参考答案</summary>
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
</details>
25. 联合类型 vs 交叉类型
<details>
<summary>参考答案</summary>
- 联合
A | B:要么 A 要么 B - 交叉
A & B:同时是 A 和 B(合并属性)
</details>
第三部分:Vue3 / Vue2(20 题)
26. Vue3 响应式原理
<details>
<summary>参考答案</summary>
用 Proxy 代替 Object.defineProperty,可拦截 13 种操作,支持新增属性、数组、嵌套对象懒代理。
new Proxy(obj, {
get(t, k) { track(t, k); return Reflect.get(t, k); },
set(t, k, v) { Reflect.set(t, k, v); trigger(t, k); return true; }
});
</details>
27. Vue2 响应式有什么缺陷?
<details>
<summary>参考答案</summary>
- 无法监听新增/删除属性(要 Vue.set)
- 无法监听数组下标修改和长度
- 需要递归遍历,性能开销大
</details>
28. ref vs reactive
<details>
<summary>参考答案</summary>
| ref | reactive | |
|---|---|---|
| 数据类型 | 任意(推荐基本类型) | 对象/数组 |
| 访问 | .value | 直接访问 |
| 解构 | 保持响应 | 失去响应(要 toRefs) |
</details>
29. computed 和 watch 区别
<details>
<summary>参考答案</summary>
- computed:有返回值、有缓存、依赖自动收集
- watch:无返回值、无缓存、显式声明依赖、知道新旧值
场景:computed 用于派生数据,watch 用于副作用
</details>
30. nextTick 是什么?
<details>
<summary>参考答案</summary>
Vue 更新 DOM 是异步批量的。nextTick 把回调推到当前 tick 的微任务队列尾部,确保 DOM 更新完后再执行。
</details>
31. v-if 和 v-show 区别
<details>
<summary>参考答案</summary>
- v-if:DOM 增删,初始开销低,切换开销高
- v-show:CSS display,初始开销高,切换开销低
频繁切换用 v-show,条件渲染用 v-if
</details>
32. Vue3 的 diff 算法做了哪些优化
<details>
<summary>参考答案</summary>
- 静态提升(hoistStatic)
- PatchFlag 标记动态节点
- Block 块缩小 diff 范围
- 最长递增子序列减少 DOM 移动
</details>
33. Composition API 优势
<details>
<summary>参考答案</summary>
- 逻辑复用(自定义 hook)
- 类型推导更好
- 相关逻辑聚合
- Tree-shaking 更友好
</details>
34. v-for 为什么要 key
<details>
<summary>参考答案</summary>
帮助 diff 算法识别哪些节点改变了,避免不必要的 DOM 操作。
禁忌:用 index 作 key(删除/插入会出错)
</details>
35. Vue 组件通信方式
<details>
<summary>参考答案</summary>
- props / emit(父子)
- provide / inject(跨层级)
- v-model(双向)
- ref(父访问子)
- EventBus(已不推荐)
- Pinia / Vuex(全局)
</details>
36. 自定义 Hook 怎么写
<details>
<summary>参考答案</summary>
export function useCounter(initial = 0) {
const count = ref(initial);
const inc = () => count.value++;
return { count, inc };
}
</details>
37. Pinia 和 Vuex 区别
<details>
<summary>参考答案</summary>
- Pinia 取消 mutation,直接改 state
- Pinia TS 体验完美
- Pinia 多 store 平级,Vuex 嵌套 modules
- Pinia ~1KB,Vuex 较大
</details>
38. Vue Router 守卫种类
<details>
<summary>参考答案</summary>
- 全局:beforeEach / beforeResolve / afterEach
- 路由级:beforeEnter
- 组件级:beforeRouteEnter / Update / Leave
</details>
39. hash vs history 模式
<details>
<summary>参考答案</summary>
- hash:URL 带 #,原理 onhashchange,无需服务器配置
- history:原理 pushState,URL 干净,但需服务器配置 404 转 index.html
</details>
40. Vue3 新特性有哪些
<details>
<summary>参考答案</summary>
- Composition API
- Teleport(传送)
- Suspense(异步加载)
- Fragment(多根节点)
- 多 v-model
- emits 选项
- 性能提升 30-50%
</details>
41. 解释 Teleport
<details>
<summary>参考答案</summary>
把组件内容渲染到 DOM 的指定位置(如 body)。常用于 Modal、Toast 跳出当前 DOM 层级。
<Teleport to="body">
<div class="modal">...</div>
</Teleport>
</details>
42. 异步组件怎么写
<details>
<summary>参考答案</summary>
import { defineAsyncComponent } from 'vue';
const AsyncComp = defineAsyncComponent(() => import('./Big.vue'));
// 高级版
const AsyncComp = defineAsyncComponent({
loader: () => import('./Big.vue'),
loadingComponent: Loading,
errorComponent: Error,
delay: 200,
timeout: 3000,
});
</details>
43. Vue3 setup 中的 props 怎么解构?
<details>
<summary>参考答案</summary>
直接解构会失去响应性,要用 toRefs:
const props = defineProps(['name']);
const { name } = toRefs(props);
或 Vue 3.5+ 用 reactive props 解构(实验)
</details>
44. Vue3 的 ref 自动解包规则
<details>
<summary>参考答案</summary>
- 模板中:自动解包(不用 .value)
- reactive 内部:自动解包
- 普通 JS 中:需要 .value
- ref 数组中:不解包
</details>
45. 项目里你封装过哪些通用组件
<details>
<summary>参考答案</summary>
至少能讲 3 个:
- BaseTable(基于 ElTable 二次封装,配置驱动 + 插槽)
- BaseForm(Schema 驱动表单)
- BaseDialog(统一 Modal 风格 + Promise 化调用)
- BaseChart(ECharts 封装 + 自动 resize)
- BasePermission(权限按钮指令 + 组件)
</details>
第四部分:Vite / 工程化(8 题)
46. Vite 为什么比 Webpack 快
<details>
<summary>参考答案</summary>
- 利用浏览器原生 ESM,不打包直接服务
- 第三方依赖用 esbuild 预构建(Go 写的)
- HMR 精准更新
- 生产用 Rollup
</details>
47. Vite 中怎么配 alias 和 proxy
<details>
<summary>参考答案</summary>
export default defineConfig({
resolve: { alias: { '@': path.resolve(__dirname, 'src') } },
server: {
proxy: {
'/api': { target: 'http://backend.com', changeOrigin: true }
}
}
});
</details>
48. 什么是 Tree Shaking
<details>
<summary>参考答案</summary>
打包时删除未使用代码,要求:
- 用 ESM(import/export)
- 设置 sideEffects: false
- 生产环境 mode: 'production'
</details>
49. Webpack 中 loader 和 plugin 区别
<details>
<summary>参考答案</summary>
- loader:处理文件转换(如 css-loader 把 CSS 变成 JS 模块)
- plugin:扩展构建流程(如 HtmlWebpackPlugin 生成 HTML)
</details>
50. ESLint + Prettier 怎么配合
<details>
<summary>参考答案</summary>
- ESLint:检查代码错误(语法、规范)
- Prettier:自动格式化代码
- 配合:用 eslint-config-prettier 关闭 ESLint 中和 Prettier 冲突的规则
- 提交时:husky + lint-staged 检查暂存区
</details>
51. Git rebase 和 merge 区别
<details>
<summary>参考答案</summary>
- merge:保留所有分支历史,产生 merge commit
- rebase:把分支基础变到目标分支,历史是直线
- 团队协作建议:feature → main 用 rebase 保持干净,公共分支别 rebase
</details>
52. CI/CD 流程
<details>
<summary>参考答案</summary>
代码提交 → 触发 CI → Lint → Test → Build → 部署
工具:GitHub Actions / GitLab CI / Jenkins
</details>
53. Monorepo 用过哪些工具
<details>
<summary>参考答案</summary>
- pnpm workspace(轻量、快)
- Turborepo(任务编排好)
- Nx(大型项目)
- Lerna(老牌)
</details>
第五部分:CSS(7 题)
54. 盒模型有几种
<details>
<summary>参考答案</summary>
- content-box(默认):width 不含 padding/border
- border-box:width 包含 padding/border
* { box-sizing: border-box; } 是开发常用配置
</details>
55. BFC 是什么?怎么触发?
<details>
<summary>参考答案</summary>
块级格式化上下文,独立的渲染区域。
触发:overflow ≠ visible / display: flex/grid/inline-block / position: absolute/fixed / float ≠ none
用途:清浮动、防 margin 重叠、自适应两栏
</details>
56. Flex 布局核心属性
<details>
<summary>参考答案</summary>
容器:
- display: flex
- flex-direction
- justify-content(主轴)
- align-items(交叉轴)
- flex-wrap
子元素:
- flex(grow shrink basis 缩写)
- align-self
</details>
57. 写出 5 种垂直居中
<details>
<summary>参考答案</summary>
/* 1 */ display: flex; align-items: center;
/* 2 */ display: grid; place-items: center;
/* 3 */ position: absolute; top: 50%; transform: translateY(-50%);
/* 4 */ position: absolute; inset: 0; margin: auto;
/* 5 */ line-height (仅文字)
</details>
58. 移动端适配方案
<details>
<summary>参考答案</summary>
- rem:根据 html 字体大小动态计算
- vw/vh:视口百分比
- postcss-px-to-viewport:px 自动转 vw
- 1px 问题:transform: scale(0.5)
</details>
59. 重排和重绘
<details>
<summary>参考答案</summary>
- 重排(Reflow):几何属性变化触发,开销大
- 重绘(Repaint):颜色等非几何变化,开销小
优化:用 transform/opacity 触发 GPU 加速避免重排
</details>
60. CSS 选择器优先级
<details>
<summary>参考答案</summary>
!important > 内联 > id > class/属性/伪类 > 标签 > 通配符
权重:1000 / 100 / 10 / 1 / 0
</details>
第六部分:HTTP / 浏览器(12 题)
61. 输入 URL 到页面渲染过程
<details>
<summary>参考答案</summary>
- URL 解析
- DNS 查询
- TCP 三次握手
- TLS 握手
- 发 HTTP 请求
- 服务器返回 HTML
- 解析 DOM 和 CSSOM
- 构建 Render Tree
- Layout、Paint、Composite
</details>
62. HTTP 缓存机制
<details>
<summary>参考答案</summary>
- 强缓存:Cache-Control / Expires,命中返回 200 (from cache)
- 协商缓存:ETag / Last-Modified,命中返回 304
强缓存失败 → 走协商缓存 → 协商失败 → 重新请求
</details>
63. 跨域解决方案
<details>
<summary>参考答案</summary>
- CORS(推荐):后端加响应头
- 代理(Vite proxy / Nginx)
- JSONP(已淘汰)
- postMessage(iframe)
</details>
64. HTTPS 加密原理
<details>
<summary>参考答案</summary>
非对称加密协商对称密钥,后续用对称密钥通信。
握手:
- Client Hello
- Server Hello + 证书
- 客户端验证证书,生成 pre-master,公钥加密
- 双方生成对称密钥
- 加密通信
</details>
65. HTTP/1.1 vs HTTP/2 vs HTTP/3
<details>
<summary>参考答案</summary>
- 1.1:文本传输,无多路复用
- 2:二进制,多路复用,HPACK 头压缩,但 TCP 队头阻塞仍存在
- 3:基于 QUIC(UDP),彻底解决队头阻塞
</details>
66. TCP 三次握手为什么不能两次
<details>
<summary>参考答案</summary>
防止历史连接干扰新连接。如果只两次握手,历史延迟到达的 SYN 会让服务端误开新连接。
</details>
67. cookie / localStorage / sessionStorage / IndexedDB 区别
<details>
<summary>参考答案</summary>
| cookie | localStorage | sessionStorage | IndexedDB | |
|---|---|---|---|---|
| 容量 | 4KB | 5MB | 5MB | 无限 |
| 生命 | 可设置 | 永久 | 标签页 | 永久 |
| 随请求 | ✅ | ❌ | ❌ | ❌ |
</details>
68. 什么是同源策略
<details>
<summary>参考答案</summary>
协议 + 域名 + 端口 三者都相同才同源。
限制:跨域请求被拦截、Cookie/LocalStorage 不能跨域读、DOM 不能跨域操作
</details>
69. 浏览器多进程架构
<details>
<summary>参考答案</summary>
- 主进程(浏览器 UI)
- 网络进程
- GPU 进程
- 渲染进程(每 Tab 一个,沙箱)
- 插件进程
</details>
70. V8 的垃圾回收
<details>
<summary>参考答案</summary>
分代回收:
- 新生代:Scavenge 复制算法(处理短命对象)
- 老生代:Mark-Sweep + Mark-Compact(标记清除 + 整理)
</details>
71. 解释 Service Worker
<details>
<summary>参考答案</summary>
独立运行的脚本,能拦截网络请求,实现离线缓存、推送通知。
是 PWA 的核心。
</details>
72. 什么是 CORS 预检请求
<details>
<summary>参考答案</summary>
复杂请求(如 PUT、自定义 header)会先发 OPTIONS 请求询问服务器是否允许,服务器返回允许的方法、头、域,然后才发真正的请求。
</details>
第七部分:性能优化(8 题)
73. 首屏优化怎么做
<details>
<summary>参考答案</summary>
- 请求:DNS 预解析、CDN、HTTP/2、Gzip、缓存
- 加载:路由懒加载、组件异步、图片懒加载、骨架屏
- 渲染:SSR、关键 CSS 内联、虚拟滚动
- 构建:Tree Shaking、代码分割、按需引入
</details>
74. 怎么优化长列表
<details>
<summary>参考答案</summary>
- 虚拟滚动(只渲染可视区)
- 分页加载
- 时间分片渲染(requestIdleCallback)
- 用 keep-alive 缓存
</details>
75. Web Vitals 指标有哪些
<details>
<summary>参考答案</summary>
| 指标 | 目标 |
|---|---|
| FCP | < 1.8s |
| LCP | < 2.5s |
| FID/INP | < 100ms |
| CLS | < 0.1 |
| TTFB | < 600ms |
</details>
76. 图片优化方法
<details>
<summary>参考答案</summary>
- WebP/AVIF 格式
- loading="lazy"
- srcset 响应式
- 雪碧图(小图标)
- iconfont/SVG 替代位图
- tinypng 压缩
</details>
77. JS 加载优化(defer/async)
<details>
<summary>参考答案</summary>
- 默认:阻塞解析
- async:异步下载,下载完立即执行(阻塞)
- defer:异步下载,DOM 解析完后执行(不阻塞)
推荐 defer
</details>
78. 怎么定位前端性能问题
<details>
<summary>参考答案</summary>
- Lighthouse:综合评分
- Performance 面板:CPU 火焰图
- Memory 面板:内存泄漏
- Network 面板:请求瀑布
- 真实用户监控:Sentry / 阿里 ARMS
</details>
79. requestAnimationFrame 和 setTimeout 区别
<details>
<summary>参考答案</summary>
- raf:跟随浏览器刷新率(60fps),动画专用
- setTimeout:固定时间,可能丢帧或过密
动画用 raf,定时用 setTimeout
</details>
80. 怎么实现虚拟滚动
<details>
<summary>参考答案</summary>
- 占位元素撑满总高度
- 根据 scrollTop 计算 startIndex 和 endIndex
- 只渲染可视区元素 + buffer
- 用 transform 偏移到正确位置
const visibleCount = Math.ceil(containerHeight / itemHeight);
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = startIndex + visibleCount + buffer;
</details>
第八部分:实时通信 / 可视化 / 地图(8 题)
81. WebSocket 怎么实现心跳和重连
<details>
<summary>参考答案</summary>
class WS {
connect() {
this.ws = new WebSocket(url);
this.ws.onopen = () => this.startHeart();
this.ws.onclose = () => this.reconnect();
}
startHeart() {
this.timer = setInterval(() => this.ws.send('ping'), 30000);
}
reconnect() {
clearInterval(this.timer);
const delay = Math.min(1000 * 2 ** this.times, 30000);
setTimeout(() => { this.times++; this.connect(); }, delay);
}
}
</details>
82. WebRTC 三大核心 API
<details>
<summary>参考答案</summary>
- getUserMedia:获取媒体流
- RTCPeerConnection:建立 P2P 连接
- RTCDataChannel:任意数据通道
</details>
83. WebRTC 信令流程
<details>
<summary>参考答案</summary>
A → 创建 Offer → 信令服务器 → B
B → 创建 Answer → 信令服务器 → A
互换 ICE Candidate → P2P 通道建立
</details>
84. MQTT 为什么适合 IoT
<details>
<summary>参考答案</summary>
- 极轻量(最小 2 字节头)
- 发布订阅模型
- QoS 0/1/2 三级可靠性
- 遗嘱消息(设备掉线通知)
</details>
85. ECharts 大数据怎么优化
<details>
<summary>参考答案</summary>
- appendData 增量加载
- dataZoom 视窗滚动
- 按需引入
- canvas 渲染器(SVG 适合静态/打印)
- progressive 渐进渲染
</details>
86. ECharts 怎么按需引入
<details>
<summary>参考答案</summary>
import * as echarts from 'echarts/core';
import { BarChart } from 'echarts/charts';
import { GridComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([BarChart, GridComponent, CanvasRenderer]);
体积从 1MB 降到 200KB。
</details>
87. 什么是电子围栏?怎么实现?
<details>
<summary>参考答案</summary>
地图上画虚拟圈,判断点是否在范围内。
// 商家绘制
new AMap.MouseTool(map).polygon();
// 判定(射线法)
AMap.GeometryUtil.isPointInRing([lng, lat], polygonPath);
应用:外卖配送范围、共享单车运营区、上门服务区域
</details>
88. 高德地图坐标系坑
<details>
<summary>参考答案</summary>
- WGS84:GPS 原始
- GCJ02:高德、腾讯(火星坐标)
- BD09:百度
GPS 坐标要先转 GCJ02 再画到高德地图,否则偏移几百米。
</details>
第九部分:项目 + 软实力(12 题)
89. 介绍一下你最近的项目
<details>
<summary>参考答案要点</summary>
按 STAR 结构:
- Situation:业务背景(什么公司、什么产品、什么用户)
- Task:你的角色和目标
- Action:技术选型、架构、难点解决
- Result:上线效果、用户数、性能指标
结尾:我从中学到了什么
</details>
90. 项目里最大的难点
<details>
<summary>参考答案要点</summary>
挑一个有技术含量的,避免说"业务复杂"。
模板:
业务上要解决 X 问题 → 技术挑战是 Y → 我用 Z 方案 → 结果是 N
举例:WebRTC 多路视频卡顿、MQTT 弱网重连、虚拟滚动、首屏优化
</details>
91. 为什么从上家公司离职
<details>
<summary>参考答案要点</summary>
✅ 正向:
- 想往架构方向发展
- 寻求更有挑战的工作
- 业务方向调整
❌ 避免:
- 抱怨领导/同事
- 钱少(1 面别说)
- 上家公司不行
</details>
92. 你最大的优点
<details>
<summary>参考答案要点</summary>
挑 1-2 个 + 案例佐证:
- 学习能力(举例:3 个月从零学 WebRTC)
- 解决问题能力(举例:定位 iOS 内存泄漏)
- 沟通协作(举例:拉齐前后端方案分歧)
不要泛泛而谈"有责任心、勤奋"
</details>
93. 你最大的缺点
<details>
<summary>参考答案要点</summary>
真缺点 + 不致命 + 改进意识:
我有时过于追求代码完美,会反复重构组件。最近开始设定"够用就好"的边界,先交付再迭代。
</details>
94. 你期望的薪资
<details>
<summary>参考答案要点</summary>
三步:
- 反问预算
- 给区间(高 20%)
- 综合考虑(base + 月数 + 福利)
不要给死数字,不要威胁
</details>
94. 5 年规划
<details>
<summary>参考答案要点</summary>
有方向但不要太具体:
1-2 年深耕业务,能独立负责中型项目
3 年成为团队技术骨干
5 年朝架构师方向发展,能从 0 到 1 设计大型系统
</details>
96. 怎么了解新技术
<details>
<summary>参考答案要点</summary>
体现持续学习:
- 关注 Vue/React/TypeScript 官方更新
- 订阅前端早早聊、奇舞团等公众号
- 看技术博客(阮一峰、张鑫旭、ruan-yifeng)
- 看源码(Vue3、Vite)
- AI 工具(Claude/Cursor)辅助学习
</details>
97. 你最自豪的代码
<details>
<summary>参考答案要点</summary>
挑一个你能讲半小时的:
- 一个解决了团队痛点的工具
- 一个性能从 X 提升到 Y 的优化
- 一个被多个项目复用的组件库
讲清楚:背景 → 方案 → 实现 → 影响
</details>
98. 你和同事有过冲突吗?怎么解决
<details>
<summary>参考答案要点</summary>
STAR:
- S:具体冲突情况
- T:要达成什么目标
- A:怎么处理(对事不对人 + 数据说话 + 找共同目标)
- R:结果
避免:说同事不好、说自己永远对
</details>
99. 你怎么看 AI 编程
<details>
<summary>参考答案要点</summary>
AI 是杠杆,但杠杆的支点是工程师本身的技术判断力。
我把 Cursor + Claude Code 集成到工作流——日常 60% 代码 AI 生成,但每行都自己 review。AI 解决重复劳动,工程师专注架构和决策。
这是趋势,不主动拥抱会被淘汰,但也不能完全依赖。
</details>
100. 你有什么要问我的
<details>
<summary>参考答案要点</summary>
至少准备 3 个:
- 团队最近一年的技术演进方向?
- 这个岗位 3-6 个月的预期?
- 您觉得我哪些经历最匹配?
绝对不要说"没有"
</details>
🎯 自测评分
| 答对题数 | 评估 |
|---|---|
| 90+ | 大厂候选,可挑战 P6+ |
| 70-90 | 中厂稳过,大厂有戏 |
| 50-70 | 还需补漏,先扎实基础 |
| <50 | 系统复习,回看专题文档 |
📌 复习方法
- 第一遍:盖住答案做,记下不会的
- 第二遍:只做不会的,对照专题文档查漏
- 第三遍:模拟面试,限时 5 分钟答 3 题
- 每周一次:随机抽 20 题快速过
📦 配套资料
- 01-基础篇:JS/TS/CSS/HTTP/浏览器/算法/手写题
- 02-框架篇:Vue3/Pinia/Vite/uni-app/微前端/SSR/测试
- 03-项目篇:实时通信/可视化/地图/工程化/项目话术
- 04-软实力篇:自我介绍/离职/薪资/反向提问/AI 工具
- 05-自测题库(本篇)
祝你早日拿到心仪 offer!💪