前端面试知识点大全(带答案)

内容由豆包生成

本文档系统整理前端面试各模块核心知识点,每道题配有标准答案,覆盖 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种)

答案:

  1. Flex 布局(推荐): 父元素 display: flex; justify-content: center; align-items: center;

  2. Grid 布局: 父元素 display: grid; place-items: center;

  3. 绝对定位 + transform: position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);(无需知道宽高)

  4. 绝对定位 + margin: auto: position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto;(需有宽高)

  5. 绝对定位 + 负 margin: position: absolute; top: 50%; left: 50%; margin-left: -宽/2; margin-top: -高/2;(需知道宽高)

  6. 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 选择器优先级如何计算?

**答案:**优先级从高到低:

  1. !important(最高,但应尽量避免使用)

  2. 行内样式(style 属性)

  3. ID 选择器(#id)

  4. 类选择器(.class)、属性选择器(attr)、伪类(:hover)

  5. 标签选择器(div)、伪元素(::before)

  6. 通配符(*)、关系选择器(+、>、~、空格)不影响优先级

**计算规则:**按 (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。

  • ==:宽松相等,会进行隐式类型转换后再比较。

== 转换规则:

  1. 类型相同:直接比较值(注意 NaN != NaN,+0 == -0)。

  2. null == undefined → true,且与其他值比较都为 false。

  3. number 和 string 比较:string 转 number。

  4. boolean 和其他类型比较:boolean 转 number(true→1,false→0)。

  5. 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 的指向由调用方式决定,而非定义位置(箭头函数除外)。

  1. **默认绑定:**独立函数调用,this 指向全局对象(浏览器 window,Node global);严格模式下为 undefined。

  2. 隐式绑定: 作为对象方法调用,this 指向调用该方法的对象。obj.fn() → this = obj。

  3. **显式绑定:**通过 call/apply/bind 强制指定 this。

  4. **new 绑定:**构造函数调用,this 指向新创建的实例对象。

  5. **箭头函数:**没有自己的 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 继承有哪些方式?

答案:

  1. **原型链继承:**子类.prototype = new 父类()。缺点:父类引用属性被所有实例共享;创建子类实例时不能向父类传参。

  2. 构造函数继承(经典继承): 在子类构造函数中调用 父类.call(this, args)。优点:可传参、避免引用共享;缺点:方法在构造函数中定义,无法复用,每个实例都有一份。

  3. **组合继承:**原型链继承方法 + 构造函数继承属性。最常用,但会调用两次父类构造函数。

  4. 原型式继承: Object.create(父类对象),创建一个以给定对象为原型的新对象。缺点:引用属性共享。

  5. **寄生式继承:**在原型式继承基础上,在函数内部增强对象。

  6. 寄生组合继承(最优): 通过构造函数继承属性,通过原型链的混成形式继承方法。子类.prototype = Object.create(父类.prototype),并修正 constructor。只调用一次父类构造函数,效率最高。

  7. **ES6 class extends:**语法糖,底层基于寄生组合继承,使用 super 调用父类。

复制代码

// 寄生组合继承 function inherit(Child, Parent) { Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; }

什么是事件循环(Event Loop)?宏任务和微任务?

**答案:**JavaScript 是单线程的,事件循环是其实现异步的机制。

执行过程:

  1. 同步代码在调用栈中依次执行。

  2. 遇到异步任务,将其注册到对应 Web API(定时器、DOM事件、fetch等)。

  3. 异步任务完成后,回调函数放入任务队列。

  4. 调用栈清空后,先执行所有微任务(microtask queue)。

  5. 微任务清空后,从宏任务队列取一个任务执行。

  6. 每个宏任务执行完后,再次清空所有微任务。

  7. 重复上述过程,即事件循环。

**宏任务(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,从块级作用域开始到声明语句之间的区域称为暂时性死区。

箭头函数和普通函数的区别?

答案:

  1. **this 指向:**箭头函数没有自己的 this,继承外层作用域的 this;普通函数 this 由调用方式决定。

  2. **无法改变 this:**箭头函数不能通过 call/apply/bind 改变 this 指向。

  3. **没有 arguments:**箭头函数没有 arguments 对象,可用 rest 参数 (...args) 替代。

  4. **不能作为构造函数:**箭头函数不能用 new 调用,没有 prototype。

  5. **没有原型对象:**箭头函数没有 prototype 属性。

  6. **语法更简洁:**单表达式可省略 return 和花括号。

  7. **不能用作 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。

  • **深拷贝:**递归复制所有层级,拷贝后的对象与原对象完全独立,互不影响。

深拷贝实现方式:

  1. JSON.parse(JSON.stringify(obj)):最简单,但有局限性------无法复制函数、undefined、Symbol、Date(会变字符串)、RegExp、循环引用会报错。

  2. structuredClone(obj):浏览器原生 API,支持循环引用、Date、RegExp、Map、Set,但不支持函数、DOM节点。

  3. 手写递归深拷贝:处理循环引用(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(双端对比算法):

  1. 新旧子节点数组各有首尾两个指针(oldStartIdx/oldEndIdx/newStartIdx/newEndIdx)。

  2. 依次比较:旧首vs新首、旧尾vs新尾、旧首vs新尾、旧尾vs新首。

  3. 找到可复用节点则移动指针,未找到则用 key 在旧节点中查找。

  4. 最后处理剩余节点(新增或删除)。

Vue3 Diff(最长递增子序列):

  1. 先从头对比相同节点,再从尾对比相同节点。

  2. 中间乱序部分:建立新节点 key 到 index 的映射。

  3. 遍历旧节点,找到在新节点中的位置,生成最长递增子序列。

  4. 不在递增子序列中的节点需要移动,最大化减少 DOM 移动次数。

**key 的作用:**作为 VNode 的唯一标识,帮助 Diff 算法更准确地判断节点是否可复用,避免复用错误导致的状态问题。不建议用 index 作为 key(列表顺序变化时会导致错误复用)。

Vue 生命周期有哪些?Vue3 组合式 API 对应关系?

答案:

Vue2 生命周期:

  1. beforeCreate:实例创建前,data 和 methods 不可用。

  2. created:实例创建完成,data/methods 可用,$el 不可用(未挂载)。适合发请求。

  3. beforeMount:挂载前,模板编译完成,未渲染到 DOM。

  4. mounted:挂载完成,DOM 可用。适合操作 DOM、初始化第三方库。

  5. beforeUpdate:数据更新前,DOM 未更新。

  6. updated:数据更新后,DOM 已更新。避免在此修改数据(可能死循环)。

  7. beforeDestroy:实例销毁前,实例仍可用。适合清除定时器、事件监听。

  8. destroyed:实例销毁完成。

  9. activated / deactivated:keep-alive 组件激活/停用时触发。

  10. 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 更新完成后执行回调。

原理:

  1. 数据变化 → watcher 更新 → 放入 queue 队列(去重)。

  2. 调用 nextTick(flushSchedulerQueue) 将刷新队列的操作放入微任务。

  3. 微任务优先级:Promise.then > MutationObserver > setImmediate > setTimeout(降级策略)。

  4. 当前同步代码执行完后,执行微任务,批量更新 DOM。

  5. 用户调用的 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 性能优化手段?

答案:

  1. **React.memo:**包裹函数组件,对 props 做浅比较,props 不变则跳过渲染。

  2. **useMemo:**缓存计算结果,避免每次渲染都重新计算昂贵值。

  3. **useCallback:**缓存函数引用,避免传给子组件的函数每次都是新引用导致子组件重渲染。

  4. **shouldComponentUpdate / PureComponent:**类组件中通过浅比较 props 和 state 决定是否更新。

  5. **虚拟列表:**长列表只渲染可视区域的元素(react-window/react-virtualized)。

  6. **React.lazy + Suspense:**路由/组件懒加载,代码分割,减少首屏 bundle 体积。

  7. **避免内联函数和对象作为 props:**内联函数/对象每次渲染都是新引用,导致子组件 memo 失效。

  8. **key 正确使用:**列表用唯一稳定 key,避免用 index。

  9. **状态下放:**将状态放在需要它的最近公共父组件,避免不必要的大范围重渲染。

  10. **Concurrent Mode / useTransition / useDeferredValue:**React 18 并发特性,将非紧急更新延迟,保持 UI 响应。

  11. **避免在 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 到页面显示发生了什么?

**答案:**完整流程如下:

  1. **URL 解析:**浏览器解析 URL(协议、域名、端口、路径、查询参数、锚点),判断是搜索还是 URL。HSTS 检查是否强制 HTTPS。

  2. **DNS 解析:**将域名解析为 IP 地址。顺序:浏览器缓存 → 系统缓存(hosts)→ 路由器缓存 → ISP DNS 缓存 → 递归查询(根域名服务器 → 顶级域名服务器 → 权威域名服务器)。

  3. **TCP 连接:**三次握手建立 TCP 连接。SYN → SYN+ACK → ACK。

  4. **TLS 握手(HTTPS):**客户端发送支持的加密套件和随机数 → 服务器返回证书、选定加密套件和随机数 → 客户端验证证书,生成预主密钥用服务器公钥加密发送 → 双方用三个随机数生成会话密钥 → 切换加密通信。TLS 1.3 简化为 1-RTT。

  5. **发送 HTTP 请求:**构建请求报文(请求行、请求头、请求体),发送到服务器。

  6. **服务器处理并响应:**服务器处理请求,返回响应报文(状态行、响应头、响应体)。可能经过负载均衡、反向代理。

  7. 浏览器解析渲染:

    1. 解析 HTML 构建 DOM 树

    2. 解析 CSS 构建 CSSOM 树

    3. DOM + CSSOM 合并为渲染树(Render Tree,只包含可见节点)

    4. 布局(Layout/Reflow):计算渲染树中每个节点的几何位置和大小

    5. 绘制(Paint):将节点像素绘制到屏幕上,分层进行

    6. 合成(Composite):将多个层合并显示到屏幕

  8. **断开连接:**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 请求。

跨域解决方案:

  1. **CORS(跨域资源共享):**最主流方案,服务器设置响应头允许跨域。

    1. 简单请求:GET/POST/HEAD,Content-Type 为 text/plain/multipart/form-data/application/x-www-form-urlencoded。浏览器直接发送,服务器返回 Access-Control-Allow-Origin。

    2. 预检请求:非简单请求先发 OPTIONS 预检,服务器返回允许的方法/头/凭证,通过后才发真实请求。

    3. 携带 Cookie:需设置 Access-Control-Allow-Credentials: true,且前端 xhr.withCredentials = true,此时 Allow-Origin 不能为 *。

  2. **JSONP:**利用 script 标签不受同源策略限制,通过回调函数获取数据。只支持 GET,有安全风险(XSS)。

  3. **代理服务器:**开发环境用 webpack devServer proxy / vite proxy,生产环境用 Nginx 反向代理,将请求转发到目标服务器。

  4. **postMessage:**窗口间通信,适用于 iframe 跨域。

  5. **WebSocket:**本身不受同源策略限制。

  6. **document.domain:**主域相同子域不同时,设置 document.domain 为主域,可跨子域操作 iframe。

  7. **window.name:**利用 window.name 在页面导航后保持值,实现跨域数据传递。

XSS 和 CSRF 区别与防御?

答案:

**XSS(跨站脚本攻击):**攻击者在网页中注入恶意脚本,在用户浏览器中执行,窃取 Cookie/Token、篡改页面、钓鱼等。

XSS 类型:

  • **存储型:**恶意脚本存入数据库,其他用户访问时执行。危害最大。如评论区、用户昵称。

  • **反射型:**恶意脚本在 URL 参数中,服务器反射回页面执行。需诱导用户点击链接。

  • **DOM 型:**前端 JS 直接将用户输入插入 DOM,不经过服务器。如 innerHTML、document.write。

XSS 防御:

  • 输入过滤/转义:对用户输入进行 HTML 转义(&lt; &gt; &amp; &quot;)。

  • 输出编码:根据插入位置(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 握手过程(简化):

  1. **Client Hello:**客户端发送支持的 TLS 版本、加密套件列表、随机数 random1。

  2. **Server Hello:**服务器选择 TLS 版本和加密套件,返回随机数 random2、数字证书。

  3. **客户端验证证书:**用 CA 根证书验证服务器证书合法性,提取服务器公钥。

  4. **Client Key Exchange:**客户端生成预主密钥(pre-master secret),用服务器公钥加密后发送。

  5. **双方计算会话密钥:**用 random1 + random2 + pre-master secret 通过 PRF 生成会话密钥(对称加密密钥)。

  6. **Change Cipher Spec + Finished:**双方通知切换加密通信,发送 Finished 消息(加密)验证握手完整性。

  7. **加密通信:**后续所有 HTTP 数据都用会话密钥对称加密传输。

**TLS 1.3 改进:**① 支持 0-RTT(恢复连接时);② 握手从 2-RTT 减为 1-RTT;③ 移除不安全的加密套件(RSA 密钥交换、SHA-1、3DES等);④ 所有握手消息加密(Server Hello 之后)。

TCP 三次握手和四次挥手?为什么?

答案:

三次握手(建立连接):

  1. 客户端发送 SYN=1, seq=x,进入 SYN_SENT 状态。

  2. 服务器回复 SYN=1, ACK=1, seq=y, ack=x+1,进入 SYN_RCVD 状态。

  3. 客户端回复 ACK=1, seq=x+1, ack=y+1,双方进入 ESTABLISHED 状态。

**为什么三次?**① 确认双方收发能力正常(两次只能确认客户端到服务器,不能确认服务器到客户端);② 防止已失效的连接请求突然到达服务器,导致服务器错误建立连接浪费资源(两次握手中服务器收到旧 SYN 就会建立连接)。

四次挥手(断开连接):

  1. 主动方发送 FIN=1, seq=u,进入 FIN_WAIT_1 状态。

  2. 被动方回复 ACK=1, ack=u+1,进入 CLOSE_WAIT 状态;主动方进入 FIN_WAIT_2。

  3. 被动方发送完剩余数据后,发送 FIN=1, seq=w, ack=u+1,进入 LAST_ACK 状态。

  4. 主动方回复 ACK=1, ack=w+1,进入 TIME_WAIT 状态;被动方收到后关闭。主动方等待 2MSL 后关闭。

**为什么四次?**TCP 是全双工的,关闭连接需要双方各自关闭发送通道。被动方收到 FIN 后可能还有数据要发送,所以先回 ACK,等数据发完再发 FIN,因此比握手多一次。

**TIME_WAIT 为什么等待 2MSL?**① 确保最后一个 ACK 能到达对方(如果丢失,对方会重发 FIN,2MSL 内可收到重发并重新计时);② 让本次连接的所有报文在网络中消失,避免影响下一个新连接(四元组复用)。

DNS 解析过程?

**答案:**DNS(域名系统)将域名解析为 IP 地址。

解析顺序(缓存优先):

  1. **浏览器缓存:**浏览器先查自身 DNS 缓存(Chrome 约 1 分钟,约 1000 条)。

  2. **系统缓存:**查操作系统 hosts 文件和系统 DNS 缓存。

  3. **路由器缓存:**查路由器 DNS 缓存。

  4. **ISP DNS 缓存:**查互联网服务提供商的本地 DNS 服务器缓存。

  5. **递归查询:**本地 DNS 服务器向根域名服务器发起查询。

递归查询过程(本地 DNS 服务器代查):

  1. 本地 DNS → 根域名服务器(.):返回顶级域名服务器地址(如 .com)。

  2. 本地 DNS → 顶级域名服务器(.com):返回权威域名服务器地址(如 example.com)。

  3. 本地 DNS → 权威域名服务器(example.com):返回对应 IP 地址。

  4. 本地 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: websocketConnection: UpgradeSec-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:**开发服务器配置,热更新、代理等。

构建流程:

  1. **初始化参数:**合并配置文件和命令行参数。

  2. **创建 Compiler 对象:**初始化编译器,加载所有插件。

  3. **run/watch:**开始构建(run 一次性,watch 监听模式)。

  4. **compilation:**每次构建创建 Compilation 对象,负责本次构建。

  5. make(从入口解析依赖):

    1. 从 entry 开始,调用 loader 转换模块源码。

    2. 用 acorn 解析转换后的代码为 AST。

    3. 遍历 AST,收集依赖(import/require)。

    4. 递归处理所有依赖模块,构建模块依赖图。

  6. **seal(封装产物):**根据入口和依赖关系,生成 chunk,对 chunk 进行代码分割、tree shaking 等优化。

  7. **emit(输出文件):**将 chunk 转换为文件,写入 output 目录(plugin 可在此阶段修改内容)。

  8. **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)允许在不刷新页面的情况下,替换、添加或删除模块,保持应用当前状态。

原理:

  1. **watch 模式:**Webpack 监听文件变化,文件修改后重新编译,生成更新后的模块。

  2. **devServer 与浏览器建立 WebSocket 连接:**编译完成后,devServer 通过 WebSocket 向浏览器发送 hash(本次编译的唯一标识)和 ok 消息。

  3. **浏览器接收消息:**webpack-dev-server/client 接收 hash,调用 webpack/lib/HotModuleReplacement.runtime 检查更新。

  4. **请求更新清单:**浏览器通过 AJAX 请求 hash.hot-update.json,获取需要更新的模块列表。

  5. **请求更新模块:**通过 JSONP 请求 hash.moduleId.hot-update.js,获取更新后的模块代码。

  6. **模块替换:**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 核心原理:

  1. **开发时基于原生 ES Modules:**利用浏览器原生支持的 ESM(import/export),不需要打包所有模块。浏览器请求哪个模块,Vite 才按需编译哪个模块。

  2. **依赖预构建(optimizeDeps):**启动时用 esbuild 预构建第三方依赖(node_modules),将 CommonJS/UMD 转为 ESM,合并多模块依赖减少请求数。预构建结果缓存到 node_modules/.vite。

  3. **按需编译:**源码文件(.vue/.ts/.jsx)在浏览器请求时实时编译,利用浏览器缓存(304),未修改的文件不重新编译。

  4. **HMR 基于 ESM:**修改文件后,只需让浏览器重新请求该模块及其依赖,无需重新打包整个 bundle,HMR 速度与项目大小无关。

  5. **生产构建用 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 代码。

编译流程(三阶段):

  1. **Parse(解析):**将源码字符串解析为 AST(抽象语法树)。词法分析(token 化)→ 语法分析(生成 AST)。使用 @babel/parser。

  2. **Transform(转换):**遍历 AST,对节点进行添加、更新、移除等操作。插件在此阶段工作。使用 @babel/traverse。

  3. **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)。

  • 类型守卫: 运行时检查缩小类型范围。typeofinstanceofin、自定义谓词函数 arg is T

  • **枚举(enum):**数字枚举/字符串枚举/常量枚举。建议优先用联合类型或 as const 替代。

  • 元组(Tuple): 固定长度和类型的数组。type Pair = [string, number]

  • **装饰器(Decorator):**实验性特性,用于类、方法、属性、参数的元编程。Angular/NestJS 大量使用。


性能优化

前端性能优化有哪些手段?

**答案:**分为加载性能优化和运行时性能优化两大类。

一、加载性能优化(减少资源体积和请求数):

  1. **资源压缩:**开启 gzip/brotli 压缩(服务器配置);HTML/CSS/JS 压缩(minify);图片压缩(TinyPNG、imagemin)。

  2. **图片优化:**使用 WebP/AVIF 现代格式;响应式图片(srcset/sizes);图片懒加载(loading="lazy" / IntersectionObserver);雪碧图(CSS Sprite);字体图标(iconfont)替代小图标;预加载关键图片。

  3. **代码分割(Code Splitting):**路由懒加载(动态 import);第三方库单独打包(vendor chunk);按需加载(lodash-es、antd 按需引入)。

  4. **Tree Shaking:**移除未使用的代码,需使用 ESM(import/export),生产模式自动启用。sideEffects 配置。

  5. **预加载:**preload(当前页面关键资源)、prefetch(下一页可能用到的资源)、dns-prefetch(DNS预解析)、preconnect(提前建立连接)。

  6. **CDN:**静态资源放 CDN,就近访问;第三方库用公共 CDN。

  7. **HTTP/2:**多路复用,减少连接开销;服务器推送。

  8. **缓存策略:**合理设置 Cache-Control(静态资源长期缓存 + hash 文件名)、协商缓存;Service Worker 离线缓存。

  9. **首屏优化:**SSR/SSG(服务端渲染,首屏直出);骨架屏(减少白屏感知);内联关键 CSS;延迟加载非关键 JS。

  10. **减少重定向:**避免不必要的 301/302 重定向。

二、运行时性能优化(减少渲染和计算开销):

  1. **减少重排重绘:**批量修改 DOM;使用 transform/opacity 做动画(只触发合成层);避免频繁读取 offsetTop 等属性;使用 will-change 提升合成层。

  2. **虚拟列表:**长列表只渲染可视区域元素(react-window/vue-virtual-scroller)。

  3. **防抖节流:**scroll/resize/input 等高频事件用 debounce/throttle。

  4. **事件委托:**将子元素事件绑定到父元素,减少事件监听器数量。

  5. **Web Worker:**将 CPU 密集型计算(大数据处理、复杂算法)放到 Worker 线程,避免阻塞主线程。

  6. **requestAnimationFrame:**动画用 rAF 保证与浏览器刷新同步(60fps),避免 setInterval 丢帧。

  7. **减少 DOM 操作:**使用 DocumentFragment 批量插入;离线操作(display:none 后修改);innerHTML vs createElement 权衡。

  8. **框架优化:**Vue:v-show vs v-if、key 正确使用、keep-alive、异步组件、Object.freeze;React:memo/useMemo/useCallback、虚拟列表、Concurrent Mode。

  9. **内存管理:**及时清除定时器/事件监听;避免闭包内存泄漏;WeakMap/WeakSet 防止内存泄漏。

  10. **避免同步阻塞:**大任务拆分(时间切片,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 动画而非影响布局的属性。

前端内存泄漏场景及排查?

答案:常见内存泄漏场景:

  1. **意外的全局变量:**未声明的变量(非严格模式下赋值未声明变量挂到 window);this 指向全局的函数。

  2. **未清除的定时器/计时器:**setInterval/setTimeout 回调中引用了 DOM 或变量,组件销毁时未 clearInterval。

  3. **未移除的事件监听:**addEventListener 后未 removeEventListener,特别是 window/document 上的全局事件。

  4. **闭包:**闭包引用外部变量,函数返回后变量无法回收,长期持有大对象。

  5. **DOM 引用:**JS 变量持有已从 DOM 中移除的元素引用(删除了 DOM 但变量仍指向它)。

  6. **console.log:**控制台打印大对象,浏览器 DevTools 保持引用。

  7. **第三方库:**图表库(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)通过将问题分解为子问题,存储子问题的解避免重复计算。

解题步骤:

  1. **确定 dp 数组含义:**dpi 或 dpij 表示什么状态。

  2. **确定状态转移方程:**当前状态如何由之前的状态推导。

  3. **确定 base case:**初始值(dp0、dp00 等)。

  4. **确定遍历顺序:**从前到后还是从后到前,是否需要滚动数组优化空间。

  5. **返回结果:**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 个阶段,按顺序循环执行:

  1. **timers:**执行 setTimeout、setInterval 的回调。

  2. **pending callbacks:**执行延迟到下一轮的 I/O 回调(如 TCP 错误)。

  3. **idle, prepare:**内部使用。

  4. **poll:**获取新的 I/O 事件,执行 I/O 回调;如果没有定时器到期且没有待处理回调,在此阻塞等待。

  5. **check:**执行 setImmediate 回调。

  6. **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 个核心项目,一个主项目深入讲;② 突出个人贡献,不要只讲团队做了什么;③ 技术难点要讲清楚为什么难、怎么解决、有没有其他方案;④ 对项目中的技术栈要非常熟悉,面试官会深挖。

项目中遇到的最大技术难点?怎么解决的?

**答案:**这是必问题,需要提前准备真实案例。回答框架:

  1. **描述难点:**什么场景下遇到的?具体表现是什么?(如首屏加载慢、大数据渲染卡顿、复杂表单状态管理、跨端兼容性问题)

  2. **分析原因:**通过什么工具排查?根因是什么?(如 Chrome DevTools Performance 分析发现长任务、Network 发现资源过大)

  3. **解决方案:**尝试了哪些方案?最终选了什么?为什么?(如虚拟列表解决长列表卡顿、代码分割减小首屏 bundle、Web Worker 处理大数据计算)

  4. **效果验证:**优化前后对比数据?(如 FPS 从 20 提升到 60、首屏从 5s 降到 1.5s)

  5. **总结反思:**学到了什么?有没有可复用的经验?

**常见可讲的难点方向:**① 性能优化(首屏、长列表、大数据);② 复杂状态管理(大型表单、多人协作);③ 架构设计(微前端、组件库、低代码);④ 跨端兼容(小程序/H5/App 多端统一);⑤ 工程化(构建优化、CI/CD、监控体系);⑥ 疑难 bug(内存泄漏、偶现白屏、竞态条件)。

如何做前端性能优化?(结合项目)

**答案:**结合项目讲,先讲分析方法,再讲具体手段,最后讲效果。

分析流程:

  1. 用 Lighthouse / Chrome DevTools Performance 跑一遍,看各项指标分数。

  2. Network 面板看资源加载:哪些资源大?哪些请求慢?有没有重复请求?

  3. Performance 面板看运行时:有没有长任务?重排重绘多不多?

  4. 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。

如何排查线上问题?

答案:

排查流程:

  1. **确认问题:**问题是什么?影响范围?是否可复现?什么时候开始出现的?有没有发版?

  2. **信息收集:**查看错误监控(Sentry/自建监控)的错误堆栈、用户信息、设备信息、操作路径;查看日志(前端日志、服务端日志);查看性能监控数据。

  3. **复现问题:**尽量在测试环境复现;如果无法复现,根据用户操作路径模拟;用 Chrome DevTools 调试。

  4. **定位根因:**根据错误堆栈定位代码;二分法排查(回滚版本、注释代码);对比正常和异常情况的差异。

  5. **修复验证:**修复后在测试环境验证;灰度发布;监控错误率是否下降。

  6. **总结复盘:**为什么会出现?如何避免?有没有需要补充的监控或测试用例?

**常见线上问题:**白屏(JS 加载失败、JS 执行错误、资源 404)、页面卡顿(内存泄漏、长任务、死循环)、样式错乱(CSS 加载失败、兼容性问题)、数据异常(接口错误、缓存问题、竞态条件)、跨域问题(CORS 配置变更)。

**监控体系:**错误监控(JS 错误、Promise 错误、资源加载错误、接口错误)、性能监控(LCP/FCP/CLS/TTFB)、行为监控(用户操作路径、PV/UV)、白屏检测。

职业规划?为什么离职/跳槽?

答案:职业规划回答要点:

  • 短期(1-2年):深入前端技术,在某个方向(性能优化/工程化/架构)形成专长,成为团队技术骨干。

  • 中期(3-5年):拓展技术广度,了解后端/运维/产品,具备全栈或架构设计能力,能独立负责复杂项目。

  • 长期:成为技术专家或技术管理者,根据公司和个人发展决定方向。

  • 结合应聘公司的业务和技术栈,说明为什么这个岗位符合你的规划。

离职原因回答要点(避免负面):

  • 正面表述:追求更大的技术挑战、希望接触更大规模的业务、个人发展遇到瓶颈、想在某个技术方向深入发展。

  • 避免说:前公司不好、领导不行、加班太多、薪资低(可以说希望有更好的发展平台和回报,但不要抱怨)。

  • 如果是被裁员:客观说明业务调整/组织优化,不要隐瞒,重点讲自己的能力和贡献。


附录:高频面试题 Top 20

  1. 从输入 URL 到页面显示发生了什么? --- 见第四章第1题

  2. 浏览器渲染流程?重排重绘区别与优化? --- 见第四章第2题

  3. 事件循环(Event Loop)机制?宏任务微任务执行顺序? --- 见第二章第7题

  4. Promise 原理?手写一个 Promise? --- 见第二章第8题

  5. 闭包是什么?应用场景与内存泄漏? --- 见第二章第3题

  6. this 指向的几种情况?箭头函数的 this? --- 见第二章第4题

  7. 原型与原型链?继承的几种方式? --- 见第二章第5、6题

  8. Vue 响应式原理?Vue2 和 Vue3 区别? --- 见第三章第1题

  9. Vue Diff 算法?key 的作用? --- 见第三章第2题

  10. React Hooks 原理?useEffect 依赖数组? --- 见第三章第9题

  11. 虚拟 DOM 的优缺点? --- 见第三章第2题

  12. 跨域解决方案?CORS 原理? --- 见第四章第5题

  13. 浏览器缓存机制?强缓存与协商缓存? --- 见第四章第3题

  14. HTTP/1.1 vs HTTP/2 vs HTTP/3? --- 见第五章第3题

  15. HTTPS 握手过程?对称加密与非对称加密? --- 见第五章第4题

  16. XSS 和 CSRF 区别与防御? --- 见第四章第6题

  17. Webpack 构建流程?HMR 原理?Loader 和 Plugin 区别? --- 见第六章第1、2、3题

  18. Vite 为什么比 Webpack 快? --- 见第六章第4题

  19. 前端性能优化方案(加载+运行时)? --- 见第七章第1题

  20. 防抖和节流区别?手写实现? --- 见第二章第12题


------ 全文完,祝面试顺利 ------

相关推荐
软件技术NINI1 小时前
传统文化html+css 5页
前端·css·html
优秀稳妥的JiaJi1 小时前
从产品原型到前端开发的需求清单,从1小时到25分钟
前端·trae
manongdashu1881 小时前
应对“SEO需要支付一年服务费”的捆绑消费,静态建站配套终身低成本服务器,全程无需额外运维开销。的方法。
运维·服务器·前端·seo·静态网站
风骏时光牛马1 小时前
AI提示词有效性深度测试
前端
Su米苏1 小时前
针对使用@vue-office 出现的包异常找不到问题
前端·javascript·vue.js
坚定信念,勇往无前1 小时前
electron打包mac ,apple m4出错:ffprobe是x86,不是arm64
前端·javascript·electron
牛油果子哥q1 小时前
C++模板万字深度精讲:函数/类模板特化、全特化与偏特化、SFINAE机制、类型萃取、模板元编程入门、工程实战落地
开发语言·c++·面试
专业抄代码选手1 小时前
01|从手写 DOM 到虚拟 DOM:把 UI 从“操作”变成“描述”
前端·javascript·react.js
喜欢睡觉1 小时前
单词管理系统
前端·后端