前端面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

前端阅读 435月27日 01:16

WeakSet、WeakMap 和 Set、Map 之间的区别是什么?

核心区别就一条:Weak 版本的 key(或元素)是弱引用,不阻止垃圾回收。Set vs WeakSet:Set 元素可以是任何类型;WeakSet 元素只能是对象Set 可迭代(forEach、size、keys);WeakSet 不可迭代Set 中对象被引用着,即使对象其他地方不再使用也不会被 GC;WeakSet 中对象没有其他引用时会被回收Map vs WeakMap:Map 的 key 可以是任何类型;WeakMap 的 key 只能是对象Map 可迭代;WeakMap 不可迭代WeakMap 条目会随 key 对象被 GC 而自动清除WeakMap 典型场景:Vue 3 的响应式依赖追踪、存储 DOM 节点的关联数据、为第三方对象附加元数据而不造成内存泄漏。WeakSet 用得少——需要标记"这个对象我见过"但不想阻止它被 GC 时用。追问为什么 WeakMap 没有 size 属性?因为 WeakMap 中条目可能随时被 GC 回收,size 值是瞬时的、不可靠的。如果 JS 引擎提供了 size,开发者的代码里依赖了这个值,但下一秒 GC 跑了一次值变了——这种不可预测性比没有 size 更糟糕。WeakMap 和 Map 在内存管理上有什么区别?Map 的 key 被引用着,即使这个 key 对象在别处都已不使用,Map 里的引用也会阻止 GC——内存泄漏风险。WeakMap 的 key 是弱引用,如果 key 对象没有其他强引用了,GC 可以回收,对应的 WeakMap 条目自动消失。
前端阅读 315月27日 01:15

ES6 中有哪些解决异步的方法?

ES6 之后异步方案演进:回调函数:最原始的方式,问题是回调地狱(callback hell)——多层嵌套,错误处理困难。Promise:ES6 引入。把回调的嵌套转成 .then() 的链式调用,用 .catch() 统一处理错误。解决了回调地狱,但长链 .then() 仍然不够直观。Generator + co:通过 yield 暂停函数执行,配合自动执行器(如 co 库)实现类似同步的写法。现在基本被 async/await 取代。async/await:ES8(ES2017)正式引入。Promise 的语法糖——async 函数返回 Promise,await 暂停执行等待 Promise 完成。写法最像同步代码:async function getData() { const res = await fetch('/api'); const data = await res.json(); return data;}追问Promise.all、Promise.allSettled、Promise.race、Promise.any 的区别?all:全成功才成功,一个失败就失败allSettled:等全部完成(不管成败),返回结果数组含状态标记race:第一个完成的就返回(不管成败)any:第一个成功的就成功,全失败才失败(和 race 相反)async/await 的错误怎么处理?try-catch 包裹 await。或者用 .catch() 链在 async 函数的返回值上。也可以用 await promise.catch(() => fallbackValue) 的模式给错误设默认值。
前端阅读 245月27日 01:12

React 组件渲染过程是怎么样的?

React 的渲染分两个阶段:Render 阶段(协调 Reconciliation):状态变化 → 创建新的 Virtual DOM 树 → 和旧的 Virtual DOM 做 diff → 标记需要更新的节点。这个阶段是纯计算,没有副作用,React 18 可以中断和恢复(并发模式)。Commit 阶段:把 diff 结果应用到真实 DOM。这个阶段不可中断,React 保证 DOM 更新的原子性。commit 结束后触发 useLayoutEffect 同步执行,然后浏览器重绘,最后异步执行 useEffect。React 18 之前 render 不可中断。18 的并发渲染(Concurrent Features)可以在 render 阶段暂停低优先级更新,优先处理用户交互等高优先级任务。追问什么情况下 React 会跳过组件的渲染?shouldComponentUpdate 返回 false(class 组件)React.memo 包裹的组件 props 没变(浅比较)state 没变化时(setState 传入相同值,React 用 Object.is 判断)Context value 没变时(但 Provider 下的所有 Consumer 会在 Provider render 时重新渲染,和 value 是否变化无关——这是常见性能陷阱)useLayoutEffect 和 useEffect 执行的时机有什么不同?useLayoutEffect 在 DOM 变更后、浏览器绘制前同步执行(阻塞渲染)。useEffect 在浏览器绘制后异步执行。需要读取 DOM 布局、同步更新防止闪烁的场景用 useLayoutEffect。React 18 并发模式的 "可中断渲染" 是怎么实现的?基于时间切片(Time Slicing)——Render 阶段被切成 5ms 的小段,每段结束后检查是否有更高优先级任务。如果有就暂停当前渲染,先处理高优任务。实现依赖的是 MessageChannel(而非 requestIdleCallback,因为后者在后台标签页可能被暂停)。
前端阅读 125月27日 01:12

