服务端5月30日 00:10
如何在 axios 中实现请求和响应拦截器?Axios 拦截器就是请求发出前、响应返回后统一插一层处理逻辑。请求拦截器常用来加 token、加请求 ID、处理 loading;响应拦截器常用来拆 `data`、统一处理业务错误、401 登录失效和网络异常。项目里一般不要直接改全局 axios,而是 `axios.create()` 建实例,再给实例挂拦截器,避免多个后端服务互相污染配置。
## 追问
### 请求拦截器和响应拦截器分别适合做什么?
请求拦截器改 `config`,比如加 `Authorization`、`baseURL`、防缓存参数。响应拦截器处理 `response` 或 `error`,比如把 `{ code, data, message }` 统一拆成 `data` 返回。
### 多个拦截器的执行顺序是什么?
请求拦截器后添加的先执行,响应拦截器先添加的先执行。排查问题时要注意顺序,否则 token 还没加上,请求日志就已经打印了旧配置。
### loading 为什么不能简单请求开始显示、结束隐藏?
并发请求会出问题。第一个请求结束就隐藏 loading,但其他请求还没回来。常见做法是维护计数器,请求加一,响应或错误减一,减到 0 再隐藏。
### 什么时候要移除拦截器?
临时调试、微前端子应用卸载、某个页面单独加拦截逻辑时要 `eject`,否则重复注册会导致同一个错误提示弹很多次。
## 写段代码
```javascript
const api = axios.create({ baseURL: '/api', timeout: 10000 });
api.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
api.interceptors.response.use(
res => res.data,
err => Promise.reject(err.response?.data || err)
);
```标签
前端
Web前端开发是从网页制作演变而来的,名称上有很明显的时代特征。在互联网的演化进程中,网页制作是Web 1.0时代的产物,那时网站的主要内容都是静态的,用户使用网站的行为也以浏览为主。2005年以后,互联网进入Web 2.0时代,各种类似桌面软件的Web应用大量涌现,网站的前端由此发生了翻天覆地的变化。网页不再只是承载单一的文字和图片,各种富媒体让网页的内容更加生动,网页上软件化的交互形式为用户提供了更好的使用体验,这些都是基于前端技术实现的。

