前端100道问答
第一部分:HTML/CSS 与 JS 基础 (1-15)
1. DOMContentLoaded 与 load 事件的区别
- 通俗解释:
DOMContentLoaded就像是”房子骨架搭好了(HTML 解析完毕,DOM 树构建完成),延迟脚本也执行完了,但大件家具(图片、视频、iframe)还在运输路上”。此时 JS 可以安全操作 DOM 了。load是”房子盖好,家具搬完,连窗帘都挂好了”。页面上所有外部资源(图片、样式表、视频、iframe)都下载并渲染完成了。
- 注意细节:CSS 本身不阻塞 DOM 解析,但如果
<link>后面跟着<script>,浏览器会等 CSS 加载完再执行脚本,从而间接延迟DOMContentLoaded。 - 应用场景:大多数 JS 初始化逻辑应该放在
DOMContentLoaded里(或使用defer脚本),因为等所有图片加载完再运行 JS,体验会很卡。
2. HTML5 语义化标签
- 通俗解释:用对的标签做对的事。不要满屏都是
<div>。 - 常用语义标签:
- 页面结构:
<header>(页头)、<footer>(页脚)、<nav>(导航)、<main>(主内容,页面唯一)、<aside>(侧边栏)。 - 内容分区:
<article>(独立完整的内容,如博客文章)、<section>(主题性分组,如章节)。 - 文本语义:
<figure>+<figcaption>(图片/图表及其说明)、<time>(日期时间)、<mark>(高亮文本)、<details>+<summary>(可折叠内容)。 - 表单增强:
<input type="email/date/range/color">、<datalist>(输入建议列表)、<output>(计算结果)。
- 页面结构:
- 为什么重要:
- 给机器看(SEO):搜索引擎更容易读懂你的网页结构,排名更高。
- 给特殊人群看(无障碍):屏幕阅读器能直接跳到
<nav>导航或<main>内容区,体验更好。 - 给开发者看(可维护性):看到
<article>就知道是独立内容,比看到第 N 个<div class="box">清晰得多。
3. BFC (块级格式化上下文)
- 通俗解释:BFC 就像是一个完全隔离的独立房间。房间里的家具(元素)怎么乱摆,都不会影响到房间外面;外面的洪水也流不进房间里。
- BFC 内部布局规则:
- 内部的块元素会在垂直方向上一个接一个放置。
- 同一个 BFC 内相邻块元素的垂直 margin 会发生折叠(取较大值)。
- BFC 区域不会与浮动元素重叠。
- BFC 在计算高度时,会把内部浮动子元素的高度也算进去。
- 如何触发(建这个房间):
display: flow-root(最推荐,语义明确,专为创建 BFC 而生,无副作用)。overflow: hidden | auto | scroll(常用,但可能裁剪溢出内容或出现多余滚动条)。float: left | right(元素本身浮动时自动成为 BFC)。display: flex | inline-flex | grid | inline-grid(Flex/Grid 容器)。display: inline-block | table-cell | table-caption。position: absolute | fixed(脱离文档流)。- 根元素
<html>本身就是一个 BFC。
- 解决了什么问题:
- 清除浮动(高度塌陷):子元素浮动了(float),父元素高度塌陷为 0。把父元素变成 BFC(如
display: flow-root),它就能包裹住浮动的子元素,撑起高度。 - 防止外边距折叠:同一个 BFC 内相邻块元素的上下 margin 会合并为一个。要阻止合并,把其中一个元素包裹在新的 BFC 容器里,两个不同 BFC 之间的 margin 不会折叠。
- 阻止元素被浮动覆盖:一个浮动元素会覆盖/重叠在相邻的普通块元素上。把那个普通块元素变成 BFC,它就不会与浮动元素重叠,而是紧贴浮动元素排列 — 这是实现两栏自适应布局的经典技巧。
- 清除浮动(高度塌陷):子元素浮动了(float),父元素高度塌陷为 0。把父元素变成 BFC(如
4. 盒模型 (Box Model)
- 通俗解释:网页里的每个元素都是一个盒子。盒子的大小怎么算?有两种流派:
- W3C 标准模型 (
content-box):你设置width: 100px,这仅仅是内容的宽度。如果你再加 10px 的 padding,盒子实际总宽度变成了 120px。坑点:布局容易撑爆。 - IE/怪异模型 (
border-box):你设置width: 100px,这 100px 包含了 内容 + padding + border。你加 padding,内容区会自动缩小,总宽度死死卡在 100px。
- W3C 标准模型 (
- 最佳实践:所有项目都在 CSS 开头写上
* { box-sizing: border-box; },这才是符合人类直觉的。
5. Flexbox 布局
- 核心概念:一维布局(主要是一行或一列)。想象成一根绳子串珠子。
- 容器属性(父元素):
flex-direction:主轴方向。row(默认,水平)|column(垂直)|row-reverse|column-reverse。justify-content:主轴对齐。flex-start|center|flex-end|space-between(两端对齐,中间等分)|space-around(每项两侧等距)|space-evenly(完全等距)。align-items:交叉轴对齐。stretch(默认,拉伸填满)|flex-start|center|flex-end|baseline(按文字基线对齐)。flex-wrap:是否换行。nowrap(默认,挤在一行)|wrap(换行)。align-content:多行时的交叉轴对齐(只在wrap时生效)。gap:项目之间的间距,替代以前用 margin 模拟间距的做法。
- 项目属性(子元素):
flex: 1是flex-grow: 1; flex-shrink: 1; flex-basis: 0%的简写。意思是"剩余空间我来分"。flex-grow:有多余空间时,按比例分配。默认 0(不放大)。flex-shrink:空间不够时,按比例缩小。默认 1(等比缩小)。设为 0 表示不缩小(常用于固定宽度侧边栏)。flex-basis:分配多余空间前,项目占据的主轴空间。可以理解为"初始宽度"。align-self:单独覆盖某个子元素的align-items。order:改变排列顺序,默认 0,值越小越靠前。
- 经典布局:
- 圣杯布局:左右固定宽度 + 中间自适应 → 左右
flex: 0 0 200px,中间flex: 1。 - 底部固定:
flex-direction: column; min-height: 100vh,内容区flex: 1,footer 自然贴底。
- 圣杯布局:左右固定宽度 + 中间自适应 → 左右
6. Grid 布局
- 核心概念:二维布局(有行也有列),像 Excel 表格或棋盘。
- 对比 Flex:Flex 适合局部组件(如导航栏、卡片内部),Grid 适合整个页面的宏观排版(如左侧菜单+右侧内容+底部)。两者可以嵌套使用。
- 容器属性:
grid-template-columns:定义列。repeat(3, 1fr)三等分;200px 1fr 200px左右固定中间自适应。grid-template-rows:定义行。auto 1fr auto头尾自适应中间撑满。gap(或row-gap/column-gap):行列间距。grid-template-areas:用字符画出布局结构,非常直观:cssgrid-template-areas: "header header header" "sidebar main aside" "footer footer footer";justify-items/align-items:单元格内内容的对齐方式。justify-content/align-content:整个网格在容器中的对齐方式。
- 项目属性:
grid-column: 1 / 3:从第 1 条列线跨到第 3 条(占两列)。简写span 2。grid-row: 1 / -1:从第一行跨到最后一行。grid-area: header:配合grid-template-areas使用,声明自己属于哪个区域。
- 响应式利器:
repeat(auto-fill, minmax(250px, 1fr)):自动填充列,每列最小 250px,多余空间等分。不需要媒体查询就能实现响应式网格。auto-fitvsauto-fill:auto-fit会把空列折叠掉让现有列撑满,auto-fill保留空列。
7. CSS 选择器优先级 (权重)
- 通俗解释:当多条规则通过不同方式选中同一个元素时,谁说了算?看谁的权重大。
- 计算公式 (A, B, C, D):
- A:
style行内样式 (1,0,0,0) - B: ID 选择器 (0,1,0,0)
- C: 类 (.class)、伪类 (:hover)、属性 ([type="text"]) (0,0,1,0)
- D: 标签 (div)、伪元素 (::before) (0,0,0,1)
!important:核武器,无视规则,直接生效(尽量少用)。
- A:
- 注意事项:
- 权重不进位:11 个 class 的权重 (0,0,11,0) 仍然低于 1 个 ID (0,1,0,0)。
- 相同权重时,后写的覆盖先写的(层叠规则)。
:not()本身不算权重,但括号里的选择器算。:is()和:where()中,:where()的权重永远为 0(用于写可被轻松覆盖的默认样式)。- 通配符
*、组合符 (>,+,~)、:where()权重为 0。
8. 水平垂直居中方案
- 方案一 (现代版):父元素
display: flex; justify-content: center; align-items: center;(最推荐)。 - 方案二 (Grid版):父元素
display: grid; place-items: center;(代码最少)。 - 方案三 (绝对定位版):子元素
absolute,top: 50%; left: 50%; transform: translate(-50%, -50%);(不用知道子元素宽高,兼容性好)。
9. 移动端 1px 边框问题
- 问题来源:现在的手机是高清屏(Retina),你写
1px,在 2倍屏上实际上渲染了 2个物理像素,看起来很粗。 - 解决方案:
- 不要直接写
border: 1px。 - 用伪元素
::after画一个边框。 - 将这个伪元素放大到 200% 或 300%,然后用
transform: scale(0.5)把它缩放回来。这样虽然逻辑占了 1px,但视觉上只有 0.5px 的粗细,在高清屏上刚刚好。
- 不要直接写
10. CSS 预处理器 (Sass/Less)
- 解决了什么:原生 CSS 不能嵌套写,不能定义变量(虽然现在原生有了),很难复用。
- 核心功能:
- 嵌套:
nav { a { color: red } },结构清晰。 - 变量:Sass 用
$theme-color: blue,Less 用@theme-color: blue,改一个地方全站换肤。 - Mixins:像函数一样封装一段 CSS,比如一段清除浮动的代码,哪里需要哪里调。
- 继承 (Extend):
@extend .btn让一个选择器继承另一个的所有样式。 - 函数与运算:
darken($color, 10%)、$width / 2等。
- 嵌套:
- Sass vs Less:Sass(SCSS 语法)功能更强大,社区更大,是目前主流。Less 语法更简单,但生态逐渐萎缩。
- 现状:随着 CSS 原生支持嵌套、变量 (
--var)、@layer等特性,预处理器的必要性在降低,但 Mixins、函数等高级功能仍有价值。
11. PostCSS
- 通俗解释:CSS 界的 Babel。它不是一种语言,而是一个工具平台,通过插件来转换 CSS。
- 常用插件:
Autoprefixer:你只管写display: flex,它自动帮你加上浏览器前缀,让你不用操心兼容性。postcss-preset-env:让你使用未来的 CSS 语法(如嵌套),自动降级为当前浏览器支持的语法。
- 生态地位:Tailwind CSS 本身就是一个 PostCSS 插件,说明 PostCSS 是现代 CSS 工具链的基础设施。
12. 响应式设计与媒体查询
- 原理:
@media screen and (max-width: 768px) { ... }。意思是:如果屏幕宽度小于 768px(手机),就执行这里面的 CSS。 - rem 与 em:
em:相对于父元素字体大小。容易套娃,计算混乱。rem:相对于根元素 (html) 字体大小。现在移动端适配的主流方案(配合 JS 动态设置 html 的 font-size)。- vw/vh:相对于视口宽高的百分比。100vw = 屏幕宽度。
13. 原型与原型链 (Prototype Chain)
- 核心概念:JS 里没有传统的”类”(class 只是语法糖),对象通过原型链实现继承。每个对象都有一个内部属性
[[Prototype]](可通过__proto__或Object.getPrototypeOf()访问)。 - 链条:当你访问对象
obj.name,JS 引擎先找obj自己有没有;没有就去obj.__proto__(它的原型)找;还没有就找原型的原型...直到找到Object.prototype,再往上就是null(尽头)。 - 关键关系:
构造函数.prototype=== 实例的__proto__。构造函数.prototype.constructor=== 构造函数本身。- 所有函数的
__proto__===Function.prototype。 Function.prototype.__proto__===Object.prototype。Object.prototype.__proto__===null(链的终点)。
- 面试常考 — 类型判断:
typeof:适合基础类型(string, number, boolean, undefined, symbol, bigint),但typeof null === 'object'(历史 bug),typeof []也是 'object'。instanceof:顺着原型链找,看能不能找到构造函数的 prototype。[] instanceof Array === true。Object.prototype.toString.call():最准确。Object.prototype.toString.call([])返回'[object Array]'。
14. 作用域与闭包 (Closure)
- 作用域:变量活着的地盘。
- 全局作用域:整个脚本都能访问。
- 函数作用域:函数内部声明的变量,外部访问不到。
- 块级作用域(ES6):
let/const在{}内有效。var没有块级作用域(这是经典的 for 循环 var 陷阱的根源)。 - 作用域链:函数嵌套时,内层函数可以访问外层函数的变量,一层层往外找,直到全局。这条链在函数定义时就确定了(词法作用域/静态作用域)。
- 闭包:
- 定义:函数能够记住并访问它的词法作用域,即使函数在其词法作用域之外执行。
- 人话:函数执行完了,本该销毁内部变量,但因为这些变量被返回出去的小函数”揪住”了,所以这些变量被迫留在了内存里,形成了一个专属的”小背包”。
- 用途:
- 私有变量:模拟面向对象的私有属性,外部无法直接访问。
- 函数工厂:
makeAdder(5)返回一个专门加 5 的函数。 - 防抖节流:timer 变量通过闭包保持在内存中。
- 模块模式:IIFE + 闭包实现模块化(ES Module 之前的主流方案)。
- 缺点:滥用会导致内存泄漏(变量一直不回收)。不再需要时应手动解除引用(赋值为 null)。
15. this 指向机制
- 一句话口诀:谁调用它,this 就指向谁(箭头函数除外)。
- 分情况:
obj.fn():this 指向obj。fn()(普通调用):this 指向全局(window/global),严格模式下是undefined。new Fn():this 指向新创建出来的实例对象。- 箭头函数:它没有自己的 this,它的 this 是写代码时定义位置的外层上下文(出生时就定死了)。
call/apply/bind:强行改变 this 指向。
第二部分:ES6+ 新特性与异步编程 (16-30)
16. var, let, const 区别
- var:老古董。
- 函数作用域:在
if或for里定义的 var,外面也能访问(坑)。 - 变量提升 (Hoisting):可以在定义之前使用(值为 undefined),非常反直觉。
- 可重复声明:写两次
var a不会报错。
- 函数作用域:在
- let:新时代的变量。
- 块级作用域:
{}也就是花括号里定义的,出了花括号就死。 - 暂时性死区 (TDZ):在定义之前坚决不能用,用了就报错。
- 块级作用域:
- const:常量。
- 规则同 let。
- 不可重新赋值:
const a = 1; a = 2;会报错。 - 注意:如果是对象
const obj = {a:1},你修改obj.a = 2是允许的,因为对象的内存地址没变,变的是房子里的东西。
17. 箭头函数 (Arrow Function)
- 写法:
const add = (a, b) => a + b;简洁优美。 - 关键区别:
- 没有自己的
this:它直接继承外层作用域的this。这解决了以前在回调函数里this乱跑的问题。 - 不能当构造函数:不能用
new调用。 - 没有
arguments对象:要拿所有参数,得用剩余参数...args。
- 没有自己的
18. 解构赋值 (Destructuring)
- 通俗解释:从数组或对象里“提取”数据的快捷方式。
- 数组解构:
const [a, b] = [1, 2];-> a=1, b=2。用来交换变量值特别方便[a, b] = [b, a]。 - 对象解构:
const { name, age } = user;。 - 重命名:
const { name: userName } = user;把提取出来的 name 改名叫 userName。
19. 扩展运算符 (...)
- 展开 (Spread):把数组或对象“炸开”。
const arr2 = [...arr1, 4, 5]:复制数组并添加新元素。const obj2 = { ...obj1, c: 3 }:浅拷贝对象并添加新属性。
- 剩余 (Rest):把剩下的东西“打包”。
function fn(first, ...others) {}:传入 (1, 2, 3),first 是 1,others 是数组[2, 3]。
20. Map 与 Set
- Set (集合):一堆不重复的值。
- 最强用途:数组去重。
[...new Set([1, 2, 2, 3])]瞬间变成[1, 2, 3]。 - 常用方法:
add()、delete()、has()、size。 - WeakSet:只能存对象引用,弱引用,不可遍历。用于标记对象(如"这个 DOM 节点已处理过")。
- 最强用途:数组去重。
- Map (字典):超级键值对。
- 对比 Object:Object 的 key 只能是字符串或 Symbol。Map 的 key 可以是任何东西(对象、函数、DOM节点)。
- 有序:Map 保持插入顺序,Object 的 key 顺序在某些情况下不可靠(数字 key 会排前面)。
- 性能:频繁增删键值对时,Map 比 Object 性能更好。
- 常用方法:
set()、get()、has()、delete()、size。 - WeakMap:弱引用的 Map,key 只能是对象。如果 key(对象)在外面被销毁了,WeakMap 里的这条数据会自动消失,不占内存。不可遍历。
- WeakMap 用途:存储 DOM 节点的私有数据、实现私有属性(Vue3 响应式系统用 WeakMap 存储对象与其响应式代理的映射)。
21. Promise 原理
- 通俗解释:承诺。就像你去买奶茶,店员给你一张小票(Promise)。这时候奶茶还没好(Pending)。
- Resolve (兑现):奶茶做好了,叫号,你拿到奶茶(Data)。状态变为 Fulfilled。
- Reject (拒绝):做奶茶的机器坏了,店员告诉你做不了(Error)。状态变为 Rejected。
- 不可逆:状态一旦从 Pending 变为 Fulfilled 或 Rejected,就不能再变了。
- 链式调用:
.then()返回一个新的 Promise,所以可以链式调用。这解决了以前”回调地狱”(Callback Hell)一层套一层的问题。 - 静态方法:
- Promise.all([ ]):所有都成功才成功,一个失败就全失败。适合并发请求且全部需要。
- Promise.allSettled([ ]):等所有都结束(不管成功失败),返回每个的状态和结果。适合”尽力而为”的场景。
- Promise.race([ ]):谁先完成(不管成功失败)就返回谁。适合超时控制。
- Promise.any([ ]):谁先成功就返回谁,全失败才失败。适合多源竞速取最快成功的。
22. Async / Await
- 本质:Promise 的语法糖。让异步代码看起来像同步代码。底层是 Generator + 自动执行器。
- 写法:javascript
async function getData() { try { const user = await fetchUser(); // 等待fetchUser完成 const posts = await fetchPosts(user.id); // 拿到id再去查帖子 } catch (err) { // 错误处理,替代 .catch() console.error(err); } } - 并发优化:多个不相互依赖的请求不要串行 await,应该并发执行:javascript
// ❌ 串行:总耗时 = A + B const a = await fetchA(); const b = await fetchB(); // ✅ 并发:总耗时 = max(A, B) const [a, b] = await Promise.all([fetchA(), fetchB()]); - 注意:
async函数永远返回 Promise。即使你return 1,外面拿到的也是Promise<1>。 - 顶层 await:ES2022 起,在 ES Module 的顶层可以直接写
await,不需要包在async函数里。
23. Event Loop (事件循环)
- 核心机制:JS 是单线程的(只有一个厨师)。那怎么处理耗时任务(炖汤)?
- 同步代码 (切菜):直接在主线程(执行栈)做。
- 异步代码 (炖汤):丢给浏览器其他线程去看着(计时器、网络请求),主线程继续切别的菜。
- 回调 (汤好了):炖汤线程把回调函数放到任务队列里排队。
- 循环:主线程切完所有菜(执行栈空了),就会去检查任务队列,把排队的任务拿过来执行。
- 宏任务与微任务:
- 微任务 (Microtask):VIP 通道。Promise 的
.then,process.nextTick。 - 宏任务 (Macrotask):普通通道。
setTimeout,setInterval。 - 顺序:主线程做完 -> 清空所有微任务 -> 渲染 UI -> 取出一个宏任务 -> ...
- 微任务 (Microtask):VIP 通道。Promise 的
24. 垃圾回收机制 (GC)
- 标记清除 (Mark and Sweep):目前主流算法。
- 垃圾回收器定期从”根”(全局对象、当前执行栈中的变量)出发,顺着引用链往下找。
- 能找到的:打上”活着”的标记。
- 找不到的(孤立的对象):视为垃圾,回收内存。
- 引用计数:老算法。
- 对象每被引用一次 +1,引用断开 -1。变成 0 就回收。
- 致命缺陷:循环引用。A 引用 B,B 引用 A,虽然没人用它们,但计数器永远是 1,内存泄漏。
- V8 引擎的优化策略:
- 分代回收:把堆内存分为新生代(存活时间短的对象)和老生代(存活时间长的对象)。
- 新生代:使用 Scavenge 算法(复制算法)。空间分为 From 和 To 两半,存活对象从 From 复制到 To,然后交换。速度快但空间利用率 50%。
- 老生代:使用标记清除 + 标记整理。标记整理会把存活对象移到一端,解决内存碎片问题。
- 增量标记:把标记过程拆成小步,穿插在 JS 执行中,避免长时间暂停(Stop-The-World)。
25. 模块化标准
- CommonJS (CJS):Node.js 用的。
require('fs'),module.exports = ...。- 特点:同步加载(适合服务器,因为文件在硬盘上读得快)。运行时加载(
require可以写在 if 里动态决定加载什么)。 - 值拷贝:导出的是值的拷贝,模块内部变化不影响外部已导入的值。
- ES Module (ESM):浏览器和现代前端标准。
import,export。- 特点:静态分析(编译时就能确定依赖关系,这是 Tree Shaking 也就是”摇树优化”去除无用代码的基础)。
- 值引用(动态绑定):导出的是值的引用,模块内部变化会反映到外部。
- 异步加载:
import()动态导入,返回 Promise,是代码分割(Code Splitting)的基础。
- 对比总结:
特性 CJS ESM 加载时机 运行时 编译时(静态) 导出方式 值拷贝 值引用 能否 Tree Shaking 不能 能 浏览器支持 不支持 原生支持
26. Proxy 与 Reflect
- Proxy (代理):像一个拦截器或保镖。
- 你想访问对象
obj.a,代理拦截下来:”等等,我要先记录日志”,或者”如果 a 不存在,我给你返回默认值”。 - 可拦截的操作(traps):
get(读取)、set(赋值)、has(in 操作符)、deleteProperty(delete)、apply(函数调用)、construct(new)等共 13 种。 - Vue3 的核心:Vue3 用 Proxy 替代了 Vue2 的
Object.defineProperty来实现响应式,因为 Proxy 可以直接监听数组变化和对象新增/删除属性,且是惰性的(只在访问时才递归代理嵌套对象)。
- 你想访问对象
- Reflect:与 Proxy 的 traps 一一对应的静态方法集。
- 为什么需要:在 Proxy 的 handler 里,用
Reflect.get(target, key, receiver)代替target[key],能正确处理this指向和继承场景。 - 规范化:把以前散落在
Object、Function、delete操作符上的反射操作统一到Reflect对象上。
- 为什么需要:在 Proxy 的 handler 里,用
27. 迭代器 (Iterator) 与 生成器 (Generator)
- Iterator:一种协议/接口,只要对象实现了
Symbol.iterator方法(返回一个含next()方法的对象),就能被for...of循环遍历。- 内置可迭代对象:Array、String、Map、Set、arguments、NodeList。普通对象
{}默认不可迭代。 next()返回{ value, done }。done 为 true 时迭代结束。
- 内置可迭代对象:Array、String、Map、Set、arguments、NodeList。普通对象
- Generator:带
*的函数,是创建迭代器的便捷方式。function* gen() { yield 1; yield 2; yield 3; }。- 特点:函数可以暂停(yield)和恢复(next)。每次调用
next()执行到下一个yield处暂停。 - 双向通信:
next(value)可以向 Generator 内部传值,作为上一个yield表达式的返回值。 - 与 async/await 的关系:
async/await的底层实现原理就是 Generator + 自动执行器(如 co 库)。await相当于yield,只是自动帮你调用next()。
- 实际应用:Redux-Saga 用 Generator 管理副作用;实现惰性求值(按需生成无限序列)。
28. 正则表达式
- 通俗解释:处理字符串的神器。
- 常用元字符:
^开头,$结尾。\d数字,\D非数字。\w字母数字下划线,\W反之。\s空白字符,\S非空白。.匹配除换行符外的任意字符。+至少一个,?0个或1个,*任意个。{n}恰好 n 个,{n,m}n 到 m 个。
- 贪婪与非贪婪:默认是贪婪的(尽可能多匹配)。加个
?变成非贪婪(尽可能少匹配)。如.*?。 - 分组与捕获:
(abc)捕获组,可以用$1反向引用。(?:abc)非捕获组,只分组不捕获。- 命名捕获:
(?<year>\d{4}),用match.groups.year获取。
- 断言(零宽):
(?=a)正向先行断言:后面跟着 a。(?!a)负向先行断言:后面不跟 a。(?<=a)正向后行断言:前面是 a。(?<!a)负向后行断言:前面不是 a。
- 常用标志:
g(全局匹配)、i(忽略大小写)、m(多行模式)、s(dotAll,.匹配换行符)。 - 实战:手机号
/^1[3-9]\d{9}$/、邮箱/^\w+@\w+\.\w+$/(简化版)、千分位'12345678'.replace(/\B(?=(\d{3})+$)/g, ',')。
29. 常用设计模式
- 单例模式:保证全局只有一个实例(比如 Vuex/Pinia 的 Store,全局唯一的弹窗管理器)。
- 发布订阅模式:
on(订阅),emit(发布)。Vue 的事件总线 (EventBus)、Node.js 的 EventEmitter 就是这个。与观察者模式的区别:有一个中间调度中心,发布者和订阅者互不认识。 - 观察者模式:Vue 的响应式原理(Dep 和 Watcher)。数据变了,直接通知所有盯着它的观察者。与发布订阅的区别:没有中间人,目标和观察者直接耦合。
- 工厂模式:根据条件创建不同的对象,隐藏创建逻辑。如
React.createElement根据 type 创建不同元素。 - 策略模式:定义一系列算法,把它们封装起来,可以互相替换。如表单校验:不同字段用不同的校验策略(邮箱策略、手机号策略),避免大量 if-else。
- 装饰器模式:不修改原有对象,动态给它添加新功能。如 HOC(高阶组件)、TypeScript/ES 装饰器
@log。 - 代理模式:控制对对象的访问。如图片懒加载(先用占位图代理)、ES6 Proxy、缓存代理。
30. 函数式编程 (FP)
- 纯函数:输入相同,输出永远相同;没有副作用(不修改外部变量,不发网络请求)。这样的函数非常容易测试和缓存(memoize)。
- 不可变性 (Immutability):数据一旦创建就不修改,要改就创建新的副本。React 的 state 更新就要求不可变(
setState必须传新对象)。工具库:Immer(用produce以可变写法生成不可变数据)。 - 柯里化 (Currying):把
add(1, 2)变成add(1)(2)。用于参数复用和函数组合。 - 函数组合 (Compose / Pipe):把多个小函数串成一个大函数。
compose(f, g, h)(x)=f(g(h(x)))。pipe是从左到右的版本。 - 高阶函数:参数是函数,或者返回一个函数(如
map,filter,reduce)。 - 实际应用:React 的函数组件 + Hooks 本质上就是函数式编程思想;Redux 的 Reducer 是纯函数;RxJS 是函数式响应式编程。
第三部分:框架原理与源码分析 (31-45)
31. Virtual DOM (虚拟 DOM)
- 通俗解释:真实的 DOM 操作(操作网页上的元素)非常慢,像是在搬砖。虚拟 DOM 就是用轻量的 JS 对象(蓝图)来描述 DOM 结构。
- 工作流程:
- 状态改变。
- 生成新的虚拟 DOM 树(新蓝图)。
- Diff:对比新旧两张蓝图,找出不一样的地方(比如只是墙纸颜色变了)。
- Patch:只把变化的部分应用到真实 DOM 上(只刷墙,不拆房)。
- 误区:虚拟 DOM 不一定比直接操作 DOM 快。它的价值在于跨平台(蓝图可以生成网页,也可以生成 App 界面)和保证了下限(不管你怎么写,性能都不会太差)。
32. Vue2 vs Vue3 核心差异
- 响应式原理:
- Vue2:
Object.defineProperty。只能劫持对象的属性读取/写入。缺点是监听不到数组下标变化,也监听不到对象新增/删除属性(需要用$set)。 - Vue3:
Proxy。直接代理整个对象。上面提到的缺点全没了,性能也更好(惰性劫持,用到才追踪)。
- Vue2:
- API 风格:
- Vue2:Options API (data, methods, mounted 分开放)。代码多了逻辑会很分散,跳来跳去。
- Vue3:Composition API (
setup函数 /<script setup>语法糖)。把同一个功能的逻辑(比如”搜索功能”的状态、方法、生命周期)写在一起,便于抽取为 Composables 复用。
- 其他重大变化:
- Teleport:把组件渲染到 DOM 的任意位置(弹窗不再被父容器 overflow 裁剪)。
- Suspense:优雅处理异步组件的加载状态。
- 多根节点(Fragment):模板不再强制单根元素。
- 更好的 TypeScript 支持:源码用 TS 重写,类型推断更准确。
33. Vue 双向绑定原理
- 核心模式:数据劫持 + 发布订阅模式。
- 三个角色:
- Observer (观察者):给数据装上监控摄像头(defineProperty/Proxy)。一变动就通知 Dep。
- Dep (依赖收集器):一个管家。谁用了这个数据,就把它记在本子上(收集依赖)。
- Watcher (订阅者):页面上的组件。当数据变了,Dep 通知 Watcher,Watcher 去更新视图。
- v-model:
- Vue2:
value属性 +input事件的语法糖。 - Vue3:
modelValue属性 +update:modelValue事件的语法糖。且支持多个v-model绑定,如v-model:title="title"。
- Vue2:
34. Vue computed vs watch vs watchEffect
- Computed (计算属性):
- 带缓存:只要依赖的数据没变,多次访问直接返回缓存值,不重新计算。
- 同步:里面不能做异步操作(如发请求)。
- 场景:一个值由其他值得出(购物车总价 = 单价 * 数量)。
- Watch (监听器):
- 无缓存。
- 支持异步:数据变了,我可以等 1秒后再去搜索。
- 场景:数据变了要做一些”副作用”(发请求、打印日志、操作 DOM)。
- 配置项:
deep: true(深度监听对象内部变化)、immediate: true(创建时立即执行一次)、flush: 'post'(DOM 更新后执行)。
- watchEffect (Vue3 新增):
- 自动收集依赖:不需要指定监听谁,回调里用了什么响应式数据就自动监听什么。
- 立即执行:创建时就执行一次(相当于 watch 的
immediate: true)。 - 场景:依赖多个数据源的副作用,不想手动列出所有依赖。
- 对比 watch:watch 更精确(明确知道什么变了),watchEffect 更简洁(自动追踪)。
35. Vue 生命周期的理解
- Vue2 Options API:
- 创建阶段:
beforeCreate:刚起步,data 和 methods 还没好。created:数据好了,可以发请求拿初始数据,但 DOM 还没渲染。
- 挂载阶段:
beforeMount:模板编译完成,即将挂载到 DOM。mounted:DOM 渲染完了。可以操作 DOM 元素(比如初始化图表)。
- 更新阶段:
beforeUpdate->updated。 - 销毁阶段:
beforeDestroy->destroyed。在这里清除定时器、解绑事件,防止内存泄漏。
- 创建阶段:
- Vue3 Composition API 对应关系:
beforeCreate/created→setup()本身(setup 在这两个钩子之间执行)。beforeMount→onBeforeMount。mounted→onMounted。beforeUpdate→onBeforeUpdate。updated→onUpdated。beforeUnmount→onBeforeUnmount(替代 beforeDestroy)。unmounted→onUnmounted(替代 destroyed)。
- 父子组件执行顺序:
- 挂载:父 beforeMount → 子 beforeMount → 子 mounted → 父 mounted。
- 销毁:父 beforeUnmount → 子 beforeUnmount → 子 unmounted → 父 unmounted。
- 请求数据放哪:一般放
created/setup(不依赖 DOM)或mounted/onMounted(需要 DOM)。SSR 场景下只能放created,因为服务端没有 mounted。
36. Vue NextTick 原理
- 问题:你修改了数据
this.msg = 'Hello',然后立刻去获取 DOM 的内容,发现内容还是旧的。为什么? - 原因:Vue 更新 DOM 是异步的。它会把所有数据变更攒起来,放到一个队列里,等当前代码执行完了一次性更新(为了性能)。
- NextTick:就是把你的回调函数放到这个队列的最后面。等到 DOM 更新完了,自然就轮到你的回调执行了。底层用的是 Promise 或 MutationObserver。
37. Keep-Alive 原理
- 作用:缓存组件。比如从列表页点进详情页,再退回来,列表页不用重新刷新,滚动条位置也在。
- 原理:组件切换时,不销毁组件实例,而是把它存到内存的一个对象里(cache)。下次再切回来,直接从内存里拿出来渲染,跳过
created和mounted钩子,触发activated钩子。 - LRU 算法:内存有限,缓存满了怎么办?最近最少使用算法。把最久没用过的那个组件踢出去。
38. React Fiber 架构
- 背景:React 15 的更新是同步的,一旦开始 Diff,就必须一口气做完。如果组件树很大,这口气太长,主线程一直被占用,页面就会卡顿(掉帧)。
- Fiber:把更新任务拆分成一个个小任务单元(每个 Fiber 节点就是一个工作单元)。
- 时间切片 (Time Slicing):React 像个懂事的打工仔。做一会任务,看看浏览器主线程有没有急事(用户点击、动画)。如果有,就暂停手里的活,让给浏览器先处理;处理完回来继续做。实现了异步可中断渲染。
- React 18 并发特性:
- useTransition:标记某些状态更新为"非紧急",用户输入等紧急更新优先渲染,列表过滤等耗时更新不阻塞 UI。
- useDeferredValue:让某个值"延迟更新",类似防抖但由 React 调度。
- Suspense + Streaming SSR:服务端可以流式发送 HTML,边加载边渲染。
39. React Hooks 原理与限制
- 为什么有 Hooks:类组件太重,逻辑复用难(HOC/Render Props 嵌套地狱)。
- 核心原理:闭包 + 链表。
- React 内部维护了一个链表来存每个 Hook 的状态。
- 常用 Hooks:
useState/useReducer:状态管理。useEffect/useLayoutEffect:副作用(前者异步,后者同步在 DOM 更新后立即执行)。useMemo/useCallback:性能优化(缓存值 / 缓存函数引用)。useRef:持有可变引用,不触发重渲染。useTransition/useDeferredValue(React 18+):并发特性,标记非紧急更新。
- 两大铁律:
- 只能在函数组件顶层调用:不能写在
if,for,function内部。因为 React 是靠调用顺序来对应状态的。如果你套了个if,下次渲染少调了一个 Hook,后面的链表全错位了,数据就乱了。 - 只能在 React 函数中调用(组件或自定义 Hook)。
- 只能在函数组件顶层调用:不能写在
40. React 合成事件 (SyntheticEvent)
- 机制:你在 React 写的
onClick并没有直接绑定到那个按钮 DOM 上,而是通过事件代理统一管理。- React 16 及以前:事件委托到
document上。 - React 17+:事件委托到应用根节点 (
rootNode) 上。这样多个 React 应用共存时不会互相干扰。
- React 16 及以前:事件委托到
- 目的:
- 跨浏览器兼容:React 帮你抹平了不同浏览器事件对象的差异。
- 性能:减少内存消耗,不用给成千上万个元素分别绑定事件。
- 注意:React 合成事件中的
e.stopPropagation()能阻止 React 事件树中的冒泡,但不会阻止原生事件监听器(通过addEventListener直接绑定的)的触发。
41. Diff 算法详解
- 核心策略 (O(n) 复杂度):理论上对比两棵树的最优算法是 O(n³),但通过三个假设降到 O(n):
- 同层比较:只比同一层级,不跨层级比。跨层级移动节点视为删除+重建。
- Key 的作用:
- 如果没有 key:框架只能按顺序逐个对比,无法识别节点移动,效率低。
- 如果有 key:框架发现”key=A 的这个元素只是从第一位跑到了第三位”,那就直接移动它,而不是删除重建。大大提升性能。
- 不要用 index 做 key:列表有增删时 index 会变,导致错误复用,尤其是包含输入框等有状态的组件时会出 Bug。
- 类型不同直接换:如果是
<div>变成了<p>,那子节点都不看了,直接整个替换。
- Vue 和 React 的 Diff 差异:
- Vue2:双端对比(头头、尾尾、头尾、尾头四种比较),效率高于 React 15 的单向遍历。
- Vue3:在双端对比基础上,对中间乱序部分使用最长递增子序列 (LIS) 算法,最小化 DOM 移动操作。
- React:单向遍历(从左到右),通过 key 建立映射。React 的 Fiber 架构使 Diff 可中断。
42. 状态管理 (Redux / Vuex / Pinia / Zustand)
- 核心思想:单一数据源。把所有组件共享的状态抽离出来,放在一个全局的大仓库(Store)里。
- Redux:
- Flux 架构:单向数据流。
- 流程:View -> Dispatch(Action) -> Reducer(纯函数,计算新状态) -> Store -> View。
- 特点:模板代码多,但非常规范,易于调试(Time Travel)。现代项目推荐搭配 Redux Toolkit (RTK) 大幅简化写法。
- Vuex / Pinia:
- Vuex:Mutation (同步改状态), Action (异步)。Vue3 项目已不推荐。
- Pinia:Vue 官方推荐的状态管理库。去掉了 Mutation,只有 Action,TypeScript 支持好,更轻量,支持 SSR。
- Zustand(React 生态热门):
- 极简 API,无 Provider 包裹,无模板代码。
const useStore = create((set) => ({ count: 0, inc: () => set(s => ({ count: s.count + 1 })) }))- 适合中小型项目,学习成本极低。类似选择还有 Jotai(原子化状态)和 Valtio(代理模式)。
43. React Context API
- 作用:避免”属性钻取” (Prop Drilling)。爷爷传给爸爸,爸爸传给孙子...太累。Context 像是搭建了一个”传送门”,爷爷可以直接传数据给孙子,中间组件不用经手。
- 用法:
const ThemeContext = createContext(defaultValue)创建 Context。<ThemeContext.Provider value={theme}>在上层提供数据。const theme = useContext(ThemeContext)在下层消费数据。
- 场景:全局主题(黑夜模式)、多语言(i18n)、当前登录用户信息。
- 缺点与优化:
- Context 里的值一变,所有消费它的组件都会强制重新渲染。如果用来做高频更新的状态管理,性能会炸。
- 优化手段:拆分 Context(把频繁变化的和不变的分开)、用
useMemo包裹 value、配合React.memo减少不必要渲染。 - 不适合:复杂的全局状态管理(这时候该用 Redux/Zustand)。适合低频变化的全局配置。
44. SSR (服务端渲染) 原理
- SPA (单页应用) 痛点:首屏慢(要等 JS 下载执行完才显示内容)、SEO 差(爬虫只看到一个空 div)。
- SSR (Server-Side Rendering):
- 用户请求页面。
- Node.js 服务器运行 React/Vue 代码,生成完整的 HTML 字符串。
- 服务器把 HTML 发给浏览器。用户立刻看到内容(首屏快)。
- 注水 (Hydration):浏览器下载 JS,接管页面,把静态的 HTML 变成可交互的应用。
- SSG (Static Site Generation):构建时就生成好 HTML,部署为纯静态文件。适合内容不常变的页面(博客、文档站)。速度最快,但不适合动态内容。
- ISR (Incremental Static Regeneration):Next.js 的方案。静态页面 + 按需重新生成。兼顾 SSG 的速度和 SSR 的动态性。
- Streaming SSR:React 18 支持流式渲染,服务器边生成 HTML 边发送,配合 Suspense 实现渐进式加载,用户更早看到内容。
- 框架:Next.js (React), Nuxt.js (Vue), Astro (多框架)。
- 权衡:SSR 增加了服务器成本和部署复杂度。不是所有项目都需要 SSR,纯后台管理系统用 SPA 就够了。
45. 微前端 (Micro-Frontends)
- 解决了什么:巨石应用(Monolith)。几百个人维护一个项目,编译一次半小时,上线风险巨大。
- 核心思想:把大应用拆成一个个独立的小应用(子应用)。
- 技术栈无关:子应用可以用 Vue,可以用 React,甚至 jQuery。
- 独立部署:A 团队发版不影响 B 团队。
- 实现方案:
- iframe:最简单,隔离最完美,但体验差(弹窗遮罩问题、通信难、路由状态丢失)。
- qiankun (基于 single-spa):社区广泛使用。通过 JS 沙箱隔离全局变量,通过样式隔离解决 CSS 冲突。加载 HTML Entry。
- Module Federation (Webpack 5 / Rspack):模块级共享方案。不同应用可以在运行时共享模块,无需打包到一起,适合渐进式拆分。
- Wujie / micro-app:新一代方案。Wujie 基于 Web Components + iframe 沙箱,兼顾隔离性和体验;micro-app 基于 Web Components,接入成本低。
第四部分:工程化与构建工具 (46-60)
46. Webpack 核心流程
- 通俗解释:Webpack 就像一个打包工厂。
- 流程:
- 初始化:读取配置文件 (
webpack.config.js)。 - 编译 (Compile):从入口文件 (
entry) 开始,递归寻找所有依赖的模块。 - 转换:遇到不认识的文件(scss, vue, ts),调用对应的 Loader 翻译成 JS。
- 构建依赖图:搞清楚谁依赖谁。
- 输出 (Emit):根据依赖图,把模块组装成一个或多个 Chunk(代码块),写入文件系统 (
dist目录)。
- 初始化:读取配置文件 (
- 关键概念:
- Loader:翻译官。
css-loader,babel-loader。 - Plugin:插件。在打包的特定时机干坏事(压缩代码、拷贝文件、生成 HTML)。
- Loader:翻译官。
47. Loader 与 Plugin 的区别
- Loader (加载器):
- 作用:文件转换。Webpack 原生只懂 JS 和 JSON,Loader 让它能看懂 CSS、图片、TS。
- 运行时机:在读取文件内容时。
- 例子:
sass-loader把 Sass 变成 CSS。
- Plugin (插件):
- 作用:功能扩展。介入打包的整个生命周期,做更复杂的事。
- 运行时机:整个构建过程的任何钩子 (Hooks) 处。
- 例子:
HtmlWebpackPlugin生成 index.html;CleanWebpackPlugin打包前清空 dist 目录。
48. Vite 原理
- 核心优势:快。为什么快?
- 开发环境 (Dev):
- 不打包 (No-Bundle)。利用现代浏览器原生支持 ES Modules (
<script type="module">) 的特性。 - 当你请求一个页面,浏览器遇到
import,再向服务器去请求对应的文件。Vite 只需要按需编译这个文件返回给你。相比 Webpack 把整个项目打包好才能启动,Vite 是秒开。 - 依赖预构建:首次启动时用 esbuild(Go 编写,极快)将第三方依赖(如 lodash)预编译为 ESM 格式并缓存,后续不再重复处理。
- 不打包 (No-Bundle)。利用现代浏览器原生支持 ES Modules (
- 生产环境 (Build):
- 使用 Rollup 进行打包(Vite 6+ 正在引入基于 Rust 的 Rolldown 替代)。因为原生 ESM 在生产环境(尤其是有几千个模块时)网络请求太多,性能不行,所以还是得打包。
- 生态:已成为 Vue、React、Svelte 等主流框架的默认脚手架工具。
49. Babel 原理
- 作用:JS 编译器。把 ES6+ 的新语法(const, arrow function, optional chaining)转译成目标环境能运行的语法,也处理 JSX、TypeScript 等非标准语法。
- 三步走:
- 解析 (Parse):把代码变成 AST (抽象语法树)。就像把句子拆解成”主谓宾”结构。
- 转换 (Transform):遍历 AST,增删改查。比如看到”箭头函数”节点,把它改成”普通函数”节点。这是 Babel 插件工作的地方。
- 生成 (Generate):把改好的 AST 重新变回代码字符串。
- 现代替代:esbuild(Go 编写)和 SWC(Rust 编写)在编译速度上比 Babel 快 10-100 倍,越来越多项目(如 Next.js、Vite)已切换到 SWC。
50. Tree Shaking (摇树优化)
- 通俗解释:拿起这棵树摇一摇,把枯萎的叶子(没用到的代码)摇下来,不打包进最终文件。
- 前提:必须使用 ES Module (
import/export)。因为 ESM 是静态分析的,打包工具在编译时就能确切知道哪个函数没被引用。CommonJS (require) 是动态的,摇不动。 - Side Effects (副作用):如果一个文件里只有
console.log('hello'),虽然没被 import,但它有副作用,不能摇掉。可以在package.json里配置"sideEffects": false告诉工具放心摇。
51. HMR (热更新) 原理
- 体验:你改了 CSS 颜色,浏览器不用刷新页面,按钮颜色直接变了,而且输入框里填的内容还在。
- 流程:
- Webpack 监听到文件变化,重新编译该模块。
- 服务端(WDS)通过 WebSocket 推送更新消息(hash值)给浏览器。
- 浏览器运行时代码收到消息,请求新的模块代码。
- 替换:尝试用新模块替换旧模块。如果失败(比如没写 accept 逻辑),则回退到整页刷新。
52. 浏览器缓存策略
- 强缓存(浏览器自作主张):
- Expires:绝对时间(如 2030 年到期)。受本地时间影响,已淘汰。
- Cache-Control:
max-age=3600(一小时内别烦我,直接读内存/硬盘缓存)。优先级最高。
- 协商缓存(浏览器问服务器):
- 强缓存失效了,浏览器带上标识问服务器:“这图过期没?”
- Last-Modified / If-Modified-Since:按修改时间。精度只到秒。
- Etag / If-None-Match:按文件指纹(Hash)。只要内容没变,Etag 就不变。最准确。
- 结果:如果没变,服务器回 304 Not Modified(没变,继续用旧的吧),省带宽。
53. DNS 解析过程
- 通俗解释:把域名
www.google.com翻译成 IP 地址142.250.1.1。 - 查找顺序:
- 浏览器缓存(之前访问过吗?)。
- 系统 hosts 文件。
- 路由器缓存。
- ISP DNS 服务器(电信/移动的服务器)。
- 根域名服务器 -> 顶级域名服务器 (.com) -> 权威域名服务器 (google.com)(递归/迭代查询)。
54. CDN (内容分发网络)
- 通俗解释:京东的快递仓库。如果仓库只在北京,海南用户买东西要等很久。CDN 就是在全国各地建仓库(节点)。
- 原理:用户访问图片时,DNS 会根据用户的 IP,把请求解析到离用户最近的那个 CDN 节点 IP。
- 好处:速度快、减轻源站压力。
55. TCP 三次握手与四次挥手
- 三次握手 (建立连接):
- Client: “我想连你,听到吗?” (SYN)
- Server: “听到了,我也想连你,你听到吗?” (SYN + ACK)
- Client: “听到了,连接建立。” (ACK)
- 为什么三次? 为了防止已失效的连接请求突然又传到了服务端,造成资源浪费。
- 四次挥手 (断开连接):
- Client: “我说完了,要挂了。” (FIN)
- Server: “知道了,等我把剩下的话说完。” (ACK)
- Server: “我说完了,挂吧。” (FIN)
- Client: “好,挂了。” (ACK) -> 等待 2MSL 时间后真正关闭。
56. HTTP/1.1 vs HTTP/2 vs HTTP/3
- HTTP/1.1:
- 队头阻塞:一个连接一次只能处理一个请求。前面的图没传完,后面的 CSS 就得等。
- 文本传输。
- HTTP/2:
- 多路复用:一个 TCP 连接可以并发处理无数个请求。解决了队头阻塞(应用层)。
- 二进制分帧:传输更高效。
- 头部压缩 (HPACK):减少重复 Header 传输。
- 服务端推送:允许服务器主动推送资源。但实践中收效有限,Chrome 已于 2022 年移除对此特性的支持,
103 Early Hints成为替代方案。
- HTTP/3:
- 基于 UDP (QUIC) 协议。
- 解决了 TCP 丢包导致的队头阻塞问题。连接建立更快(0-RTT)。
57. HTTPS 握手流程
- HTTPS = HTTP + SSL/TLS (安全层)。
- 流程:
- Client Hello:支持的加密算法。
- Server Hello:选定算法,发来数字证书(含公钥)。
- 验证证书:Client 找操作系统确认证书是不是真的(防中间人攻击)。
- 生成密钥:Client 生成一个随机数(对称密钥),用公钥加密发给 Server。
- 握手完成:Server 用私钥解密拿到随机数。之后双方就用这个随机数进行对称加密通信(速度快)。
58. CSRF 攻击与防御
- CSRF (跨站请求伪造):借刀杀人。
- 场景:你登录了银行网站 A(Cookie 存着)。然后你手贱点开了钓鱼网站 B。B 网站里有一个隐藏的图片
<img src="http://bank.com/transfer?to=hacker&money=1000">。 - 后果:浏览器会自动带上银行 A 的 Cookie 发送请求,银行以为是你本人操作的,钱没了。
- 场景:你登录了银行网站 A(Cookie 存着)。然后你手贱点开了钓鱼网站 B。B 网站里有一个隐藏的图片
- 防御:
- SameSite Cookie:设置 Cookie 只能在同站发送。
- CSRF Token:提交表单时必须带一个随机 Token,钓鱼网站拿不到这个 Token。
- 验证 Referer:检查请求是从哪来的。
59. XSS 攻击与防御
- XSS (跨站脚本攻击):代码注入。
- 三种类型:
- 存储型 (Stored):恶意脚本存入数据库(如评论区),所有访问该页面的用户都会执行。危害最大。
- 反射型 (Reflected):恶意脚本在 URL 参数中,服务器将其"反射"回页面。需要诱导用户点击特定链接。
- DOM 型:纯前端漏洞。JS 直接把不可信数据插入 DOM(如
innerHTML = userInput),不经过服务器。
- 防御:
- 转义输出:把
<变成<,>变成>。永远不要相信用户的输入。对不同上下文(HTML、JS、URL、CSS)使用对应的转义规则。 - CSP (内容安全策略):告诉浏览器只准加载哪里的脚本,禁止内联脚本。
Content-Security-Policy: script-src 'self'。 - HttpOnly Cookie:禁止 JS 读取 Cookie,即使 XSS 成功也偷不走 Cookie。
- 避免
innerHTML:用textContent代替,或使用框架的自动转义(React 的 JSX、Vue 的模板默认转义)。 - 输入校验:白名单校验,只允许预期格式的输入。
- 转义输出:把
60. 前端性能优化指标 (Core Web Vitals)
- Google 定义的网页体检报告(2024 年 3 月起更新):
- LCP (最大内容渲染时间):网页里最大的那张图或那段字多久显示出来?(衡量加载速度,越快越好,<2.5s)。
- INP (Interaction to Next Paint):用户的所有交互(点击、键入、触控)中,从操作到页面视觉响应的最慢延迟。(衡量整体交互响应性,越短越好,<200ms)。已于 2024 年 3 月正式替代 FID。
- CLS (累积布局偏移):看网页的时候,图片有没有突然加载出来把文字顶下去了?(衡量视觉稳定性,越稳越好,<0.1)。
第五部分:浏览器原理、性能优化与 TypeScript (61-75)
61. 浏览器渲染原理 (关键路径)
- 通俗解释:浏览器拿到 HTML 代码后,怎么把它画到屏幕上?
- 五步走:
- 解析 HTML:生成 DOM 树(骨架)。
- 解析 CSS:生成 CSSOM 树(皮肤样式)。
- 合成 (Render Tree):把 DOM 和 CSSOM 结合。注意:
display: none的节点不会出现在渲染树中,但visibility: hidden会。 - 回流/重排 (Layout/Reflow):计算每个节点在屏幕上的确切位置和大小(算坐标)。
- 重绘 (Repaint):填充像素(画颜色、阴影)。
- 合成 (Composite):如果有多个图层(比如用了
transform或z-index),GPU 会把它们像 PS 图层一样合并成一张图。
62. 重排 (Reflow) 与 重绘 (Repaint)
- 区别:
- 重排:布局变了(改宽、高、位置、字体大小、删元素)。非常消耗性能,因为牵一发而动全身,周围的元素都要重新算位置。
- 重绘:样子变了但位置没变(改颜色、背景)。性能消耗较小。
- 结论:重排一定导致重绘,重绘不一定导致重排。
- 优化:
- 不要一条条改样式,用
class一次性改。 - 操作 DOM 时,先把元素
display: none(这时操作不触发重排),改完再放回去,只触发两次重排。 - 使用
transform代替top/left做动画(transform会开启 GPU 加速,跳过重排和重绘,直接合成)。
- 不要一条条改样式,用
63. 本地存储方案对比
- Cookie:
- 老旧。只有 4KB。每次请求都会自动带给服务器(浪费带宽)。主要用于存 Session ID。
- LocalStorage:
- 持久存储。5MB 左右。除非你手动删,否则一直都在。不参与服务器通信。
- SessionStorage:
- 会话存储。5MB 左右。页面关了就没了。适合存表单临时数据。
- IndexedDB:
- 数据库。容量大(几百MB)。异步操作。适合存大量结构化数据。
64. 跨域 (CORS) 解决方案
- 同源策略:浏览器为了安全,禁止 协议、域名、端口 任一不同的网页互相访问数据。
- CORS (跨域资源共享):最正统的方案。
- 后端设置:后端在响应头加
Access-Control-Allow-Origin: https://example.com(指定域名比*更安全)。 - 简单请求:GET/POST/HEAD + 常见 Content-Type,浏览器直接发送,带上
Origin头。 - 预检请求 (Preflight):如果是复杂请求(如 PUT/DELETE、自定义 Header、
Content-Type: application/json),浏览器会先发一个 OPTIONS 请求问服务器:”我能发这个吗?”服务器通过Access-Control-Allow-Methods、Access-Control-Allow-Headers等头部回应。预检结果可通过Access-Control-Max-Age缓存。 - 携带 Cookie:需要前端设置
credentials: 'include',后端设置Access-Control-Allow-Credentials: true,且Allow-Origin不能为*。
- 后端设置:后端在响应头加
- Proxy (代理):开发环境常用。
- 前端
localhost:5173-> Vite/Webpack 代理服务器 (同源) -> 目标服务器。服务器之间没有同源策略。
- 前端
- JSONP:古老方案。利用
<script>标签不受同源策略限制。只能发 GET 请求,有安全风险,已基本淘汰。 - 其他方案:
window.postMessage(跨窗口通信)、Nginx 反向代理(生产环境)。
65. 防抖 (Debounce) 与 节流 (Throttle)
- 防抖 (Debounce):“最后一个人说了算”。
- 场景:搜索框输入。你一直打字我就不搜,等你停下来 500ms 不动了,我再去搜。
- 代码思路:清除旧定时器,设置新定时器。
- 节流 (Throttle):“按规定频率执行”。
- 场景:滚动条监听、按钮防狂按。不管你滑得多快,我每隔 100ms 只执行一次。
- 代码思路:如果有定时器在跑,就直接 return,直到定时器结束。
66. 事件委托 (Event Delegation)
- 原理:利用 事件冒泡。不给每个子元素(li)绑事件,而是绑在父元素(ul)上。
- 好处:
- 省内存:绑 1 个事件 vs 绑 1000 个事件。
- 动态支持:后来通过 JS 新增的 li 元素,不用重新绑定,天然就有事件。
- 实现:
ul.addEventListener('click', (e) => { if (e.target.tagName === 'LI') { ... } })。
67. TypeScript 核心优势
- 静态类型:代码还没跑,写的时候就能发现错误(比如拼写错误、参数传错了)。
- 智能提示:IDE 知道在这个对象里有什么属性,点一下全出来,开发效率极高。
- 可维护性:对于大型项目,TS 就是文档。看一眼接口定义 (
interface) 就知道数据长啥样。 - 重构安全:改了一个类型定义,所有用到它的地方立刻报错,不会遗漏。
- 常用高级类型:
- 泛型 (Generics):
function identity<T>(arg: T): T。让函数/类型可以适配多种类型,保持类型安全。 - 联合类型:
string | number,值可以是其中之一。 - 交叉类型:
A & B,同时满足 A 和 B。 - 字面量类型:
type Direction = 'up' | 'down' | 'left' | 'right'。 - 内置工具类型:
Partial<T>(所有属性可选)、Required<T>(所有属性必填)、Pick<T, K>(选取部分属性)、Omit<T, K>(排除部分属性)、Record<K, V>(键值对映射)、ReturnType<T>(获取函数返回类型)。
- 泛型 (Generics):
68. TypeScript: Interface vs Type
- Interface (接口):主要用来定义对象的形状。
- 特性:可以合并(同名 interface 会自动合并属性,称为"声明合并")。支持
extends继承。 - 适合:定义公共 API 契约、第三方库的类型扩展(利用声明合并)。
- 特性:可以合并(同名 interface 会自动合并属性,称为"声明合并")。支持
- Type (类型别名):更强大,可以定义基本类型、联合类型、交叉类型、元组等。
type Status = 'success' | 'fail';(联合类型,Interface 做不到)type Point = { x: number } & { y: number };(交叉类型)type Pair = [string, number];(元组)type Callback = (data: string) => void;(函数类型)- 不能声明合并(同名 type 会报错)。
- 选谁:
- 写库或插件用 Interface(为了让别人能通过声明合并扩展)。
- 写业务逻辑一般用 Type 更灵活(联合类型、映射类型等 Interface 做不了)。
- 两者在定义对象形状时几乎等价,团队统一即可。
69. TypeScript: Any vs Unknown vs Never
- any:放弃治疗。告诉编译器:”别管我,我爱咋用咋用”。失去了 TS 的意义。尽量用
unknown替代。 - unknown:安全的 any。表示”我不知道这是啥”。但是!在你使用它之前(比如调用它的方法),必须先进行类型判断(类型收窄),否则报错。
- 收窄方式:
typeof、instanceof、自定义类型守卫 (is)、断言 (as)。
- 收窄方式:
- never:不可能存在的类型。
- 函数永远不会正常返回(抛异常或死循环):
function fail(): never { throw new Error() }。 - 穷尽检查:在 switch 的 default 分支赋值给
never类型变量,如果漏了某个 case,TS 会报错。这是保证联合类型被完全处理的技巧。
- 函数永远不会正常返回(抛异常或死循环):
70. 移动端适配方案
- Viewport:
<meta name="viewport" content="width=device-width, initial-scale=1.0">。必须加,否则手机会把网页当成电脑版缩放。 - rem:相对于根元素 (
html) 的font-size。- 通常配合 JS 或 CSS 媒体查询,根据屏幕宽度动态修改
html的font-size。
- 通常配合 JS 或 CSS 媒体查询,根据屏幕宽度动态修改
- vw/vh:1vw = 屏幕宽度的 1%。目前最主流的方案。
- 1px 边框问题:高清屏(Retina)下,CSS 的 1px 看起来很粗(实际占了 2 或 3 个物理像素)。
- 解决:用伪元素
::after设置 1px 边框,然后transform: scale(0.5)。
- 解决:用伪元素
71. 图片懒加载 (Lazy Load)
- 原理:图片的
src先不填真地址,填一个占位图。把真地址存在data-src里。 - 检测:监听滚动,当图片即将出现在视口(Viewport)时,把
data-src赋值给src。 - API:IntersectionObserver。比传统的监听 scroll 事件性能好得多,浏览器原生帮你检测元素是否可见。
72. Web Worker
- 痛点:JS 是单线程的,计算量太大(比如图像处理、加密解密、大数据排序)会卡死主线程,页面就动不了了。
- Web Worker:允许你在后台开启一个新的线程运行 JS。
- 限制:Worker 线程不能操作 DOM,不能访问
window和document。它只能做纯计算,通过postMessage和主线程通信(结构化克隆算法传递数据)。 - 类型:
- Dedicated Worker:专属于创建它的页面,最常用。
- Shared Worker:可以被多个页面/标签页共享。
- Service Worker:独立于页面,用于离线缓存和推送通知(见第 91 题)。
- Transferable Objects:对于大型 ArrayBuffer,可以用
postMessage(data, [data])转移所有权而非拷贝,零拷贝传输,性能极高。 - 实际场景:大文件 hash 计算(分片上传)、Canvas 离屏渲染(OffscreenCanvas)、复杂数据处理。
73. 显式类型转换 vs 隐式类型转换
- 显式转换:开发者主动调用转换方法。
Number('123')→ 123,Number('abc')→ NaN。String(123)→ '123'。Boolean(0)→ false。假值只有 6 个:false、0、''、null、undefined、NaN。其他全是真值(包括[]、{}、'0')。parseInt('12px')→ 12(从左往右解析,遇到非数字停止)。
- 隐式转换 (坑):
+运算符:有字符串就变拼接。1 + '2'='12'。-、*、/运算符:全转数字。'6' - 1=5。[] + []=""(两个数组都转为空字符串再拼接)。[] + {}="[object Object]"。{} + []=0({}被解析为空代码块,实际执行的是+[])。
- == vs ===:
==会尝试转换类型再比较(1 == '1'为 true,null == undefined为 true)。===严格比较,类型不同直接 false。永远推荐用 ===。- 特殊:
NaN !== NaN(NaN 不等于任何值包括自身),用Number.isNaN()判断。
74. 常见的内存泄漏场景
- 未清理的定时器:组件销毁了,
setInterval还在跑。解决:在beforeUnmount/useEffect的 cleanup 中clearInterval。 - 未移除的事件监听:
addEventListener了但没有removeEventListener。解决:组件卸载时移除,或使用AbortController统一管理。 - 全局变量:不小心把变量挂到了
window上(如函数内未用let/const声明的变量)。 - 闭包:虽然好用,但如果持有了不再需要的大对象引用,GC 无法回收。
- DOM 引用:JS 里存了一个 DOM 节点的引用(如存在变量或 Map 中),后来这个节点从页面删了,但 JS 里还指着它,内存就释放不掉。
- 未取消的网络请求:组件销毁后请求才返回,回调中操作已卸载的组件。解决:使用
AbortController取消 fetch。 - console.log:开发时打印的大对象会被 DevTools 持有引用,生产环境应移除。
- 排查工具:Chrome DevTools → Memory 面板 → Heap Snapshot(堆快照对比)、Allocation Timeline(分配时间线)。
75. 进程 (Process) 与 线程 (Thread)
- 比喻:
- 进程:一个工厂(拥有独立的资源、内存空间)。比如 Chrome 的每一个 Tab 页通常是一个独立的进程。
- 线程:工厂里的工人(共享工厂的资源)。一个进程可以有多个线程。
- 浏览器是多进程的:主进程、渲染进程、GPU 进程、插件进程。
- JS 是单线程的:渲染进程里,JS 引擎线程和GUI 渲染线程是互斥的。JS 执行时,页面渲染就会停(所以 JS 写了死循环页面就卡死)。
在面试中,能说出原理是“银”,能手写出代码才是“金”。请务必在一个编辑器里亲自敲一遍这些逻辑。
第六部分:手写代码与算法基础 (76-90)
76. 手写防抖 (Debounce)
- 核心逻辑:
- 定义一个变量
timer保存定时器 ID。 - 返回一个函数。
- 每次调用时,先
clearTimeout(timer)清除之前的定时器。 - 重新
setTimeout,在延迟时间后执行真正的函数。
- 定义一个变量
- 用途:搜索框输入、窗口调整大小。
77. 手写深拷贝 (Deep Clone)
- 乞丐版:
JSON.parse(JSON.stringify(obj))。缺点:会丢失函数、undefined、Symbol,无法处理循环引用、Date 变字符串、RegExp 丢失。 - 现代版:
structuredClone(obj)(ES2022 原生 API)。支持循环引用、Date、RegExp、Map、Set、ArrayBuffer 等。但不支持函数、DOM 节点、Symbol。日常开发首选。 - 面试手写版逻辑:
- 判断类型,如果是基本类型(null、非对象)直接返回。
- 处理特殊对象:Date →
new Date(obj),RegExp →new RegExp(obj)。 - 如果是对象/数组,创建一个新的
{}或[]。 - 循环引用:使用
WeakMap做一个缓存字典。每次拷贝前先看字典里有没有这个对象,有就直接返回,没有就存进去再递归。 - 递归遍历属性(用
Reflect.ownKeys()可以拿到 Symbol key),赋值给新对象。
- 第三方库:lodash 的
_.cloneDeep()是最完善的实现,处理了几乎所有边界情况。
78. 手写 Promise.all
- 核心逻辑:
- 接收一个 Promise 数组。
- 返回一个新的 Promise。
- 内部维护一个计数器
count和结果数组results。 - 遍历数组,执行每个 Promise。
- 每成功一个,把结果存入
results(注意索引要对应),count++。 - 当
count === 数组长度时,resolve(results)。 - 只要有一个失败,立刻
reject(error)。
79. 手写发布订阅模式 (Event Emitter)
- 结构:一个类,里面有一个对象
events = {}存事件。 - 方法:
on(name, fn): 订阅。this.events[name].push(fn)。emit(name, ...args): 发布。找到this.events[name]数组,遍历执行里面的函数。off(name, fn): 取消。filter掉那个函数。once(name, fn): 也就是包装一层函数,执行完立刻调用off。
80. 数组扁平化 (Flatten)
- 需求:
[1, [2, [3, 4]]]->[1, 2, 3, 4]。 - 方案一:
arr.flat(Infinity)(ES6)。 - 方案二 (递归):遍历数组,如果是数组就递归调用,不是就 push。
- 方案三 (Reduce):
arr.reduce((prev, cur) => prev.concat(Array.isArray(cur) ? flatten(cur) : cur), [])。
81. 数组去重
- 方案一 (最快):
[...new Set(arr)]。 - 方案二 (indexOf):遍历,如果你在结果数组里找不到 (
indexOf === -1),就 push 进去。 - 方案三 (filter):
arr.filter((item, index) => arr.indexOf(item) === index)。保留第一次出现的,过滤掉后面重复的。
82. 手写 call / apply / bind
- 核心原理:如何改变
this指向? - Call 实现:
- 把函数挂到目标对象上:
context.fn = this。 - 执行这个函数:
context.fn(...args)。 - 删除这个属性:
delete context.fn。
- 把函数挂到目标对象上:
- Bind 实现:返回一个新的函数,这个新函数内部去调用
apply。注意处理new调用的情况。
83. 手写 instanceof 原理
- 作用:判断
A instanceof B,即 A 是否是 B 的实例。 - 逻辑:沿着 A 的原型链 (
__proto__) 往上找。 - 循环:如果
A.__proto__ === B.prototype,返回 true。如果找到了顶端null还没找到,返回 false。
84. 手写 new 操作符
- new 干了四件事:
- 创建一个新对象
obj。 - 把
obj的原型指向构造函数的原型 (obj.__proto__ = Constructor.prototype)。 - 执行构造函数,并把
this绑定到obj上。 - 关键:如果构造函数显式返回了一个对象,就返回那个对象;否则返回
obj。
- 创建一个新对象
85. 函数柯里化 (Currying)
- 概念:把
add(1, 2, 3)变成add(1)(2)(3)。 - 逻辑:
- 判断当前传入参数的个数 (
args.length)。 - 如果参数够了 (>= 原函数需要的参数),直接执行原函数。
- 如果不够,返回一个新的函数,继续收集参数,直到参数凑齐。
- 判断当前传入参数的个数 (
86. 解析 URL 参数
- 需求:
?name=jack&age=18->{name: 'jack', age: '18'}。 - 方案:
window.location.search拿到字符串。- 去掉
?,用&分割成数组。 - 遍历数组,用
=分割 key 和 value。 - 神器:
Object.fromEntries(new URLSearchParams('?name=jack&age=18'))一行代码搞定。
87. 快速排序 (Quick Sort)
- 算法思路 (分治法):
- 基准:找一个中间数(Pivot)。
- 分区:比它小的放左边数组,比它大的放右边数组。
- 递归:对左边和右边重复这个过程。
- 合并:
[...quickSort(left), pivot, ...quickSort(right)]。
- 复杂度:平均 O(n log n)。
88. 列表转树形结构 (List to Tree)
- 场景:后端给你一个扁平数组(包含 id, parentId),你要转成嵌套的树给菜单组件用。
- 逻辑:
- 用一个
Map(或对象) 存储所有节点,id 为 key。 - 遍历数组,找到每个节点的 parent。
- 如果 parent 存在,就把当前节点 push 到 parent 的
children数组里。 - 如果 parent 不存在(是根节点),放入结果数组。
- 用一个
- 优势:利用对象引用,时间复杂度 O(n),比双重循环 O(n^2) 快得多。
89. LRU 缓存算法 (最近最少使用)
- 需求:设计一个缓存,容量有限。满了之后,删除最久没用过的那个。
- 数据结构:JS 的
Map。因为Map的 Key 是有序的(按插入顺序)。 - Get 逻辑:如果有,先删除,再重新 set 进去(这样它就跑到了链表末尾,变成“最近使用”)。
- Put 逻辑:如果满了,删除
map.keys().next().value(头部的就是最久没用的),然后 set 新的。
90. 图片懒加载 (IntersectionObserver 代码)
- 传统做法:监听
scroll,计算scrollTop+windowHeight>imgOffsetTop。太麻烦且卡顿。 - 现代做法:javascript
const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { // 进入视口 const img = entry.target; img.src = img.dataset.src; // 替换真实地址 observer.unobserve(img); // 停止观察 } }); }); document.querySelectorAll('img').forEach(img => observer.observe(img));
🎉 第七部分:现代前端进阶 (91-100)
91. Service Worker 与 PWA
- Service Worker:运行在浏览器后台的独立线程,充当网页和服务器之间的代理。
- 生命周期:安装 (install) -> 激活 (activate) -> 监听 (fetch/push/sync)。
- 核心能力:拦截网络请求、管理缓存、实现离线访问。
- PWA (Progressive Web App):利用 Service Worker + Web Manifest,让网页拥有类似原生 App 的体验。
- 可安装:添加到主屏幕,有自己的图标和启动画面。
- 离线可用:Service Worker 缓存关键资源,没网也能用。
- 消息推送:通过 Push API 实现推送通知。
- 缓存策略:
- Cache First:先找缓存,没有再请求网络(适合静态资源)。
- Network First:先请求网络,失败再用缓存(适合 API 数据)。
- Stale While Revalidate:先返回缓存,同时后台更新缓存(兼顾速度和新鲜度)。
92. WebSocket 与 Server-Sent Events (SSE)
- WebSocket:
- 全双工通信:客户端和服务器可以随时互发消息(聊天室、实时协作)。
- 握手:先走 HTTP 升级协议 (
Upgrade: websocket),之后切换为 WebSocket 协议。 - 场景:即时通讯、多人协作编辑、实时游戏。
- SSE (Server-Sent Events):
- 单向通信:服务器 -> 客户端的实时推送。
- 基于 HTTP:不需要特殊协议,天然支持断线重连和事件 ID。
- 场景:股票行情、新闻推送、AI 大模型流式输出(ChatGPT 就用的 SSE)。
- 对比:需要双向通信选 WebSocket;只需服务器推送选 SSE(更简单、自动重连)。
93. 前端测试策略
- 测试金字塔:
- 单元测试 (Unit):测试最小功能单元(工具函数、Hooks)。速度快、数量多。工具:Vitest、Jest。
- 组件测试 (Component):测试组件渲染和交互。工具:Testing Library(Vue/React 通用)。
- E2E 测试 (End-to-End):模拟真实用户操作整个流程。速度慢、数量少但价值高。工具:Playwright、Cypress。
- 核心原则:
- 测试行为而非实现:不要测 "state 变成了 X",而是测 "用户点击后页面显示了 Y"。
- Arrange-Act-Assert 模式:准备数据 -> 执行操作 -> 断言结果。
- 覆盖率:不要盲目追求 100%。核心业务逻辑、边界情况、回归 Bug 是重点。
94. Monorepo 与包管理
- Monorepo:把多个相关项目放在同一个 Git 仓库中管理。
- 优势:代码共享方便、原子化提交(改了公共库和使用方在一个 commit)、统一工具链。
- 劣势:仓库体积大、权限管理粗粒度、CI 需要优化。
- 工具链:
- pnpm workspace:包管理器层面的 Monorepo 支持。
pnpm通过硬链接 + 符号链接实现磁盘空间节约和严格依赖隔离(不会出现幽灵依赖)。 - Turborepo:构建编排工具。智能缓存(本地 + 远程)、并行执行、只构建受影响的包。
- Nx:功能更全面的 Monorepo 框架,内置依赖图分析和增量构建。
- pnpm workspace:包管理器层面的 Monorepo 支持。
95. CSS 新特性
- Container Queries(容器查询):
- 比媒体查询更精细。媒体查询只能检测视口宽度,容器查询可以检测父容器宽度。
@container (min-width: 400px) { .card { flex-direction: row; } }- 场景:同一个卡片组件,在侧边栏里纵向排列,在主内容区横向排列。
:has()选择器(父选择器):- CSS 终于能"向上看"了。
div:has(> img)选中"包含 img 子元素的 div"。 - 场景:表单校验 —
input:has(~ .error)让有错误提示的输入框变红。
- CSS 终于能"向上看"了。
- CSS 嵌套(原生):
- 不再需要 Sass/Less 就能写嵌套。
.card { h2 { color: red; } &:hover { opacity: 0.8; } }
- 不再需要 Sass/Less 就能写嵌套。
- Subgrid:子网格继承父 Grid 的轨道定义,对齐更精确。
96. React Server Components (RSC)
- 核心思想:组件在服务器上渲染,不发送组件 JS 代码到客户端,只发送渲染结果。
- 与 SSR 的区别:
- SSR:组件在服务器渲染成 HTML,然后客户端下载 JS 进行 Hydration(注水),组件代码仍然会到达客户端。
- RSC:Server Component 的代码永远不到达客户端。可以直接访问数据库、读文件系统,零客户端 JS 开销。
- 分类:
- Server Component(默认):不能用
useState、useEffect等客户端 Hook。 - Client Component(标记
'use client'):普通的交互式组件。
- Server Component(默认):不能用
- 框架:Next.js App Router 是 RSC 的主要载体。
97. Web Accessibility (无障碍访问)
- 为什么重要:全球约 15% 人口有某种形式的残障。无障碍不仅是道德要求,很多国家和地区已有法律强制要求。
- WCAG 标准:Web Content Accessibility Guidelines,分 A、AA、AAA 三个级别。
- 实践要点:
- 语义化 HTML:使用正确的标签(
<button>而非<div onclick>),屏幕阅读器才能识别。 - ARIA 属性:
aria-label(为无文字元素提供标签)、aria-hidden(对辅助技术隐藏装饰元素)、role(声明元素角色)。 - 键盘导航:所有交互都必须能通过键盘完成(Tab 切换焦点、Enter/Space 触发)。
- 颜色对比度:文字与背景的对比度至少 4.5:1(AA 标准)。不能仅用颜色传达信息。
- alt 属性:所有有意义的图片必须有描述性的
alt文字。
- 语义化 HTML:使用正确的标签(
98. 前端错误监控
- 错误类型:
- JS 运行时错误:
window.onerror或window.addEventListener('error')捕获。 - Promise 未处理的拒绝:
window.addEventListener('unhandledrejection')捕获。 - 资源加载错误:图片、脚本加载失败,通过
error事件(捕获阶段)监听。 - 接口错误:封装
fetch/axios拦截器统一上报。
- JS 运行时错误:
- 上报方式:
navigator.sendBeacon(url, data):页面即将关闭时也能可靠发送(不阻塞卸载)。new Image().src = url:简单但有 URL 长度限制。
- Source Map:生产环境代码压缩后报错行号没意义。上传 Source Map 到监控平台(Sentry 等),可还原出源码位置。
- 主流工具:Sentry、阿里 ARMS、腾讯 Aegis。
99. 大文件上传方案
- 核心思路:分片上传。
- 前端使用
file.slice(start, end)将大文件切成小块(如每块 5MB)。 - 并发上传各分片(控制并发数,如最多 3 个)。
- 后端收到所有分片后进行合并。
- 前端使用
- 断点续传:
- 每个分片用文件内容的 hash(如 SparkMD5)标识。
- 上传前先问后端"哪些分片已经传过了?",已传过的跳过。
- 网络中断后重新打开页面,只需传剩余分片。
- 秒传:
- 上传前先计算整个文件的 hash,发给后端查询。
- 如果后端已有相同 hash 的文件,直接返回成功,无需实际传输。
- Web Worker:hash 计算量大,放到 Worker 线程避免卡主线程。
100. 前端发展趋势
- AI 驱动开发:AI 辅助编码(Copilot、Claude Code)、AI 组件生成、自然语言转 UI 正在重塑开发流程。
- Rust 工具链:SWC(编译)、Rolldown(打包)、Biome(Lint + Format)、Turbopack 等 Rust 编写的工具正在替代 JS 工具链,构建速度提升数十倍。
- 边缘计算 (Edge Runtime):Cloudflare Workers、Vercel Edge Functions。代码运行在离用户最近的边缘节点,冷启动极快。
- Islands Architecture(孤岛架构):页面大部分是静态 HTML,只有交互部分(孤岛)加载 JS。代表框架:Astro。
- WebAssembly (Wasm):将 C/C++/Rust 编译为浏览器可执行的二进制格式。适合计算密集型场景(图像处理、视频编辑、3D 渲染)。Figma 就大量使用 Wasm。
- 类型安全全栈:tRPC、Zod 等工具实现前后端类型共享,从 API 到数据库的端到端类型安全。
全系列完结
你已经浏览完了前端面试最核心的 100 个知识点。
如何复习:
- 查漏补缺:看着标题,如果你能自己讲出个大概,就过;如果卡住了,去搜详细解析。
- 深度优先:对于框架原理(Vue/React)和工程化(Webpack/Vite),不仅要背结论,最好能看过源码浅析。
- 手写练习:第 76-90 题,请务必在电脑上敲一遍,肌肉记忆很重要。
- 关注趋势:第 91-100 题的内容在近年面试中出现频率越来越高,尤其是 RSC、Monorepo、错误监控等。
祝你面试顺利,拿到心仪的 Offer!