XML 和 JSON 的区别是什么?

JSON 现在是默认的数据交换格式。XML 在上一个时代扮演过同样角色。最大的区别:JSON 是数据格式,XML 是标记语言。JSON 直接表达对象、数组、字符串,XML 用标签包裹数据,更侧重文档结构。// JSON{ "name": "张三", "age": 30 }<!-- XML --><person> <name>张三</name> <age>30</age></person>JSON 的优势:更轻量、解析更快(JSON.parse vs DOM/SAX 解析)、天然映射 JS 对象、Schema 更简单。XML 仍有用的场景:需要属性+值+命名空间(SOAP 协议)、需要 DTD/Schema 验证、大量文档处理时 XPath/XSLT 更灵活。追问为什么 JSON 取代了 XML 做 Web API 的数据格式?两点:JS 生态的爆炸式增长(JSON 是 JS 原生支持的,XML 需要额外解析)和移动端的带宽敏感(JSON 更紧凑)。REST API + JSON 几乎成了标配。JSON 有什么缺点?不支持注释(JSONC 是变体)不支持日期类型(需转换成字符串或时间戳)浮点数精度问题(0.1 + 0.2 !== 0.3 在不同 JSON 解析器中处理不一致)没有二进制支持(Base64 编码后体积膨胀)XML 现在还用在哪?银行、政府系统(SOAP 协议)Office 文档格式(docx、xlsx 本质上是 XML 的 ZIP 包)SVG 图像Android 布局文件RSS/Atom 订阅
前端阅读 685月27日 01:12

TCP 建立连接需要经过哪几步?

TCP 三次握手建立连接:客户端 → SYN:客户端发 SYN=1,seq=x(随机初始序列号)。客户端进入 SYN-SENT 状态服务端 → SYN+ACK:服务端回 SYN=1,ACK=1,seq=y,ack=x+1。服务端进入 SYN-RCVD 状态客户端 → ACK:客户端发 ACK=1,seq=x+1,ack=y+1。双方进入 ESTABLISHED 状态为什么是三次不是两次?因为要防止已失效的连接请求到达服务端。如果只有两次,客户端发了一个 SYN 因为网络延迟没到,客户端超时重发了一个新的 SYN 建立了连接。之后旧的 SYN 到达服务端,服务端以为这是新连接,回 SYN+ACK 就建立了连接——但客户端根本不知道这个连接的存在。追问为什么不是四次握手?理论上四次(SYN → SYN+ACK → ACK → 服务端收到 ACK 确认)更稳妥。但第三步的 ACK 可以和服务端收到 ACK 合并——服务端只需要知道客户端收到了自己的 SYN+ACK 就够。额外的第四次是冗余的。SYN 泛洪攻击是什么?攻击者发送大量 SYN 但不回 ACK,导致服务端大量连接处于 SYN-RCVD 半连接状态,耗尽服务端资源。防御:SYN Cookie(服务端不分配资源,用 Cookie 验证客户端是真的)、减少 SYN-RCVD 超时时间。TCP 四次挥手为什么多一次?因为 TCP 是全双工的——每个方向都要独立关闭。客户端 FIN 表示"我说完了",服务端 ACK 表示"知道了",但如果服务端还有数据要发,发完后再 FIN。所以是 FIN → ACK → FIN → ACK 四步。
前端阅读 245月27日 01:10