服务端5月30日 00:10
axios 中如何进行错误处理?有哪些常见错误类型?Axios 错误处理先看三类:`error.response` 表示服务端返回了非 2xx 状态码,重点处理 400、401、403、404、5xx;`error.request` 表示请求发出但没收到响应,多半是网络、超时、CORS;两者都没有通常是请求配置写错。实际项目里建议:业务层只处理当前页面关心的错误,全局拦截器统一做登录失效、错误提示、日志和重试。
## 追问
### error.response 和 error.request 有什么区别?
`response` 说明后端有响应,只是状态码失败;`request` 说明请求发出去了但没有拿到响应。前者看 `status` 和 `data.message`,后者看 `code`,比如 `ECONNABORTED` 或 `ERR_NETWORK`。
### 401 应该在每个接口里处理吗?
不要。401 属于全局认证问题,放响应拦截器里统一清 token、跳登录页或刷新 token。页面里只关心业务错误,比如表单校验失败。
### 超时和 5xx 要不要自动重试?
可以,但只重试幂等请求,比如 GET。POST、支付、下单这类接口不能盲目重试,否则可能造成重复提交。
### 实际项目里最容易踩什么坑?
拦截器里 `return Promise.reject(error)` 忘了写,外层 catch 拿不到错误;另一个坑是把所有错误都弹 toast,导致 401 跳转时还弹一堆无意义提示。
## 写段代码
```javascript
axios.interceptors.response.use(r => r.data, error => {
if (error.response) {
const { status, data } = error.response;
if (status === 401) {
localStorage.removeItem('token');
location.href = '/login';
}
return Promise.reject(new Error(data?.message || `HTTP ${status}`));
}
if (error.code === 'ECONNABORTED') {
return Promise.reject(new Error('请求超时'));
}
return Promise.reject(new Error(error.message || '网络错误'));
});
```服务端5月29日 22:54
axios 如何实现并发请求和取消请求?代码怎么写?axios 的并发和取消分别基于 **Promise.all 和 AbortController/CancelToken**。
**并发请求:** 使用 `Promise.all` 同时发起多个请求,全部成功返回结果数组,任一失败则整体失败。`axios.all` 已废弃(v0.27+),直接用 `Promise.all`。需要逐个处理结果用 `Promise.allSettled`,无论成功失败都返回每个请求的状态。
```js
// 并发请求
const [users, posts] = await Promise.all([
axios.get('/api/users'),
axios.get('/api/posts')
]);
// 容错并发
const results = await Promise.allSettled([
axios.get('/api/a'),
axios.get('/api/b')
]);
results.forEach(r => {
if (r.status === 'fulfilled') handleData(r.value.data);
});
```
**取消请求:** 新方案用 `AbortController`(v0.22+推荐),旧方案 `CancelToken` 已废弃。AbortController 是浏览器原生 API,与 fetch 通用。
```js
// AbortController 取消
const controller = new AbortController();
axios.get('/api/data', { signal: controller.signal });
// 取消
controller.abort();
// 封装自动取消:同一接口新请求自动取消旧请求
const pending = new Map();
function fetchWithCancel(url) {
pending.get(url)?.abort();
const ctrl = new AbortController();
pending.set(url, ctrl);
return axios.get(url, { signal: ctrl.signal }).finally(() => pending.delete(url));
}
```
> **追问:**
> 1. Promise.all 和 Promise.allSettled 在错误处理上的区别是什么?
> 2. 如何控制并发数量(如最多同时 3 个请求)?
> 3. 取消请求后 axios 抛出的是什么错误?如何区分取消和真正的请求失败?
> 4. 路由切换时如何批量取消未完成的请求?
> 5. CancelToken 为什么被废弃?它和 AbortController 的实现原理有什么不同?服务端5月29日 22:54
axios 文件上传下载怎么做?进度监控和 CSRF 防护如何实现?axios 的高级特性主要有 **文件上传下载、进度监控、CSRF 防护、实例封装**。
**文件上传:** 使用 FormData 构建请求体,设置 `Content-Type: multipart/form-data`(axios 自动识别 FormData 并设置)。分片上传需手动将文件切片(Blob.slice),逐片上传并在后端合并。上传示例:`const form = new FormData(); form.append('file', fileBlob); axios.post('/upload', form)`。
**文件下载:** 配置 `responseType: 'blob'` 获取二进制数据,通过 `URL.createObjectURL(blob)` 创建临时链接,触发 `<a>` 标签下载。大文件下载注意内存,可用流式处理(Node 环境)。
**进度监控:** 上传用 `onUploadProgress`,下载用 `onDownloadProgress`,回调参数包含 `loaded` 和 `total`,计算百分比:`Math.round(loaded / total * 100)`。底层基于 XMLHttpRequest 的 progress 事件,Node 环境不支持。
**CSRF 防护:** 配置 `xsrfCookieName` 和 `xsrfHeaderName`,axios 自动从 Cookie 读取 XSRF-Token 并写入请求头。后端需在登录时设置 `Set-Cookie: XSRF-TOKEN=xxx; Path=/`。
**实例封装:** `axios.create()` 创建独立实例,拥有自己的拦截器、默认配置和适配器,适合多 API 服务的项目隔离配置。
> **追问:**
> 1. 分片上传如何实现断点续传?需要后端配合什么接口?
> 2. onUploadProgress 在 Node 环境下为什么不生效?有替代方案吗?
> 3. 大文件下载如何避免浏览器内存溢出?
> 4. axios.create 创建的实例和全局 axios 对象的拦截器是共享的吗?
> 5. XSRF-Token 的 Cookie 为什么不能设 HttpOnly?这和 XSS 风险如何权衡?服务端5月29日 22:54
axios 和 fetch 有什么区别?什么时候该用 axios 而非 fetch?axios 和 fetch 的核心区别在 **错误处理、拦截器、请求取消、数据转换、兼容性** 五个方面。
**错误处理:** fetch 只有网络故障才 reject,HTTP 4xx/5xx 仍走 resolve,需手动检查 `response.ok`;axios 自动将 4xx/5xx 转为 reject,错误处理更符合直觉。
**拦截器:** axios 内建请求/响应拦截器,统一添加 Token、处理错误码;fetch 无拦截器,需手动包装或用第三方库。
**请求取消:** axios 支持 CancelToken 和 AbortController;fetch 只支持 AbortController。axios 的超时配置(timeout)是内建的,fetch 需自己用 AbortController + setTimeout 实现。
**数据转换:** axios 自动 JSON 转换(请求时 stringify、响应时 parse);fetch 需手动调用 `response.json()`。axios 自动处理 URL 编码和 FormData。
**兼容性:** fetch 是浏览器原生 API,无依赖;axios 约 13KB gzipped。fetch 在旧浏览器需 polyfill(如 whatwg-fetch)。
**选 axios 的场景:** 需要拦截器、自动错误处理、超时控制、请求/响应转换、取消请求。**选 fetch 的场景:** 追求零依赖、简单请求、Service Worker 中使用、项目已在用 SWR/React Query 等上层库。
> **追问:**
> 1. fetch 的 `credentials: 'include'` 和 axios 的 `withCredentials: true` 行为是否一致?
> 2. fetch 如何实现和 axios 拦截器类似的功能?
> 3. 在 Service Worker 中为什么推荐用 fetch 而非 axios?
> 4. axios 的响应拦截器能拿到原始的 Response 对象吗?
> 5. React Query 或 SWR 底层用的是 axios 还是 fetch?能切换吗?服务端5月29日 22:54
axios 性能优化有哪些技巧?如何减少冗余请求?axios 性能优化围绕 **请求去重、缓存、并发控制、体积优化** 四个维度。
**请求去重:** 对同一接口的并发请求做合并,避免重复发送。实现方式:在请求拦截器中用 URL+参数生成 key,维护一个 Map 记录正在进行的请求,相同 key 的请求复用同一个 Promise,响应后删除 key。也叫"请求锁"或"请求合并"。
**请求缓存:** 对不常变的数据(配置信息、字典表)做本地缓存。在响应拦截器中按 URL+参数缓存响应,设置 TTL 过期。也可用 HTTP 缓存头(Cache-Control、ETag)配合浏览器缓存,axios 的 `adapter` 可实现自定义缓存适配器。
**并发控制:** 使用 `Promise.all` 并发请求提升加载速度;对大量并发用并发池控制(如 p-limit),避免浏览器同域 6 连接限制导致的排队。批量接口优先用后端聚合 API,减少请求次数。
**体积优化:** 开启 gzip 压缩(服务端配置);请求参数精简,只传必要字段;响应数据按需获取(分页、字段过滤);大文件上传用分片上传减少超时风险。
> **追问:**
> 1. 请求去重的 Map 在请求失败时如何清理?避免后续请求被阻塞?
> 2. 自定义缓存 adapter 如何处理 POST 请求的缓存策略?
> 3. 浏览器同域并发连接限制对 axios 有什么影响?如何绕过?
> 4. 如何实现 axios 请求的优先级队列?
> 5. 分片上传的断点续传如何在 axios 中实现?服务端5月29日 22:54
axios 存在哪些安全风险?如何防范 XSS 和 CSRF 攻击?axios 的安全风险集中在 **CSRF、XSS、敏感数据泄露、SSRF** 四个方面。
**CSRF 防护:** axios 内置 CSRF 防护,通过 `xsrfCookieName` 和 `xsrfHeaderName` 配置自动从 Cookie 读取 Token 并附加到请求头。后端需设置 `Set-Cookie: XSRF-TOKEN=xxx`,前端配置 `axios.defaults.xsrfCookieName = 'XSRF-TOKEN'`。对于 SameSite Cookie 策略,建议后端设置 `SameSite=Strict` 或 `Lax` 作为双重保障。
**XSS 防护:** axios 本身不执行 HTML,但返回数据若直接插入 DOM(如 v-html、dangerouslySetInnerHTML)会导致 XSS。必须对响应数据做转义,或使用 DOMPurify 净化。另外避免在 URL 参数中拼接用户输入,防止反射型 XSS。
**敏感数据泄露:** Token 不要存 localStorage(XSS 可读取),优先存 HttpOnly Cookie;请求拦截器中不要把 Token 打印到日志;响应拦截器中敏感字段(密码、密钥)应在日志中脱敏。
**SSRF 防护:** 服务端使用 axios 时,若 URL 来自用户输入,需校验目标地址,禁止请求内网 IP(127.0.0.1、10.x.x.x、192.168.x.x),使用白名单域名策略。
> **追问:**
> 1. axios 的 XSRF 防护机制在前后端分离架构下如何实现?
> 2. SameSite Cookie 的 Strict 和 Lax 模式对 CSRF 防护有什么影响?
> 3. HttpOnly Cookie 方案在跨域场景下如何配置 CORS?
> 4. 如何对 axios 响应数据做自动化脱敏?
> 5. SSRF 攻击中如何绕过 IP 黑名单?白名单方案怎么设计?服务端5月29日 22:54
axios 代码怎么做单元测试?Mock 请求的常用方案有哪些?测试 axios 代码的核心是 **隔离网络请求**,常用方案有三种:axios-mock-adapter、jest.mock + msw。
**axios-mock-adapter:** 直接拦截 axios 实例的请求,按 URL 和方法注册 mock 响应。适合纯 axios 项目,API 直观。示例:`mock.onGet('/api/user').reply(200, { name: 'test' })`。可模拟超时、网络错误、指定状态码。缺点是绑定了 axios 实例,切换请求库需重写 mock。
**jest.mock('axios'):** 直接 mock axios 模块,控制 `axios.get/post` 等方法的返回值。适合快速编写测试,`jest.mock('axios')` 后用 `axios.get.mockResolvedValue()` 设定返回。优点是零依赖,缺点是绕过了拦截器逻辑,无法测试中间件行为。
**MSW(Mock Service Worker):** 在 Service Worker 层拦截请求,不依赖任何 HTTP 库。最接近真实网络行为,可共享给集成测试和 E2E。通过 handler 匹配请求并返回 mock 数据。推荐用于新项目。
**选型建议:** 项目只用 axios → axios-mock-adapter;需要快速单元测试 → jest.mock;多请求库或需要更真实模拟 → MSW。
> **追问:**
> 1. jest.mock 和 jest.spyOn 在 mock axios 时有什么区别?
> 2. 如何测试请求拦截器中的 Token 注入逻辑?
> 3. MSW 的 Service Worker 在 Node 环境下如何工作?
> 4. 如何模拟 axios 的网络超时和 5xx 错误场景?
> 5. axios-mock-adapter 能否模拟请求进度(onUploadProgress)?服务端5月29日 22:54
axios 底层是怎么实现的?核心架构和请求流程是怎样的?axios 本质是 **请求/响应拦截器管道 + 适配器模式**,核心流程为:配置合并 → 请求拦截器 → 分发请求 → 响应拦截器 → 返回结果。
**适配器模式:** axios 不直接发请求,而是通过 adapter 抽象层。浏览器环境用 `xhr.js`(基于 XMLHttpRequest),Node 环境用 `http.js`(基于 Node 的 http/https 模块)。通过 `adapter` 配置项可自定义适配器,这是 axios 跨平台的关键。
**拦截器机制:** 维护两个链式数组 `request interceptors` 和 `response interceptors`,每个拦截器有 fulfilled 和 rejected 两个回调。请求时将配置、拦截器和 adapter 按 Promise 链顺序串联执行,形成 `[req1 → req2 → ... → adapter → ... → res2 → res1]` 的管道。
**配置合并策略:** 三层配置合并——默认配置(defaults)→ 实例配置(instance.defaults)→ 请求配置(请求参数),通过 `mergeConfig` 按策略合并,headers 单独处理。
**请求流程:** `axios.request()` 是统一入口,其他方法(get/post 等)最终都调用 request。内部创建 Promise 链,将拦截器成对插入链的两端,adapter 在链中间执行实际请求,支持取消(CancelToken)和超时控制。
> **追问:**
> 1. 拦截器的执行顺序是什么?请求拦截器和响应拦截器分别是正序还是逆序?
> 2. 如何自定义 adapter 实现 Mock 数据或缓存层?
> 3. CancelToken 基于什么原理实现的?为什么新增 AbortController 方案?
> 4. mergeConfig 对不同类型的配置项(如 headers、transformRequest)采用了什么合并策略?
> 5. axios 的 Promise 链如果中间某个拦截器抛异常,后续拦截器还能执行吗?服务端5月29日 01:38
axios 和 fetch 有什么区别?什么时候该用 axios?axios 是基于 Promise 的 HTTP 客户端,相比原生 fetch 的核心优势在于:请求/响应拦截器(统一添加 token、错误处理)、自动 JSON 转换(fetch 需手动 .json())、请求超时配置(fetch 需封装 AbortController+setTimeout)、上传进度监控、XSRF 防护,以及 4xx/5xx 自动 reject(fetch 只在网络故障时才 reject)。但 fetch 是浏览器原生 API,零体积开销,且正逐步补齐能力(AbortController 已支持取消)。
## 追问
**axios 的拦截器机制是怎么实现的?**
内部维护请求和响应两个拦截器数组(数组链),发送请求时按序执行请求拦截器,收到响应后按序执行响应拦截器,本质是 Promise 链式调用。
**fetch 如何实现请求超时?**
用 AbortController 创建 signal,配合 setTimeout 调用 controller.abort(),fetch 接收 signal 参数,超时后抛出 AbortError。axios 直接配置 timeout 字段即可。
**axios 在浏览器和 Node.js 端分别用什么发送请求?**
浏览器端基于 XMLHttpRequest,Node.js 端基于 http/https 模块,通过适配器模式统一 API。fetch 在 Node 18+ 才原生支持。
**axios 如何实现 XSRF 防护?**
读取指定 cookie(默认 XSRF-TOKEN)的值,自动写入请求头(默认 X-XSRF-TOKEN),配合后端双重 cookie 验证机制防跨站请求伪造。
## 写段代码
```javascript
const instance = axios.create({
baseURL: '/api',
timeout: 5000,
});
instance.interceptors.request.use(cfg => {
cfg.headers.Authorization = `Bearer ${token}`;
return cfg;
});
instance.interceptors.response.use(
res => res.data,
err => { if (err.response?.status === 401) redirectToLogin(); }
);
```前端5月28日 03:39
什么是事件代理?原理、优缺点和应用场景是什么?事件代理(事件委托)是利用事件冒泡机制,将子元素的事件监听器统一绑定到父元素上的一种模式。面试中常从原理、优缺点、边界问题、实战场景四个层面考察。
## 核心原理
DOM 事件流经历三个阶段:捕获阶段(从 window 向下传播到目标元素)→ 目标阶段(事件到达目标元素)→ 冒泡阶段(从目标元素向上传播回 window)。事件代理利用的就是冒泡阶段——子元素触发事件后,事件沿 DOM 树逐层向上传播,因此在父元素上可以统一捕获并处理。
```javascript
// 传统方式:每个子元素各自绑定,N 个元素需要 N 个监听器
document.querySelectorAll('li').forEach(li => {
li.addEventListener('click', handler);
});
// 事件代理:只在父元素绑定一次,无论多少子元素都只需 1 个监听器
document.querySelector('ul').addEventListener('click', (e) => {
if (e.target.matches('li')) {
handler(e);
}
});
```
## 优点
- **减少内存占用**:100 个按钮只需 1 个监听器,而非 100 个,显著降低内存消耗
- **动态元素自动响应**:新增的子元素无需重新绑定,天然具备事件响应能力,特别适合动态渲染的列表
- **减少 DOM 操作**:绑定和解绑只涉及父元素,降低与 DOM 的交互次数
- **代码更易维护**:事件处理逻辑集中在父元素,修改时只需改一处
## 缺点
- **不适用于不冒泡的事件**:`focus`、`blur`、`scroll`、`mouseenter`/`mouseleave` 不冒泡,无法使用事件代理(可改用 `focusin`/`focusout`,它们冒泡)
- **嵌套元素干扰判断**:子元素内部还有子元素时,`e.target` 可能不是期望的目标元素
- **非目标点击误触发**:父元素区域内非目标元素的点击也会进入回调,需要手动过滤
- **层级过深可能被拦截**:冒泡链路中间如果调用了 `stopPropagation()`,事件无法到达代理层
## 嵌套子元素干扰如何解决
用 `e.target.closest('li')` 替代 `e.target.matches('li')`。`closest` 会沿 DOM 树向上查找最近匹配的祖先元素,即使点击的是 li 内部的 span 也能正确定位。而 `matches` 只检查元素自身,不向上查找。
```javascript
// matches 版本:点击 li 内的 span 会匹配失败
ul.addEventListener('click', (e) => {
if (e.target.matches('li')) handler(e); // 内部有 span 时失效
});
// closest 版本:点击 li 内的 span 仍能找到 li
ul.addEventListener('click', (e) => {
const li = e.target.closest('li');
if (li) handler(e);
});
```
## e.target 与 e.currentTarget 的区别
- `e.target`:实际触发事件的最深层元素(用户真正点击的那个元素)
- `e.currentTarget`:绑定监听器的元素,在事件代理中就是父元素
代理场景下两者始终不同:`e.currentTarget` 是挂载监听器的父元素,`e.target` 是用户实际点击的子元素。理解这个区别是掌握事件代理的关键。
## 实际应用场景
- **列表/表格的行点击**:导航菜单选中、数据表格行操作
- **动态表单项**:可增减的输入行、标签列表的添加与删除
- **React 合成事件体系**:React 17 前将所有事件代理到 document,17+ 代理到 root 节点,本质上就是事件代理思想在框架层的工程化实践
- **事件代理 + 防抖**:在滚动容器上代理子元素的点击,配合防抖避免误触
就近委托是最佳实践:在最近的公共父元素上代理,而非一律挂载到 document 或 body,这样可以减少不必要的事件冒泡路径和回调触发次数。前端5月28日 03:36
some、every、find、filter、map、forEach 有什么区别?这 6 个方法是 JavaScript 数组最常用的迭代方法,面试几乎必考。核心区别在于**返回值类型**和**是否短路**,按返回值分三类记忆最清晰。
## 一、遍历类(无返回值)
### forEach
纯遍历,对每个元素执行回调,返回值永远是 `undefined`。
- **不能中断**:`return` 只跳过当前回调,`break` 语法不支持,想中途退出只能用 `try/catch` 抛异常(不推荐)
- **不支持异步**:回调里写 `async/await` 不会等待 Promise,因为 `forEach` 不关心返回值
```javascript
const list = [1, 2, 3];
list.forEach(item => console.log(item)); // 1, 2, 3
// return 只跳过当次,不会中断循环
```
## 二、返回新数组
### map
每个元素经回调映射后返回**等长新数组**,不改变原数组。
```javascript
const nums = [1, 2, 3];
const doubled = nums.map(n => n * 2); // [2, 4, 6]
```
### filter
返回**满足条件的元素**组成的新数组,长度可能小于原数组,不改变原数组。
```javascript
const nums = [1, 2, 3, 4, 5];
const big = nums.filter(n => n > 3); // [4, 5]
```
## 三、返回布尔值或单个元素
### find
返回**第一个**满足条件的元素,找到即停止遍历(短路)。找不到返回 `undefined`。
```javascript
const users = [{id: 1, name: 'A'}, {id: 2, name: 'B'}];
users.find(u => u.id === 2); // {id: 2, name: 'B'}
```
### some
有**任意一个**满足条件就返回 `true`,找到即短路。全不满足返回 `false`。**空数组返回 `false`**。
```javascript
[1, 2, 3].some(n => n > 2); // true
[1, 2, 3].some(n => n > 5); // false
[].some(n => n > 0); // false
```
### every
**所有元素**都满足条件才返回 `true`,遇到不满足即短路。**空数组返回 `true`**(空真逻辑 vacuous truth)。
```javascript
[1, 2, 3].every(n => n > 0); // true
[1, 2, 3].every(n => n > 1); // false
[].every(n => n > 0); // true(空真)
```
## 四、对比速查表
| 方法 | 返回值 | 是否短路 | 空数组返回 | 链式调用 | 修改原数组 |
|------|--------|----------|-----------|---------|-----------|
| forEach | undefined | 否 | undefined | 否 | 否 |
| map | 新数组 | 否 | [] | 是 | 否 |
| filter | 新数组 | 否 | [] | 是 | 否 |
| find | 单个元素/undefined | 是 | undefined | 否 | 否 |
| some | boolean | 是 | false | 否 | 否 |
| every | boolean | 是 | true | 否 | 否 |
## 五、高频追问
### map 和 forEach 怎么选?
需要返回新数组用 `map`,纯副作用(如 console.log、DOM 操作)用 `forEach`。关键区别:`map` 可链式调用,`forEach` 返回 `undefined` 不可链式。
### some 和 includes 有什么区别?
- `includes(val)` 判断数组是否包含某个**具体值**,用严格相等(`===`)比较
- `some(fn)` 判断是否有元素满足**自定义条件**
- `includes` 只能判断值存在性,`some` 可以写任意判断逻辑
```javascript
[1, 2, 3].includes(2); // true
[1, 2, 3].some(n => n > 2); // true
[{a: 1}].includes({a: 1}); // false(引用不同)
[{a: 1}].some(o => o.a === 1); // true
```
### 这些方法支持异步回调吗?
都不原生支持。`forEach` 里写 `async/await` 不会等待 Promise resolve。需要异步迭代用 `for...of` + `await` 或 `Promise.all` + `map`。
```javascript
// 错误:forEach 不会等待 async
ids.forEach(async id => {
const data = await fetch(id); // 并发执行,不会依次等待
});
// 正确方式1:for...of
for (const id of ids) {
const data = await fetch(id);
}
// 正确方式2:Promise.all + map(并行)
const results = await Promise.all(ids.map(id => fetch(id)));
```
### find 和 filter 怎么选?
只需第一个匹配用 `find`(性能更好,短路),需要所有匹配用 `filter`。
### reduce 为什么没列进来?
`reduce` 是这 6 个方法的基础——`map`、`filter`、`some`、`every`、`find` 都可以用 `reduce` 实现。面试中常追问 reduce 的用法,但 reduce 更偏向"累加器"模式,功能更强大也更复杂,属于另一个考点的范畴。前端5月28日 03:36
ES5 和 ES6 有什么区别?ES6(ES2015)是 JavaScript 历史上最大的一次版本更新,面试中这道题考查的是你对 JS 语言演进的理解深度。回答的关键不是罗列特性,而是讲清楚**每个变化解决了什么问题**。
## 变量声明:从 var 到 let/const
ES5 只有 `var`,存在两大问题:
```js
// 问题1:变量提升
console.log(a); // undefined(不会报错,但容易出 bug)
var a = 1;
// 问题2:无块级作用域
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3, 3, 3
}
```
ES6 用 `let`/`const` 解决了这两个问题:
```js
// let 有块级作用域 + 暂时性死区
console.log(b); // ReferenceError(声明前访问直接报错)
let b = 1;
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0, 1, 2
}
// const 不可重新赋值(但对象属性仍可修改)
const obj = { a: 1 };
obj.a = 2; // OK
obj = { a: 2 }; // TypeError
```
**面试要点**:`const` 保证的是绑定不可变,不是值不可变。想冻结对象用 `Object.freeze()`。
## 函数:箭头函数与 this 绑定
ES5 中 `this` 指向取决于调用方式,经常需要 `var self = this` 或 `.bind(this)`:
```js
// ES5
var obj = {
name: 'ES5',
say: function() {
var self = this;
setTimeout(function() {
console.log(self.name); // 必须用 self/cache
}, 0);
}
};
// ES6 — 箭头函数继承外层 this
const obj2 = {
name: 'ES6',
say() {
setTimeout(() => {
console.log(this.name); // 直接用 this
}, 0);
}
};
```
**注意**:箭头函数没有自己的 `arguments`、`super`、`new.target`,不能用作构造函数。
## 字符串:模板字符串
```js
// ES5
var greeting = 'Hello, ' + name + '! You are ' + age + ' years old.';
// ES6
const greeting = `Hello, ${name}! You are ${age} years old.`;
```
模板字符串支持多行、变量插值、标签模板,彻底告别字符串拼接。
## 解构赋值与展开运算符
```js
// 对象解构
const { name, age } = user;
// 数组解构
const [first, ...rest] = [1, 2, 3, 4]; // first=1, rest=[2,3,4]
// 展开运算符 — 浅拷贝与合并
const copy = [...arr];
const merged = { ...defaults, ...config };
```
解构让数据提取更简洁,展开运算符替代了 `Object.assign` 和 `concat` 的大多数场景。
## 类与继承:class 语法
```js
// ES5 — 构造函数 + 原型链
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return this.name + ' makes a sound';
};
// ES6 — class 语法
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
}
class Dog extends Animal {
speak() {
return `${this.name} barks`;
}
}
```
`class` 本质是原型继承的语法糖,但有行为差异:内部默认严格模式、方法不可枚举、必须用 `new` 调用。
## 模块系统:import/export
```js
// ES5 — CommonJS(Node.js)
const module = require('./module');
module.exports = { foo };
// ES6 — ES Modules
import { foo } from './module';
export const bar = 1;
export default function() {}
```
ES Modules 是静态的,支持 Tree Shaking;CommonJS 是动态的,运行时加载。现代项目(Vite/Webpack)均以 ESM 为优先。
## 异步编程:Promise 与 async/await
```js
// ES5 — 回调地狱
getData(function(a) {
getMore(a, function(b) {
getEvenMore(b, function(c) {
console.log(c);
});
});
});
// ES6 — Promise 链式调用
getData()
.then(a => getMore(a))
.then(b => getEvenMore(b))
.then(c => console.log(c));
// ES8 — async/await(同步写法)
const a = await getData();
const b = await getMore(a);
const c = await getEvenMore(b);
```
Promise 解决了回调地狱,async/await 让异步代码看起来像同步,是面试高频追问点。
## 新数据结构与 API
| 特性 | 用途 |
|------|------|
| `Map` | 键值对集合,键可以是任意类型(Object 的键只能是字符串/Symbol) |
| `Set` | 去重数组:`[...new Set(arr)]` |
| `WeakMap/WeakSet` | 键是弱引用,适合缓存和关联私有数据,不阻止 GC |
| `Symbol` | 创建唯一标识符,用于私有属性和内置协议 |
| `Proxy/Reflect` | 拦截对象操作(Vue 3 响应式核心) |
| `Generator/Iterator` | 可暂停函数,`for...of` 遍历统一接口 |
## 追问
### ES6 之后还有什么重要的新特性?
| 版本 | 关键特性 |
|------|----------|
| ES7 | `Array.prototype.includes`、指数运算符 `**` |
| ES8 | `async/await`、`Object.values/entries` |
| ES9 | `Promise.finally`、异步迭代 `for await...of` |
| ES10 | `flat/flatMap`、`Object.fromEntries` |
| ES11 | `??`(空值合并)、`?.`(可选链)、`Promise.allSettled` |
| ES12 | `replaceAll`、逻辑赋值 `||=` `&&=` `??=` |
| ES13 | `at()`、`Object.hasOwn`、Top-level await |
### let/const 和 var 最大的实际区别?
1. **块级作用域** — 解决 for 循环闭包问题
2. **暂时性死区** — 声明前访问报 ReferenceError,var 是 undefined
3. **不可重复声明** — 同一作用域内 let/const 不能重复声明同名变量
4. **const 不可重新赋值** — 但对象/数组内容仍可修改
### class 只是语法糖吗?
基本是。`class` 编译后就是原型链模式(构造函数 + prototype + Object.create)。但有几个行为差异:
- class 内部默认严格模式
- class 方法不可枚举(`for...in` 遍历不到)
- 只能用 `new` 调用(有 `new.target` 检查,直接调用报错)
- `extends` 内部用 `Object.create` 设置原型链,比 ES5 手动写更规范
### 面试回答策略
面试官问这道题,不是让你背特性列表。推荐的回答结构:
1. **一句话概括**:ES6 让 JS 从脚本语言变成工程化语言
2. **按类别讲 3-4 个重点**,每个说清楚"ES5 什么问题 → ES6 怎么解决"
3. **追问时深入**:挑一个你最熟悉的特性展开(如 class 的原型链原理、Promise 的微任务机制)前端5月28日 03:35
ES6 中的 Map 和原生的 Object 有什么区别?Map 和 Object 都能存键值对,但 Map 是专门为"字典"场景设计的,解决了 Object 做字典时的几个硬伤。
**键的类型**:Object 的 key 只能是字符串或 Symbol,数字 1 和字符串 "1" 是同一个 key。Map 的 key 可以是任意类型——对象、函数、NaN 都行,用 SameValueZero 算法比较(NaN 等于 NaN)。
**原型链污染**:Object 有原型链,`obj.__proto__`、`obj.toString` 这类属性名会冲突。`Object.create(null)` 能规避,但写法不直觉。Map 天然没有这个问题。
**大小**:Map 有 `size` 属性直接取。Object 要 `Object.keys(obj).length`。
**顺序**:Map 严格按插入顺序迭代。Object 在 ES6 后基本也按插入顺序,但整数 key 会被提前排列,容易踩坑。
**遍历**:Map 直接 `for...of` 或 `forEach`。Object 要先转数组(`Object.entries()`)或用 `for...in`(还会遍历原型链)。
**性能**:频繁增删键值对时 Map 更快。Object 在 V8 中对连续整数 key 有快属性优化,但这种优化对字典场景没帮助。
**序列化**:`JSON.stringify` 能直接处理 Object。Map 不行,需要先转成数组或对象。
```js
const m = new Map();
const obj = {};
m.set(obj, 'value'); // 对象做 key,Object 做不到
m.set(1, 'num');
m.set('1', 'str'); // 1 和 '1' 是不同 key
console.log(m.size); // 3
```
一句话:需要字典数据结构时优先用 Map,需要 JSON 序列化或简单配置对象时用 Object。
## 追问
### WeakMap 和 Map 有什么区别?
WeakMap 的 key 必须是对象,值任意。key 是弱引用——被 GC 回收后对应条目自动消失。不可迭代(没有 `size`、`forEach`、`keys()`),因为条目随时可能被回收。
| | Map | WeakMap |
|---|---|---|
| key 类型 | 任意 | 仅对象 |
| 引用方式 | 强引用 | 弱引用 |
| 可迭代 | 是 | 否 |
| size | 有 | 无 |
| 典型场景 | 字典存储 | 关联私有数据 |
### 项目里 WeakMap 用在什么地方?
Vue 3 的响应式系统用 WeakMap 存对象 → 依赖关系,对象被销毁时依赖自动清理,不会内存泄漏。另一个常见场景:给 DOM 节点绑定额外数据,节点移除后数据自动释放。
### Object.create(null) 能替代 Map 吗?
能解决原型链污染问题,但解决不了键类型限制、size 获取、顺序保证、迭代便利性。Map 是更完整的方案。
### Map 的 key 用 NaN 会怎样?
Map 用 SameValueZero 算法比较键,NaN 等于 NaN,所以 NaN 可以正常作为 key,且不会重复。Object 中 NaN 作为 key 会被转成字符串 "NaN",行为一致,但 Map 的语义更明确。前端5月28日 03:34
前端模块规范有哪些?模块如何异步加载?JavaScript 模块化经历了从全局变量污染到标准化模块系统的漫长演进,不同规范解决了不同阶段的问题。
## IIFE:最早的模块化尝试
在规范出现之前,开发者用立即执行函数表达式创建独立作用域:
```javascript
var MyModule = (function() {
var privateVar = 'hidden';
function privateMethod() { return privateVar; }
return {
publicMethod: function() { return privateMethod(); }
};
})();
```
IIFE 通过闭包隔离内部变量,只暴露全局接口。缺点是依赖关系靠全局变量传递,script 标签顺序一旦出错就全局崩溃。
## CommonJS:Node.js 的选择
```javascript
// math.js
module.exports = { add: (a, b) => a + b };
// main.js
const { add } = require('./math');
console.log(add(1, 2));
```
CommonJS 用 `require` 同步加载模块,`module.exports` 导出。核心特征:**运行时加载**,`require` 执行时才确定依赖;**输出值的拷贝**,模块内部变化不会影响已导入的值;`this` 指向当前模块。
同步加载在服务端不是问题——文件在本地磁盘,读取极快。但在浏览器中,模块要从网络下载,同步阻塞会让页面卡死。
## AMD:为浏览器而生
```javascript
define(['jquery', './utils'], function($, utils) {
return {
init: function() { $('body').append(utils.format()); }
};
});
```
AMD(Asynchronous Module Definition)用 `define` 声明模块和依赖,依赖在回调执行前全部加载完成。RequireJS 是最知名的实现。依赖必须前置声明,不管是否马上用到都会先加载。
## CMD:依赖就近
```javascript
define(function(require, exports, module) {
var $ = require('jquery'); // 用到时才加载
exports.init = function() { $('body').append('hello'); };
});
```
CMD 由 SeaJS 推广,和 AMD 的核心区别是依赖就近声明——只有执行到 `require` 时才加载对应模块。两者在浏览器端都已退出主流,被 ESModule 取代。
## UMD:兼容方案
```javascript
(function(root, factory) {
if (typeof exports === 'object') module.exports = factory();
else if (typeof define === 'function') define(factory);
else root.MyModule = factory();
})(this, function() {
return { version: '1.0' };
});
```
UMD 判断运行环境,兼容 CommonJS、AMD 和全局变量三种方式。库开发者打包时常用,确保代码在任何环境都能正常加载。
## ESModule:统一标准
```javascript
// math.js
export const add = (a, b) => a + b;
// main.js
import { add } from './math.js';
```
ESModule 是 JavaScript 语言层面的模块标准。与 CommonJS 的关键区别:**编译时静态分析**,`import/export` 必须在顶层,引擎在执行前就确定依赖关系;**输出值的引用**,模块内部变化会同步反映到导入方;顶层 `this` 为 `undefined`;天然支持 Tree-Shaking。
## 模块异步加载
异步加载的核心场景是按需加载——首屏不需要的代码延迟到使用时再请求,减少初始包体积。
**ESModule 动态导入**:`import()` 返回 Promise,可在任意位置调用,是实现代码分割和路由懒加载的标准方式:
```javascript
button.addEventListener('click', async () => {
const { openDialog } = await import('./dialog.js');
openDialog();
});
```
**AMD 异步加载**:`require([deps], callback)` 本身就是异步的,依赖列表中的模块并行下载后再执行回调。
**CommonJS**:`require` 本身是同步的,不支持浏览器原生的异步加载。但打包工具(Webpack 等)可以将 `import()` 语法编译为 CommonJS 环境下的异步加载 chunk。
## 追问
### import() 和顶层 import 有什么区别?
顶层 `import` 是静态声明,必须在模块顶层,编译时确定依赖关系,引擎可以做静态分析和 Tree-Shaking。`import()` 是动态函数调用,返回 Promise,可以在任何位置调用,运行时才加载模块。后者用于代码分割、路由懒加载等按需加载场景。静态 `import` 在严格模式下还会被提升到模块顶部执行。
### 为什么浏览器不支持 CommonJS 的 require?
`require` 是同步调用——读取文件、编译、执行,然后返回结果。在服务端文件在本地磁盘上,同步读取耗时可以忽略;但在浏览器里模块要从网络下载,网络延迟不可控,同步阻塞意味着页面卡死直到所有依赖下载完成。AMD 和 ESModule 都采用异步加载模型,不阻塞主线程。
### 库作者应该发布什么格式?
ESM + CJS 双格式——`package.json` 的 `exports` 字段同时声明两种格式的入口,CJS 兼容老工具和老版本 Node.js,ESM 支持 Tree-Shaking 和静态分析。典型配置:
```json
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}
```
单独只发 CJS 会丢失 Tree-Shaking 能力,单独只发 ESM 会排除不支持 ESM 的旧环境。双格式是当前最稳妥的方案。前端5月28日 03:32
React 组件抽离公共逻辑代码有哪些方式?React 逻辑复用经历了三代方案的演进:Mixin → HOC / Render Props → Hooks。Mixin 已随 Class 组件淘汰,当前面试重点在后面三种。
## HOC(高阶组件)
函数接受一个组件,返回增强后的新组件:
```jsx
function withAuth(WrappedComponent) {
return function AuthComponent(props) {
const isAuthenticated = checkAuth();
return isAuthenticated
? <WrappedComponent {...props} />
: <Navigate to="/login" />;
};
}
// 使用
const ProtectedPage = withAuth(Dashboard);
```
**核心问题**:
- **Wrapper Hell**:多层 HOC 嵌套后,DevTools 里组件树极深,调试困难
- **Props 来源不透明**:`<WrappedComponent {...props} />` 透传的 props 来自哪里不直观,容易命名冲突
- **静态方法丢失**:HOC 返回新组件,原组件的静态方法不会自动复制,需要 `hoist-non-react-statics` 手动提升
- **Ref 丢失**:ref 不属于 props,会被绑定到外层 HOC 组件而非原组件,需配合 `React.forwardRef` 转发
## Render Props
组件接受一个返回 React 元素的函数 prop,由该函数决定渲染内容:
```jsx
function Mouse({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener(mousemove, handler);
return () => window.removeEventListener(mousemove, handler);
}, []);
return render(pos);
}
// 使用
<Mouse render={pos => <Cursor pos={pos} />} />
```
**核心问题**:
- **嵌套地狱**:多个 Render Props 嵌套时,回调层级极深,可读性急剧下降
- **性能隐患**:每次父组件渲染,render 函数都会重新创建,导致子组件不必要的重渲染,需要额外做 `useCallback` 优化
## Hooks(推荐)
在函数组件内调用自定义 Hook,逻辑与 UI 完全分离,无组件层级嵌套:
```jsx
function useAuth() {
const [user, setUser] = useState(null);
useEffect(() => {
const unsub = onAuthStateChanged(setUser);
return unsub;
}, []);
return user;
}
// 使用
function Dashboard() {
const user = useAuth();
if (!user) return <Navigate to="/login" />;
return <main>...</main>;
}
```
**Hooks 的注意事项**:
- 不能在条件语句、循环或嵌套函数中调用——React 依靠调用顺序匹配 Fiber 链表上的 Hook 节点
- 闭包陷阱:`useEffect` 内部如果引用了 state 但未加入依赖数组,回调中捕获的始终是旧值,需用 `useRef` 或函数式更新 `setState(prev => prev + 1)` 解决
## 三种方案对比
| 维度 | HOC | Render Props | Hooks |
|------|-----|-------------|-------|
| 组件嵌套 | 多层包裹 | 回调嵌套 | 无嵌套 |
| Props 透明度 | 来源不透明 | 显式传递 | 显式调用 |
| 类型推导 | 困难(泛型丢失) | 较好 | 好 |
| 适用场景 | 旧代码维护、Class 组件 | 旧代码维护 | 新代码首选 |
三种方式的核心思想一致——**把可复用逻辑从 UI 中分离**。Hooks 胜在零组件嵌套、逻辑内聚、类型友好,是当前最佳实践。
## 追问
### 为什么 Hooks 不能放在条件语句里?
React 用 Fiber 节点上的链表结构存储 Hook 状态。每次渲染时,Hook 按调用顺序依次匹配链表上的节点。如果某个 Hook 在某次渲染被跳过,后续 Hook 就会错位匹配到前一个 Hook 的状态节点,导致状态混乱。这是 React 内部实现机制决定的,而非 API 设计限制。
### HOC 还用在哪些场景?
- `React.memo`(性能优化,浅比较 props)
- `connect(mapStateToProps, mapDispatchToProps)`(Redux v5 以前)
- `withRouter`(React Router v5)
- 权限控制:`withAuth(ProtectedComponent)`
- 日志/埋点:`withTracker(InteractiveComponent)`
### 如何把 Class 组件中的 HOC 迁移到 Hooks?
| HOC 模式 | Hooks 替代 |
|----------|-----------|
| `withRouter` | `useNavigate()` + `useLocation()` + `useParams()` |
| `connect()` | `useSelector()` + `useDispatch()` |
| `withAuth` | 自定义 `useAuth()` |
| `withTracker` | 自定义 `useTracker()` + `useEffect` |
| 通用 HOC | 自定义 Hook + 组件内直接调用 |
### Hooks 有哪些常见陷阱?
1. **闭包陷阱**:`useEffect` 中引用了 state 但依赖数组遗漏,回调拿到旧值。用 `useRef` 存最新值或函数式更新解决
2. **无限渲染**:`useEffect` 依赖项传入每次新建的对象/数组引用,用 `useMemo` 稳定引用
3. **依赖缺失**:遗漏依赖导致 effect 不按预期执行,启用 `eslint-plugin-react-hooks` 的 `exhaustive-deps` 规则自动检查前端5月28日 03:31
React setState 是同步还是异步?原理是什么?`setState` 并非真正“异步”——它是**批量延迟执行**。调用 `setState` 时,React 把更新对象推入当前 Fiber 节点的 `updateQueue`(环形链表),然后调度一次重新渲染,而不是立即修改 state 和触发 DOM 更新。等调度机制在下一个工作单元执行时,才遍历 `updateQueue` 计算新 state 并渲染。
批量更新的核心逻辑:同一个事件循环内的多次 `setState`,只会产生一次渲染调度。合并发生在遍历 `updateQueue` 阶段——React 依次执行每个 update 的计算函数,得到最终 state,再进入 render。
```javascript
// 直接值更新:三次调用,updateQueue 里三个 update
// 但都基于同一个闭包中的 count,最终 count = 0 + 1 = 1
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// 函数式更新:每个 update 拿到前一个 update 的结果
// count = ((0+1)+1)+1 = 3
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
```
## 追问
### setState 是同步还是异步?
都不是。它本质是**同步入队 + 延迟渲染**。React 17 中,事件处理器外(setTimeout、原生事件)没有批量机制,`setState` 后能立即在同步代码中读到新值,看起来像“同步”——但这不是真正的同步,只是批量边界不同。React 18 用 `createRoot` 统一了所有场景的批处理。
### React 18 自动批处理的原理是什么?
React 18 引入新调度入口 `scheduleUpdateOnFiber`,替代了原来的 `enqueueSetState`。无论更新来自事件处理器、setTimeout 还是 Promise,都走同一条调度路径。内部用 **lane 模型**(替代 expiration time)管理优先级,`Scheduler` 模块按优先级安排回调执行时机,实现所有场景统一批处理。
### 什么情况下 setState 后组件不会重新渲染?
**bailout 机制**:React 在渲染前会比较新旧 state(浅比较)。如果值没变,直接跳过该组件的渲染。常见陷阱——`setState(obj)` 传入同一个引用,浅比较相等,不会触发更新。必须创建新对象:`setState({...obj, key: newVal})`。
### 函数式更新什么场景必须用?
依赖前一个 state 时必须用。典型场景:计数器、队列操作(往数组追加元素)。函数式更新能保证每次拿到的都是链表中上一个 update 计算后的最新值,而不是闭包捕获的旧值。前端5月28日 03:31
浏览器渲染页面的过程是怎样的?浏览器从收到 HTML 到画出画面,走的是这条流水线:**DOM 树 → CSSOM 树 → Render 树 → Layout → Paint → Composite**。面试时把这六个词按顺序说出来,再展开每步做了什么,就够了。
1. **解析 HTML → DOM 树**:字节流 → 字符 → Token → 节点 → DOM 树。遇到 `<script>` 暂停解析,下载执行完 JS 再继续——因为 JS 可能改 DOM。CSS 和图片不阻塞 DOM 构建,但 **CSS 会阻塞后续 JS 执行**:JS 可能读 `getComputedStyle()`,浏览器必须等 CSSOM 好了才让 JS 跑。所以 CSS 放 head 不只是避免 FOUC(无样式内容闪烁),还防止 JS 等待 CSS 造成的卡顿。
2. **解析 CSS → CSSOM 树**:样式表从右向左匹配选择器(`.a .b p` 先找所有 `p` 再逐级向上匹配),构建 CSSOM。CSS 不阻塞 DOM 构建,但阻塞渲染——CSSOM 没好之前页面白屏。
3. **DOM + CSSOM → Render 树**:只包含可见节点。`display: none` 连 Render 树都进不了(不占空间),`visibility: hidden` 占位不可见,还在树里。伪元素 `::before`/`::after` 也会进 Render 树。
4. **Layout(重排/回流)**:计算每个可见节点的精确位置和尺寸。首次叫 Initial Layout,后续改动触发 Reflow。一个元素的几何属性变了,可能级联触发整个子树重排——这就是为什么频繁操作 DOM 性能差。
5. **Paint(重绘)**:把 Layout 结果光栅化成像素。改 `color`、`background` 只触发重绘,不触发 Layout。重排一定触发重绘,反过来不会。
6. **Composite(合成)**:浏览器把页面分成多个图层(层叠上下文、`will-change`、3D transform 等会创建新层),GPU 合成各层。`transform` 和 `opacity` 的变化只走合成,跳过 Layout 和 Paint,所以动画性能最好——即使主线程卡死也能保持流畅。
## 追问
### CSS 放 head,JS 放底部——还有更好的方案吗?
CSS 必须放 head,没商量。JS 有三个选择:放底部(简单粗暴)、`defer`(下载不阻塞解析,DOM 构建完按顺序执行,推荐)、`async`(下载不阻塞,下载完立刻执行,顺序不可控,适合统计脚本)。`defer` 和 `async` 只对外部 `<script src>` 有效,内联脚本不支持。
### 重排和重绘哪个更贵?怎么减少重排?
重排贵得多——要重算布局,可能级联影响子树;重绘只更新像素。减少重排的实战方法:用 class 切换代替逐条改 style、离线操作 DOM(DocumentFragment 克隆节点改完再插回去)、读写分离(先把 `offsetWidth` 等布局信息读完缓存到变量,再批量写样式,避免强制同步布局)。Chrome DevTools Performance 面板里紫色 Layout 块高频出现,就说明重排有问题。
### 为什么 transform 动画比 top/left 流畅?
transform 和 opacity 在合成器线程(Compositor Thread)处理,跟主线程无关。主线程被 JS 堵住时合成器照样跑,动画不卡。`top`/`left` 动画走 Layout → Paint → Composite 全流程,绑死主线程。一句话:动画用 transform,别用 top/left。
### 关键渲染路径如何优化?
目标是缩短首屏白屏时间。三件事:减少关键资源数量(CSS 内联首屏样式、JS 用 defer)、减少关键资源体积(压缩 + Brotli)、减少关键路径往返(`<link rel="preload">` 预加载关键资源)。用 Lighthouse 看 FCP 和 LCP 两个指标,直接反映渲染路径优化效果。
### 哪些操作会触发重排?
改几何属性:`width`/`height`/`padding`/`margin`/`top`/`left`。改 DOM 结构:`appendChild`/`removeChild`。读布局信息也会:`offsetWidth`/`scrollTop`/`getComputedStyle()`——浏览器被迫立刻算出最新值,强制同步布局。最坑的是循环里交替读写:`for` 里先读 `offsetWidth` 再改 `style.width`,每次迭代都触发一次重排,性能灾难。前端5月28日 03:29
ES6 类继承中 super 关键字的作用是什么?`super` 在 ES6 类继承中有两种用法:**作为函数调用**和**作为对象引用**。核心要点是——`super()` 调用父类构造函数,`super.method()` 调用父类原型方法,`super.staticMethod()` 在静态方法中调用父类静态方法。
## super() 作为函数调用
在子类 `constructor` 中,`super()` 调用父类构造函数。ES6 的继承机制规定:父类负责创建 `this` 对象,子类负责在此基础上添加属性。因此 `super()` 必须在 `this` 之前调用,否则会抛出 `ReferenceError`。
```javascript
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 必须先调 super,否则下面用 this 会报错
this.age = age;
}
}
```
如果子类没有显式定义 `constructor`,引擎会自动插入一个默认的 `constructor(...args) { super(...args); }`。
## super.method() 作为对象引用
在子类普通方法中,`super` 指向父类的 `prototype`,可以调用父类原型上的方法:
```javascript
class Parent {
greet() {
return 'hello from Parent';
}
}
class Child extends Parent {
greet() {
return super.greet() + ' and Child';
}
}
new Child().greet(); // "hello from Parent and Child"
```
## 静态方法中的 super
在子类静态方法中,`super` 指向父类本身(而非 `prototype`),因此可以调用父类的静态方法:
```javascript
class Parent {
static create() {
return new this();
}
}
class Child extends Parent {
static create() {
return super.create(); // 调用 Parent.create()
}
}
```
## super 的内部指向总结
| 使用场景 | super 指向 |
|---------|-----------|
| `super()` 在 constructor 中 | 父类构造函数 |
| `super.method()` 在普通方法中 | `Parent.prototype` |
| `super.method()` 在静态方法中 | 父类本身(`Parent`) |
| `super.x = value` | 触发父类原型上的 setter(如果有) |
## 追问:子类 constructor 为什么必须先调 super?
ES6 类的继承与 ES5 的寄生组合继承有本质区别。ES5 中是先创建 `this`(子类自己的对象),再用 `Parent.apply(this)` 借用父类构造函数挂属性。ES6 反过来了——由父类构造函数先创建并初始化 `this`,子类再修改。这个顺序由 `new.target` 控制:当 `new Child()` 执行时,`new.target` 是 `Child`,但 `this` 的创建权在 `Parent` 那里。`super()` 执行后 `this` 才可用。
## 追问:super.x = value 有什么陷阱?
给 `super` 的属性赋值时,并不会像直觉那样去修改父类原型上的属性。实际行为是:如果父类原型上定义了该属性的 `setter`,赋值操作会触发那个 `setter`,`this` 指向当前子类实例;如果没有 `setter`,则相当于直接在 `this` 上创建属性:
```javascript
class Parent {
set x(val) {
console.log('setter called with', val);
}
}
class Child extends Parent {
setX() {
super.x = 42; // 触发 Parent.prototype 的 setter
}
}
new Child().setX(); // "setter called with 42"
```
## 追问:ES6 继承与 ES5 原型继承的区别
| | ES5 寄生组合继承 | ES6 class 继承 |
|--|----------------|---------------|
| this 创建 | 子类先创建 this,再借用父类 | 父类构造函数创建 this |
| super | 无,用 Parent.call(this) | super() 必须 |
| 原型链 | 手动 Object.create(Parent.prototype) | extends 自动建立 |
| 静态方法 | 不会继承 | 自动继承 |
| new.target | 不存在 | 控制实例化行为 |
前端5月28日 03:29
如何实现 Web 图片懒加载?面试官问:如何实现 Web 图片懒加载?
图片懒加载的核心思路:图片不在视口内时不加载,滚动到接近视口时再加载,减少首屏请求数和带宽消耗。
## 答案
现代前端有三种主流实现方式,按推荐优先级排列:
### 1. 原生 `loading="lazy"`(首选)
```html
<img src="image.jpg" loading="lazy" alt="描述文字" />
```
一行搞定。Chrome 76+、Edge、Firefox 均支持,Safari 15.4+ 也已支持。浏览器自动根据视口距离判断加载时机,无需 JS。
**关键细节**:对首屏内的图片不要加 `loading="lazy"`,否则可能延迟 LCP。建议只对视口下方的图片使用。
### 2. Intersection Observer(兼容方案)
```javascript
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img); // 加载完停止观察
}
});
},
{ rootMargin: '200px' } // 提前 200px 开始加载,减少白屏
);
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
```
`rootMargin` 可以让图片在进入视口前就开始加载,用户体验更平滑。`threshold` 控制交叉比例触发阈值,懒加载场景一般用默认 0 即可。
### 3. scroll 事件监听(不推荐)
```javascript
// 需要配合 throttle 使用
function throttle(fn, delay) {
let timer = null;
return function (...args) {
if (timer) return;
timer = setTimeout(() => { fn.apply(this, args); timer = null; }, delay);
};
}
window.addEventListener('scroll', throttle(() => {
document.querySelectorAll('img[data-src]').forEach(img => {
if (img.getBoundingClientRect().top < window.innerHeight + 200) {
img.src = img.dataset.src;
}
});
}, 200));
```
**为什么不推荐**:scroll 事件在主线程频繁触发,即使 throttle 也无法避免 `getBoundingClientRect` 触发强制重排(reflow)。Intersection Observer 在浏览器合成器线程异步执行,完全不阻塞主线程,性能差距显著。
## 方案对比
| 方案 | 兼容性 | 性能 | 代码量 | SEO 友好 |
|------|--------|------|--------|----------|
| `loading="lazy"` | Chrome 76+, Safari 15.4+ | 最优 | 1 行 | 最佳 |
| Intersection Observer | 所有现代浏览器 | 优 | ~10 行 | 需配合 noscript |
| scroll 监听 | 全兼容 | 差 | ~20 行 | 需配合 noscript |
## 追问
### Intersection Observer 和 scroll 监听有什么区别?
核心区别在执行线程和触发机制:
- **执行线程**:scroll 监听在主线程同步执行,频繁触发即使 throttle 也会有性能开销;Intersection Observer 在浏览器合成器线程异步执行,不阻塞主线程
- **位置计算**:scroll 监听需要手动调用 `getBoundingClientRect`,每次调用触发强制重排;Intersection Observer 由浏览器内部计算,无重排开销
- **触发精度**:scroll 监听依赖 throttle 间隔,可能错过或延迟;Intersection Observer 在元素进入视口时精确触发
- **内存管理**:Intersection Observer 的 `unobserve` 语义清晰;scroll 监听需要手动维护已加载列表
### 懒加载对 SEO 有影响吗?
有影响。搜索引擎爬虫一般不执行 JS,`data-src` 中的图片地址对爬虫不可见。解决方案:
1. **`loading="lazy"` 原生方式**:爬虫能识别 `src` 属性,SEO 影响最小
2. **`<noscript>` 提供替代**:
```html
<img data-src="image.jpg" alt="描述" />
<noscript><img src="image.jpg" alt="描述" /></noscript>
```
3. **SSR/SSG 时直接渲染 `src`**:服务端渲染时输出完整 `src`,客户端再按需懒加载
4. **配合 `srcset` + `sizes`**:让浏览器和爬虫都能选择合适尺寸
### 懒加载图片会导致 CLS 问题吗?
会。图片加载前高度为 0,加载后撑开页面内容产生布局偏移——这是 CLS 扣分的主要原因。解决办法:
```css
/* 方案一:aspect-ratio(推荐) */
.img-wrapper {
aspect-ratio: 16 / 9;
width: 100%;
}
/* 方案二:padding-bottom 百分比技巧(兼容旧浏览器) */
.img-wrapper {
position: relative;
width: 100%;
padding-bottom: 56.25%; /* 16:9 = 9/16 * 100% */
}
.img-wrapper img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
```
也可以用固定高度的占位容器或低分辨率模糊占位图(LQIP)预留空间。
### 首屏图片应该懒加载吗?
不应该。首屏图片是 LCP 关键元素,懒加载会延迟加载时机,直接拖慢 LCP 指标。正确做法:
1. 首屏图片直接设置 `src`,并加上 `fetchpriority="high"` 提升优先级
2. 对首屏以下图片使用 `loading="lazy"` 或 Intersection Observer
3. 可以结合 `preload` 提前加载首屏关键图片
```html
<!-- 首屏:不懒加载,提升优先级 -->
<link rel="preload" as="image" href="hero.jpg" />
<img src="hero.jpg" fetchpriority="high" alt="首屏主图" />
<!-- 非首屏:懒加载 -->
<img src="below-fold.jpg" loading="lazy" alt="描述" />
```
## 最佳实践总结
1. 优先使用原生 `loading="lazy"`,兼容性已足够
2. 需要精细控制时用 Intersection Observer + `rootMargin`
3. 避免对首屏图片懒加载
4. 必须预留图片空间防止 CLS
5. SEO 敏感场景用 `<noscript>` 或 SSR 兜底
6. 废弃 scroll 监听方案