内容由豆包生成
本文档系统整理前端面试各模块核心知识点,每道题配有标准答案,覆盖 HTML/CSS、JavaScript、框架、浏览器、网络、工程化、性能、算法、Node.js 及项目软技能,适合考前系统复习与查漏补缺。
HTML / CSS
HTML 语义化的理解?
**答案:**语义化是指用恰当的 HTML 标签来描述内容的含义,而非仅关注样式。
-
**常用语义标签:**header、nav、main、article、section、aside、footer、figure、figcaption、time、mark 等。
-
**好处:**① 结构清晰,便于团队维护;② 利于 SEO,搜索引擎能更好地理解页面结构;③ 提升无障碍访问(a11y),屏幕阅读器可正确朗读;④ 即使没有 CSS,页面也有良好的可读性。
-
**注意:**不要过度语义化,div/span 在无语义场景下仍可使用;article 是独立完整的内容块,section 是主题性分组。
盒模型有哪几种?区别是什么?
答案:
-
标准盒模型(W3C):
box-sizing: content-box,width = 内容宽度,元素实际占据宽度 = content + padding + border。 -
IE 盒模型(怪异模式):
box-sizing: border-box,width = 内容 + padding + border,内容宽度 = width - padding - border。 -
通用实践: 全局设置
* { box-sizing: border-box; }便于布局计算。
什么是 BFC?如何触发?有什么用?
**答案:**BFC(Block Formatting Context,块级格式化上下文)是一个独立的渲染区域,内部元素的布局不会影响外部。
触发条件:
-
根元素 html
-
float 不为 none
-
position 为 absolute 或 fixed
-
display 为 inline-block、flex、grid、table-cell 等
-
overflow 不为 visible(如 hidden、auto、scroll)
**应用场景:**① 清除浮动(父元素 overflow: hidden);② 防止 margin 折叠(两个相邻 BFC 之间 margin 不折叠);③ 自适应两栏布局(左侧浮动,右侧 overflow: hidden 形成 BFC 不被浮动覆盖)。
如何实现元素水平垂直居中?(至少5种)
答案:
-
Flex 布局(推荐): 父元素
display: flex; justify-content: center; align-items: center; -
Grid 布局: 父元素
display: grid; place-items: center; -
绝对定位 + transform:
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);(无需知道宽高) -
绝对定位 + margin: auto:
position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto;(需有宽高) -
绝对定位 + 负 margin:
position: absolute; top: 50%; left: 50%; margin-left: -宽/2; margin-top: -高/2;(需知道宽高) -
table-cell: 父元素
display: table-cell; vertical-align: middle; text-align: center;
Flex 布局常用属性有哪些?
答案:
容器属性:
-
flex-direction:主轴方向 row / row-reverse / column / column-reverse -
justify-content:主轴对齐 flex-start / flex-end / center / space-between / space-around / space-evenly -
align-items:交叉轴对齐 flex-start / flex-end / center / baseline / stretch -
flex-wrap:换行 nowrap / wrap / wrap-reverse -
align-content:多根轴线对齐(需 wrap)
子项属性:
-
flex-grow:放大比例(默认0) -
flex-shrink:缩小比例(默认1) -
flex-basis:初始大小(默认auto) -
flex:简写,flex: 1= 1 1 0% -
align-self:单独覆盖 align-items -
order:排列顺序(默认0,越小越靠前)
CSS 选择器优先级如何计算?
**答案:**优先级从高到低:
-
!important(最高,但应尽量避免使用) -
行内样式(style 属性)
-
ID 选择器(#id)
-
类选择器(.class)、属性选择器(attr)、伪类(:hover)
-
标签选择器(div)、伪元素(::before)
-
通配符(*)、关系选择器(+、>、~、空格)不影响优先级
**计算规则:**按 (a, b, c, d) 四位计数,a=行内,b=ID数,c=类/属性/伪类数,d=标签/伪元素数。从高位开始比较,高位相同才比低位。同优先级时,后写的覆盖先写的。
什么是层叠上下文?z-index 什么时候不生效?
**答案:**层叠上下文(Stacking Context)是 HTML 元素的三维概念,元素在 z 轴上有层叠顺序。
触发层叠上下文的条件:
-
根元素 html
-
z-index 不为 auto 的定位元素(relative/absolute/fixed/sticky)
-
flex/grid 子项且 z-index 不为 auto
-
opacity 小于 1
-
transform、filter、perspective 等属性不为 none
-
will-change 指定了上述属性
**z-index 不生效的常见原因:**① 元素没有设置 position(static 定位下 z-index 无效);② 父元素创建了层叠上下文且 z-index 较低,子元素 z-index 再高也无法超越父级的兄弟元素;③ 父元素设置了 overflow: hidden 等可能影响层叠的属性。
移动端适配方案有哪些?
答案:
-
viewport 设置:
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> -
**rem 方案:**通过 JS 动态设置 html 的 font-size(如 flexible.js),设计稿 px 转 rem。
-
**vw/vh 方案:**1vw = 视口宽度的1%,直接用 vw 作为单位,无需 JS。可配合 postcss-px-to-viewport 自动转换。
-
媒体查询:@media 根据不同屏幕宽度设置不同样式,适合响应式布局。
-
**百分比布局:**宽度用百分比,高度配合固定值或 padding-top 实现等比。
-
**1px 问题:**Retina 屏 1px 边框变粗,解决方案:border-image、background-image 渐变、transform: scaleY(0.5)、box-shadow。
CSS 中 transition 和 animation 的区别?
答案:
| 对比项 | transition | animation |
|---|---|---|
| 触发方式 | 需要触发条件(hover、class变化等) | 可自动执行,无需触发 |
| 关键帧 | 只有开始和结束两个状态 | 通过 @keyframes 定义多帧 |
| 循环 | 不支持循环 | 支持 iteration-count 无限循环 |
| 控制 | 属性较少 | 支持 direction、play-state、fill-mode 等 |
| 性能 | 较好 | 功能更强但开销略大 |
JavaScript 核心
JavaScript 有哪些数据类型?如何检测?
答案:
**7 种原始类型:**String、Number、Boolean、Null、Undefined、Symbol(ES6)、BigInt(ES2020)。
**1 种引用类型:**Object(包括 Array、Function、Date、RegExp 等)。
检测方法:
-
typeof:返回字符串,能检测原始类型(除 null 返回 "object")和 function,其余引用类型都返回 "object"。 -
instanceof:检测原型链,适用于引用类型,返回布尔值。不能检测原始类型。 -
Object.prototype.toString.call():最准确,返回 "object Type" 格式,可检测所有类型。 -
Array.isArray():专门检测数组。
typeof null // "object"(历史遗留bug) typeof undefined // "undefined" typeof Symbol() // "symbol" typeof [] // "object" typeof function(){} // "function" Object.prototype.toString.call(null) // "[object Null]" Object.prototype.toString.call([]) // "[object Array]" Object.prototype.toString.call(function(){}) // "[object Function]"
== 和 === 的区别?隐式类型转换规则?
答案:
-
===:严格相等,不进行类型转换,类型和值都相等才为 true。 -
==:宽松相等,会进行隐式类型转换后再比较。
== 转换规则:
-
类型相同:直接比较值(注意 NaN != NaN,+0 == -0)。
-
null == undefined → true,且与其他值比较都为 false。
-
number 和 string 比较:string 转 number。
-
boolean 和其他类型比较:boolean 转 number(true→1,false→0)。
-
object 和原始类型比较:object 调用 valueOf()/toString() 转原始类型。
[] == ![] // true:![]→false→0,[]→""→0,0==0 null == undefined // true "1" == true // true:"1"→1,true→1 NaN == NaN // false
什么是闭包?应用场景和缺点?
答案:闭包是指有权访问另一个函数作用域中变量的函数。即使外部函数执行完毕,内部函数仍然能记住并访问外部函数的作用域。
**形成条件:**① 函数嵌套;② 内部函数引用外部函数的变量;③ 内部函数被返回或在外部被调用。
应用场景:
-
数据私有化 / 模块化(IIFE + 闭包模拟私有变量)
-
函数柯里化(currying)
-
防抖(debounce)和节流(throttle)
-
事件处理函数中保持对外部变量的引用
-
循环中创建独立作用域(let 出现前的解决方案)
**缺点:**闭包会导致外部函数的变量无法被垃圾回收,长期驻留内存,使用不当会造成内存泄漏。解决:不再使用时手动赋值为 null。
// 经典闭包:计数器 function createCounter() { let count = 0; return function() { return ++count; }; } const counter = createCounter(); counter(); // 1 counter(); // 2
this 指向有哪几种情况?如何改变 this?
**答案:**this 的指向由调用方式决定,而非定义位置(箭头函数除外)。
-
**默认绑定:**独立函数调用,this 指向全局对象(浏览器 window,Node global);严格模式下为 undefined。
-
隐式绑定: 作为对象方法调用,this 指向调用该方法的对象。
obj.fn()→ this = obj。 -
**显式绑定:**通过 call/apply/bind 强制指定 this。
-
**new 绑定:**构造函数调用,this 指向新创建的实例对象。
-
**箭头函数:**没有自己的 this,继承外层作用域的 this,且无法被 call/apply/bind 改变。
**优先级:**new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定。
改变 this 的方法:
-
fn.call(thisArg, arg1, arg2, ...):立即执行,参数逐个传 -
fn.apply(thisArg, [argsArray]):立即执行,参数传数组 -
fn.bind(thisArg, arg1, ...):返回新函数,不立即执行,可柯里化
原型和原型链?
答案:
-
**原型(prototype):**每个函数都有一个 prototype 属性,指向一个对象,这个对象就是通过该构造函数创建的实例的原型。原型上的属性和方法可被所有实例共享。
-
proto: 每个对象(除 null)都有 proto 属性,指向其构造函数的 prototype 对象。即
实例.__proto__ === 构造函数.prototype。 -
**constructor:**原型对象上有 constructor 属性,指回构造函数本身。
-
**原型链:**当访问对象的属性时,先在对象自身查找,找不到则通过 proto 到原型对象上查找,再找不到继续向上查找,直到 Object.prototype.proto === null,这条链就是原型链。
function Person(name) { this.name = name; } const p = new Person('Tom'); p.__proto__ === Person.prototype // true Person.prototype.constructor === Person // true Person.prototype.__proto__ === Object.prototype // true Object.prototype.__proto__ === null // true // 函数也是对象 Person.__proto__ === Function.prototype // true Function.prototype.__proto__ === Object.prototype // true
JavaScript 继承有哪些方式?
答案:
-
**原型链继承:**子类.prototype = new 父类()。缺点:父类引用属性被所有实例共享;创建子类实例时不能向父类传参。
-
构造函数继承(经典继承): 在子类构造函数中调用
父类.call(this, args)。优点:可传参、避免引用共享;缺点:方法在构造函数中定义,无法复用,每个实例都有一份。 -
**组合继承:**原型链继承方法 + 构造函数继承属性。最常用,但会调用两次父类构造函数。
-
原型式继承:
Object.create(父类对象),创建一个以给定对象为原型的新对象。缺点:引用属性共享。 -
**寄生式继承:**在原型式继承基础上,在函数内部增强对象。
-
寄生组合继承(最优): 通过构造函数继承属性,通过原型链的混成形式继承方法。
子类.prototype = Object.create(父类.prototype),并修正 constructor。只调用一次父类构造函数,效率最高。 -
**ES6 class extends:**语法糖,底层基于寄生组合继承,使用 super 调用父类。
// 寄生组合继承 function inherit(Child, Parent) { Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; }
什么是事件循环(Event Loop)?宏任务和微任务?
**答案:**JavaScript 是单线程的,事件循环是其实现异步的机制。
执行过程:
-
同步代码在调用栈中依次执行。
-
遇到异步任务,将其注册到对应 Web API(定时器、DOM事件、fetch等)。
-
异步任务完成后,回调函数放入任务队列。
-
调用栈清空后,先执行所有微任务(microtask queue)。
-
微任务清空后,从宏任务队列取一个任务执行。
-
每个宏任务执行完后,再次清空所有微任务。
-
重复上述过程,即事件循环。
**宏任务(macrotask):**script整体代码、setTimeout、setInterval、setImmediate(Node)、I/O、UI渲染、requestAnimationFrame。
**微任务(microtask):**Promise.then/catch/finally、async/await(本质是Promise)、MutationObserver、queueMicrotask、process.nextTick(Node,优先级高于Promise)。
console.log('1'); setTimeout(() => console.log('2'), 0); Promise.resolve().then(() => console.log('3')); console.log('4'); // 输出:1, 4, 3, 2 // 同步:1,4 → 微任务:3 → 宏任务:2
Promise 是什么?有哪些方法?手写 Promise?
**答案:**Promise 是异步编程的解决方案,表示一个异步操作的最终完成或失败及其结果值。
**三种状态:**pending(进行中)、fulfilled(已成功)、rejected(已失败)。状态只能从 pending 变为 fulfilled 或 rejected,且不可逆。
常用方法:
-
Promise.prototype.then(onFulfilled, onRejected):返回新 Promise,支持链式调用。 -
Promise.prototype.catch(onRejected):捕获错误,等价于 then(null, onRejected)。 -
Promise.prototype.finally(onFinally):无论成功失败都执行。 -
Promise.all(iterable):全部成功才成功,返回结果数组;一个失败即失败。 -
Promise.race(iterable):第一个完成的 Promise 决定结果。 -
Promise.allSettled(iterable):等待所有完成,返回每个的状态和结果。 -
Promise.any(iterable):一个成功即成功,全部失败才失败。 -
Promise.resolve(value)/Promise.reject(reason):快速创建。
// 手写简易 Promise(符合 Promise/A+ 核心) 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; }; return new MyPromise((resolve, reject) => { if (this.state === 'fulfilled') { setTimeout(() => { try { resolve(onFulfilled(this.value)); } catch (e) { reject(e); } }); } if (this.state === 'rejected') { setTimeout(() => { try { resolve(onRejected(this.reason)); } catch (e) { reject(e); } }); } if (this.state === 'pending') { this.onFulfilledCallbacks.push(() => { setTimeout(() => { try { resolve(onFulfilled(this.value)); } catch (e) { reject(e); } }); }); this.onRejectedCallbacks.push(() => { setTimeout(() => { try { resolve(onRejected(this.reason)); } catch (e) { reject(e); } }); }); } }); } }
async/await 原理?错误处理?
**答案:**async/await 是 ES2017 引入的异步编程语法糖,基于 Promise 和 Generator 实现,让异步代码看起来像同步代码。
-
async函数返回一个 Promise,return 的值会被 Promise.resolve 包装。 -
await只能在 async 函数内使用,等待 Promise 完成并返回结果。await 会暂停函数执行(不阻塞主线程),Promise 完成后恢复。 -
await 后面如果不是 Promise,会自动用 Promise.resolve 包装。
错误处理:
-
try/catch:最常用,捕获 await 的 rejected Promise。
-
await 后接 .catch():
const result = await promise.catch(err => handle(err)); -
Promise.all 中一个失败会全部失败,可用 Promise.allSettled 或给每个 Promise 加 catch。
async function fetchData() { try { const res = await fetch('/api'); const data = await res.json(); return data; } catch (err) { console.error('请求失败:', err); return null; } } // 并发执行 async function concurrent() { const [a, b] = await Promise.all([fetchA(), fetchB()]); }
var、let、const 的区别?
答案:
| 对比项 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 有,提升并初始化为undefined | 有提升但存在暂时性死区 | 有提升但存在暂时性死区 |
| 重复声明 | 允许 | 不允许 | 不允许 |
| 重新赋值 | 允许 | 允许 | 不允许(引用不可变,对象属性可变) |
| 初始化 | 可先声明后赋值 | 可先声明后赋值 | 声明时必须初始化 |
| 挂到window | 是 | 否 | 否 |
**暂时性死区(TDZ):**在 let/const 声明之前访问变量会抛出 ReferenceError,从块级作用域开始到声明语句之间的区域称为暂时性死区。
箭头函数和普通函数的区别?
答案:
-
**this 指向:**箭头函数没有自己的 this,继承外层作用域的 this;普通函数 this 由调用方式决定。
-
**无法改变 this:**箭头函数不能通过 call/apply/bind 改变 this 指向。
-
**没有 arguments:**箭头函数没有 arguments 对象,可用 rest 参数 (...args) 替代。
-
**不能作为构造函数:**箭头函数不能用 new 调用,没有 prototype。
-
**没有原型对象:**箭头函数没有 prototype 属性。
-
**语法更简洁:**单表达式可省略 return 和花括号。
-
**不能用作 Generator:**箭头函数不能使用 yield。
// 箭头函数适合:回调函数、数组方法(map/filter) // 不适合:对象方法(this不指向对象)、构造函数、需要arguments的函数 const obj = { name: 'test', say: () => console.log(this.name) // this 指向外层(window),非 obj };
防抖和节流的区别?手写实现?
答案:
-
**防抖(debounce):**在事件触发后延迟 n 秒执行,如果 n 秒内再次触发则重新计时。适用于搜索框输入、窗口 resize、按钮防重复提交。
-
**节流(throttle):**规定在 n 秒内函数只执行一次,无论触发多少次。适用于滚动加载、鼠标移动、拖拽。
// 防抖 function debounce(fn, delay) { let timer = null; return function(...args) { if (timer) 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) { last = now; fn.apply(this, args); } }; } // 节流(定时器版,最后一次会执行) function throttle2(fn, delay) { let timer = null; return function(...args) { if (!timer) { timer = setTimeout(() => { timer = null; fn.apply(this, args); }, delay); } }; }
深拷贝和浅拷贝的区别?如何实现深拷贝?
答案:
-
**浅拷贝:**只复制第一层属性,引用类型的属性仍共享同一块内存地址。修改拷贝对象的引用属性会影响原对象。方法:Object.assign、展开运算符 {...obj}、Array.prototype.slice/concat。
-
**深拷贝:**递归复制所有层级,拷贝后的对象与原对象完全独立,互不影响。
深拷贝实现方式:
-
JSON.parse(JSON.stringify(obj)):最简单,但有局限性------无法复制函数、undefined、Symbol、Date(会变字符串)、RegExp、循环引用会报错。 -
structuredClone(obj):浏览器原生 API,支持循环引用、Date、RegExp、Map、Set,但不支持函数、DOM节点。 -
手写递归深拷贝:处理循环引用(WeakMap)、各种类型。
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 clone = Array.isArray(obj) ? [] : {}; hash.set(obj, clone); for (let key in obj) { if (obj.hasOwnProperty(key)) { clone[key] = deepClone(obj[key], hash); } } return clone; }
ES6+ 有哪些常用新特性?
答案:
-
**let/const:**块级作用域,替代 var。
-
**箭头函数:**简洁语法,词法 this。
-
**模板字符串:**反引号,支持多行和插值 ${}。
-
**解构赋值:**数组解构 a, b = arr,对象解构 {a, b} = obj,支持默认值和重命名。
-
**展开运算符 ...:**数组/对象展开,函数剩余参数。
-
**Set/Map:**Set 不重复集合,Map 键值对集合(键可为任意类型);WeakSet/WeakMap 弱引用,防止内存泄漏。
-
**Proxy/Reflect:**Proxy 代理对象拦截操作,Reflect 提供对象操作的方法集合。Vue3 响应式基于 Proxy。
-
**Promise:**异步编程解决方案。
-
**async/await:**Promise 语法糖。
-
**Iterator & for...of:**迭代器协议,可遍历数据结构。
-
**Generator:**带 * 的函数,yield 暂停,next 恢复。
-
**Symbol:**唯一值,用作对象属性名防止冲突。
-
**Class:**类语法糖,extends 继承,super 调用父类。
-
**模块化:**import/export,静态分析,Tree Shaking。
-
**可选链 ?. 和空值合并 ??:**ES2020,安全访问嵌套属性,区分 null/undefined 与 falsy 值。
-
**BigInt:**ES2020,大整数。
-
**Promise.allSettled/any:**ES2020/ES2021。
框架篇
Vue2 和 Vue3 的响应式原理区别?
答案:
Vue2: 使用 Object.defineProperty 劫持对象属性的 getter/setter。
-
初始化时递归遍历 data,对每个属性调用 defineProperty。
-
缺点:① 无法监听对象属性的新增和删除(需用 Vue.set/delete);② 无法监听数组下标变化和长度变化(Vue2 重写了数组的7个方法:push/pop/shift/unshift/splice/sort/reverse);③ 初始化时递归遍历大对象性能差。
Vue3: 使用 Proxy 代理整个对象。
-
Proxy 可以拦截对象的各种操作(get/set/deleteProperty/has/ownKeys等),无需递归遍历。
-
优点:① 可监听属性新增和删除;② 可监听数组下标和长度变化;③ 懒代理(访问时才递归代理子对象),性能更好;④ 支持 Map/Set/WeakMap/WeakSet。
-
缺点:Proxy 不兼容 IE11。
**核心流程:**reactive 创建 Proxy → get 时 track 收集依赖(effect)→ set 时 trigger 触发依赖更新 → effect 执行重新渲染。
Vue 的虚拟 DOM 和 Diff 算法?
答案:
**虚拟 DOM(VNode):**用 JS 对象描述真实 DOM 结构,包含 tag、props、children、key 等属性。好处:① 跨平台(可渲染到 Web/Weex/小程序);② 减少直接操作 DOM,通过 Diff 最小化更新;③ 声明式编程。
**Diff 算法:**同层比较,深度优先遍历。
Vue2 Diff(双端对比算法):
-
新旧子节点数组各有首尾两个指针(oldStartIdx/oldEndIdx/newStartIdx/newEndIdx)。
-
依次比较:旧首vs新首、旧尾vs新尾、旧首vs新尾、旧尾vs新首。
-
找到可复用节点则移动指针,未找到则用 key 在旧节点中查找。
-
最后处理剩余节点(新增或删除)。
Vue3 Diff(最长递增子序列):
-
先从头对比相同节点,再从尾对比相同节点。
-
中间乱序部分:建立新节点 key 到 index 的映射。
-
遍历旧节点,找到在新节点中的位置,生成最长递增子序列。
-
不在递增子序列中的节点需要移动,最大化减少 DOM 移动次数。
**key 的作用:**作为 VNode 的唯一标识,帮助 Diff 算法更准确地判断节点是否可复用,避免复用错误导致的状态问题。不建议用 index 作为 key(列表顺序变化时会导致错误复用)。
Vue 生命周期有哪些?Vue3 组合式 API 对应关系?
答案:
Vue2 生命周期:
-
beforeCreate:实例创建前,data 和 methods 不可用。
-
created:实例创建完成,data/methods 可用,$el 不可用(未挂载)。适合发请求。
-
beforeMount:挂载前,模板编译完成,未渲染到 DOM。
-
mounted:挂载完成,DOM 可用。适合操作 DOM、初始化第三方库。
-
beforeUpdate:数据更新前,DOM 未更新。
-
updated:数据更新后,DOM 已更新。避免在此修改数据(可能死循环)。
-
beforeDestroy:实例销毁前,实例仍可用。适合清除定时器、事件监听。
-
destroyed:实例销毁完成。
-
activated / deactivated:keep-alive 组件激活/停用时触发。
-
errorCaptured:捕获子孙组件错误。
Vue3 组合式 API 对应:
-
setup 本身替代 beforeCreate 和 created(在两者之间执行)。
-
onBeforeMount / onMounted
-
onBeforeUpdate / onUpdated
-
onBeforeUnmount / onUnmounted(替代 beforeDestroy/destroyed)
-
onActivated / onDeactivated
-
onErrorCaptured
Vue 组件通信方式有哪些?
答案:
| 方式 | 场景 | 用法 |
|---|---|---|
| props / $emit | 父子组件 | 父传子用 props,子传父用 $emit 触发事件 |
| v-model | 父子双向绑定 | 语法糖,props: modelValue + emit: update:modelValue |
| .sync(Vue2) | 父子双向绑定 | Vue3 移除,统一用 v-model 参数 |
| parent / children | 父子直接访问 | 访问父/子实例,Vue3 移除 $children |
| $refs | 父访问子 | 给子组件加 ref,通过 this.$refs.xxx 访问 |
| provide / inject | 跨层级(祖先→后代) | 祖先 provide 数据,后代 inject 注入。非响应式(Vue3 可传 ref 使其响应式) |
| EventBus / mitt | 任意组件 | Vue2 用 new Vue() 作为事件中心,Vue3 移除 $on,用 mitt 库 |
| Vuex / Pinia | 全局状态管理 | 跨组件共享状态,适合大型应用 |
| attrs / listeners | 跨级透传 | Vue2:attrs 透传属性,listeners 透传事件;Vue3 合并到 $attrs |
computed 和 watch 的区别?
答案:
| 对比项 | computed | watch |
|---|---|---|
| 用途 | 计算派生数据 | 监听数据变化执行副作用 |
| 缓存 | 有缓存,依赖不变时直接返回缓存 | 无缓存,数据变化就执行 |
| 异步 | 不支持异步 | 支持异步操作 |
| 返回值 | 必须 return 新值 | 不需要 return |
| 触发 | 读取时才计算(懒计算) | 数据变化立即触发 |
**watch 配置项:**deep(深度监听对象/数组)、immediate(立即执行一次)、flush(回调时机 pre/sync/post)。
**watchEffect:**Vue3 新增,自动收集依赖,不需要指定监听属性,回调中用到的响应式数据变化时自动执行。
Vue nextTick 原理?
**答案:**Vue 是异步更新 DOM 的。当响应式数据变化时,Vue 不会立即更新 DOM,而是将更新操作放入队列,在下一个事件循环 tick 中批量执行。nextTick 用于在 DOM 更新完成后执行回调。
原理:
-
数据变化 → watcher 更新 → 放入 queue 队列(去重)。
-
调用 nextTick(flushSchedulerQueue) 将刷新队列的操作放入微任务。
-
微任务优先级:Promise.then > MutationObserver > setImmediate > setTimeout(降级策略)。
-
当前同步代码执行完后,执行微任务,批量更新 DOM。
-
用户调用的 nextTick 回调排在 flushSchedulerQueue 之后,因此能拿到更新后的 DOM。
**使用场景:**① 在 created 中操作 DOM;② 数据变化后基于新 DOM 操作(如获取元素高度、初始化第三方库)。
Vue Router 的 hash 和 history 模式区别?
答案:
| 对比项 | hash 模式 | history 模式 |
|---|---|---|
| URL 外观 | 带 #,如 /#/home | 正常路径,如 /home |
| 原理 | 监听 hashchange 事件 | 使用 History API(pushState/replaceState/popstate) |
| 服务器配置 | 无需配置,# 后内容不发送到服务器 | 需配置服务器 fallback 到 index.html,否则刷新 404 |
| 兼容性 | 好,支持 IE8+ | 需 IE10+(History API) |
| 美观度 | 较差 | 较好 |
**路由守卫:**全局前置守卫 beforeEach、全局解析守卫 beforeResolve、全局后置钩子 afterEach、路由独享守卫 beforeEnter、组件内守卫 beforeRouteEnter/beforeRouteUpdate/beforeRouteLeave。
Vuex 和 Pinia 的区别?
答案:
| 对比项 | Vuex | Pinia |
|---|---|---|
| 核心概念 | state/getters/mutations/actions/modules | state/getters/actions(无 mutations) |
| 修改状态 | 必须通过 mutations(同步),actions 异步提交 mutations | actions 可直接修改 state,支持同步/异步 |
| TypeScript | 支持较差,类型推导复杂 | 原生支持,类型友好 |
| 模块化 | 嵌套 modules,命名空间 | 扁平结构,每个 store 独立,通过组合实现嵌套 |
| Vue3 支持 | Vuex 4 支持,但 API 较繁琐 | Vue3 官方推荐,组合式 API 友好 |
| 体积 | 较大 | 更小(约1KB) |
| DevTools | 支持 | 支持,时间旅行调试 |
React Hooks 有哪些?原理是什么?
答案:
常用 Hooks:
-
useState:函数组件状态管理,返回 state, setState。 -
useEffect:副作用处理,替代类组件生命周期。依赖数组控制执行时机:不传(每次渲染后执行)、空数组(仅挂载后执行)、有依赖(依赖变化后执行)。返回清理函数在卸载/下次执行前调用。 -
useContext:消费 Context,避免 props 层层传递。 -
useReducer:useState 的替代方案,适合复杂状态逻辑(类似 Redux reducer)。 -
useMemo:缓存计算结果,依赖变化才重新计算,避免不必要的昂贵计算。 -
useCallback:缓存函数引用,避免子组件因函数引用变化而不必要重渲染。useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。 -
useRef:创建可变引用对象,.current 可存任意值;也可获取 DOM 元素。修改 .current 不触发重渲染。 -
useImperativeHandle:配合 forwardRef,自定义暴露给父组件的 ref 实例值。 -
useLayoutEffect:与 useEffect 类似,但在 DOM 变更后同步执行(浏览器绘制前),适合读取 DOM 布局后同步重渲染。
Hooks 原理:
-
React 用链表存储 Hooks,每个组件有一个 Hooks 链表,按调用顺序排列。
-
每次渲染时,useState/useEffect 等按顺序从链表中读取对应 Hook 的状态。
-
这就是为什么 Hooks 不能在条件/循环中调用------会导致顺序错乱,读取错误的状态。
-
useState 的更新函数触发组件重渲染,重新执行函数组件,按顺序读取 Hooks 链表中的最新值。
React 类组件生命周期?setState 是同步还是异步?
答案:
生命周期(React 16.3+):
-
**挂载:**constructor → static getDerivedStateFromProps → render → componentDidMount
-
**更新:**static getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate
-
**卸载:**componentWillUnmount
-
**错误处理:**static getDerivedStateFromError / componentDidCatch
**已废弃生命周期:**componentWillMount、componentWillReceiveProps、componentWillUpdate(UNSAFE_ 前缀)。
setState 同步/异步:
-
在 React 事件处理函数和生命周期中:异步批量更新。React 会将多个 setState 合并,在当前事件处理结束后统一更新。
-
在 setTimeout、setInterval、原生 DOM 事件、Promise 回调中:同步更新。因为这些回调脱离了 React 的事件系统,React 无法批量处理。
-
React 18 引入自动批处理(Automatic Batching):在 Promise、setTimeout 等异步回调中也会批量更新。可通过 flushSync 强制同步刷新。
-
setState 第二个参数是回调函数,在状态更新且 DOM 重渲染后执行。
React 性能优化手段?
答案:
-
**React.memo:**包裹函数组件,对 props 做浅比较,props 不变则跳过渲染。
-
**useMemo:**缓存计算结果,避免每次渲染都重新计算昂贵值。
-
**useCallback:**缓存函数引用,避免传给子组件的函数每次都是新引用导致子组件重渲染。
-
**shouldComponentUpdate / PureComponent:**类组件中通过浅比较 props 和 state 决定是否更新。
-
**虚拟列表:**长列表只渲染可视区域的元素(react-window/react-virtualized)。
-
**React.lazy + Suspense:**路由/组件懒加载,代码分割,减少首屏 bundle 体积。
-
**避免内联函数和对象作为 props:**内联函数/对象每次渲染都是新引用,导致子组件 memo 失效。
-
**key 正确使用:**列表用唯一稳定 key,避免用 index。
-
**状态下放:**将状态放在需要它的最近公共父组件,避免不必要的大范围重渲染。
-
**Concurrent Mode / useTransition / useDeferredValue:**React 18 并发特性,将非紧急更新延迟,保持 UI 响应。
-
**避免在 render 中执行副作用:**如直接修改 DOM、发起请求等。
Vue 和 React 的核心区别?
答案:
| 对比项 | Vue | React |
|---|---|---|
| 设计理念 | 渐进式,模板+脚本+样式单文件组件 | All in JS,JSX 语法 |
| 响应式 | 数据劫持(Proxy/defineProperty),自动追踪依赖 | 不可变数据 + 手动 setState,自上而下重渲染 |
| 渲染优化 | 自动优化(编译时标记静态节点) | 需手动优化(memo/useMemo/useCallback) |
| 状态管理 | Vuex/Pinia | Redux/Zustand/Context |
| 路由 | Vue Router | React Router |
| 学习曲线 | 较平缓,模板接近 HTML | 较陡,需理解 JSX 和函数式编程 |
| Diff 算法 | 双端对比(Vue2)/ 最长递增子序列(Vue3) | 同层比较,Fiber 架构可中断 |
| 生态 | 官方维护核心库,国内流行 | 社区驱动,生态更丰富,全球流行 |
浏览器原理
从输入 URL 到页面显示发生了什么?
**答案:**完整流程如下:
-
**URL 解析:**浏览器解析 URL(协议、域名、端口、路径、查询参数、锚点),判断是搜索还是 URL。HSTS 检查是否强制 HTTPS。
-
**DNS 解析:**将域名解析为 IP 地址。顺序:浏览器缓存 → 系统缓存(hosts)→ 路由器缓存 → ISP DNS 缓存 → 递归查询(根域名服务器 → 顶级域名服务器 → 权威域名服务器)。
-
**TCP 连接:**三次握手建立 TCP 连接。SYN → SYN+ACK → ACK。
-
**TLS 握手(HTTPS):**客户端发送支持的加密套件和随机数 → 服务器返回证书、选定加密套件和随机数 → 客户端验证证书,生成预主密钥用服务器公钥加密发送 → 双方用三个随机数生成会话密钥 → 切换加密通信。TLS 1.3 简化为 1-RTT。
-
**发送 HTTP 请求:**构建请求报文(请求行、请求头、请求体),发送到服务器。
-
**服务器处理并响应:**服务器处理请求,返回响应报文(状态行、响应头、响应体)。可能经过负载均衡、反向代理。
-
浏览器解析渲染:
-
解析 HTML 构建 DOM 树
-
解析 CSS 构建 CSSOM 树
-
DOM + CSSOM 合并为渲染树(Render Tree,只包含可见节点)
-
布局(Layout/Reflow):计算渲染树中每个节点的几何位置和大小
-
绘制(Paint):将节点像素绘制到屏幕上,分层进行
-
合成(Composite):将多个层合并显示到屏幕
-
-
**断开连接:**TCP 四次挥手(或保持连接 keep-alive)。
浏览器渲染流程?重排(Reflow)和重绘(Repaint)区别?
答案:
**渲染流程:**DOM 树 → CSSOM 树 → 渲染树(Render Tree) → 布局(Layout/Reflow) → 绘制(Paint) → 合成(Composite)
-
**DOM 树:**解析 HTML,构建节点树。
-
**CSSOM 树:**解析 CSS,构建样式规则树。
-
**渲染树:**DOM + CSSOM 合并,只包含可见节点(display:none 不在渲染树中,visibility:hidden 在)。
-
**布局:**计算每个节点的位置和大小。
-
**绘制:**将节点的颜色、文字、图像等绘制到层上。
-
**合成:**将多个层按正确顺序合并,显示到屏幕。
**重排(Reflow/回流):**当元素的几何属性(位置、大小)变化时,浏览器重新计算布局。重排必定触发重绘。
**触发重排的操作:**添加/删除可见 DOM 元素、元素位置变化、元素尺寸变化(margin/padding/border/width/height)、内容变化(文字/图片)、浏览器窗口大小变化、读取某些属性(offsetTop/scrollTop/clientTop 等会强制同步布局)。
**重绘(Repaint):**元素外观变化但不影响布局(如 color、background-color、visibility),浏览器重新绘制像素。重绘不一定触发重排。
优化策略:
-
避免频繁操作 DOM,批量修改(用 class 或 cssText 一次性修改)。
-
离线操作 DOM(display:none 后修改,再显示,只触发一次重排)。
-
使用 DocumentFragment 批量插入节点。
-
避免循环读取 offsetTop 等属性(会强制刷新渲染队列)。
-
使用 transform 和 opacity 做动画(只触发合成,不触发重排重绘)。
-
使用 will-change: transform 提升为合成层。
-
防抖 resize/scroll 事件。
浏览器缓存机制?强缓存和协商缓存?
**答案:**浏览器缓存分为强缓存和协商缓存。
**强缓存:**不发送请求到服务器,直接从本地缓存读取。状态码 200 (from disk cache / from memory cache)。
-
Expires:HTTP/1.0,绝对时间,受客户端时间影响,已过时。 -
Cache-Control:HTTP/1.1,相对时间,优先级高于 Expires。常用值:max-age(缓存时长秒数)、no-cache(协商缓存)、no-store(不缓存)、public/private、must-revalidate。
**协商缓存:**强缓存过期后,发送请求到服务器验证资源是否更新。未更新返回 304 Not Modified,使用缓存;已更新返回 200 和新资源。
-
Last-Modified / If-Modified-Since:服务器返回资源最后修改时间,下次请求带 If-Modified-Since,服务器比较。缺点:精度只到秒,文件修改后内容可能没变,时间会变。 -
ETag / If-None-Match:服务器根据文件内容生成唯一标识(哈希),下次请求带 If-None-Match,服务器比较。优先级高于 Last-Modified。缺点:计算 ETag 有性能开销。
**缓存优先级:**Cache-Control > Expires > ETag > Last-Modified。
**缓存位置:**Service Worker → Memory Cache(内存,快但小,关闭标签页清除)→ Disk Cache(磁盘,慢但大,持久化)→ Push Cache(HTTP/2,会话级)。
Cookie、SessionStorage、LocalStorage、IndexedDB 区别?
答案:
| 对比项 | Cookie | SessionStorage | LocalStorage | IndexedDB |
|---|---|---|---|---|
| 容量 | 约 4KB | 约 5MB | 约 5MB | 无上限(受磁盘限制) |
| 生命周期 | 可设置过期时间,默认关闭浏览器清除 | 会话级,关闭标签页/浏览器清除 | 永久,需手动清除 | 永久,需手动清除 |
| 作用域 | 同源 + path,可设置 domain 跨子域 | 同源 + 同标签页 | 同源(协议+域名+端口) | 同源 |
| 随请求发送 | 是,自动携带在请求头 | 否 | 否 | 否 |
| 数据类型 | 字符串 | 字符串 | 字符串 | 任意类型(对象、文件等) |
| API | document.cookie | setItem/getItem/removeItem/clear | 同左 | 异步 API,支持索引和事务 |
| 安全性 | 有 HttpOnly/Secure/SameSite 属性 | 无 | 无 | 无 |
跨域解决方案?CORS 原理?
**答案:同源策略:**协议、域名、端口三者相同即为同源。不同源之间限制:Cookie/LocalStorage 读取、DOM 操作、AJAX 请求。
跨域解决方案:
-
**CORS(跨域资源共享):**最主流方案,服务器设置响应头允许跨域。
-
简单请求:GET/POST/HEAD,Content-Type 为 text/plain/multipart/form-data/application/x-www-form-urlencoded。浏览器直接发送,服务器返回 Access-Control-Allow-Origin。
-
预检请求:非简单请求先发 OPTIONS 预检,服务器返回允许的方法/头/凭证,通过后才发真实请求。
-
携带 Cookie:需设置 Access-Control-Allow-Credentials: true,且前端 xhr.withCredentials = true,此时 Allow-Origin 不能为 *。
-
-
**JSONP:**利用 script 标签不受同源策略限制,通过回调函数获取数据。只支持 GET,有安全风险(XSS)。
-
**代理服务器:**开发环境用 webpack devServer proxy / vite proxy,生产环境用 Nginx 反向代理,将请求转发到目标服务器。
-
**postMessage:**窗口间通信,适用于 iframe 跨域。
-
**WebSocket:**本身不受同源策略限制。
-
**document.domain:**主域相同子域不同时,设置 document.domain 为主域,可跨子域操作 iframe。
-
**window.name:**利用 window.name 在页面导航后保持值,实现跨域数据传递。
XSS 和 CSRF 区别与防御?
答案:
**XSS(跨站脚本攻击):**攻击者在网页中注入恶意脚本,在用户浏览器中执行,窃取 Cookie/Token、篡改页面、钓鱼等。
XSS 类型:
-
**存储型:**恶意脚本存入数据库,其他用户访问时执行。危害最大。如评论区、用户昵称。
-
**反射型:**恶意脚本在 URL 参数中,服务器反射回页面执行。需诱导用户点击链接。
-
**DOM 型:**前端 JS 直接将用户输入插入 DOM,不经过服务器。如 innerHTML、document.write。
XSS 防御:
-
输入过滤/转义:对用户输入进行 HTML 转义(< > & ")。
-
输出编码:根据插入位置(HTML/JS/CSS/URL)使用对应编码。
-
CSP(内容安全策略):设置 Content-Security-Policy 响应头,限制脚本来源,禁止内联脚本。
-
HttpOnly Cookie:防止 JS 读取 Cookie。
-
使用框架自带的转义(Vue/React 默认转义插值),避免 v-html/dangerouslySetInnerHTML。
-
对富文本使用白名单过滤(DOMPurify)。
**CSRF(跨站请求伪造):**攻击者诱导已登录用户访问恶意网站,利用用户的登录态(Cookie 自动携带)发起伪造请求,执行用户不知情的操作(转账、改密码)。
CSRF 防御:
-
Token 验证:在请求头/参数中携带 CSRF Token,服务器验证。
-
SameSite Cookie:设置 SameSite=Strict/Lax,限制跨站请求携带 Cookie。
-
验证 Referer/Origin:检查请求来源是否合法。
-
关键操作二次验证(短信验证码、密码)。
-
使用 JWT 放在请求头(Authorization)而非 Cookie,天然防 CSRF。
**核心区别:**XSS 是注入脚本在用户端执行,CSRF 是利用用户登录态伪造请求。XSS 比 CSRF 危害更大,XSS 可以实施 CSRF。
浏览器多进程架构?渲染进程有哪些线程?
**答案:**现代浏览器是多进程架构,每个标签页通常是独立的渲染进程。
主要进程:
-
**浏览器进程(Browser Process):**主进程,负责地址栏、书签、前进后退、网络请求、文件访问等。只有一个。
-
**渲染进程(Renderer Process):**每个标签页一个,负责页面渲染、JS 执行、事件处理。运行在沙箱中。
-
**GPU 进程:**负责 GPU 加速渲染、3D 绘制。只有一个。
-
**网络进程(Network Process):**负责网络请求处理。
-
**插件进程(Plugin Process):**每个插件一个进程,如 Flash。
-
**实用程序进程(Utility Process):**音频、视频解码等。
渲染进程的主要线程:
-
**GUI 渲染线程:**负责渲染页面(解析 HTML/CSS、构建 DOM/CSSOM、布局、绘制、合成)。与 JS 引擎线程互斥。
-
**JS 引擎线程:**负责解析和执行 JavaScript(V8 引擎)。单线程。与 GUI 渲染线程互斥(JS 执行会阻塞渲染)。
-
**事件触发线程:**管理事件队列,将满足条件的事件回调加入任务队列。
-
**定时器线程:**管理 setTimeout/setInterval,计时完成后将回调加入任务队列。
-
**异步 HTTP 请求线程:**管理 XMLHttpRequest/fetch 请求,响应完成后将回调加入任务队列。
**为什么 JS 是单线程?**避免多线程操作 DOM 产生冲突(一个线程修改 DOM,另一个删除 DOM)。HTML5 引入 Web Worker 允许后台线程,但不能操作 DOM。
网络协议
HTTP 常见状态码?
答案:
-
**1xx 信息性:**100 Continue(继续发送)、101 Switching Protocols(切换协议,WebSocket)
-
**2xx 成功:**200 OK、201 Created(资源已创建)、204 No Content(无响应体)、206 Partial Content(范围请求)
-
**3xx 重定向:**301 Moved Permanently(永久重定向)、302 Found(临时重定向)、303 See Other(POST后重定向为GET)、304 Not Modified(协商缓存命中)、307 Temporary Redirect(临时重定向,保持请求方法)、308 Permanent Redirect(永久重定向,保持请求方法)
-
**4xx 客户端错误:**400 Bad Request(请求语法错误)、401 Unauthorized(未认证)、403 Forbidden(禁止访问)、404 Not Found(资源不存在)、405 Method Not Allowed(方法不允许)、408 Request Timeout(请求超时)、409 Conflict(资源冲突)、413 Payload Too Large(请求体过大)、415 Unsupported Media Type、429 Too Many Requests(限流)
-
**5xx 服务器错误:**500 Internal Server Error(服务器内部错误)、501 Not Implemented(不支持该功能)、502 Bad Gateway(网关错误)、503 Service Unavailable(服务不可用)、504 Gateway Timeout(网关超时)
GET 和 POST 的区别?
答案:
| 对比项 | GET | POST |
|---|---|---|
| 语义 | 获取资源,幂等安全 | 提交/创建资源,非幂等非安全 |
| 参数位置 | URL query string | 请求体 body |
| 参数长度 | 受 URL 长度限制(浏览器/服务器) | 理论无限制,受服务器配置限制 |
| 缓存 | 可被浏览器缓存 | 默认不缓存 |
| 书签/历史 | 可收藏为书签,保留在历史记录 | 不可收藏 |
| 编码类型 | application/x-www-form-urlencoded | 多种:form-urlencoded/multipart/form-data/json/plain |
| 安全性 | 参数在 URL 中可见,相对不安全 | 参数在 body 中,相对安全(但 HTTPS 下都加密) |
| TCP 包 | 浏览器可能一次发完(header+body) | 部分浏览器先发 header 等 100 再发 body(非标准) |
**注意:**HTTP 规范本身没有限制 GET 不能带 body,也没有限制 POST 参数长度,这些是浏览器和服务器的实现约定。GET 和 POST 本质都是 TCP 连接,没有本质区别,只是语义和约定不同。
HTTP/1.1、HTTP/2、HTTP/3 的区别?
答案:
HTTP/1.1:
-
基于文本,请求/响应纯文本格式。
-
持久连接(keep-alive),默认开启,一个 TCP 连接可发多个请求。
-
管道化(pipelining):请求可连续发送,但响应必须按顺序返回(队头阻塞)。实际很少用。
-
队头阻塞:一个请求阻塞后续所有请求。
-
头部冗余:每次请求都携带完整头部。
HTTP/2:
-
二进制分帧:所有消息都分为二进制帧,更高效解析。
-
多路复用:一个 TCP 连接上可并行发送多个请求/响应,互不阻塞,解决应用层队头阻塞。
-
头部压缩(HPACK):静态表+动态表+Huffman编码,减少头部传输。
-
服务器推送:服务器可主动向客户端推送资源。
-
流优先级:可设置请求优先级。
-
仍基于 TCP,存在 TCP 层队头阻塞(丢包后整个连接阻塞)。
HTTP/3:
-
基于 QUIC 协议(运行在 UDP 上),不再使用 TCP。
-
解决 TCP 队头阻塞:QUIC 的流之间独立,一个流丢包不影响其他流。
-
0-RTT / 1-RTT 握手:连接建立更快,QUIC 内置 TLS 1.3。
-
连接迁移:网络切换(WiFi→4G)时连接不中断,基于 Connection ID 而非 IP+端口。
-
无队头阻塞、更快的握手、更好的移动网络体验。
HTTPS 工作原理?TLS 握手过程?
**答案:**HTTPS = HTTP + TLS/SSL,在 HTTP 和 TCP 之间加了一层加密层。
加密方式:
-
**对称加密:**加密解密用同一密钥,速度快。如 AES、ChaCha20。问题:密钥如何安全传输?
-
**非对称加密:**公钥加密、私钥解密,或私钥签名、公钥验签。如 RSA、ECDHE。速度慢,用于密钥交换和身份验证。
-
**哈希函数:**单向不可逆,用于完整性校验和数字签名。如 SHA-256。
-
**数字证书:**由 CA 签发,包含服务器公钥、域名、有效期、CA 签名等。客户端用 CA 根证书验证证书合法性。
TLS 1.2 握手过程(简化):
-
**Client Hello:**客户端发送支持的 TLS 版本、加密套件列表、随机数 random1。
-
**Server Hello:**服务器选择 TLS 版本和加密套件,返回随机数 random2、数字证书。
-
**客户端验证证书:**用 CA 根证书验证服务器证书合法性,提取服务器公钥。
-
**Client Key Exchange:**客户端生成预主密钥(pre-master secret),用服务器公钥加密后发送。
-
**双方计算会话密钥:**用 random1 + random2 + pre-master secret 通过 PRF 生成会话密钥(对称加密密钥)。
-
**Change Cipher Spec + Finished:**双方通知切换加密通信,发送 Finished 消息(加密)验证握手完整性。
-
**加密通信:**后续所有 HTTP 数据都用会话密钥对称加密传输。
**TLS 1.3 改进:**① 支持 0-RTT(恢复连接时);② 握手从 2-RTT 减为 1-RTT;③ 移除不安全的加密套件(RSA 密钥交换、SHA-1、3DES等);④ 所有握手消息加密(Server Hello 之后)。
TCP 三次握手和四次挥手?为什么?
答案:
三次握手(建立连接):
-
客户端发送 SYN=1, seq=x,进入 SYN_SENT 状态。
-
服务器回复 SYN=1, ACK=1, seq=y, ack=x+1,进入 SYN_RCVD 状态。
-
客户端回复 ACK=1, seq=x+1, ack=y+1,双方进入 ESTABLISHED 状态。
**为什么三次?**① 确认双方收发能力正常(两次只能确认客户端到服务器,不能确认服务器到客户端);② 防止已失效的连接请求突然到达服务器,导致服务器错误建立连接浪费资源(两次握手中服务器收到旧 SYN 就会建立连接)。
四次挥手(断开连接):
-
主动方发送 FIN=1, seq=u,进入 FIN_WAIT_1 状态。
-
被动方回复 ACK=1, ack=u+1,进入 CLOSE_WAIT 状态;主动方进入 FIN_WAIT_2。
-
被动方发送完剩余数据后,发送 FIN=1, seq=w, ack=u+1,进入 LAST_ACK 状态。
-
主动方回复 ACK=1, ack=w+1,进入 TIME_WAIT 状态;被动方收到后关闭。主动方等待 2MSL 后关闭。
**为什么四次?**TCP 是全双工的,关闭连接需要双方各自关闭发送通道。被动方收到 FIN 后可能还有数据要发送,所以先回 ACK,等数据发完再发 FIN,因此比握手多一次。
**TIME_WAIT 为什么等待 2MSL?**① 确保最后一个 ACK 能到达对方(如果丢失,对方会重发 FIN,2MSL 内可收到重发并重新计时);② 让本次连接的所有报文在网络中消失,避免影响下一个新连接(四元组复用)。
DNS 解析过程?
**答案:**DNS(域名系统)将域名解析为 IP 地址。
解析顺序(缓存优先):
-
**浏览器缓存:**浏览器先查自身 DNS 缓存(Chrome 约 1 分钟,约 1000 条)。
-
**系统缓存:**查操作系统 hosts 文件和系统 DNS 缓存。
-
**路由器缓存:**查路由器 DNS 缓存。
-
**ISP DNS 缓存:**查互联网服务提供商的本地 DNS 服务器缓存。
-
**递归查询:**本地 DNS 服务器向根域名服务器发起查询。
递归查询过程(本地 DNS 服务器代查):
-
本地 DNS → 根域名服务器(.):返回顶级域名服务器地址(如 .com)。
-
本地 DNS → 顶级域名服务器(.com):返回权威域名服务器地址(如 example.com)。
-
本地 DNS → 权威域名服务器(example.com):返回对应 IP 地址。
-
本地 DNS 将结果返回给客户端,并缓存。
相关概念:
-
**递归查询:**客户端只发一次请求,DNS 服务器代查到底并返回最终结果。
-
**迭代查询:**DNS 服务器之间,每次返回下一级服务器地址,由查询者继续查询。
-
DNS 预解析:
<link rel="dns-prefetch" href="//example.com">,提前解析域名减少等待。 -
**HTTPDNS:**用 HTTP 请求直接获取域名解析结果,绕过运营商 LocalDNS,防止 DNS 劫持和解析异常。
-
**DNS 污染/劫持:**攻击者返回错误 IP,解决方案:HTTPDNS、DNS over HTTPS (DoH)、DNS over TLS (DoT)。
WebSocket 与 HTTP 的区别?
答案:
| 对比项 | HTTP | WebSocket |
|---|---|---|
| 连接模式 | 请求-响应,短连接/长连接 | 全双工持久连接 |
| 通信方向 | 半双工,客户端请求服务器响应 | 全双工,双方可随时发送 |
| 头部开销 | 每次请求携带完整头部 | 连接建立后帧头仅2-14字节 |
| 服务器推送 | 需轮询/SSE | 原生支持主动推送 |
| 协议标识 | http:// / https:// | ws:// / wss:// |
| 默认端口 | 80 / 443 | 80 / 443 |
| 握手 | TCP 三次握手 | 基于 HTTP 升级握手(Upgrade: websocket) |
| 同源策略 | 受同源策略限制 | 不受同源策略限制(但有 Origin 校验) |
WebSocket 握手: 客户端发送 HTTP 请求,带 Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Key 等头;服务器返回 101 Switching Protocols,带 Sec-WebSocket-Accept,连接建立后切换为 WebSocket 协议。
**心跳机制:**WebSocket 长连接可能因网络问题断开,需定时发送 ping/pong 帧检测连接状态,断开后自动重连。
工程化
Webpack 核心概念和构建流程?
答案:
核心概念:
-
**entry:**入口,构建的起点,Webpack 从入口开始递归构建依赖图。
-
**output:**输出,构建产物的路径和文件名。
-
**loader:**模块转换器,将非 JS 文件(CSS、图片、TS、Vue)转换为 Webpack 可处理的模块。如 babel-loader、css-loader、style-loader、file-loader、vue-loader。
-
**plugin:**插件,在 Webpack 构建的各个生命周期钩子中执行扩展功能。如 HtmlWebpackPlugin、MiniCssExtractPlugin、CleanWebpackPlugin、DefinePlugin。
-
**mode:**模式,development/production/none,决定内置优化策略。
-
**devtool:**source map 配置,控制调试信息生成。
-
**resolve:**模块解析规则,如 alias 别名、extensions 扩展名。
-
**devServer:**开发服务器配置,热更新、代理等。
构建流程:
-
**初始化参数:**合并配置文件和命令行参数。
-
**创建 Compiler 对象:**初始化编译器,加载所有插件。
-
**run/watch:**开始构建(run 一次性,watch 监听模式)。
-
**compilation:**每次构建创建 Compilation 对象,负责本次构建。
-
make(从入口解析依赖):
-
从 entry 开始,调用 loader 转换模块源码。
-
用 acorn 解析转换后的代码为 AST。
-
遍历 AST,收集依赖(import/require)。
-
递归处理所有依赖模块,构建模块依赖图。
-
-
**seal(封装产物):**根据入口和依赖关系,生成 chunk,对 chunk 进行代码分割、tree shaking 等优化。
-
**emit(输出文件):**将 chunk 转换为文件,写入 output 目录(plugin 可在此阶段修改内容)。
-
**done:**构建完成。
Loader 和 Plugin 的区别?
答案:
| 对比项 | Loader | Plugin |
|---|---|---|
| 作用 | 模块转换器,处理特定类型文件 | 扩展 Webpack 功能,在构建生命周期执行任务 |
| 执行时机 | 模块加载阶段(make) | 整个构建生命周期的各个钩子 |
| 配置位置 | module.rules 中 | plugins 数组中 |
| 本质 | 函数,接收源码返回转换后源码 | 带 apply 方法的类/对象,监听 compiler 钩子 |
| 执行顺序 | 从右到左/从下到上(pitch 阶段从左到右) | 按钩子触发顺序和注册顺序 |
| 示例 | babel-loader、css-loader、file-loader | HtmlWebpackPlugin、MiniCssExtractPlugin |
Webpack HMR(热模块替换)原理?
**答案:**HMR(Hot Module Replacement)允许在不刷新页面的情况下,替换、添加或删除模块,保持应用当前状态。
原理:
-
**watch 模式:**Webpack 监听文件变化,文件修改后重新编译,生成更新后的模块。
-
**devServer 与浏览器建立 WebSocket 连接:**编译完成后,devServer 通过 WebSocket 向浏览器发送 hash(本次编译的唯一标识)和 ok 消息。
-
**浏览器接收消息:**webpack-dev-server/client 接收 hash,调用 webpack/lib/HotModuleReplacement.runtime 检查更新。
-
**请求更新清单:**浏览器通过 AJAX 请求 hash.hot-update.json,获取需要更新的模块列表。
-
**请求更新模块:**通过 JSONP 请求 hash.moduleId.hot-update.js,获取更新后的模块代码。
-
**模块替换:**HMR runtime 执行模块替换,调用模块的 hot.accept 回调处理更新。如果模块没有 accept 处理,冒泡到父模块,最终无法处理则触发页面刷新。
关键 API: module.hot.accept(dependency, callback) 注册模块更新后的处理函数;module.hot.dispose(callback) 模块被替换前的清理函数。
**CSS HMR:**style-loader 内置了 HMR 支持,CSS 修改后直接替换 style 标签,无需刷新。Vue/React 通过 vue-loader/react-hot-loader 支持组件 HMR。
Vite 为什么比 Webpack 快?
答案:
Vite 核心原理:
-
**开发时基于原生 ES Modules:**利用浏览器原生支持的 ESM(import/export),不需要打包所有模块。浏览器请求哪个模块,Vite 才按需编译哪个模块。
-
**依赖预构建(optimizeDeps):**启动时用 esbuild 预构建第三方依赖(node_modules),将 CommonJS/UMD 转为 ESM,合并多模块依赖减少请求数。预构建结果缓存到 node_modules/.vite。
-
**按需编译:**源码文件(.vue/.ts/.jsx)在浏览器请求时实时编译,利用浏览器缓存(304),未修改的文件不重新编译。
-
**HMR 基于 ESM:**修改文件后,只需让浏览器重新请求该模块及其依赖,无需重新打包整个 bundle,HMR 速度与项目大小无关。
-
**生产构建用 Rollup:**生产环境仍需打包(兼容性、性能),用 Rollup 构建,输出优化后的 bundle。
与 Webpack 的核心区别:
-
Webpack 开发时需要打包所有模块生成 bundle,项目越大启动越慢。
-
Vite 开发时不打包,利用浏览器 ESM 按需加载,启动速度与项目大小无关。
-
Webpack HMR 需要重新构建受影响的 chunk;Vite HMR 只需重新请求单个模块。
-
Vite 用 esbuild(Go 编写)做依赖预构建,比 Webpack 的 JS 构建快 10-100 倍。
**Vite 缺点:**① 开发环境和生产环境构建工具不一致(esbuild+Rollup),可能有差异;② 对 CommonJS 依赖兼容性需预构建处理;③ 生态不如 Webpack 成熟。
Babel 原理?polyfill 和 preset-env?
**答案:**Babel 是 JavaScript 编译器,将 ES6+ 代码转换为向后兼容的 ES5 代码。
编译流程(三阶段):
-
**Parse(解析):**将源码字符串解析为 AST(抽象语法树)。词法分析(token 化)→ 语法分析(生成 AST)。使用 @babel/parser。
-
**Transform(转换):**遍历 AST,对节点进行添加、更新、移除等操作。插件在此阶段工作。使用 @babel/traverse。
-
**Generate(生成):**将转换后的 AST 打印为目标代码字符串,同时生成 source map。使用 @babel/generator。
核心概念:
-
**Plugin:**单个转换功能,如 @babel/plugin-transform-arrow-functions。
-
**Preset:**一组插件的集合,如 @babel/preset-env(根据目标环境自动启用需要的插件)、@babel/preset-react、@babel/preset-typescript。
-
**polyfill:**在运行时为旧浏览器提供新 API 的实现(如 Promise、Array.includes、Map)。Babel 只转换语法,不提供新 API,需单独引入 polyfill。
@babel/preset-env:
-
根据 browserslist 配置的目标浏览器,自动确定需要的转换插件和 polyfill。
-
useBuiltIns: 'usage':按需引入 polyfill(根据代码中使用的 API),需安装 core-js。 -
useBuiltIns: 'entry':在入口手动 import 'core-js',根据目标环境全量引入需要的 polyfill。 -
useBuiltIns: false:不自动引入 polyfill。 -
corejs: 3:指定 core-js 版本。
**@babel/plugin-transform-runtime:**将 Babel 辅助函数(如 _classCallCheck)从内联改为从 @babel/runtime 引入,减少打包体积;同时提供不污染全局的 polyfill(core-js-pure),适合库开发。
CommonJS 和 ES Module 的区别?
答案:
| 对比项 | CommonJS (CJS) | ES Module (ESM) |
|---|---|---|
| 语法 | require() / module.exports / exports | import / export |
| 加载方式 | 运行时同步加载,require 是函数 | 编译时静态加载,import 是声明 |
| 导出值 | 值的拷贝(导出后修改不影响原值) | 值的引用(动态绑定,导出后修改会影响) |
| this 指向 | 模块内 this 指向 module.exports | 模块内 this 为 undefined |
| Tree Shaking | 不支持(动态加载无法静态分析) | 支持(静态结构可分析未使用导出) |
| 循环依赖 | 输出已执行部分,未执行部分为 undefined | 输出引用,执行时取值(可能暂时性死区) |
| 异步加载 | 不支持(同步) | 支持动态 import() 返回 Promise |
| 使用环境 | Node.js 默认(.cjs) | 浏览器原生支持,Node.js .mjs 或 package.json type:module |
| 导入路径 | 可省略扩展名,可导入目录(找 index.js) | 需完整扩展名(浏览器),Node 需明确 |
值拷贝 vs 值引用示例:
// CommonJS:值拷贝 // a.js let count = 1; setTimeout(() => count++, 100); module.exports = { count }; // b.js const a = require('./a'); console.log(a.count); // 1 setTimeout(() => console.log(a.count), 200); // 1(拷贝,不变) // ESM:值引用 // a.js export let count = 1; setTimeout(() => count++, 100); // b.js import { count } from './a.js'; console.log(count); // 1 setTimeout(() => console.log(count), 200); // 2(引用,变化)
npm、yarn、pnpm 的区别?
答案:
| 对比项 | npm | yarn | pnpm |
|---|---|---|---|
| 出现时间 | 2010(最早) | 2016(解决 npm 问题) | 2017(性能优化) |
| 安装速度 | 较慢(串行) | 较快(并行+缓存) | 最快(硬链接+符号链接) |
| 磁盘占用 | 大(每个项目独立 node_modules) | 较大(有缓存但仍复制) | 小(全局 store 硬链接,项目只存符号链接) |
| 依赖结构 | 扁平(hoisting) | 扁平(hoisting) | 非扁平(.pnpm 目录 + 符号链接) |
| 幽灵依赖 | 有(hoisting 导致可访问未声明依赖) | 有 | 无(严格隔离,只能访问 package.json 声明的依赖) |
| lock 文件 | package-lock.json | yarn.lock | pnpm-lock.yaml |
| Monorepo | npm workspaces | yarn workspaces | pnpm workspace(内置,更强) |
| 离线安装 | 支持(缓存) | 支持(yarn-offline-mirror) | 支持(store) |
pnpm 原理:
-
全局 store(~/.pnpm-store)存储所有包的真实文件。
-
项目 node_modules/.pnpm 中以硬链接方式引用 store 中的文件。
-
项目 node_modules 顶层只放 package.json 中声明的依赖(符号链接指向 .pnpm)。
-
非扁平结构避免了幽灵依赖和依赖提升的版本冲突问题。
TypeScript 常用高级类型?
答案:
-
**基础类型:**string、number、boolean、null、undefined、symbol、bigint、any、unknown、never、void。
-
联合类型(|):
type A = string | number,值可为多种类型之一。 -
交叉类型(&):
type C = A & B,同时具有多种类型的属性。 -
**接口(interface):**定义对象结构,支持 extends 继承、声明合并。
-
**类型别名(type):**定义类型别名,支持联合、交叉、元组、条件类型等。interface vs type:interface 支持声明合并和 extends,type 更灵活(支持联合/条件/映射)。
-
泛型(Generics):
function fn<T>(arg: T): T {},类型参数化,增强复用性。约束:<T extends Constraint>。 -
keyof: 获取类型的所有键名组成的联合类型。
keyof {a:1, b:2} = 'a' | 'b'。 -
typeof: 获取值的类型。
const obj = {a:1}; type T = typeof obj; -
in: 遍历联合类型,用于映射类型。
type Mapped = { [K in 'a'|'b']: string }。 -
infer: 条件类型中推断类型。
type ReturnType<T> = T extends (...args: any) => infer R ? R : never。 -
条件类型:
T extends U ? X : Y,类似三元运算。 -
**映射类型:**基于已有类型创建新类型。内置:Partial(全可选)、Required(全必填)、Readonly(全只读)、Pick(选取部分键)、Omit(排除部分键)、Record(键值映射)、ReturnType(函数返回值)、Parameters(函数参数)、Awaited(解包 Promise)。
-
类型守卫: 运行时检查缩小类型范围。
typeof、instanceof、in、自定义谓词函数arg is T。 -
**枚举(enum):**数字枚举/字符串枚举/常量枚举。建议优先用联合类型或 as const 替代。
-
元组(Tuple): 固定长度和类型的数组。
type Pair = [string, number]。 -
**装饰器(Decorator):**实验性特性,用于类、方法、属性、参数的元编程。Angular/NestJS 大量使用。
性能优化
前端性能优化有哪些手段?
**答案:**分为加载性能优化和运行时性能优化两大类。
一、加载性能优化(减少资源体积和请求数):
-
**资源压缩:**开启 gzip/brotli 压缩(服务器配置);HTML/CSS/JS 压缩(minify);图片压缩(TinyPNG、imagemin)。
-
**图片优化:**使用 WebP/AVIF 现代格式;响应式图片(srcset/sizes);图片懒加载(loading="lazy" / IntersectionObserver);雪碧图(CSS Sprite);字体图标(iconfont)替代小图标;预加载关键图片。
-
**代码分割(Code Splitting):**路由懒加载(动态 import);第三方库单独打包(vendor chunk);按需加载(lodash-es、antd 按需引入)。
-
**Tree Shaking:**移除未使用的代码,需使用 ESM(import/export),生产模式自动启用。sideEffects 配置。
-
**预加载:**preload(当前页面关键资源)、prefetch(下一页可能用到的资源)、dns-prefetch(DNS预解析)、preconnect(提前建立连接)。
-
**CDN:**静态资源放 CDN,就近访问;第三方库用公共 CDN。
-
**HTTP/2:**多路复用,减少连接开销;服务器推送。
-
**缓存策略:**合理设置 Cache-Control(静态资源长期缓存 + hash 文件名)、协商缓存;Service Worker 离线缓存。
-
**首屏优化:**SSR/SSG(服务端渲染,首屏直出);骨架屏(减少白屏感知);内联关键 CSS;延迟加载非关键 JS。
-
**减少重定向:**避免不必要的 301/302 重定向。
二、运行时性能优化(减少渲染和计算开销):
-
**减少重排重绘:**批量修改 DOM;使用 transform/opacity 做动画(只触发合成层);避免频繁读取 offsetTop 等属性;使用 will-change 提升合成层。
-
**虚拟列表:**长列表只渲染可视区域元素(react-window/vue-virtual-scroller)。
-
**防抖节流:**scroll/resize/input 等高频事件用 debounce/throttle。
-
**事件委托:**将子元素事件绑定到父元素,减少事件监听器数量。
-
**Web Worker:**将 CPU 密集型计算(大数据处理、复杂算法)放到 Worker 线程,避免阻塞主线程。
-
**requestAnimationFrame:**动画用 rAF 保证与浏览器刷新同步(60fps),避免 setInterval 丢帧。
-
**减少 DOM 操作:**使用 DocumentFragment 批量插入;离线操作(display:none 后修改);innerHTML vs createElement 权衡。
-
**框架优化:**Vue:v-show vs v-if、key 正确使用、keep-alive、异步组件、Object.freeze;React:memo/useMemo/useCallback、虚拟列表、Concurrent Mode。
-
**内存管理:**及时清除定时器/事件监听;避免闭包内存泄漏;WeakMap/WeakSet 防止内存泄漏。
-
**避免同步阻塞:**大任务拆分(时间切片,requestIdleCallback);避免长任务(Long Task > 50ms)。
Core Web Vitals 有哪些指标?
**答案:**Google 定义的核心网页指标,衡量用户体验。
| 指标 | 全称 | 含义 | 良好阈值 |
|---|---|---|---|
| LCP | Largest Contentful Paint | 最大内容绘制,首屏最大元素渲染时间 | < 2.5s |
| INP | Interaction to Next Paint | 交互到下一次绘制,响应交互的延迟(替代 FID) | < 200ms |
| CLS | Cumulative Layout Shift | 累积布局偏移,页面视觉稳定性 | < 0.1 |
其他重要指标:
-
**TTFB(Time To First Byte):**首字节时间,服务器响应速度,< 800ms 良好。
-
**FCP(First Contentful Paint):**首次内容绘制,第一个内容像素出现时间,< 1.8s 良好。
-
**FID(First Input Delay):**首次输入延迟,已被 INP 替代,< 100ms 良好。
-
**TTI(Time To Interactive):**可交互时间,页面完全可响应。
**优化 LCP:**优化服务器响应(TTFB)、渲染关键资源(preload 最大图片)、优化资源加载(图片压缩、懒加载非首屏)、减少阻塞渲染的 JS/CSS。
**优化 INP:**减少长任务、拆分大 JS、优化事件处理函数、使用 requestIdleCallback 延迟非关键工作、Web Worker 处理计算。
**优化 CLS:**给图片/视频设置 width/height、为广告/嵌入内容预留空间、避免在已有内容上方插入内容、使用 transform 动画而非影响布局的属性。
前端内存泄漏场景及排查?
答案:常见内存泄漏场景:
-
**意外的全局变量:**未声明的变量(非严格模式下赋值未声明变量挂到 window);this 指向全局的函数。
-
**未清除的定时器/计时器:**setInterval/setTimeout 回调中引用了 DOM 或变量,组件销毁时未 clearInterval。
-
**未移除的事件监听:**addEventListener 后未 removeEventListener,特别是 window/document 上的全局事件。
-
**闭包:**闭包引用外部变量,函数返回后变量无法回收,长期持有大对象。
-
**DOM 引用:**JS 变量持有已从 DOM 中移除的元素引用(删除了 DOM 但变量仍指向它)。
-
**console.log:**控制台打印大对象,浏览器 DevTools 保持引用。
-
**第三方库:**图表库(ECharts)、地图等未正确 dispose。
排查工具:
-
Chrome DevTools → Memory 面板:Heap Snapshot(堆快照)对比、Allocation instrumentation on timeline(分配时间线)、Allocation sampling。
-
Performance 面板:观察内存曲线是否持续上升不回落。
-
三步法:① 拍快照(操作前)→ ② 执行操作 → ③ 拍快照(操作后),对比快照找 Detached DOM tree、未释放的对象。
**解决方案:**组件卸载时清除定时器、移除事件监听、断开闭包引用(赋值 null)、调用第三方库的 dispose 方法、避免全局变量。
算法与数据结构
常见排序算法对比?
答案:
| 算法 | 平均时间 | 最坏时间 | 空间 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 选择排序 | O(n²) | O(n²) | O(1) | 不稳定 |
| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 希尔排序 | O(n^1.3) | O(n²) | O(1) | 不稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | 不稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 |
| 计数排序 | O(n+k) | O(n+k) | O(k) | 稳定 |
| 桶排序 | O(n+k) | O(n²) | O(n+k) | 稳定 |
| 基数排序 | O(n*k) | O(n*k) | O(n+k) | 稳定 |
**稳定性:**相等元素排序后相对顺序不变。稳定:冒泡、插入、归并、计数、桶、基数。不稳定:选择、希尔、快排、堆排。
快速排序实现:
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)]; } // 原地快排(空间 O(log n)) function quickSortInPlace(arr, left = 0, right = arr.length - 1) { if (left >= right) return; const pivotIndex = partition(arr, left, right); quickSortInPlace(arr, left, pivotIndex - 1); quickSortInPlace(arr, pivotIndex + 1, right); return arr; } function partition(arr, left, right) { const pivot = arr[right]; let i = left; for (let j = left; j < right; j++) { if (arr[j] < pivot) { [arr[i], arr[j]] = [arr[j], arr[i]]; i++; } } [arr[i], arr[right]] = [arr[right], arr[i]]; return i; }
手写防抖节流、深拷贝、柯里化、compose?
**答案:**防抖节流和深拷贝见 JavaScript 章节。以下补充柯里化和 compose。
// 柯里化:将多参数函数转为单参数函数序列 function curry(fn) { return function curried(...args) { if (args.length >= fn.length) { return fn.apply(this, args); } return function(...nextArgs) { return curried.apply(this, [...args, ...nextArgs]); }; }; } // 使用:const add = (a,b,c) => a+b+c; const curriedAdd = curry(add); curriedAdd(1)(2)(3) // 函数组合 compose:从右到左执行 function compose(...fns) { return function(x) { return fns.reduceRight((acc, fn) => fn(acc), x); }; } // pipe:从左到右执行 function pipe(...fns) { return function(x) { return fns.reduce((acc, fn) => fn(acc), x); }; } // 数组扁平化 function flatten(arr, depth = Infinity) { if (depth === 0) return arr.slice(); return arr.reduce((acc, item) => { if (Array.isArray(item)) { acc.push(...flatten(item, depth - 1)); } else { acc.push(item); } return acc; }, []); } // 数组去重 function unique(arr) { return [...new Set(arr)]; } // 去重(对象数组按属性) function uniqueBy(arr, key) { const seen = new Set(); return arr.filter(item => { const k = item[key]; if (seen.has(k)) return false; seen.add(k); return true; }); }
链表、树、栈队列常见操作?
答案:
反转链表:
function reverseList(head) { let prev = null, curr = head; while (curr) { const next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev; }
二叉树遍历(前中后序,迭代版):
// 前序:根-左-右 function preorder(root) { if (!root) return []; const res = [], stack = [root]; while (stack.length) { const node = stack.pop(); res.push(node.val); if (node.right) stack.push(node.right); if (node.left) stack.push(node.left); } return res; } // 中序:左-根-右 function inorder(root) { const res = [], stack = []; let curr = root; while (curr || stack.length) { while (curr) { stack.push(curr); curr = curr.left; } curr = stack.pop(); res.push(curr.val); curr = curr.right; } return res; } // 层序遍历(BFS) function levelOrder(root) { if (!root) return []; const res = [], queue = [root]; while (queue.length) { const level = [], size = queue.length; for (let i = 0; i < size; i++) { const node = queue.shift(); level.push(node.val); if (node.left) queue.push(node.left); if (node.right) queue.push(node.right); } res.push(level); } return res; }
用栈实现队列 / 用队列实现栈:
// 两个栈实现队列 class MyQueue { constructor() { this.inStack = []; this.outStack = []; } push(x) { this.inStack.push(x); } pop() { if (!this.outStack.length) { while (this.inStack.length) this.outStack.push(this.inStack.pop()); } return this.outStack.pop(); } peek() { if (!this.outStack.length) { while (this.inStack.length) this.outStack.push(this.inStack.pop()); } return this.outStack[this.outStack.length - 1]; } empty() { return !this.inStack.length && !this.outStack.length; } }
LRU 缓存实现?
**答案:**LRU(Least Recently Used)最近最少使用缓存,超出容量时删除最久未使用的数据。用 Map(有序)或 哈希表+双向链表实现。
// Map 实现(利用 Map 键的插入顺序) class LRUCache { constructor(capacity) { this.capacity = capacity; this.cache = new Map(); } get(key) { if (!this.cache.has(key)) return -1; const value = this.cache.get(key); this.cache.delete(key); this.cache.set(key, value); // 重新插入,移到末尾(最新) return value; } put(key, value) { if (this.cache.has(key)) this.cache.delete(key); this.cache.set(key, value); if (this.cache.size > this.capacity) { // 删除第一个(最久未使用) const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } } } // 哈希表 + 双向链表实现(更标准,O(1) 操作) class ListNode { constructor(key, value) { this.key = key; this.value = value; this.prev = null; this.next = null; } } class LRUCache2 { constructor(capacity) { this.capacity = capacity; this.map = new Map(); this.head = new ListNode(); // 虚拟头 this.tail = new ListNode(); // 虚拟尾 this.head.next = this.tail; this.tail.prev = this.head; } // 节点移到头部(最新使用) moveToHead(node) { this.removeNode(node); this.addToHead(node); } removeNode(node) { node.prev.next = node.next; node.next.prev = node.prev; } addToHead(node) { node.prev = this.head; node.next = this.head.next; this.head.next.prev = node; this.head.next = node; } get(key) { if (!this.map.has(key)) return -1; const node = this.map.get(key); this.moveToHead(node); return node.value; } put(key, value) { if (this.map.has(key)) { const node = this.map.get(key); node.value = value; this.moveToHead(node); } else { const node = new ListNode(key, value); this.map.set(key, node); this.addToHead(node); if (this.map.size > this.capacity) { const lru = this.tail.prev; // 尾部是最久未使用 this.removeNode(lru); this.map.delete(lru.key); } } } }
动态规划入门思路?
**答案:**动态规划(DP)通过将问题分解为子问题,存储子问题的解避免重复计算。
解题步骤:
-
**确定 dp 数组含义:**dpi 或 dpij 表示什么状态。
-
**确定状态转移方程:**当前状态如何由之前的状态推导。
-
**确定 base case:**初始值(dp0、dp00 等)。
-
**确定遍历顺序:**从前到后还是从后到前,是否需要滚动数组优化空间。
-
**返回结果:**dpn 或 dp 中的最大值等。
经典题目:
// 爬楼梯:dp[i] = dp[i-1] + dp[i-2] function climbStairs(n) { if (n <= 2) return n; let prev2 = 1, prev1 = 2; for (let i = 3; i <= n; i++) { const curr = prev1 + prev2; prev2 = prev1; prev1 = curr; } return prev1; } // 最大子数组和:dp[i] = max(dp[i-1]+nums[i], nums[i]) function maxSubArray(nums) { let max = nums[0], curr = nums[0]; for (let i = 1; i < nums.length; i++) { curr = Math.max(curr + nums[i], nums[i]); max = Math.max(max, curr); } return max; } // 零钱兑换:dp[i] = min(dp[i-coin]+1) function coinChange(coins, amount) { const dp = new Array(amount + 1).fill(Infinity); dp[0] = 0; for (let i = 1; i <= amount; i++) { for (const coin of coins) { if (i >= coin) dp[i] = Math.min(dp[i], dp[i - coin] + 1); } } return dp[amount] === Infinity ? -1 : dp[amount]; } // 最长递增子序列(LIS):dp[i] = max(dp[j]+1) for j<i and nums[j]<nums[i] function lengthOfLIS(nums) { const dp = new Array(nums.length).fill(1); let max = 1; for (let i = 1; i < nums.length; i++) { for (let j = 0; j < i; j++) { if (nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1); } max = Math.max(max, dp[i]); } return max; }
Node.js 与服务端
Node.js 事件循环?与浏览器有何区别?
**答案:**Node.js 事件循环基于 libuv,分为 6 个阶段,按顺序循环执行:
-
**timers:**执行 setTimeout、setInterval 的回调。
-
**pending callbacks:**执行延迟到下一轮的 I/O 回调(如 TCP 错误)。
-
**idle, prepare:**内部使用。
-
**poll:**获取新的 I/O 事件,执行 I/O 回调;如果没有定时器到期且没有待处理回调,在此阻塞等待。
-
**check:**执行 setImmediate 回调。
-
**close callbacks:**执行关闭回调(如 socket.on('close'))。
**微任务(process.nextTick 和 Promise):**在每个阶段切换之间、每个宏任务执行完后,清空所有微任务。process.nextTick 优先级高于 Promise(微任务中的微任务)。
与浏览器事件循环的区别:
-
浏览器:宏任务队列 + 微任务队列,一个宏任务后清空所有微任务。
-
Node:6 个阶段队列,每个阶段执行完该阶段所有回调后清空微任务;同一阶段内多个回调之间不执行微任务(Node 11 之前)。Node 11+ 改为每个回调后都执行微任务,与浏览器行为一致。
-
Node 特有:process.nextTick、setImmediate;浏览器特有:requestAnimationFrame、MutationObserver、UI 渲染。
-
setTimeout(fn, 0) vs setImmediate:在主模块中顺序不确定(受性能影响);在 I/O 回调中 setImmediate 先执行(poll 后是 check)。
Express 和 Koa 的区别?Koa 洋葱模型?
答案:
| 对比项 | Express | Koa |
|---|---|---|
| 异步处理 | 回调函数,回调地狱 | async/await + Promise,优雅异步 |
| 中间件模型 | 线性执行,next() 后不回来 | 洋葱模型,请求进入和响应返回都经过中间件 |
| 错误处理 | 需 try-catch 或错误中间件,异步错误难捕获 | async/await + try-catch,统一错误处理中间件 |
| 内置功能 | 内置路由、静态文件、视图等 | 极简核心,路由等需第三方中间件(koa-router) |
| 上下文 | req/res 分离 | ctx 封装 req/res,ctx.request/ctx.response |
| 体积 | 较大 | 更小(约500行) |
**Koa 洋葱模型:**中间件按注册顺序从外到内执行(next 之前的代码),到达最内层后,从内到外执行(next 之后的代码),像剥洋葱一样。
const Koa = require('koa'); const app = new Koa(); app.use(async (ctx, next) => { console.log('1 - 进入'); await next(); console.log('1 - 退出'); }); app.use(async (ctx, next) => { console.log('2 - 进入'); await next(); console.log('2 - 退出'); }); app.use(async (ctx) => { console.log('3 - 核心'); ctx.body = 'Hello'; }); // 输出顺序:1-进入 → 2-进入 → 3-核心 → 2-退出 → 1-退出
JWT 鉴权原理?与 Session 的区别?
**答案:JWT(JSON Web Token):**由三部分组成,用 . 分隔:Header.Payload.Signature。
-
Header:{"alg": "HS256", "typ": "JWT"},Base64Url 编码。
-
**Payload:**声明信息(用户ID、用户名、过期时间exp、签发时间iat等),Base64Url 编码。注意不要放敏感信息(可解码)。
-
**Signature:**HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret),用密钥签名防止篡改。
**工作流程:**用户登录 → 服务器验证通过生成 JWT 返回 → 客户端存储(localStorage/cookie)→ 后续请求在 Authorization: Bearer <token> 头中携带 → 服务器验证签名和有效期 → 通过则处理请求。
JWT vs Session:
| 对比项 | Session | JWT |
|---|---|---|
| 存储位置 | 服务器(内存/Redis/数据库) | 客户端(localStorage/cookie) |
| 服务器状态 | 有状态(需存储 session) | 无状态(只需验证签名) |
| 扩展性 | 差(多服务器需共享 session,如 Redis) | 好(无状态,任意服务器可验证) |
| 安全性 | 较安全(sessionID 随机,服务端可控) | 需注意 XSS(localStorage 可被窃取)、无法主动失效 |
| 注销/续签 | 简单(删除服务端 session) | 困难(token 未过期前有效,需黑名单机制) |
| 跨域 | cookie 跨域需配置 | 放在 header 中,天然支持跨域 |
| 数据量 | sessionID 小 | token 较大(含用户信息) |
项目与软技能
如何介绍项目?STAR 法则?
**答案:**用 STAR 法则组织项目介绍,突出个人贡献和量化成果。
-
**S(Situation 情境):**项目背景是什么?业务规模多大?为什么做这个项目?技术栈是什么?你在团队中的角色?
-
**T(Task 任务):**你负责哪些模块?面临什么技术挑战或业务难题?需要达成什么目标?
-
**A(Action 行动):**你具体做了什么?技术方案如何设计?遇到了什么难点怎么解决的?用了什么技术手段?(重点讲,体现技术深度)
-
**R(Result 结果):**取得了什么成果?尽量量化:性能提升 X%、加载时间减少 Xs、用户量 X 万、bug 率下降 X%、节省 X 人日。
项目介绍模板:"我参与了 XX 项目,这是一个面向 XX 用户的 XX 系统,日活 XX 万。我负责 XX 模块的开发。项目中遇到了 XX 难点,我通过 XX 技术方案解决了,最终实现了 XX 效果,性能提升了 XX%。"
**注意事项:**① 准备 2-3 个核心项目,一个主项目深入讲;② 突出个人贡献,不要只讲团队做了什么;③ 技术难点要讲清楚为什么难、怎么解决、有没有其他方案;④ 对项目中的技术栈要非常熟悉,面试官会深挖。
项目中遇到的最大技术难点?怎么解决的?
**答案:**这是必问题,需要提前准备真实案例。回答框架:
-
**描述难点:**什么场景下遇到的?具体表现是什么?(如首屏加载慢、大数据渲染卡顿、复杂表单状态管理、跨端兼容性问题)
-
**分析原因:**通过什么工具排查?根因是什么?(如 Chrome DevTools Performance 分析发现长任务、Network 发现资源过大)
-
**解决方案:**尝试了哪些方案?最终选了什么?为什么?(如虚拟列表解决长列表卡顿、代码分割减小首屏 bundle、Web Worker 处理大数据计算)
-
**效果验证:**优化前后对比数据?(如 FPS 从 20 提升到 60、首屏从 5s 降到 1.5s)
-
**总结反思:**学到了什么?有没有可复用的经验?
**常见可讲的难点方向:**① 性能优化(首屏、长列表、大数据);② 复杂状态管理(大型表单、多人协作);③ 架构设计(微前端、组件库、低代码);④ 跨端兼容(小程序/H5/App 多端统一);⑤ 工程化(构建优化、CI/CD、监控体系);⑥ 疑难 bug(内存泄漏、偶现白屏、竞态条件)。
如何做前端性能优化?(结合项目)
**答案:**结合项目讲,先讲分析方法,再讲具体手段,最后讲效果。
分析流程:
-
用 Lighthouse / Chrome DevTools Performance 跑一遍,看各项指标分数。
-
Network 面板看资源加载:哪些资源大?哪些请求慢?有没有重复请求?
-
Performance 面板看运行时:有没有长任务?重排重绘多不多?
-
Bundle Analyzer 分析打包产物:哪些包大?有没有重复依赖?
常见优化手段(结合项目举例):
-
**加载优化:**路由懒加载、第三方库按需引入、图片懒加载+WebP、gzip 压缩、CDN、HTTP 缓存策略、预加载关键资源。
-
**运行时优化:**虚拟列表、防抖节流、Web Worker、减少重排重绘、框架层面优化(memo/keep-alive)。
-
**构建优化:**Tree Shaking、代码分割、压缩、esbuild/swc 加速构建、依赖预构建。
**效果:**首屏加载时间从 Xs 降到 Ys,LCP 从 Xs 降到 Ys,bundle 体积从 X KB 降到 Y KB,FPS 稳定在 60。
如何排查线上问题?
答案:
排查流程:
-
**确认问题:**问题是什么?影响范围?是否可复现?什么时候开始出现的?有没有发版?
-
**信息收集:**查看错误监控(Sentry/自建监控)的错误堆栈、用户信息、设备信息、操作路径;查看日志(前端日志、服务端日志);查看性能监控数据。
-
**复现问题:**尽量在测试环境复现;如果无法复现,根据用户操作路径模拟;用 Chrome DevTools 调试。
-
**定位根因:**根据错误堆栈定位代码;二分法排查(回滚版本、注释代码);对比正常和异常情况的差异。
-
**修复验证:**修复后在测试环境验证;灰度发布;监控错误率是否下降。
-
**总结复盘:**为什么会出现?如何避免?有没有需要补充的监控或测试用例?
**常见线上问题:**白屏(JS 加载失败、JS 执行错误、资源 404)、页面卡顿(内存泄漏、长任务、死循环)、样式错乱(CSS 加载失败、兼容性问题)、数据异常(接口错误、缓存问题、竞态条件)、跨域问题(CORS 配置变更)。
**监控体系:**错误监控(JS 错误、Promise 错误、资源加载错误、接口错误)、性能监控(LCP/FCP/CLS/TTFB)、行为监控(用户操作路径、PV/UV)、白屏检测。
职业规划?为什么离职/跳槽?
答案:职业规划回答要点:
-
短期(1-2年):深入前端技术,在某个方向(性能优化/工程化/架构)形成专长,成为团队技术骨干。
-
中期(3-5年):拓展技术广度,了解后端/运维/产品,具备全栈或架构设计能力,能独立负责复杂项目。
-
长期:成为技术专家或技术管理者,根据公司和个人发展决定方向。
-
结合应聘公司的业务和技术栈,说明为什么这个岗位符合你的规划。
离职原因回答要点(避免负面):
-
正面表述:追求更大的技术挑战、希望接触更大规模的业务、个人发展遇到瓶颈、想在某个技术方向深入发展。
-
避免说:前公司不好、领导不行、加班太多、薪资低(可以说希望有更好的发展平台和回报,但不要抱怨)。
-
如果是被裁员:客观说明业务调整/组织优化,不要隐瞒,重点讲自己的能力和贡献。
附录:高频面试题 Top 20
-
从输入 URL 到页面显示发生了什么? --- 见第四章第1题
-
浏览器渲染流程?重排重绘区别与优化? --- 见第四章第2题
-
事件循环(Event Loop)机制?宏任务微任务执行顺序? --- 见第二章第7题
-
Promise 原理?手写一个 Promise? --- 见第二章第8题
-
闭包是什么?应用场景与内存泄漏? --- 见第二章第3题
-
this 指向的几种情况?箭头函数的 this? --- 见第二章第4题
-
原型与原型链?继承的几种方式? --- 见第二章第5、6题
-
Vue 响应式原理?Vue2 和 Vue3 区别? --- 见第三章第1题
-
Vue Diff 算法?key 的作用? --- 见第三章第2题
-
React Hooks 原理?useEffect 依赖数组? --- 见第三章第9题
-
虚拟 DOM 的优缺点? --- 见第三章第2题
-
跨域解决方案?CORS 原理? --- 见第四章第5题
-
浏览器缓存机制?强缓存与协商缓存? --- 见第四章第3题
-
HTTP/1.1 vs HTTP/2 vs HTTP/3? --- 见第五章第3题
-
HTTPS 握手过程?对称加密与非对称加密? --- 见第五章第4题
-
XSS 和 CSRF 区别与防御? --- 见第四章第6题
-
Webpack 构建流程?HMR 原理?Loader 和 Plugin 区别? --- 见第六章第1、2、3题
-
Vite 为什么比 Webpack 快? --- 见第六章第4题
-
前端性能优化方案(加载+运行时)? --- 见第七章第1题
-
防抖和节流区别?手写实现? --- 见第二章第12题
------ 全文完,祝面试顺利 ------