webpack 的热更新原理是什么?详细聊聊热更新的流程

HMR(Hot Module Replacement)的核心流程:webpack-dev-server 启动 WebSocket 服务,和浏览器建立长连接文件变化 → webpack 重新编译 → 生成两个文件:[hash].hot-update.json(变更模块清单)和 [hash].hot-update.js(变更模块代码)通过 WebSocket 推送 hash 事件给浏览器浏览器用 AJAX 拉 hot-update.json,用 JSONP 拉 hot-update.jsHMR runtime 找到变更的模块,执行 module.hot.accept 注册的回调如果模块没有注册 accept,HMR runtime 向上冒泡,直到找到 accept 或刷新整个页面Vue-loader、style-loader 等 loader 内部自动调用了 module.hot.accept,所以改 .vue 文件能无刷新更新。追问为什么需要两步拉取(json + js)?hot-update.json 先确认哪些模块变了,如果当前页面没有引用变更模块,就不需要拉 hot-update.js。另外 json 由 webpack-dev-server 直接提供(内存文件系统),js 则走 JSONP 来避免跨域问题。React Fast Refresh 和普通 HMR 有什么区别?React Fast Refresh 在 HMR 基础上做了 React 组件的"签名"匹配——换掉的组件如果有相同的 hooks 调用和组件的导出签名,就保留下面的组件状态(state 不丢失)。普通 HMR 只是替换模块代码,状态会重置。HMR 失效了怎么排查?module.hot.accept 是否注册(检查对应 loader 是否配置正确)文件路径大小写不一致(macOS 不敏感但 webpack 敏感)配置文件中 hot: true 和 HotModuleReplacementPlugin 是否启用
前端阅读 345月27日 01:10

Chrome 打开一个页面需要启动多少进程?分别有哪些进程?

Chrome 是多进程架构,主要进程:Browser 进程(1 个):主进程,管 UI(地址栏、书签)、网络请求、文件访问。协调其他进程。Renderer 进程(每个标签页一个):渲染页面内容(HTML、CSS、JS)。沙箱隔离,一个页面崩不影响其他页面。GPU 进程(1 个):处理 GPU 任务(CSS 3D 变换、WebGL、视频解码)。Plugin 进程(按需):隔离运行的插件(Flash 等,现已很少)。Network 进程(1 个,较新版本):独立的网络进程。Utility 进程(按需):音频、扩展等。同一站点(same-site)的标签页可能共享 Renderer 进程以节省内存。具体数量取决于你开了多少标签页、多少扩展。追问为什么 Chrome 用多进程而不是多线程?稳定性和安全性。如果一个标签页崩溃或卡死,只影响那个 Renderer 进程,不会拖垮整个浏览器。沙箱机制也只限制单进程的权限。缺点是内存占用高,所以 Chrome 引入了进程共享(同一站点共享 Renderer 进程)。Renderer 进程里是什么?Renderer 进程包含:主线程(JS 执行、样式计算、布局)、合成线程(处理滚动、动画)、Raster 线程(将绘制命令转成位图)、Worker 线程等。主线程阻塞是页面卡顿的主要原因。内存不够时 Chrome 会怎么做?Chrome 会动态管理:冻结后台标签页的进程(节省内存)、优先杀掉非活动标签页的 Renderer 进程、同一站点合用一个 Renderer 进程。开大量标签页时内存压力还是很大。
前端阅读 275月27日 01:09

浏览器为什么要有同源限制?

