外观
高级前端工程师面试题大全
从实际项目出发,覆盖 JavaScript、Vue、工程化、系统设计等核心方向
基于全网热门面试题库(febobo/web-interview、lgwebdream/FE-Interview、Advanced-Frontend/Daily-Interview-Question 等)、当前学习文档体系及项目实战资料整理
目录
第一篇:JavaScript 核心面试题
第二篇:Vue 面试题
- Vue 2 vs Vue 3 核心差异
- 响应式原理详解
- Composition API vs Options API
- 组件通信方案
- 状态管理(Vuex vs Pinia)
- Vue Router 核心
- 性能优化实战
- 实际项目场景题
第三篇:工程化与架构
第四篇:项目经验与系统设计
第五篇:代码实现题
第一篇:JavaScript 核心面试题
1.1 基础进阶题
1.1.1 var / let / const 的区别(含暂时性死区)
面试考察点:理解作用域、变量提升、暂时性死区等核心概念
答案:
javascript
// 1. 作用域
var a = 1; // 函数作用域
let b = 2; // 块级作用域
const c = 3; // 块级作用域
// 2. 变量提升
console.log(v); // undefined(var 被提升,默认值 undefined)
console.log(l); // ReferenceError(let 在暂时性死区)
var v = 1;
let l = 2;
// 3. 重复声明
var x = 1; var x = 2; // OK
let y = 1; let y = 2; // SyntaxError
// 4. const 本质
const obj = { a: 1 };
obj.a = 2; // OK(引用不变即可)
// obj = {}; // TypeError项目场景:在大型项目中统一使用 const 声明不变值,let 声明可变值,禁止使用 var,可以有效避免变量提升带来的隐式 bug。
1.1.2 数据类型检测的多种方式
答案:
javascript
// 1. typeof(适合基本类型)
typeof "hello"; // "string"
typeof 42; // "number"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof Symbol(); // "symbol"
typeof 42n; // "bigint"
typeof null; // "object" — 历史遗留 bug!
typeof []; // "object"
typeof function(){};// "function"
// 2. instanceof(适合检测引用类型的具体类)
[] instanceof Array; // true
[] instanceof Object; // true
// 3. Object.prototype.toString.call(终极方案)
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(null); // "[object Null]"
// 4. Array.isArray(数组专用)
Array.isArray([]); // true项目场景:处理 API 返回数据时,经常需要判断数据类型。使用 Object.prototype.toString.call 是最可靠的方案。
1.1.3 深浅拷贝的区别与实现
面试题:如何实现深拷贝?有哪些注意事项?
答案:
javascript
// 浅拷贝
const shallow1 = { ...obj };
const shallow2 = Object.assign({}, obj);
// 深拷贝
// 方式一:JSON 序列化(有缺陷)
const deep1 = JSON.parse(JSON.stringify(obj));
// 缺陷:忽略 undefined、Symbol、函数、RegExp、Date 等
// 方式二:structuredClone(现代浏览器/Node 17+)
const deep2 = structuredClone(obj);
// 方式三:递归实现
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== "object") return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (hash.has(obj)) return hash.get(obj); // 解决循环引用
const result = Array.isArray(obj) ? [] : {};
hash.set(obj, result);
for (const key of Reflect.ownKeys(obj)) {
result[key] = deepClone(obj[key], hash);
}
return result;
}项目场景:在状态管理(Vuex/Pinia)中修改深层嵌套数据时,经常需要深拷贝来避免意外修改原数据。
1.1.4 == 与 === 的区别 + 类型转换规则
答案:
javascript
// ===:严格相等,不进行类型转换
1 === "1"; // false
null === undefined; // false
// ==:抽象相等,会进行类型转换
1 == "1"; // true
null == undefined; // true — 特殊规则
[] == false; // true
"" == false; // true
// 类型转换规则(ToPrimitive)
// 1. 对象先调用 valueOf(),再调用 toString()
// 2. null == undefined(特殊规则,结果为 true)
// 3. 数字与字符串比较时,字符串转数字
// 4. 布尔值与其他类型比较时,布尔值转数字项目场景:实际开发中始终使用 ===,仅在判断 null/undefined 时可以使用 == 的简写:if (val == null) 等价于 if (val === null || val === undefined)。
1.2 闭包与作用域
1.2.1 闭包的定义与应用场景
面试题:什么是闭包?列举实际项目中的使用场景。
答案:
闭包是指有权访问另一个函数作用域中变量的函数。当一个函数在其定义的作用域之外执行时,它仍然可以访问该作用域中的变量。
实际项目场景:
javascript
// 场景1:数据私有化(封装)
function createCounter() {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count
};
}
// 项目中的 hook,计数器等
// 场景2:防抖与节流
function debounce(fn, delay = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// 场景3:函数柯里化
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return (...args2) => curried(...args, ...args2);
};
}
// 场景4:单例模式
function createSingleton() {
let instance;
return function() {
return instance || (instance = {});
};
}1.2.2 闭包的内存泄漏问题
面试题:闭包可能导致内存泄漏吗?如何避免?
答案:
javascript
// 问题:闭包长期持有外部变量,导致无法被 GC 回收
function setupHandler() {
const largeData = new Array(1000000).fill("data");
document.getElementById("btn").onclick = function() {
console.log("clicked");
// 这里引用了 largeData,即使函数中没有使用
};
}
// 解决方式:
// 1. 及时清理引用
function setupHandler() {
const largeData = new Array(1000000).fill("data");
document.getElementById("btn").onclick = function() {
console.log("clicked");
};
largeData = null; // 显式释放
}
// 2. 使用 WeakMap/WeakSet
const cache = new WeakMap();
// WeakMap 的键是弱引用,不会阻止 GC项目场景:在 SPA 应用中,页面切换时未销毁的组件中如果存在闭包引用,容易造成内存泄漏。Vue 3 的 onBeforeUnmount / onUnmounted 就是用来做清理的。
1.3 this 指向与函数上下文
1.3.1 this 的四种绑定规则
面试题:请说出 JavaScript 中 this 的指向规则。
答案:
javascript
// 规则1:默认绑定(非严格模式 -> window,严格模式 -> undefined)
function show() { console.log(this); }
show(); // window(非严格模式)
// 规则2:隐式绑定(谁调用指向谁)
const obj = { name: "obj", show() { console.log(this.name); } };
obj.show(); // "obj"
// 规则3:显式绑定(call/apply/bind)
function greet() { console.log(`Hello, ${this.name}`); }
greet.call({ name: "Alice" }); // "Hello, Alice"
greet.apply({ name: "Bob" }); // "Hello, Bob"
const bound = greet.bind({ name: "Charlie" });
bound(); // "Hello, Charlie"
// 规则4:new 绑定
function Person(name) { this.name = name; }
const p = new Person("Alice");
console.log(p.name); // "Alice"
// 优先级:new > 显式 > 隐式 > 默认1.3.2 箭头函数的 this
面试题:箭头函数的 this 和普通函数的 this 有什么区别?
答案:
javascript
// 箭头函数没有自己的 this,继承外层作用域的 this
const obj = {
name: "obj",
normal: function() {
console.log(this.name); // "obj"
},
arrow: () => {
console.log(this.name); // undefined(继承 window 的 this)
},
nested() {
const innerArrow = () => {
console.log(this.name); // "obj"(继承 nested 的 this)
};
innerArrow();
}
};
// 应用场景:定时器/事件回调中保持 this
class Timer {
constructor() { this.seconds = 0; }
start() {
// 使用箭头函数避免 this 丢失
setInterval(() => {
this.seconds++; // 这里的 this 指向 Timer 实例
}, 1000);
}
}1.3.3 call / apply / bind 的区别与手写实现
面试题:实现一个 bind 方法。
答案:
javascript
Function.prototype.myBind = function(context, ...args) {
const fn = this;
return function(...args2) {
// 区分 new 调用和普通调用
if (this instanceof fn) {
// new 调用
return new fn(...args, ...args2);
}
return fn.apply(context, [...args, ...args2]);
};
};
// 使用示例
const obj = { name: "test" };
function log(prefix) { console.log(prefix + this.name); }
const bound = log.myBind(obj, "Name: ");
bound(); // "Name: test"1.4 原型链与继承
1.4.1 原型链原理
面试题:请描述 JavaScript 的原型链机制。
答案:
javascript
// 每个对象都有一个 __proto__ 属性(隐式原型)
// 每个函数都有一个 prototype 属性(显式原型)
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, I am ${this.name}`);
};
const alice = new Person("Alice");
alice.sayHello(); // "Hello, I am Alice"
// 原型链查找:
// alice -> Person.prototype -> Object.prototype -> null
console.log(alice.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null1.4.2 ES6 Class 继承与原型链继承的对比
面试题:ES6 的 class 继承是如何实现的?
答案:
javascript
class Animal {
constructor(name) { this.name = name; }
speak() { console.log(`${this.name} makes a sound`); }
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 必须先调用 super
this.breed = breed;
}
speak() {
super.speak(); // 调用父类方法
console.log(`${this.name} barks`);
}
}
// 本质:class 是语法糖
// Dog.prototype.__proto__ === Animal.prototype
// Reflect.construct + 寄生组合式继承1.4.3 多种继承方式对比
面试题:请说说 JavaScript 中有哪些继承方式,各自的优缺点。
答案:
javascript
// 1. 原型链继承(引用类型共享问题)
function Parent() { this.colors = ["red"]; }
function Child() {}
Child.prototype = new Parent();
// 缺点:所有实例共享 colors 数组
// 2. 构造函数继承(方法无法复用)
function Child() {
Parent.call(this);
}
// 3. 组合继承(最常用)
function Child(name) {
Parent.call(this, name); // 第一次调用 Parent
}
Child.prototype = new Parent(); // 第二次调用 Parent
// 4. 寄生组合式继承(完美方案)
function inherit(child, parent) {
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
// 5. ES6 class extends(现代标准)
class Child extends Parent {}1.5 异步编程(Promise/async/await)
1.5.1 Promise 的三种状态及链式调用
面试题:手写 Promise.all / Promise.race / Promise.allSettled。
答案:
javascript
// 手写 Promise.all
Promise.myAll = function(promises) {
return new Promise((resolve, reject) => {
const results = [];
let completed = 0;
if (promises.length === 0) resolve(results);
promises.forEach((p, i) => {
Promise.resolve(p).then(val => {
results[i] = val;
completed++;
if (completed === promises.length) resolve(results);
}).catch(reject);
});
});
};
// 手写 Promise.race
Promise.myRace = function(promises) {
return new Promise((resolve, reject) => {
promises.forEach(p => {
Promise.resolve(p).then(resolve).catch(reject);
});
});
};
// 手写 Promise.allSettled
Promise.myAllSettled = function(promises) {
return Promise.all(promises.map(p =>
Promise.resolve(p).then(
value => ({ status: "fulfilled", value }),
reason => ({ status: "rejected", reason })
)
));
};项目场景:仪表盘页面需要同时请求多个接口,使用 Promise.all 并行请求并等待所有结果;需要容错时使用 Promise.allSettled。
1.5.2 async/await 的错误处理模式
面试题:实际项目中如何优雅地处理 async/await 的错误?
答案:
javascript
// 方式一:try/catch(推荐)
async function fetchUser(id) {
try {
const response = await fetch(`/api/user/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
console.error("请求失败:", error);
throw error; // 继续向上抛
}
}
// 方式二:包装函数(Go 风格)
function to(promise) {
return promise.then(data => [null, data]).catch(err => [err, null]);
}
async function main() {
const [err, user] = await to(fetchUser(1));
if (err) {
// 处理错误
return;
}
console.log(user.name);
}
// 方式三:Promise.all 的错误隔离
async function getDashboardData() {
const results = await Promise.allSettled([
fetchUser(1),
fetchOrders(1),
fetchNotifications(1)
]);
const [userResult, ordersResult, notifResult] = results;
// 分别处理每个结果的成功/失败
}1.5.3 事件循环(Event Loop)与宏任务/微任务
面试题:以下代码的输出顺序是什么?
javascript
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
Promise.resolve().then(() => {
console.log("4");
setTimeout(() => console.log("5"), 0);
});
console.log("6");
// 答案:1, 6, 3, 4, 2, 5
// 解释:同步代码 -> 清空微任务队列 -> 取一个宏任务执行项目场景:在 Vue 的 nextTick 中执行 DOM 操作,利用的就是微任务机制。
1.5.4 异步并发控制
面试题:如何实现一个限制最大并发数的异步调度器?
答案:
javascript
class Scheduler {
constructor(maxConcurrency = 2) {
this.max = maxConcurrency;
this.running = 0;
this.queue = [];
}
add(promiseFactory) {
return new Promise((resolve, reject) => {
this.queue.push({ factory: promiseFactory, resolve, reject });
this.run();
});
}
run() {
while (this.running < this.max && this.queue.length) {
const { factory, resolve, reject } = this.queue.shift();
this.running++;
factory()
.then(resolve, reject)
.finally(() => {
this.running--;
this.run();
});
}
}
}
// 使用场景:控制大文件上传或批量 API 请求的并发数
const scheduler = new Scheduler(3);
for (let i = 0; i < 10; i++) {
scheduler.add(() => fetch(`/api/data/${i}`));
}1.6 ES6+ 核心新特性
1.6.1 Map / Set / WeakMap / WeakSet 的区别
面试题:WeakMap 的应用场景是什么?
答案:
javascript
// Map vs Object
const map = new Map();
map.set("key", "value");
map.get("key");
map.has("key");
map.delete("key");
// WeakMap:键必须是对象,弱引用
const wm = new WeakMap();
let obj = { data: "temp" };
wm.set(obj, "metadata");
obj = null; // 对象被 GC 回收,WeakMap 中的键值对也被自动移除
// 实际应用:Vue 3 响应式系统中,使用 WeakMap 存储对象的依赖关系
// 应用:存储 DOM 节点的绑定数据,DOM 移除后自动清理
const domData = new WeakMap();
const btn = document.getElementById("btn");
domData.set(btn, { clicks: 0 });
btn.onclick = function() {
const data = domData.get(btn);
data.clicks++;
};
// 当 btn 从 DOM 中移除后,domData 中的条目自动被 GC 清理1.6.2 Symbol 的作用与使用场景
答案:
javascript
// 1. 创建唯一键,防止属性名冲突
const NAME = Symbol("name");
const obj = { [NAME]: "Alice" };
// 2. 定义常量
const Colors = {
RED: Symbol("red"),
GREEN: Symbol("green"),
BLUE: Symbol("blue")
};
// 3. 自定义迭代器
const iterable = {
[Symbol.iterator]: function*() {
yield 1; yield 2; yield 3;
}
};1.6.3 Proxy 的应用场景
面试题:Vue 3 为什么要从 Object.defineProperty 换成 Proxy?
答案:
javascript
// Proxy 可以拦截整个对象的操作
const handler = {
get(target, key) { /* 拦截读取 */ },
set(target, key, value) { /* 拦截设置 */ },
deleteProperty(target, key) { /* 拦截删除 */ },
has(target, key) { /* 拦截 in 操作 */ }
};
// 实际应用:数据校验
const validator = {
set(target, key, value) {
if (key === "age" && typeof value !== "number") {
throw new TypeError("age must be a number");
}
target[key] = value;
return true;
}
};
// 实际应用:日志追踪
function createLogger(target) {
return new Proxy(target, {
get(obj, key) {
console.log(`GET ${key}: ${obj[key]}`);
return obj[key];
},
set(obj, key, value) {
console.log(`SET ${key}: ${value}`);
obj[key] = value;
return true;
}
});
}1.7 实际项目场景题
Q1:长列表渲染优化
问题:项目中有一个包含 10 万条数据的列表需要渲染,你会怎么做?
考察点:虚拟滚动、时间分片、Web Worker
答案:
- 虚拟滚动(推荐):只渲染可视区域内的 DOM 节点。可以使用
vue-virtual-scroller或react-window - 时间分片:使用
requestAnimationFrame分批渲染 - Web Worker:在 Worker 中处理数据计算,主线程只负责渲染
javascript
// 时间分片示例
function renderChunk(list, chunkSize = 100) {
let index = 0;
function processChunk() {
const chunk = list.slice(index, index + chunkSize);
// 渲染当前 chunk
index += chunkSize;
if (index < list.length) {
requestAnimationFrame(processChunk);
}
}
requestAnimationFrame(processChunk);
}Q2:前端权限控制
问题:项目中如何实现前端权限控制(按钮级权限)?
考察点:路由权限、指令权限、权限状态管理
答案:
- 路由级权限:动态添加路由(
router.addRoute) - 按钮级权限:自定义指令 + 权限 store
- 权限数据:登录后后端返回权限列表,存储在 Pinia/Vuex 中
javascript
// Vue 3 自定义权限指令
const vPermission = {
mounted(el, binding) {
const { value } = binding;
const permissions = usePermissionStore();
if (value && !permissions.has(value)) {
el.parentNode?.removeChild(el);
}
}
};
// 动态路由
router.beforeEach(async (to) => {
const store = usePermissionStore();
if (!store.loaded) {
const routes = await store.fetchRoutes();
routes.forEach(r => router.addRoute(r));
return to.fullPath; // 重新导航
}
});Q3:大文件上传
问题:如何实现大文件上传,支持断点续传?
考察点:文件切片、MD5 计算、进度监控、并发控制
答案:
- 切片:将文件按固定大小(如 5MB)切片
- Hash 计算:使用
SparkMD5计算文件唯一标识 - 断点续传:先请求已上传分片列表,只上传未上传的
- 并发控制:限制同时上传的分片数(如 3 个)
javascript
async function uploadFile(file) {
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
const chunks = [];
for (let start = 0; start < file.size; start += CHUNK_SIZE) {
chunks.push(file.slice(start, start + CHUNK_SIZE));
}
// 计算文件 hash
const fileHash = await calculateHash(file);
// 查询已上传分片
const uploaded = await fetch(`/api/check/${fileHash}`).then(r => r.json());
// 并发上传未上传的分片
const scheduler = new Scheduler(3);
const tasks = chunks.map((chunk, index) => {
if (uploaded.includes(index)) return; // 已上传,跳过
return scheduler.add(() => {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("index", index);
formData.append("hash", fileHash);
return fetch("/api/upload", { method: "POST", body: formData });
});
});
await Promise.all(tasks);
// 通知合并
await fetch("/api/merge", {
method: "POST",
body: JSON.stringify({ hash: fileHash, chunks: chunks.length })
});
}Q4:前端错误监控
问题: 如何设计前端错误监控系统?
考察点:错误分类、捕获方式、上报策略
答案:
- JS 错误:
window.onerror+window.addEventListener("unhandledrejection") - 资源加载错误:
window.addEventListener("error", handler, true) - Vue 错误:
app.config.errorHandler - 请求错误:封装 fetch/axios 拦截器
- 上报策略:错误去重、批量上报、采样率控制
第二篇:Vue 面试题
2.1 Vue 2 vs Vue 3 核心差异
Q1:Vue 3 相比 Vue 2 有哪些核心优化?
答案:
| 维度 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式系统 | Object.defineProperty(递归遍历) | Proxy(惰性代理) |
| 虚拟 DOM | 全量比较 | PatchFlag + Block Tree |
| 静态提升 | 每次都重新创建 | 静态节点提升到渲染函数外 |
| 事件缓存 | 每次重新创建 | 缓存事件处理函数 |
| Tree-shaking | 不支持 | 原生支持 |
| TypeScript | 通过装饰器支持 | 原生 TS 重写 |
| 包体积 | ~30KB(gzip) | ~13KB(gzip) |
Vue 3 编译优化细节:
vue
<!-- 模板 -->
<div>
<span>静态文本</span>
<span>{{ dynamic }}</span>
</div>
<!-- Vue 3 编译结果(简化) -->
// 静态节点提升到函数外部,只创建一次
const _hoisted_1 = _createVNode("span", null, "静态文本");
function render(_ctx, _cache) {
return _createVNode("div", null, [
_hoisted_1,
_createVNode("span", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
]);
}
// PatchFlag = 1(动态文本),只更新文本内容,无需全量 diffQ2:Vue 3 的 Fragment、Teleport、Suspense 是什么?
答案:
vue
<!-- Fragment:支持多根节点 -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
<!-- Teleport:将子节点渲染到指定 DOM 位置 -->
<template>
<Teleport to="body">
<div class="modal">模态框</div>
</Teleport>
</template>
<!-- Suspense:处理异步组件加载状态 -->
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<Loading />
</template>
</Suspense>
</template>项目场景:
- Teleport:全局模态框、通知、工具提示
- Suspense:异步组件加载时的 loading 状态管理
2.2 响应式原理详解
Q1:Vue 2 和 Vue 3 的响应式实现区别?
答案:
Vue 2(Object.defineProperty):
javascript
// 递归遍历所有属性,劫持 getter/setter
function defineReactive(obj, key, val) {
observe(val); // 递归子属性
Object.defineProperty(obj, key, {
get() {
Dep.depend(); // 收集依赖
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
Dep.notify(); // 触发更新
}
});
}Vue 3(Proxy):
javascript
// 惰性代理,访问时才递归
function reactive(target) {
if (!isObject(target)) return target;
return new Proxy(target, {
get(target, key, receiver) {
track(target, key); // 收集依赖
const result = Reflect.get(target, key, receiver);
return isObject(result) ? reactive(result) : result; // 惰性递归
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) trigger(target, key); // 触发更新
return result;
}
});
}Q2:Vue 2 中为什么无法检测数组索引和长度的变化?
答案:
Vue 2 重写了数组的 7 个方法(push/pop/shift/unshift/splice/sort/reverse)来触发更新,但:
- 数组索引:
arr[0] = newVal无法被 Object.defineProperty 检测到 - 数组长度:
arr.length = 0无法被检测到
javascript
// Vue 2 的数组响应式处理
const arrayMethods = Object.create(Array.prototype);
["push", "pop", "shift", "unshift", "splice", "sort", "reverse"].forEach(method => {
const original = Array.prototype[method];
Object.defineProperty(arrayMethods, method, {
value: function(...args) {
const result = original.apply(this, args);
ob.dep.notify(); // 手动触发更新
return result;
}
});
});Vue 3 中这些都不是问题,因为 Proxy 可以直接拦截 set 和 deleteProperty 操作。
Q3:Vue 3 的 ref 和 reactive 的区别,什么时候用哪个?
答案:
javascript
import { ref, reactive, shallowRef, shallowReactive } from "vue";
// ref:用于基本类型 + 可整体替换的对象
const count = ref(0);
const user = ref({ name: "Alice" });
// 可以整体替换
user.value = { name: "Bob" };
// reactive:用于对象,不可整体替换
const state = reactive({ count: 0, user: { name: "Alice" } });
// 不可这样:state = reactive({})
// 官方推荐:
// - 基本类型 + 需要重新赋值 → ref
// - 复杂对象,无需整体替换 → reactive
// - 统一使用 ref(方便记忆,模板中自动解包)
// 性能优化:浅层响应
const shallow = shallowRef({ deep: { data: "不追踪" } });
// 只有 .value 变化才触发,深层变化不触发2.3 Composition API vs Options API
Q1:Composition API 相比 Options API 有什么优势?
答案:
markdown
Options API 的问题:
- 相关逻辑分散在 data/computed/methods/watch/lifecycle 中
- 逻辑复用困难(Mixins 有命名冲突、来源不清等问题)
- TypeScript 支持弱
- 大型组件难以维护
Composition API 的优势:
- 按功能逻辑组织代码,而不是按选项类型
- 逻辑复用更优雅(Composables 函数式)
- 更好的 TypeScript 类型推导
- tree-shaking 友好,按需引入代码对比:
vue
<!-- Options API:相关逻辑分散 -->
<script>
export default {
data() { return { searchQuery: "", results: [], loading: false }; },
computed: { hasResults() { return this.results.length > 0; } },
watch: {
searchQuery: {
handler: debounce(function(q) { this.search(q); }, 500),
immediate: true
}
},
methods: { async search(q) { this.loading = true; this.results = await fetchResults(q); this.loading = false; } }
};
</script>
<!-- Composition API:按功能组织 -->
<script setup>
import { ref, computed, watch } from "vue";
import { useDebounce } from "@/composables/useDebounce";
const searchQuery = ref("");
const results = ref([]);
const loading = ref(false);
const hasResults = computed(() => results.value.length > 0);
watch(useDebounce(searchQuery, 500), async (q) => {
loading.value = true;
results.value = await fetchResults(q);
loading.value = false;
});
</script>Q2:script setup 相比普通 setup 函数有什么优势?
答案:
markdown
1. 更简洁的语法:无需 export default,顶层的变量/函数自动暴露给模板
2. 更少的 boilerplate:无需 return 语句
3. 更好的 IDE 支持:直接导入 html/css 等资源
4. 更好的类型推导:defineProps / defineEmits 泛型参数
5. defineExpose 控制暴露给父组件的内容2.4 组件通信方案
Q1:列举 Vue 中所有的组件通信方式,并说明适用场景
答案:
| 方式 | 方向 | 适用场景 |
|---|---|---|
| props / emit | 父子双向 | 最基本的通信方式 |
| v-model | 父子双向 | 表单类组件封装 |
| ref + defineExpose | 父调子 | 父组件需要调用子组件方法 |
| $attrs / $listeners | 祖孙透传 | 高阶组件(HOC)透传属性 |
| provide / inject | 祖先到任意后代 | 主题、语言、配置等全局注入 |
| Event Bus | 任意组件 | 简单场景(Vue 3 不推荐) |
| Pinia / Vuex | 全局状态 | 复杂跨组件/跨页面状态 |
| slots | 父子模板插槽 | 布局和模板复用 |
Vue 3 的 provide/inject 响应式:
javascript
// 父组件
const count = ref(0);
provide("count", count);
provide("updateCount", (val) => { count.value = val; });
// 子组件
const count = inject("count");
const update = inject("updateCount");Q2:Vue 3 中 $attrs 的变化
答案:
markdown
Vue 2:$attrs 不含 class/style,$listeners 独立
Vue 3:$listeners 合并到 $attrs,包含 class/style
多根节点时需要手动绑定 $attrs:vue
<!-- 多根节点组件 -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main> <!-- 手动指定继承 $attrs 的节点 -->
<footer>...</footer>
</template>2.5 状态管理(Vuex vs Pinia)
Q1:Pinia 相比 Vuex 有哪些优势?为什么 Pinia 是推荐方案?
答案:
markdown
1. 移除 Mutation:直接修改 state,无需 commit,开发体验更好
2. 更好的 TypeScript 支持:自动推断类型
3. 更轻量:~1KB vs Vuex 的 ~10KB
4. 隐式模块化:每个 store 独立 define,无需命名空间
5. Composition API 风格:支持 options API 和 composition API 两种写法
6. Devtools 支持:与 Vuex 同级别
7. 支持 Vue 2 和 Vue 3:同一 API,不同版本代码对比:
javascript
// Vuex(需要 mutation + action)
const store = new Vuex.Store({
state: { count: 0 },
mutations: { increment(state) { state.count++; } },
actions: { incrementAsync({ commit }) { setTimeout(() => commit("increment"), 1000); } }
});
store.commit("increment"); // 同步:commit mutation
store.dispatch("incrementAsync"); // 异步:dispatch action
// Pinia(直接修改)
const store = defineStore("counter", () => {
const count = ref(0);
function increment() { count.value++; }
async function incrementAsync() {
await delay(1000);
count.value++;
}
return { count, increment, incrementAsync };
});
store.count++; // 直接修改!
store.increment(); // 或者调用 actionQ2:Pinia 的 storeToRefs 的作用是什么?
答案:
javascript
import { useCounterStore } from "@/stores/counter";
import { storeToRefs } from "pinia";
const store = useCounterStore();
// 直接解构会失去响应性
const { count } = store; // 非响应式!
// 使用 storeToRefs 保持响应性
const { count, name } = storeToRefs(store); // 响应式
// 方法可以直接解构
const { increment, reset } = store;2.6 Vue Router 核心
Q1:Vue Router 3 和 4 的主要区别
答案:
| 特性 | Vue Router 3 | Vue Router 4 |
|---|---|---|
| 创建方式 | new Router() | createRouter() |
| 历史模式 | mode: "history" | history: createWebHistory() |
| next 参数 | 必须调用 | 可选,推荐返回路径 |
| Composition API | 不支持 | useRouter()/useRoute() |
| TS 支持 | 弱 | 原生支持 |
| 导航故障 | 有限处理 | 完整导航故障对象 |
javascript
// Vue Router 4 新特性:导航故障
const result = await router.push("/login");
if (result === NavigationFailureType.redirected) {
// 处理重定向
}Q2:动态路由如何实现权限控制?
答案:
javascript
router.beforeEach(async (to, from) => {
const store = usePermissionStore();
// 1. 未登录重定向到登录页
if (to.meta.requiresAuth && !store.isLoggedIn) {
return { path: "/login", query: { redirect: to.fullPath } };
}
// 2. 首次登录,获取权限并动态添加路由
if (store.isLoggedIn && !store.routesLoaded) {
const userRoutes = await store.fetchUserRoutes();
userRoutes.forEach(route => {
router.addRoute(route); // 动态添加路由
});
store.routesLoaded = true;
return to.fullPath; // 重新导航
}
});2.7 性能优化实战
Q1:Vue 项目性能优化可以从哪些方面入手?
答案:
markdown
1. 代码层面:
- 使用 v-show 替代频繁切换的 v-if
- 使用 computed 缓存计算结果
- v-for 绑定唯一 key
- 合理使用 keep-alive 缓存组件
- 函数式组件(无状态组件)
2. 打包优化:
- 路由懒加载 const User = () => import("@/views/User.vue")
- 第三方库按需引入(Element Plus、lodash-es)
- 使用 Vite 替代 Webpack(开发模式)
- gzip / brotli 压缩
3. Vue 3 特有优化:
- shallowRef / shallowReactive 浅层响应
- markRaw 标记静态数据
- v-memo 指令(Vue 3.2+)
- Teleport 避免深层 DOM
4. 运行时优化:
- 虚拟滚动(大数据列表)
- Object.freeze() 冻结纯静态数据
- 避免过深的 watcher
- 及时清理定时器和事件监听Q2:keep-alive 的原理和使用场景
答案:
vue
<!-- 缓存动态组件 -->
<KeepAlive :max="10">
<component :is="currentTab" />
</KeepAlive>
<!-- 缓存路由组件 -->
<router-view v-slot="{ Component }">
<KeepAlive :include="["Home", "About"]">
<component :is="Component" />
</KeepAlive>
</router-view>原理:keep-alive 将缓存的组件实例存储在 cache 对象中,组件切换时不会销毁,而是从缓存中复用。LRU 策略控制最大缓存数量。
2.8 实际项目场景题
Q1:设计一个通用表单组件
问题:项目中有大量表单页面,如何设计一个通用、可扩展的表单组件?
答案:
vue
<template>
<form @submit.prevent="handleSubmit">
<FormField
v-for="field in schema"
:key="field.name"
:field="field"
v-model="formData[field.name]"
:error="errors[field.name]"
/>
<slot name="actions">
<button type="submit">提交</button>
</slot>
</form>
</template>
<script setup>
const props = defineProps({
schema: { type: Array, required: true },
// schema 结构:
// [{ name: "username", label: "用户名", type: "input", rules: [...] }]
});
const formData = reactive({});
const errors = ref({});
const { schema } = toRefs(props);
// 初始化表单数据
watch(schema, () => {
schema.value.forEach(field => {
formData[field.name] ??= field.default ?? "";
});
}, { immediate: true });
async function handleSubmit() {
// 校验逻辑
const valid = await validate(formData);
if (!valid) return;
emit("submit", { ...formData });
}
</script>考察点:组件抽象能力、schema 驱动设计、表单校验、v-model 双向绑定
Q2:SSR 与 CSR 的选择
问题:你们项目的性能瓶颈在哪里?如何判断是否需要 SSR?
答案:
markdown
需要 SSR 的场景:
- SEO 要求高(电商、内容网站)
- 首屏加载速度要求高
- 社交分享需要预览
不需要 SSR 的场景:
- 后台管理系统(B 端)
- 需要用户登录的 SaaS 应用
- 交互复杂、动画多的应用
SSR 的代价:
- 服务器成本增加(需要 Node.js 服务器)
- 开发复杂度增加
- 部分浏览器 API 不可用
- TTFB 变慢
推荐方案:
- Nuxt 3(Vue 生态系统中的 SSR 方案)
- SSG(静态生成)是 SSR 的轻量替代方案Q3:Vue 3 项目中如何做状态管理选型?
问题:一个中大型项目,如何选择状态管理方案?
答案:
markdown
小型项目(< 5 人):
- 简单共享:provide/inject + ref
- 或直接使用 Pinia
中型项目(10-20 人):
- Pinia(推荐)
- 模块化 store,按业务拆分
大型项目(50+ 人):
- Pinia + 模块化分层
- 配合 normalized 数据管理(类似 Vuex ORM)
- 考虑使用 Zustand / XState 处理复杂状态机
选型决策流程:
简单场景(props + emit + provide/inject)
|
v
中等复杂(Pinia)
|
v
复杂状态流(Pinia + 中间件 / XState)第三篇:工程化与架构
3.1 构建工具(Webpack/Vite)
Q1:Vite 的工作原理是什么?为什么比 Webpack 快?
答案:
markdown
Webpack 的构建流程:
启动 Dev Server → 打包所有模块 → 生成 bundle → 浏览器刷新
问题:冷启动慢、热更新慢,项目越大越明显
Vite 的构建流程:
启动 Dev Server → 按需编译(原生 ESM)→ 浏览器实时请求javascript
// Vite 开发模式:
// 1. 使用 esbuild 预构建依赖(Go 编写,比 TS/JS 快 10-100 倍)
// 2. 原生 ESM 加载源码,无需打包
// 3. 按需编译,只编译浏览器请求的文件
// 4. 利用浏览器缓存,模块级别的热更新
// Vite 生产模式:
// 使用 Rollup 打包(成熟、插件丰富、支持 Tree-shaking)Webpack vs Vite 核心差异:
| 维度 | Webpack | Vite |
|---|---|---|
| 冷启动 | 全量打包(分钟级) | 启动即用(毫秒级) |
| 热更新 | 模块编译(秒级) | 文件级 ESM(毫秒级) |
| 构建 | 自研打包 | 生产用 Rollup |
| 配置复杂度 | 较高 | 较低 |
| 生态 | 丰富成熟 | 快速成长 |
Q2:Webpack 的 Loader 和 Plugin 的区别
答案:
javascript
// Loader:文件转换器,处理模块源文件
// 作用:将文件从 A 格式转为 B 格式(如.ts → .js, .scss → .css)
module.exports = {
module: {
rules: [
{ test: /\.ts$/, use: "ts-loader" },
{ test: /\.scss$/, use: ["style-loader", "css-loader", "sass-loader"] }
]
}
};
// Plugin:扩展 Webpack 功能,在构建过程中注入额外行为
// 作用:打包优化、资源管理、环境变量注入
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
plugins: [
new HtmlWebpackPlugin({ template: "./index.html" })
]
};
// 区别总结:
// Loader → 处理文件(文件级别)
// Plugin → 扩展功能(生命周期级别)Q3:Webpack 构建优化策略
答案:
javascript
module.exports = {
// 1. 多线程编译
module: {
rules: [
{ test: /\.js$/, use: "thread-loader" } // 使用 Worker 池
]
},
// 2. 缓存
cache: { type: "filesystem" }, // 持久化缓存
// 3. 代码分割
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
vendor: { test: /[\\/]node_modules[\\/]/, name: "vendor" },
common: { minChunks: 2, name: "common" }
}
}
},
// 4. 摇树优化
optimization: { usedExports: true },
// 5. 压缩
optimization: {
minimizer: [new TerserPlugin({ parallel: true })]
}
};3.2 模块化与组件化
Q1:ES Module vs CommonJS 的区别
答案:
javascript
// CommonJS(Node.js 传统方式)
const fs = require("fs"); // 动态导入(运行时)
module.exports = { add }; // 导出值拷贝
// ES Module(标准方式)
import fs from "fs"; // 静态导入(编译时)
import("./module.js"); // 动态导入(运行时)
export const add = (a, b) => a + b; // 导出引用绑定
// 核心差异
// - ESM 静态分析 -> Tree-shaking 支持
// - ESM 异步加载 -> 浏览器原生支持
// - CJS 动态同步 -> 适合 Node.js
// - ESM 导出的是实时绑定,CJS 导出的是值的拷贝Q2:Modular Monorepo(模块化单体仓库)的优缺点
答案:
markdown
优点:
- 代码复用简单,共享组件/工具库
- 统一构建和发布流程
- 原子性提交(跨包改动一次提交)
- 团队协作更透明
缺点:
- 仓库体积大,git 操作慢
- 权限管理粒度粗
- 构建需要更复杂的工具链
工具:
- Nx / Turborepo(构建编排)
- pnpm Workspaces / Yarn Workspaces
- Changesets(版本管理和发布)3.3 微前端架构
Q1:微前端的核心实现方式有哪些?
答案:
markdown
主流方案对比:
1. iframe:
- 优点:隔离彻底、技术无关
- 缺点:通信复杂、SEO 差、性能开销
- 适用:简单集成、三方系统嵌入
2. Web Components(Custom Elements + Shadow DOM):
- 优点:原生支持、样式隔离
- 缺点:浏览器兼容性、生态不成熟
3. 基座模式(Single-SPA / qiankun):
- 优点:应用隔离、独立部署、技术无关
- 缺点:接入成本高、沙箱性能开销
- 代表:qiankun(基于 Single-SPA)
4. 模块联邦(Module Federation, Webpack 5):
- 优点:运行时加载、共享依赖
- 缺点:Webpack 绑定、版本管理复杂javascript
// qiankun 示例
import { registerMicroApps, start } from "qiankun";
registerMicroApps([
{
name: "vue-app",
entry: "//localhost:8081",
container: "#sub-app-container",
activeRule: "/vue",
props: { userInfo: { id: 1 } }
},
{
name: "react-app",
entry: "//localhost:8082",
container: "#sub-app-container",
activeRule: "/react"
}
]);
start({ sandbox: { experimentalStyleIsolation: true } });3.4 性能优化体系
Q1:前端性能优化的核心指标和监控方式
答案:
markdown
核心指标(Web Vitals):
- LCP(Largest Contentful Paint)< 2.5s — 最大内容绘制
- FID(First Input Delay)< 100ms — 首次输入延迟
- CLS(Cumulative Layout Shift)< 0.1 — 累计布局偏移
- TTFB(Time to First Byte)< 800ms — 首字节时间
- FCP(First Contentful Paint)< 1.8s — 首次内容绘制
监控方式:
1. Performance API(浏览器原生)
2. PerformanceObserver(监听 LCP/CLS/FID)
3. 自行上报或使用开源方案(Sentry、Web Vitals)javascript
// 使用 PerformanceObserver 监控
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === "largest-contentful-paint") {
console.log("LCP:", entry.startTime);
}
if (entry.entryType === "first-input") {
console.log("FID:", entry.processingStart - entry.startTime);
}
}
});
observer.observe({ type: "largest-contentful-paint", buffered: true });
observer.observe({ type: "first-input", buffered: true });Q2:图片优化策略
答案:
html
<!-- 1. 响应式图片 -->
<img
srcset="photo-320w.jpg 320w, photo-640w.jpg 640w, photo-1280w.jpg 1280w"
sizes="(max-width: 320px) 280px, (max-width: 640px) 600px, 1200px"
src="photo-1280w.jpg"
loading="lazy"
decoding="async"
/>
<!-- 2. 使用 WebP/AVIF -->
<picture>
<source type="image/avif" srcset="photo.avif">
<source type="image/webp" srcset="photo.webp">
<img src="photo.jpg" alt="photo">
</picture>
<!-- 3. 懒加载 -->
<img loading="lazy" src="photo.jpg" alt="photo">3.5 前端安全
Q1:XSS 和 CSRF 的原理及防御措施
答案:
markdown
XSS(跨站脚本攻击):
- 原理:注入恶意脚本到页面中执行
- 类型:存储型、反射型、DOM 型
- 防御:
- 输入过滤与输出转义(HTML 实体编码)
- CSP(Content Security Policy)
- HttpOnly Cookie
- 使用 DOMPurify 清洗用户输入
CSRF(跨站请求伪造):
- 原理:利用用户登录态发起恶意请求
- 防御:
- SameSite Cookie(Lax/Strict)
- Token 验证
- Referer/Origin 校验
- 关键操作二次验证javascript
// CSP 配置示例
// <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://trusted.com">
// 或通过 HTTP 头:Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.com
// 输出转义
function escapeHTML(str) {
return str.replace(/[&<>"']/g, function(match) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[match];
});
}第四篇:项目经验与系统设计
4.1 项目架构设计
Q1:你参与过的最有挑战性的项目是什么?你是如何设计架构的?
考察要点:
markdown
回答框架(STAR 原则):
1. Situation(情境):项目背景、规模、团队
2. Task(任务):你的角色和职责
3. Action(行动):你做的架构决策和原因
4. Result(结果):数据化的成果
项目架构示例回答:
"在一个电商后台管理系统中,我主导了从 Vue 2 + Webpack 到 Vue 3 + Vite 的技术栈升级。
主要挑战是 30+ 个业务模块需要平滑迁移,且不能影响线上运营。
架构设计:
1. 采用微前端(qiankun)拆分应用
2. 先迁移核心模块到 Vue 3 + TypeScript
3. 旧模块通过 qiankun 接入,逐步替换
4. 统一封装了业务组件库和 API 层
结果:3 个月内完成全量迁移,构建速度提升 80%,线上零故障。"Q2:如何设计一个高可用的前端监控系统?
答案:
javascript
// 监控系统架构
class Monitor {
constructor() {
this.queue = [];
this.config = { sampleRate: 0.1 };
this.init();
}
init() {
// 1. JS 运行时错误
window.onerror = (msg, url, line, col, error) => {
this.report({ type: "error", subtype: "js", msg, url, line, col, stack: error?.stack });
};
// 2. Promise 未捕获错误
window.addEventListener("unhandledrejection", (event) => {
this.report({ type: "error", subtype: "promise", reason: event.reason?.message });
});
// 3. 资源加载错误
window.addEventListener("error", (event) => {
if (event.target?.tagName) {
this.report({ type: "error", subtype: "resource", tag: event.target.tagName, src: event.target.src });
}
}, true);
// 4. 请求错误(axios 拦截器)
// 5. 性能指标(PerformanceObserver)
// 6. 用户行为追踪(点击、路由跳转)
}
report(data) {
// 采样控制
if (Math.random() > this.config.sampleRate) return;
// 去重(3 秒内相同错误不重复上报)
const key = JSON.stringify(data);
if (this.lastReport?.[key] > Date.now() - 3000) return;
this.lastReport = { ...this.lastReport, [key]: Date.now() };
// 批量上报
this.queue.push({ ...data, timestamp: Date.now(), url: location.href, ua: navigator.userAgent });
if (this.queue.length >= 10) {
this.flush();
}
}
flush() {
const batch = this.queue.splice(0);
navigator.sendBeacon?.("/api/monitor", JSON.stringify({ events: batch }));
}
}4.2 难点与解决方案
Q1:项目中的技术选型决策过程
问题:你是如何在项目中选择技术方案的?举例说明。
答案框架:
markdown
以"状态管理选型"为例:
背景:一个中大型 CRM 系统,30+ 模块,需要管理大量跨组件状态
方案对比:
1. Vuex 4:成熟但需要 mutation、TS 支持弱
2. Pinia:轻量、TS 原生、无 mutation、官方推荐
3. Provide/Inject:不够正式,不适合复杂状态
决策因素:
- 团队熟悉度(Vuex,但愿意学习新方案)
- TypeScript 支持(Pinia 完胜)
- 未来维护成本(Pinia 更简洁)
- 社区趋势(官方推荐 Pinia)
结果:采用 Pinia,模块化 split storeQ2:线上事故处理流程
问题:如果线上出现重大 bug,你的处理流程是什么?
答案:
markdown
1. 立即止损(5 分钟内)
- 评估影响范围
- 回滚/热修复/开关控制
- 通知相关方
2. 定位问题(30 分钟内)
- 查看监控告警
- 查看错误日志(Sentry)
- 定位根因
3. 修复验证(1 小时内)
- 编写修复代码
- code review
- 灰度发布
4. 复盘总结(24 小时内)
- 事故报告(5W1H)
- 改进措施(监控补充、流程优化)
- 知识沉淀(避免重复犯错)4.3 系统设计题
Q1:设计一个活动页面搭建平台
问题:如果让你设计一个可视化拖拽搭建平台(类似 易企秀/稿定设计),你会如何设计前端架构?
答案:
markdown
核心模块:
1. 画布引擎
- 渲染层:组件树 + 绝对定位
- 操作层:拖拽、缩放、旋转、对齐
- 选中层:显示选中组件的控制手柄
2. 组件库
- 基础组件:文本、图片、按钮、形状
- 业务组件:商品卡片、表单、轮播
- 每个组件实现:renderer、props、configPanel
3. 属性面板
- 根据选中组件的 props schema 动态渲染
- 支持:样式、数据、交互配置
4. 历史管理(撤销/重做)
- Command 模式 + 状态快照
- 限制历史栈最大步数
5. 导出发布
- 生成静态 HTML
- 或渲染为 JSON 配置,由渲染引擎消费
技术选型:
- 框架:Vue 3 + TypeScript
- 状态:Pinia(管理画布数据)
- 拖拽:vuedraggable(基于 SortableJS)
- 撤销/重做:undo-redo-stack
- 渲染器:自研 JSON → Vue 组件渲染器Q2:设计一个实时协作系统
问题:如何设计一个多人实时协作编辑系统(如 Google Docs)?
答案:
核心技术:
- WebSocket(实时通信)
- CRDT / OT(操作转换算法)
- 操作合并与冲突解决
前端架构:
- 本地操作立即应用(乐观更新)
- 操作通过 WebSocket 广播
- 收到服务端确认/冲突结果
- 如有冲突,根据 CRDT/OT 合并
简化实现:
- 使用 ShareDB / Y.js 等成熟库
- 或使用 Operational Transform 算法
- 通过 WebSocket 同步操作日志
Vue 中的应用:
javascript
// 使用 y-websocket + yjs
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
const doc = new Y.Doc();
const wsProvider = new WebsocketProvider("ws://localhost:1234", "room-name", doc);
// 创建一个共享数组
const sharedArray = doc.getArray("items");
sharedArray.observe(event => {
// 更新本地视图
});4.4 基于当前知识框架的项目专项面试题
本节对应当前文件夹中的
10-项目实战与产出、01-编程语言基础、02-前端基础与框架、03-前端工程化与构建、04-前端架构师进阶、05-后端与全栈框架、06-数据库与数据访问、07-AI与智能体开发、08-部署运维与容器。回答时不要只背技术名词,要把“业务背景、技术方案、关键难点、落地结果”连成闭环。
Q1:如何介绍一个 AI Prompt 工作台项目?
面试官想看:你是否理解 AI 应用的基本交互链路、流式输出、Prompt 模板管理、参数调优和历史记录。
参考回答:
我做的是一个面向研发、运营和产品同学的 Prompt 调试工作台,核心能力是编辑 Prompt、配置模型参数、发起运行、查看流式结果、沉淀模板和历史记录。前端使用 Vue 3 + TypeScript + Pinia 管理编辑态、参数态和运行态,接口层通过统一 request 封装处理鉴权、错误和取消请求;后端可以用 Node/NestJS 或其他服务转发模型调用,并把模板、运行记录、评分结果落到 PostgreSQL 或 MongoDB。
项目难点主要有三类:
- 流式输出体验:AI 返回内容不是一次性完成,需要用 SSE、ReadableStream 或 WebSocket 持续渲染,并支持停止生成、失败重试和 partial content 保留。
- Prompt 参数校验:temperature、top_p、max_tokens 等参数要做范围校验,模板变量要在运行前检查是否缺失。
- 历史记录与复现:每次运行要保存 prompt、参数、模型、输入变量、输出结果和耗时,保证后续可以复盘。
项目亮点可以这样表达:
- 把 Prompt 模板、变量、运行参数和结果版本化,避免“只看到一次结果,无法复现”的问题。
- 对流式输出做了状态机管理:idle、running、paused、success、failed,UI 可以稳定展示加载、取消、重试和错误。
- 增加 token 用量、耗时、失败率等指标,方便比较不同 Prompt 版本的效果和成本。
常见追问:
- 为什么 AI 流式输出更常用 SSE,而不是一定用 WebSocket?
- 用户点击“停止生成”时,前端和后端分别要做什么?
- Prompt 模板变量如何设计?如何避免运行时变量缺失?
- 如果接口返回一半失败,历史记录应该保存什么状态?
Q2:如何介绍一个 AI 产品组件工作室或组件库项目?
面试官想看:你是否具备组件抽象、类型设计、设计系统、文档化和工程化交付能力。
参考回答:
这个项目的目标是沉淀 AI 产品常用组件,例如 ChatMessage、PromptEditor、StreamingOutput、AgentTimeline、ToolCallCard、CitationList、CostBadge 等。组件库不只是 UI 展示,还需要覆盖 AI 场景中的流式内容、工具调用、引用来源、失败重试、空状态和长文本折叠。
架构上我会把组件拆成四层:
- 基础组件层:Button、Input、Modal、Table、Tabs、Toast。
- 业务组件层:PromptEditor、StreamingOutput、AgentTimeline。
- 场景页面层:Prompt 调试页、RAG 问答页、Agent 运行页。
- 文档和演示层:Props 表、状态矩阵、示例代码、Live Preview。
重难点:
- 类型设计:组件 props 要用 TypeScript 明确输入输出,事件名和数据结构不能随意变。
- 状态覆盖:AI 组件常见状态很多,包括 pending、streaming、success、error、cancelled、empty、timeout。
- 主题和可访问性:组件要支持明暗主题、键盘操作、aria 属性和足够的对比度。
- 打包交付:如果作为组件库发布,需要考虑 Vite library mode、按需引入、类型声明和样式隔离。
亮点表达:
- 我不是简单堆组件,而是建立了组件 API 规范、状态矩阵和文档示例,使组件可以被多个 AI 项目复用。
- 对 StreamingOutput 和 AgentTimeline 这类 AI 组件抽象了统一事件模型,让不同模型、不同后端流式协议都能接入。
- 通过 Storybook、VitePress 或自建文档页沉淀组件用法,降低团队接入成本。
常见追问:
- 如何判断一个组件应该沉淀到组件库,而不是留在业务里?
- 组件库如何做版本管理和破坏性变更?
- 如何让组件库同时支持 Tailwind CSS 和项目自定义主题?
Q3:AI 任务管理全栈平台的项目难点是什么?
面试官想看:你是否能把前端、后端、数据库、权限、AI 能力结合成完整业务系统。
参考回答:
AI 任务管理平台不是一个纯 ToDo,它涉及用户、组织、项目、任务、评论、附件、AI 建议等业务对象。前端负责看板、任务详情抽屉、筛选排序、拖拽变更状态和 AI 建议中心;后端提供 RESTful API,使用 Prisma 或 MyBatis 操作数据库,任务状态变更要考虑事务、权限和审计。
核心难点:
- 看板拖拽的一致性:前端可以乐观更新,但后端保存失败时要回滚;多人同时操作时要处理版本冲突。
- 权限模型:组织管理员、项目成员、访客对项目和任务的读写权限不同,不能只在前端隐藏按钮。
- AI 建议接入:AI 可以生成任务拆解、优先级建议和风险提示,但结果需要人工确认后才能写入业务数据。
- 查询性能:任务列表常有状态、负责人、标签、时间范围等组合筛选,需要合理设计索引和分页。
亮点表达:
- 我把 AI 能力设计成“建议层”,不会直接修改核心任务数据,降低误操作风险。
- 任务状态变更采用乐观更新 + 失败回滚 + 服务端校验,兼顾体验和数据正确性。
- 通过统一 DTO、接口契约和错误码,让前后端协作更稳定。
常见追问:
- 拖拽排序字段如何设计?整数排序、浮点排序、链表式排序各有什么问题?
- 如果两个用户同时移动同一个任务,如何处理冲突?
- AI 生成内容如何标记来源、置信度和人工确认状态?
Q4:RAG 个人知识库项目如何讲清楚?
面试官想看:你是否理解 RAG 的完整链路,而不是只会说“上传文档后问答”。
参考回答:
RAG 知识库的链路可以拆成文档上传、文本解析、切片、向量化、入库、检索、重排、生成答案和引用来源展示。前端侧重点不是模型算法本身,而是异步任务进度、文档状态、问答体验、引用来源和检索调试能力。
核心流程:
- 用户上传 PDF、Markdown、Word 或网页内容。
- 后端解析文本,并按标题、段落或 token 长度切片。
- 对每个 chunk 生成 embedding,写入向量库或支持向量能力的数据库。
- 问答时先检索相关片段,再把片段和问题一起交给模型生成答案。
- 前端展示答案、引用来源、相关片段和命中分数。
重难点:
- 文档处理是异步过程,前端要展示 queued、parsing、embedding、indexed、failed 等状态。
- 检索质量依赖 chunk 策略、召回数量、重排和过滤条件,不是简单把全文塞给模型。
- 引用来源必须可追溯,否则回答可信度不足。
- 大文档上传、解析失败、重复上传和权限隔离都要处理。
亮点表达:
- 我增加了检索调试页面,可以查看 query、召回片段、分数、来源文档和最终上下文,方便定位“答非所问”的原因。
- 文档入库采用任务状态机,前端通过轮询、SSE 或 WebSocket 展示处理进度。
- 答案展示强制绑定引用来源,降低 AI 幻觉对用户的影响。
常见追问:
- chunk size 怎么选?过大和过小分别有什么问题?
- 如何处理用户更新文档后的向量数据同步?
- RAG 和直接长上下文问答有什么区别?
- 检索结果为空时应该怎样降级?
Q5:Agent 工作流编排台的项目亮点是什么?
面试官想看:你是否理解 Agent 的状态、工具调用、人工审批、运行日志和可观测性。
参考回答:
Agent 工作流编排台可以理解为一个可视化的 AI 自动化流程系统。用户配置工作流模板,模板中包含 LLM 节点、工具节点、条件分支、人工审批节点和结束节点。运行时后端按图执行,前端实时展示每个节点的输入、输出、耗时、状态和错误。
核心难点:
- 工作流不是线性请求,而是有状态的执行图,需要保存 runId、nodeId、status、input、output、error、startedAt、finishedAt。
- 工具调用可能失败,要支持 retry、timeout、fallback 和人工介入。
- 人工审批会让流程暂停,必须能持久化上下文,审批后继续执行。
- 前端要把后端事件流转成 Timeline、节点状态和 Inspector 面板。
亮点表达:
- 我把运行事件抽象为统一事件模型,例如 node_started、token_delta、tool_called、human_required、node_finished、run_failed。
- 引入 checkpoint 思路,流程中断后可以从最近状态恢复,而不是整条链路重跑。
- 对工具调用做了参数展示、结果折叠、敏感字段脱敏和失败重试,提升排查效率。
常见追问:
- Agent 工作流和普通工作流引擎有什么不同?
- 为什么人工审批节点需要持久化?
- 前端如何展示一个长时间运行的工作流?
- 工具调用失败会不会导致数据不一致?如何保证幂等?
Q6:企业 AI 助手控制台如何体现工程能力?
面试官想看:你是否理解企业系统中的权限、审计、数据安全、后端集成和可维护性。
参考回答:
企业 AI 助手控制台通常面向客服、销售、运营或内部知识管理场景。它不是简单聊天窗口,而是要接入客户数据、工单数据、知识库、权限体系和审计日志。前端通常包括客户列表、客户详情、AI Copilot 侧栏、推荐话术、风险提示和审计日志页面。
重难点:
- 权限与数据隔离:不同角色能看到的客户、字段、操作不同,前端展示和后端接口都要校验。
- 审计日志:AI 建议、用户采纳、敏感数据访问和关键操作都要可追踪。
- 后端集成:可能要和 Java/Spring/MyBatis 老系统集成,接口字段、分页、权限模型不一定统一。
- 敏感信息处理:手机号、邮箱、身份证、合同金额等字段需要脱敏、权限校验和日志记录。
亮点表达:
- 我把 AI Copilot 设计为业务辅助侧栏,不打断原有 CRM 操作流。
- 对 AI 生成建议加入“采纳、复制、编辑后采纳、弃用”的反馈闭环,为后续优化提供数据。
- 关键操作接入审计日志,方便企业合规和问题追溯。
常见追问:
- 前端脱敏和后端脱敏分别解决什么问题?
- 如果 AI 建议错误导致业务损失,系统上如何降低风险?
- 企业旧系统接口不规范时,前端如何做适配层?
Q7:AI 产品运营中台毕业项目怎么讲成作品集亮点?
面试官想看:你是否具备从单点功能升级为平台化系统的能力。
参考回答:
AI 产品运营中台可以作为前面多个项目的整合:Prompt Center 管理 Prompt 版本,Knowledge Center 管理知识库,Agent Center 管理工作流,Feedback Center 收集用户反馈,Cost Center 统计 token 和调用成本,Release Center 管理灰度发布和配置变更。
架构重点:
- 前端模块化:按业务域拆分 Prompt、Knowledge、Agent、Feedback、Cost、Release。
- 数据模型统一:用户、组织、项目、版本、运行记录、反馈、成本记录之间要有明确关系。
- 权限统一:菜单权限、页面权限、按钮权限和数据权限要贯通。
- 可观测性:每次模型调用、检索、工具调用和发布变更都要有日志和指标。
项目亮点:
- 从“功能型 AI 应用”升级为“运营型 AI 平台”,能讲清楚 Prompt、知识库、Agent、反馈、成本、发布之间的关系。
- 引入成本中心和反馈中心,让 AI 功能可评估、可迭代,而不是一次性 Demo。
- 通过 Release Center 做灰度和回滚,避免 Prompt 或知识库变更直接影响全部用户。
常见追问:
- Prompt 版本如何设计?如何回滚?
- Cost Center 如何统计 token 成本和接口耗时?
- Feedback Center 的反馈如何反哺 Prompt 或 RAG 优化?
- 如果多个模块由不同团队维护,前端工程如何组织?
Q8:如何介绍 ECharts + Canvas + WebSocket 实时数据大屏?
面试官想看:你是否能把可视化、Canvas 性能、实时通信和数据节流讲到一起。
参考回答:
实时数据大屏一般包含趋势图、排行榜、告警流、地图、指标卡和实时日志。ECharts 适合常规图表和交互,Canvas 适合大量粒子、轨迹、热力或自定义高性能绘制,WebSocket 用于服务端主动推送实时数据。
重难点:
- 数据频率高:WebSocket 可能每秒推送多次,图表不能每条消息都 setOption。
- 数据量大:长时间运行会造成数组无限增长和内存上升,需要滑动窗口。
- 页面性能:图表、动画、表格和日志同时更新,容易造成主线程卡顿。
- 断线重连:网络波动时要有心跳、重连、补偿拉取和状态提示。
解决方案:
- WebSocket 收到消息后先进入缓冲队列,用 requestAnimationFrame 或固定时间窗口批量刷新。
- ECharts 使用增量更新、dataset、appendData 或只保留最近 N 个点。
- Canvas 绘制做离屏缓存、分层渲染,复杂计算尽量放到 Web Worker。
- 实时连接设计心跳、指数退避重连和 lastEventId / timestamp 补偿。
亮点表达:
- 我没有让图表跟着消息频率盲目刷新,而是做了消息缓冲和渲染节流,保证实时性和帧率之间平衡。
- 对历史数据采用滑动窗口,控制内存增长。
- 连接层加入心跳和重连策略,弱网下也能恢复。
常见追问:
- ECharts 的 setOption 为什么可能造成性能问题?
- Canvas 和 SVG 的适用场景有什么区别?
- WebSocket 断线后如何补齐丢失数据?
- 实时大屏如何做首屏加载和后续增量更新?
Q9:Vite / Webpack 工程化迁移项目怎么回答?
面试官想看:你是否理解构建工具原理、迁移风险、插件生态和团队协作成本。
参考回答:
如果项目从 Vue CLI + Webpack 迁移到 Vite,核心目标通常是提升开发启动速度和 HMR 体验,同时保持生产构建稳定。迁移不是简单替换命令,而是要处理环境变量、路径别名、CSS/PostCSS、静态资源、代理、按需引入、插件替换和 CI 构建流程。
重难点:
- 路径别名差异:Webpack alias 和 Vite resolve.alias 写法不同,TypeScript paths 也要同步。
- 环境变量差异:Vue CLI 使用
VUE_APP_,Vite 默认暴露VITE_。 - CSS 处理差异:PostCSS、Less/Sass、CSS Modules、Autoprefixer、Tailwind 配置要验证。
- 插件替换:Webpack loader/plugin 不能直接迁到 Vite,需要找 Rollup/Vite 插件或改写。
- 生产构建差异:Vite 开发期基于原生 ESM,生产期基于 Rollup,不能只测 dev。
亮点表达:
- 我先梳理构建链路和风险清单,再做小模块试点,最后逐步迁移,避免一次性大爆炸式改造。
- 对 alias、env、proxy、PostCSS、静态资源和构建产物做了迁移对照表。
- 建立 dev、build、preview、E2E 验证流程,确保开发体验和线上行为一致。
常见追问:
- Vite 为什么开发环境启动更快?
- Vite 如何解析 Vue 单文件组件?
- Webpack loader 和 plugin 的区别是什么?
- PostCSS 在 Vite 中处于什么位置?
Q10:动态权限、动态路由和页面权限如何讲?
面试官想看:你是否理解前端权限只是体验层,真正安全必须依赖后端校验。
参考回答:
权限系统通常分为菜单权限、路由权限、按钮权限、接口权限和数据权限。前端根据后端返回的角色、权限码或菜单树生成路由和页面操作按钮,后端在接口层继续校验权限,避免用户绕过前端直接请求接口。
实现方案:
- 登录后获取用户信息、角色、权限码和菜单树。
- 前端把菜单树转换成 Vue Router 路由配置,并用 addRoute 动态注册。
- 路由守卫检查 token、白名单、路由权限和页面初始化状态。
- 按钮使用权限指令或 composable 判断权限码。
- 后端接口继续做鉴权和数据范围校验。
重难点:
- 刷新页面后动态路由丢失,需要在路由守卫中恢复权限和路由。
- 后端菜单树和前端组件路径要建立稳定映射,不能直接信任任意路径。
- 按钮隐藏不等于接口安全,接口必须二次校验。
- 多租户或组织权限下还要处理数据范围。
亮点表达:
- 我把权限拆成路由级、按钮级和数据级,不把“隐藏菜单”当成完整权限方案。
- 动态路由注册做了白名单映射,避免后端返回任意组件路径造成安全风险。
- 权限初始化支持刷新恢复,避免白屏和 404 问题。
Q11:大文件上传项目有哪些高频追问?
面试官想看:你是否理解分片、断点续传、秒传、并发控制和服务端合并。
参考回答:
大文件上传的基本链路是前端选择文件、计算 hash、切片、查询已上传分片、并发上传缺失分片、服务端合并、校验完整性。前端重点是并发控制、进度展示、失败重试、暂停继续和弱网恢复。
重难点:
- hash 计算可能阻塞主线程,大文件应使用 Web Worker 或增量计算。
- 分片并发过高会打满网络和服务端连接,过低又影响速度,需要限制并发数。
- 上传失败要支持单片重试,而不是整个文件重传。
- 服务端合并后要校验文件完整性,并清理临时分片。
亮点表达:
- 我把上传任务抽象为状态机:waiting、hashing、uploading、paused、merging、success、failed。
- 通过 hash + chunkIndex 实现秒传和断点续传。
- 上传队列支持并发限制、失败重试和暂停恢复,用户体验更稳定。
常见追问:
- 文件 hash 放前端算还是后端算?
- 分片大小怎么定?
- 如何保证分片合并顺序正确?
- 暂停上传后如何恢复?
Q12:如何把部署、缓存和质量保障讲成项目亮点?
面试官想看:你是否有完整交付意识,而不是只会写页面。
参考回答:
项目上线不只是 npm run build,还包括构建产物、Nginx 配置、Docker 镜像、环境变量、静态资源缓存、接口反向代理、SPA history fallback、灰度发布、监控告警和回滚方案。
重难点:
- Nginx 要处理静态资源缓存和
index.html不缓存,避免发版后用户拿到旧入口文件。 - SPA history 路由需要 fallback 到
index.html,否则刷新深层路由会 404。 - Docker 镜像要区分构建阶段和运行阶段,减少镜像体积。
- 监控要覆盖 JS 错误、Promise 错误、资源加载失败、接口失败和性能指标。
亮点表达:
- 我把部署配置、环境变量、反向代理和缓存策略文档化,减少交接成本。
- 对静态资源使用 hash 文件名 + 长缓存,对 HTML 使用 no-cache,兼顾性能和发版正确性。
- 上线后接入错误监控和性能指标,能及时发现白屏、接口异常和首屏性能问题。
4.5 项目重难点与亮点表达
4.5.1 项目难点表达矩阵
| 项目场景 | 面试官真正想确认 | 项目难点 | 可落地方案 | 亮点表达 |
|---|---|---|---|---|
| AI 流式问答 | 是否理解流式协议和体验细节 | 内容分片返回、取消生成、失败恢复 | SSE / WebSocket / ReadableStream + AbortController + 状态机 | 流式渲染不是简单拼字符串,而是管理连接、状态、取消和异常 |
| RAG 知识库 | 是否理解检索增强生成链路 | 文档解析、切片、向量化、召回质量 | 异步任务状态 + 检索调试页 + 引用来源 | 通过可视化召回片段定位回答质量问题 |
| Agent 工作流 | 是否理解有状态 AI 编排 | 节点状态、工具失败、人工审批 | 事件模型 + checkpoint + retry/fallback | 把黑盒 Agent 运行变成可观察、可恢复的流程 |
| 动态权限 | 是否理解权限安全边界 | 刷新恢复、按钮权限、接口绕过 | 动态路由 + 权限码 + 后端二次校验 | 前端负责体验,后端负责安全,权限分层设计 |
| 大文件上传 | 是否理解复杂异步任务 | hash、分片、断点续传、并发 | Web Worker + chunk 上传 + 并发池 + 重试 | 上传任务状态机提升稳定性和可维护性 |
| 实时大屏 | 是否理解高频渲染性能 | 数据高频推送、图表卡顿、内存增长 | 消息缓冲 + 渲染节流 + 滑动窗口 | 在实时性和性能之间做平衡,而不是每条消息都刷新 |
| 组件库 | 是否具备抽象和工程化能力 | API 设计、状态覆盖、主题适配 | TypeScript 类型 + 状态矩阵 + 文档演示 | 组件沉淀服务多个项目,而不是只解决单页需求 |
| Vite 迁移 | 是否理解构建链路 | alias、env、PostCSS、插件差异 | 迁移清单 + 试点模块 + build/preview 验证 | 迁移不是换工具,而是控制工程风险 |
| 可观测性 | 是否具备线上意识 | 错误采集、性能指标、告警闭环 | Sentry/自建监控 + PerformanceObserver + sendBeacon | 线上问题能发现、定位、复盘和预防 |
| 部署缓存 | 是否理解交付链路 | 缓存错配、路由 404、环境混乱 | Nginx cache-control + history fallback + Docker 多阶段构建 | 构建、部署、缓存、回滚形成闭环 |
4.5.2 项目亮点回答模板
模板一:技术闭环型
text
这个项目我主要负责 X 模块。它的难点不是页面本身,而是 Y。
我先把问题拆成 A、B、C 三层,然后选择了 D 技术方案。
落地时重点处理了 E 风险,例如异常、性能、权限或一致性。
最后的结果是 F,至少在体验、稳定性、效率或可维护性上有明确改善。模板二:架构选型型
text
当时有几个方案:A 成本低但扩展性弱,B 能力强但引入复杂度高,C 和团队当前技术栈更匹配。
我最后选 C,原因是它满足当前业务规模,同时保留后续升级空间。
为了降低风险,我做了试点、灰度、监控和回滚方案。模板三:问题复盘型
text
项目中最难的问题是 X。最开始我们遇到 Y 现象,通过日志、监控和最小复现定位到 Z。
修复上我没有只改表面 bug,而是补了边界处理、测试用例和监控指标。
这件事之后,我们把相关流程沉淀成规范,避免同类问题再次出现。4.5.3 项目数据不要乱编
面试中可以量化成果,但不要编造无法解释的数据。更稳妥的表达方式:
- 性能类:首屏时间、构建耗时、接口耗时、图表帧率、错误率。
- 效率类:组件复用数量、重复代码减少、配置时间缩短、发版步骤减少。
- 稳定性类:错误告警覆盖、失败重试成功率、上传恢复成功率。
- 业务类:模板使用次数、知识库命中率、AI 建议采纳率、人工处理时间缩短。
如果没有真实数据,可以说:
text
这个项目是学习/作品集项目,所以没有生产环境真实业务数据。
但我在设计时按生产标准预留了性能指标、错误监控和可量化埋点,例如 X、Y、Z。4.6 项目追问清单
4.6.1 架构与技术选型
- 这个项目为什么选择 Vue 3,而不是 React 或 Vue 2?
- 为什么使用 TypeScript?它在项目中解决了哪些具体问题?
- Pinia、provide/inject、URL state、localStorage 分别适合存什么状态?
- 哪些模块适合抽成组件库?哪些不适合?
- 你如何判断项目需要 Monorepo?
- 你如何设计前后端接口契约?
- 你如何处理技术债?如何决定重构优先级?
4.6.2 前端工程化
- Vite 开发环境和生产构建为什么是两套机制?
- Vite 如何处理
.vue文件、CSS、静态资源和路径别名? - Webpack 的 compiler、compilation、loader、plugin 分别是什么?
- PostCSS、Sass、Tailwind、CSS Modules 在构建链路中的位置是什么?
- 如何做构建产物分析和拆包优化?
- 如何保证迁移构建工具后线上行为不变?
- CI 中应该跑哪些检查?
4.6.3 Vue 与业务复杂度
- Composition API 如何组织复杂业务逻辑?
- 一个通用表单组件如何设计校验、联动、异步选项和权限?
- 动态路由刷新白屏如何解决?
- keep-alive 什么时候会带来副作用?
- 大型列表、树形菜单、看板拖拽分别如何优化?
- 如何避免组件通信混乱?
4.6.4 AI 应用专项
- Prompt 模板如何做版本管理?
- 流式输出如何取消?取消后后端还会继续消耗资源吗?
- RAG 为什么需要引用来源?
- chunk 策略如何影响检索质量?
- Agent 工具调用失败如何处理?
- 人工审批节点为什么需要持久化?
- AI 生成内容如何做风险提示和人工确认?
- 如何统计 token 成本、耗时和失败率?
4.6.5 实时通信与可视化
- SSE 和 WebSocket 如何选型?
- WebSocket 心跳和重连怎么做?
- 高频消息如何避免把 Vue 页面打卡顿?
- ECharts 如何处理大数据量更新?
- Canvas 如何做分层绘制和离屏缓存?
- 实时大屏如何控制内存增长?
4.6.6 后端、数据库与缓存
- RESTful API 如何设计资源和状态码?
- Prisma 和 MyBatis 的适用场景有什么差异?
- PostgreSQL、MySQL、MongoDB 如何选型?
- Redis 在项目中可以做缓存、限流、队列还是分布式锁?
- 数据库索引如何根据查询条件设计?
- 事务在任务状态流转、分片上传合并、订单类业务中解决什么问题?
- 如何处理接口幂等?
4.6.7 部署、质量和线上问题
- Nginx 如何配置 SPA history fallback?
- 为什么
index.html不应该强缓存,而 hash 静态资源可以长缓存? - Docker 多阶段构建解决什么问题?
- 前端监控需要采集哪些错误和性能指标?
- 白屏问题如何定位?
- 线上事故如何止损、定位、修复和复盘?
- 灰度发布和回滚方案如何设计?
第五篇:代码实现题
5.1 手写实现
1. 手写 Promise(核心实现)
javascript
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
class MyPromise {
constructor(executor) {
this.state = PENDING;
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
const reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : v => v;
onRejected = typeof onRejected === "function" ? onRejected : e => { throw e; };
const promise2 = new MyPromise((resolve, reject) => {
if (this.state === FULFILLED) {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
this.resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}
if (this.state === REJECTED) {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}
if (this.state === PENDING) {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
this.resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
});
return promise2;
}
resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError("Chaining cycle detected"));
}
if (x && (typeof x === "object" || typeof x === "function")) {
let called = false;
try {
const then = x.then;
if (typeof then === "function") {
then.call(x,
y => {
if (called) return;
called = true;
this.resolvePromise(promise2, y, resolve, reject);
},
r => {
if (called) return;
called = true;
reject(r);
}
);
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
catch(onRejected) {
return this.then(null, onRejected);
}
finally(callback) {
return this.then(
value => MyPromise.resolve(callback()).then(() => value),
reason => MyPromise.resolve(callback()).then(() => { throw reason; })
);
}
static resolve(value) {
if (value instanceof MyPromise) return value;
return new MyPromise(resolve => resolve(value));
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
}2. 防抖(Debounce)和节流(Throttle)
javascript
// 防抖:多次触发只执行最后一次
function debounce(fn, delay = 300) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 节流:固定频率执行
function throttle(fn, limit = 300) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 节流(时间戳版:保证第一次立即执行)
function throttleTS(fn, limit = 300) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= limit) {
fn.apply(this, args);
lastTime = now;
}
};
}3. 深拷贝(支持循环引用)
javascript
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== "object") return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Map) return new Map(Array.from(obj.entries(), ([k, v]) => [deepClone(k, hash), deepClone(v, hash)]));
if (obj instanceof Set) return new Set(Array.from(obj, v => deepClone(v, hash)));
if (hash.has(obj)) return hash.get(obj);
const result = Array.isArray(obj) ? [] : {};
hash.set(obj, result);
for (const key of Reflect.ownKeys(obj)) {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (descriptor && !descriptor.enumerable) continue;
result[key] = deepClone(obj[key], hash);
}
return result;
}4. 函数柯里化
javascript
function curry(fn, arity = fn.length) {
return function curried(...args) {
if (args.length >= arity) {
return fn.apply(this, args);
}
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
};
}
// 使用
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1, 2, 3); // 65. 组合函数(Compose)
javascript
// 左到右:pipe
const pipe = (...fns) => (x) => fns.reduce((v, fn) => fn(v), x);
// 右到左:compose
const compose = (...fns) => (x) => fns.reduceRight((v, fn) => fn(v), x);
// 使用
const add1 = (x) => x + 1;
const multiply2 = (x) => x * 2;
const toString = (x) => `Result: ${x}`;
const pipeline = pipe(add1, multiply2, toString);
pipeline(5); // "Result: 12"6. Event Bus(事件总线)
javascript
class EventBus {
constructor() {
this.events = {};
}
on(event, callback) {
(this.events[event] ||= []).push(callback);
return () => this.off(event, callback); // 返回取消订阅函数
}
off(event, callback) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
emit(event, ...args) {
this.events[event]?.forEach(cb => cb(...args));
}
once(event, callback) {
const wrapper = (...args) => {
callback(...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}7. 实现 new 操作符
javascript
function myNew(constructor, ...args) {
// 1. 创建空对象,原型指向 constructor.prototype
const obj = Object.create(constructor.prototype);
// 2. 执行构造函数,绑定 this 到新对象
const result = constructor.apply(obj, args);
// 3. 如果构造函数返回对象,则返回该对象;否则返回新对象
return typeof result === "object" && result !== null ? result : obj;
}
// 使用
function Person(name) { this.name = name; }
const p = myNew(Person, "Alice");
console.log(p.name); // "Alice"
console.log(p instanceof Person); // true8. 实现 Array.prototype.reduce
javascript
Array.prototype.myReduce = function(callback, initialValue) {
if (this.length === 0 && initialValue === undefined) {
throw new TypeError("Reduce of empty array with no initial value");
}
let accumulator = initialValue !== undefined ? initialValue : this[0];
let startIndex = initialValue !== undefined ? 0 : 1;
for (let i = startIndex; i < this.length; i++) {
accumulator = callback(accumulator, this[i], i, this);
}
return accumulator;
};
// 使用
[1, 2, 3].myReduce((acc, cur) => acc + cur, 0); // 65.2 算法与数据结构(前端常考)
1. 数组扁平化
javascript
// 递归实现
function flatten(arr) {
return arr.reduce((acc, val) =>
acc.concat(Array.isArray(val) ? flatten(val) : val), []
);
}
// 迭代实现(栈)
function flattenIter(arr) {
const stack = [...arr];
const result = [];
while (stack.length) {
const item = stack.pop();
if (Array.isArray(item)) {
stack.push(...item);
} else {
result.unshift(item);
}
}
return result;
}
// 指定深度
function flattenDepth(arr, depth = 1) {
return arr.reduce((acc, val) =>
acc.concat(depth > 1 && Array.isArray(val) ? flattenDepth(val, depth - 1) : val), []
);
}2. 二分查找
javascript
function binarySearch(arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) return mid;
if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}3. 常见排序(快速排序)
javascript
function quickSort(arr) {
if (arr.length <= 1) return arr;
const pivot = arr[Math.floor(arr.length / 2)];
const left = arr.filter(x => x < pivot);
const middle = arr.filter(x => x === pivot);
const right = arr.filter(x => x > pivot);
return [...quickSort(left), ...middle, ...quickSort(right)];
}4. 树的遍历(场景:树形菜单/组件树)
javascript
// 深度优先遍历(DFS)
function dfs(tree, callback) {
if (!tree) return;
callback(tree);
tree.children?.forEach(child => dfs(child, callback));
}
// 广度优先遍历(BFS)
function bfs(root, callback) {
const queue = [root];
while (queue.length) {
const node = queue.shift();
callback(node);
node.children?.forEach(child => queue.push(child));
}
}5. URL 解析
javascript
// 将 URL 的 query string 解析为对象
function parseQuery(url) {
const params = new URLSearchParams(url.split("?")[1] || "");
const result = {};
for (const [key, value] of params) {
// 支持多层嵌套:a.b.c=val
const keys = key.split(/[\[\]\.]/).filter(Boolean);
keys.reduce((acc, k, i) => {
if (i === keys.length - 1) {
acc[k] = value;
} else {
acc[k] = acc[k] || (isNaN(keys[i + 1]) ? {} : []);
}
return acc[k];
}, result);
}
return result;
}
// parseQuery("?user.name=Alice&user.age=25&ids[0]=1&ids[1]=2")
// { user: { name: "Alice", age: "25" }, ids: ["1", "2"] }附:面试准备建议
项目经验准备
markdown
1. 准备 2-3 个核心项目,每个项目能讲清楚:
- 项目背景和规模
- 你的角色和职责
- 技术选型及原因
- 遇到的难点和解决方案
- 数据化的成果
2. STAR 法则回答面试官的问题
3. 准备常见场景题的回答:
- "如果让你重新做这个项目,你会怎么改进?"
- "这个项目中你遇到的最大的技术挑战是什么?"
- "你是如何进行技术选型的?"
- "团队合作中遇到冲突怎么处理?"面试答题技巧
markdown
1. 先说结论,再展开细节(金字塔原理)
2. 多维度回答:理论 + 实践 + 对比
3. 如果不会,坦诚说不会,但可以说出自己的思路
4. 回答项目问题时,用数据说话(性能提升% / 减少多少代码量 / 提升多少效率)
5. 准备好反问面试官的问题(技术栈、团队规模、技术挑战等)高频面试考点汇总
markdown
JavaScript:闭包、this、原型链、Promise、事件循环、深拷贝、手写实现
Vue:响应式原理、Composition API、组件通信、Pinia/Vuex、性能优化
工程化:Webpack/Loader/Plugin、Vite、PostCSS、CI/CD、微前端、Monorepo
网络:HTTP/HTTPS、TCP、缓存策略、WebSocket、SSE、流式输出
浏览器:渲染机制、事件机制、跨域、安全
性能:Web Vitals、LCP/FID/CLS、首屏优化、懒加载、ECharts/Canvas 大数据渲染
AI 项目:Prompt 工作台、RAG 知识库、Agent 工作流、企业 AI 助手、AI 运营中台
全栈与交付:RESTful API、Node/NestJS、Prisma/MyBatis、数据库选型、Redis、Docker、Nginx
项目表达:项目背景、技术选型、重难点、亮点、数据化结果、事故复盘
算法:数组/树/排序/去重/扁平化本文档基于全网热门前端面试题库(febobo/web-interview、lgwebdream/FE-Interview、Advanced-Frontend/Daily-Interview-Question 等)及大厂面试真题综合整理。
来源参考:
- github.com/febobo/web-interview(Vue/JS/HTTP/算法面试题)
- github.com/Advanced-Frontend/Daily-Interview-Question(每日一题)
- github.com/haizlin/fe-interview(6000+ 前端面试题)
- github.com/lgwebdream/FE-Interview(1000+ 前端面试真题)
建议结合实际项目经验,将理论知识转化为自己的表达。祝面试顺利!