同源策略是浏览器最核心的安全机制——限制一个源(协议+域名+端口都相同)的脚本去访问另一个源的资源。没有同源限制会怎样?你打开了 bank.com 登录了,另一个标签页打开了 evil.com。evil.com 的 JS 可以直接用 fetch 发请求到 bank.com/api/transfer——浏览器会自动带上你的登录 cookie,钱就转走了。同源策略阻止了这个。同源限制主要限制:1) AJAX 跨域请求;2) 跨 iframe/窗口的 DOM 操作;3) localStorage/Cookie 的跨域访问。跨域解决方案:CORS(服务端设 Access-Control-Allow-Origin)、JSONP(老方法,利用 script 标签不受同源限制)、代理(同源服务端转发)。追问为什么 script/img 标签不受同源限制?这是历史原因和功能需求——网页需要加载 CDN 上的脚本和图片。但 <script> 加载完会以当前域执行(所以有 XSS 风险),<img> 只能渲染不能读取像素数据(防止窃取)。资源加载的宽松和 API 访问的严格是两个层面的事。同源策略能防止 CSRF 吗?不完全能。CSRF 的请求能发出去(form 提交、img 加载都可以跨域),同源策略只阻止 JS 读响应。防止 CSRF 得靠 CSRF Token 或 SameSite Cookie。跨域请求发出去但响应被拦截,服务端收到请求了吗?收到了。浏览器是收到响应后发现没有 CORS 头才拦截的。所以服务端需要用 CORS 头来控制,不能用"浏览器会拦截"来保护服务端。
前端阅读 1325月27日 01:06

Vue 3 如何在 setup 方法中获取组件实例?

Vue 3 Composition API 中,setup 里没有 this。要获取组件实例相关的能力,用对应的 API 而不是拿整个实例:获取 DOM 元素:const el = ref(null) + ref="el" 绑定,通过 el.value 访问获取路由实例:const router = useRouter(); const route = useRoute()获取 store:const store = useStore()(Pinia 或 Vuex 4)需要组件内部数据:直接在 setup 里定义 ref/reactive 并 return,模板可直接访问需要父组件数据:defineProps + defineEmits如果非拿实例不可(极少场景),Vue 3 提供了 getCurrentInstance():import { getCurrentInstance } from 'vue';const instance = getCurrentInstance();// instance.proxy 近似 Vue 2 的 this但官方不推荐依赖它,内部实现在版本间可能变化。追问Vue 3 为什么不给 this?Composition API 设计目标是函数式、可复用、类型推导友好。this 会绑定上下文,函数提取出去 this 就丢了。而 ref/reactive 是普通的 JS 值/对象,可以自由传递不丢失响应性。template ref 和 document.getElementById 怎么选?template ref 是 Vue 的方式——能保证在组件挂载后拿到元素,且不同组件实例互不干扰。document.getElementById 能拿到但可能拿到错误组件的元素(同页面多实例时),也不符合 Vue 的数据驱动理念。getCurrentInstance 在 setup 外能用吗?不能在 setup 外调用(如生命周期钩子的回调外)。它只在 setup 同步执行期间有效,异步回调里拿到的可能是 null。
前端阅读 725月27日 01:06

什么是双向绑定?Vue 是如何实现双向绑定功能的?

双向绑定 = 数据变化自动更新视图 + 用户输入自动更新数据。最典型的场景就是 <input v-model="msg">——你修改 input 的值,msg 跟着变;JS 里改 msg,input 显示跟着变。Vue 2 用 Object.defineProperty 劫持对象属性的 getter/setter 实现数据 → 视图的响应式,用 v-model 监听 input 事件实现视图 → 数据的反向同步。Vue 3 换成了 Proxy,能拦截更多操作(属性增删、数组索引),不需要像 Vue 2 那样用 $set 手动处理新增属性。追问v-model 的本质是什么?v-model="msg" 是语法糖,等价于 :value="msg" + @input="msg = $event.target.value"。对于 checkbox 是 :checked + @change,对于 select 是 :value + @change。双向绑定和单向数据流矛盾吗?不矛盾。Vue 仍然是单向数据流(父 → 子通过 props,子 → 父通过 emit)。v-model 只是父子通信的一种语法糖,本质上还是 prop + event 的模式。什么场景不适合用双向绑定?需要在赋值前做复杂转换的(用 computed + emit 更清晰)多个输入源同时修改一个数据的(容易互相覆盖)需要中间确认操作的(如提交时才正式更新)
前端阅读 285月27日 01:06

什么是 CSS 弹性盒布局模型?

Flexbox(弹性盒布局)是一维布局模型,控制元素在一条轴(主轴或交叉轴)上的排列和对齐。设 display: flex 后,容器变成弹性容器,子元素变成弹性项目。核心概念:主轴(flex-direction 决定方向)和交叉轴容器属性:justify-content(主轴对齐)、align-items(交叉轴对齐)、flex-wrap(换行)、gap(间距)项目属性:flex-grow(放大比例)、flex-shrink(缩小比例)、flex-basis(基础大小)、align-self(单独对齐)最常用的三件套:display: flex; justify-content: center; align-items: center; —— 一行代码实现水平垂直居中。追问flex: 1 是什么意思?flex: 1 = flex-grow: 1; flex-shrink: 1; flex-basis: 0%。表示元素按比例瓜分剩余空间,常用于等分布局。flex: auto 不同在于 flex-basis: auto(先看元素自身尺寸再瓜分剩余空间)。Flexbox 和 Grid 什么时候用哪个?Flexbox 是一维的,适合组件内部排列(导航栏、按钮组、表单行)。Grid 是二维的,适合页面级布局(一行多列、复杂网格)。实际项目混用居多:Grid 做页面骨架,Flexbox 做组件内部。为什么 flex 子元素设置 width 有时不生效?flex-shrink 默认是 1,空间不够时项目会被压缩。加上 flex-shrink: 0 或 min-width: <你的宽度> 阻止收缩即可。
前端阅读 145月27日 01:06

什么是深拷贝?什么是浅拷贝?

浅拷贝只复制对象的第一层属性,如果属性值是引用类型(对象、数组),拷贝的是引用地址——修改拷贝会影响原对象。深拷贝递归复制所有层级,新旧对象完全独立。// 浅拷贝const obj = { a: 1, b: { c: 2 } };const shallow = { ...obj }; // 或 Object.assign({}, obj)shallow.b.c = 3;console.log(obj.b.c); // 3 — 原对象被影响!// 深拷贝const deep = JSON.parse(JSON.stringify(obj)); // 最简单的方式... 展开和 Object.assign 都只做浅拷贝。JSON.parse(JSON.stringify(...)) 能做深拷贝但有局限:无法处理 undefined、函数、Date、RegExp、循环引用。追问JSON 方式深拷贝有哪些坑?undefined、函数、Symbol 直接被丢弃NaN 和 Infinity 变成 nullDate 变成字符串RegExp 变成空对象 {}循环引用直接报错原型链丢失生产环境怎么实现深拷贝?structuredClone()(浏览器原生 API,支持循环引用、Date、Map、Set 等,但不支持函数和 DOM 节点)lodash 的 cloneDeep自己实现递归拷贝 + WeakMap 处理循环引用(面试手写题常考这个)手写深拷贝时循环引用怎么处理?用 WeakMap 缓存已拷贝过的对象。每次拷贝前检查是否已经拷贝过,如果拷贝过直接返回缓存值。
前端阅读 675月27日 01:05

localStorage 对象有哪些 API?

localStorage 是浏览器提供的持久化键值存储,数据不随页面关闭而清除。API 一共就 5 个方法和 1 个属性:setItem(key, value):存数据。key 和 value 都是字符串。存对象需要用 JSON.stringifygetItem(key):取数据。key 不存在返回 nullremoveItem(key):删单个 keyclear():清空所有数据(注意:清的是当前域名下的所有 localStorage)key(index):按索引获取 key 名,用于遍历length:只读属性,返回存储的条目数存储对象的正确姿势:localStorage.setItem('user', JSON.stringify({ name: 'John', age: 30 }));const user = JSON.parse(localStorage.getItem('user'));追问localStorage 和 sessionStorage 有什么区别?生命周期:localStorage 永久存储,sessionStorage 关闭标签页就清除作用域:两者都是按域名隔离,但 sessionStorage 额外按标签页隔离API 完全一致localStorage 有什么限制?容量 5-10MB(不同浏览器不同)只能存字符串同步 API,大容量读写会阻塞主线程不能存敏感信息(明文存储,XSS 可直接读取)不支持过期时间,需要自己实现Cookie 和 localStorage 怎么选?需要随 HTTP 请求自动发送(如身份认证)→ Cookie纯前端存储、不随请求发送 → localStorage需要服务端可读 → Cookie(设置 HttpOnly 防止 XSS 读取)容量超过 4KB → localStorage(Cookie 限制 4KB)
前端阅读 265月27日 01:05

当添加原生事件不移除时,为什么会出现内存泄露?

核心原因:事件监听器持有 DOM 元素的引用,导致 DOM 元素被移除后无法被 GC 回收。具体链路:button.addEventListener('click', handler) 时,浏览器内部建立了 DOM元素 → handler 的引用关系你用 button.remove() 从 DOM 树移除按钮,但事件系统仍然保留着 handler 对 button 的引用如果 handler 又是闭包,引用着外层变量 → 整个闭包链上的变量都无法 GC多次重复后,内存中堆满了"已从 DOM 移出但无法回收的元素和函数",这就是内存泄漏解决办法:移除 DOM 元素前调用 removeEventListener。button.removeEventListener('click', handler);button.remove();或者在不需要关心个别元素时,用事件委托——把事件绑在父元素上。追问现代浏览器还会因为不移除事件导致泄漏吗?IE 时代这个问题最严重(IE 的 JS 引擎和 DOM 使用不同的 GC,循环引用是死穴)。现代浏览器的标记清除 GC 大部分循环引用能处理,但事件监听器 + 闭包的组合仍然会让本应被回收的对象变成"可达"——技术上不是传统意义的泄漏,但效果一样:内存释放不了。事件委托能解决这个问题吗?能。事件委托只绑一个监听器在父元素上,子元素增删都不需要管理事件。而且性能更好(减少监听器数量)。唯一注意:focus、blur、mouseenter、mouseleave 这些不冒泡的事件不能用委托。怎么在 DevTools 中排查内存泄漏?Performance 面板录一段操作,看 JS Heap 是否持续上升不回落 → Memory 面板取 Heap Snapshot,对比操作前后,看 Detached DOM 节点数量和 @ 引用关系。
前端阅读 345月27日 01:05

JavaScript 定时器为什么是不精确的?

setTimeout 和 setInterval 的执行时间不是精确的,根本原因就一条:JavaScript 是单线程的,定时器回调必须等主线程空闲才能执行。比如你设置 setTimeout(fn, 10),但此时主线程有个耗时 100ms 的计算任务在执行——fn 最早也要等 100ms 后才能跑,10ms 只是"最早执行时间",不是"准时执行时间"。其他影响因素:浏览器最小延迟:嵌套 5 次以上的 setTimeout 强制最小 4ms 间隔,后台标签页可能被降低到 1000ms事件循环优先级:微任务(Promise)比宏任务(定时器)优先执行GC 暂停:垃圾回收会短暂阻塞主线程追问需要精确计时怎么办?短时间精度:用 performance.now() 获取微秒级时间戳,自己在 requestAnimationFrame 里判断时间长时间精度:用 Web Worker(独立线程,不会被主线程阻塞),Worker 里用 setTimeout 相对更准音频场景:用 AudioContext.currentTime,这个不受主线程影响requestAnimationFrame 比 setInterval 更精确吗?rAF 本身也不是计时工具——它是"在浏览器下一次重绘前执行",频率取决于刷新率(60Hz 约 16.67ms)。它的优势是自动与渲染同步,不会做无效的更新。但作为定时器,它也有偏差。
前端阅读 225月27日 01:05

什么是 defineProperty 方法?什么时候需要用到它?

Object.defineProperty(obj, prop, descriptor) 让你精确控制对象属性的行为——是否可写、可枚举、可配置,还能定义 getter/setter。const obj = {};Object.defineProperty(obj, 'name', { value: '张三', writable: false, // 不可修改 enumerable: true, // for...in 可遍历 configurable: false // 不可删除、不可再次配置});核心价值在于拦截属性的读写。Vue 2 的响应式系统就是用 Object.defineProperty 劫持对象属性的 getter/setter,才能在数据变化时通知视图更新。追问defineProperty 和直接赋值有什么区别?直接赋值 obj.x = 1 创建的属性,writable、enumerable、configurable 默认都是 true,get/set 是 undefined。defineProperty 创建的属性,不显式指定的描述符默认都是 false。Vue 3 为什么换成 Proxy?defineProperty 有硬伤:无法检测属性的添加/删除(所以 Vue 2 需要 $set、$delete),对数组索引和 length 的拦截有坑。Proxy 能拦截 13 种操作,包括属性增删、in、delete、apply,不需要遍历对象属性递归劫持。什么场景下需要把属性设成不可枚举?给原型添加方法时(不想污染 for...in)框架内部属性(如 Vue 的 __ob__)任何不希望被 JSON.stringify 序列化或被 Object.keys 返回的属性
前端阅读 395月27日 01:05

什么是 CSRF 攻击?如何防御?

CSRF(跨站请求伪造)的攻击逻辑很简单:利用用户在其他网站已登录的身份,伪造请求执行非授权操作。典型场景:你登录了银行网站,cookie 还在。这时你打开了攻击者的网页,里面有一张隐藏的图片 <img src="https://bank.com/transfer?to=attacker&amount=10000">。浏览器加载这张"图片"时,自动带上了你的银行 cookie,银行收到请求以为是你的操作,转账就发生了。关键在于:浏览器发跨站请求会自动带上目标域名的 cookie——这是浏览器的默认行为。防御方式:Anti-CSRF Token:服务端生成随机 token,表单提交时带上,服务端验证。攻击者无法获取这个 tokenSameSite Cookie:Set-Cookie: SameSite=Strict,告诉浏览器跨站请求不要带这个 cookie。这是现代浏览器的标配方案Referer/Origin 检查:服务端验证请求来源,合法的请求才处理二次验证:关键操作(转账、改密码)要求重新输入密码或验证码追问CSRF 和 XSS 有什么区别?XSS 是在用户浏览器里执行恶意 JS(利用了网站对输入的信任),CSRF 是伪造用户的请求(利用了网站对浏览器身份的信任)。一句话:XSS 是"偷你的权限",CSRF 是"借用你的身份"。SameSite 三个值分别什么意思?Strict:任何跨站请求都不带 cookie,最安全。但用户从外部链接点进来是未登录状态Lax:大部分跨站请求不带 cookie,但"安全"的 GET 请求(如 <a> 链接点击)会带。兼顾安全和体验None:和以前一样,跨站请求都带 cookie,但必须同时设置 Secure(仅 HTTPS)Anti-CSRF Token 怎么防止攻击者自己获取一个 token?token 绑定到用户 session,攻击者拿到的 token 是绑给他自己的 session 的,放到跨站请求里服务端一对不上就拒了。关键不是"token 不能被获取",而是"token 和用户 session 绑定"。
前端阅读 375月27日 01:03

什么是 XSS 攻击?

XSS(跨站脚本攻击)的本质是攻击者把自己的 JS 代码注入到你的网页里,在真实用户的浏览器上执行。能做的坏事包括:偷 cookie、篡改页面内容、劫持用户操作。三种类型:存储型:恶意脚本存到了服务器(数据库、评论、留言),用户访问页面时被返回执行。危害最大,不需要诱导点击。反射型:恶意脚本放在 URL 参数里,服务器直接拼到响应页面中。需要诱导用户点击构造好的链接。DOM 型:纯客户端问题——前端 JS 把 URL 参数或用户输入直接用 innerHTML 插到 DOM 里,没有经过后端。防御三件套:输出转义:所有用户输入在输出到 HTML 前做转义(< → < 等),不信任任何用户数据CSP(内容安全策略):HTTP 头告诉浏览器只允许执行特定来源的脚本HttpOnly Cookie:关键 cookie 标记 HttpOnly,JS 读不到,即使被 XSS 也偷不走追问XSS 和 CSRF 有什么区别?XSS 是在受害者浏览器里执行恶意 JS,利用的是对网站的信任。CSRF 是诱导受害者发请求到已登录的网站,利用的是网站对浏览器的信任。CSP 怎么配置?Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-xxx'。核心思想:白名单机制,不在白名单里的脚本一律不执行。但 CSP 配置需要充分测试,容易把正常功能挡掉。富文本编辑器怎么防 XSS?不能用简单的转义(会把格式也干掉)。正确做法:白名单过滤标签和属性(如 DOMPurify),只允许 <b>、<i>、<a> 等安全的标签,移除 onclick、onerror 等事件处理器和 javascript: 协议。
前端阅读 175月27日 00:59

Vite 为什么启动这么快?

Vite 之所以快,核心就两点:开发阶段不打包 + 按需编译。传统打包工具(Webpack 等)在启动时会把整个应用打包一次,项目越大越慢。Vite 反过来,利用浏览器原生支持的 ES Module,让浏览器按需请求模块——只有被用到的文件才会被编译。加上 Vite 用 esbuild(Go 写的)做依赖预构建,速度比 JS 打包器快几十倍。HMR 方面也是模块级别的:改了哪个文件,只重新编译那个文件和依赖它的链,不是整体刷新。一句话:传统打包工具是把整个项目"煮好"再上菜,Vite 是你点什么它现做——启动当然快。追问Vite 生产环境还是用 Rollup 打包的,那开发快有什么用?开发体验直接影响效率。你一天可能启动几十次 dev server、改几百次代码,每次快几秒加起来就是大量时间。生产打包只在 CI 里跑一次,慢点无所谓。而且 Vite 生产构建用的是 Rollup,本身也比 Webpack 快。esbuild 这么快,为什么生产环境不用它打包?esbuild 快是因为它功能少——不支持 AST 操作、不支持插件生态里那些复杂转换。Vite 用 esbuild 做依赖预构建(只需要转译),用 Rollup 做生产打包(需要 Tree-Shaking、代码分割、插件等),各取所长。实际项目从 Webpack 迁移到 Vite 遇到过什么坑?CommonJS 依赖可能不兼容,需要配置 optimizeDeps.includerequire.context 这种 Webpack 特有的 API 不能用了CSS Modules 的导入方式有差异(.module.css 后缀强制要求)环境变量从 process.env.X 变成 import.meta.env.X为什么 Vite 不用 webpack 的 bundleless 方案(如 Snowpack)?Snowpack 已经停止维护了。Vite 和 Snowpack 思路类似,但 Vite 胜在:依赖预构建(esbuild)、开箱即用的 HMR、更完善的插件体系(兼容 Rollup 插件)。生态和社区活跃度完全不在一个量级。
前端阅读 255月27日 00:54

fetch 和 XHR 有什么区别?

fetch 和 XHR 都能在浏览器发 HTTP 请求,但 fetch 是现代 API,基于 Promise,语法更简洁。XHR 是回调时代的设计,API 复杂但老浏览器兼容性好。关键区别:fetch 默认不携带 cookie(需要 credentials: 'include'),XHR 默认携带。fetch 只有网络错误才 reject,HTTP 404/500 算成功需要手动判断 response.ok,XHR 靠 onerror 和 status 判断。fetch 可以中断(AbortController),XHR 靠 xhr.abort()。fetch 支持 ReadableStream 流式读取,XHR 可以监听 progress 事件——下载进度条场景 XHR 反而更方便。另外 fetch 不支持超时设置(要配合 AbortController 模拟),也没有原生的上传进度回调。Service Worker 里只能用 fetch,XHR 不可用。追问什么场景下 XHR 比 fetch 更好?需要上传/下载进度条时,XHR 有 progress 事件,fetch 没有原生的。需要超时控制时 XHR 有 timeout 属性。老浏览器兼容也选 XHR。fetch 怎么取消请求?AbortController:controller.abort() 发出取消信号,fetch 收到后抛出 AbortError。注意这只是前端取消,服务端可能还在处理。fetch 怎么带 cookie?fetch(url, { credentials: 'include' })。跨域时服务端还要设 Access-Control-Allow-Credentials: true 且 origin 不能用 *。