业务场景代码✅
统计前端请求耗时
答案
| 方法类别 | 技术手段/工具 | 实现方式简述 | 代码示例/要点 | 优缺点说明 |
|---|---|---|---|---|
| 初级 | fetch + 时间戳 | 请求前后用 performance.now() 记录时间,计算差值即为耗时 | const start = performance.now(); fetch(...).then(() => { const end = performance.now(); ... }) | 简单易用,受主线程阻塞影响,统计不一定准确 |
| 初级 | XMLHttpRequest + 时间戳 | 请求前后用 performance.now() 记录时间,事件回调中计算耗时 | xhr.onload = function () { ... } | 同上 |
| 初级 | axios 拦截器 | 请求拦截器记录开始时间,响应拦截器记录结束时间,计算耗时 | axios.interceptors.request.use(...) | 适合 axios 用户,易集成 |
| 进阶 | Performance API | 用 performance.timing 或 performance.getEntriesByType('resource') 获取详细性能数据 | performance.timing.responseEnd - performance.timing.requestStart | 能统计页面各阶段耗时,适合分析整体性能 |
| 高级 | Web Worker | 在 Worker 线程中发起请求并统计耗时,主线程不被阻塞 | Worker 文件中用 performance.now() 统计,主线程通过 postMessage 获取结果 | 不阻塞主线程,适合高并发或复杂场景 |
说明:
- 初级手段适合简单场景,易于实现,但统计结果可能受主线程阻塞影响。
- 进阶和高级手段能获得更准确或更详细的性能数据,适合对性能要求较高的业务场景。
cookie 解析函数
答案
核心概念
HTTP Cookie 在前端通过 document.cookie 获取,表现为由分号和空格分隔的键值对字符串(如 "user=john; token=abc%3D123; logged_in=true"):
- 键值拆分:按
;切分出各个独立的条目。 - 解码处理:Cookie 的键与值可能经过 URL 编码,必须使用
decodeURIComponent正确解码。 - 边界防御:处理值中包含等号(如 Base64 编码的 token 包含
=)的情况,需只在第一个=处分割键与值。
手写实现:
function parseCookie (cookieStr = document.cookie) {
const result = {}
if (!cookieStr || typeof cookieStr !== 'string') {
return result
}
const pairs = cookieStr.split(';')
for (const pair of pairs) {
const trimmed = pair.trim()
if (!trimmed) continue
const equalIndex = trimmed.indexOf('=')
if (equalIndex === -1) {
// 只有 key 没有 value
result[decodeURIComponent(trimmed)] = ''
} else {
const key = decodeURIComponent(trimmed.slice(0, equalIndex).trim())
const val = decodeURIComponent(trimmed.slice(equalIndex + 1).trim())
result[key] = val
}
}
return result
}
// 验证
const rawCookie = 'username=Alice; token=xyz%3D%3D; theme=dark; empty_key'
console.log(parseCookie(rawCookie))
// 输出: { username: 'Alice', token: 'xyz==', theme: 'dark', empty_key: '' }
面试官视角
- 考查对真实业务边界的敏锐度:切勿简单使用
item.split('='),否则 Base64 编码的 Cookie 值会被错误切碎。
JS 执行 100 万个任务, 如何保证浏览器不卡顿?
答案
| 方法 | 实现方式简述 | 代码要点/示例 | 优缺点说明 |
|---|---|---|---|
| Web Worker | 将任务转移到 Worker 线程,主线程不阻塞 | new Worker('worker.js') | 彻底避免主线程卡顿,适合大量计算 |
| requestAnimationFrame | 将大任务分割为小块,每帧处理一部分,保持页面流畅 | requestAnimationFrame(fn) | 简单易用,适合与渲染相关的分步任务 |
| 动态 chunkSize | 动态调整每帧处理任务量,保证每帧耗时不超限 | 根据 performance.now() 动态调整 | 进一步优化不卡顿体验,适应不同设备性能 |
| requestIdleCallback | 在浏览器空闲时分批处理任务,避免影响主线程响应 | requestIdleCallback(fn) | 最大化利用空闲时间,兼容性需注意 |
- Web Worker 适合 CPU 密集型任务,完全不阻塞主线程。
- requestAnimationFrame/requestIdleCallback 适合与 UI 相关或需分步处理的任务。
- 动态 chunkSize 可进一步提升分块处理的平滑性。
- 可根据业务场景组合使用上述方法。
如何封装一个请求,让其多次调用的时候,实际只发起一个请求的时候,返回同一份结果
答案
最优解:利用 Promise 缓存,将请求结果复用,避免重复请求。
实现思路如下:
- 用一个对象(如 Map)缓存每个请求的 Promise。
- 多次调用时,如果缓存中已有对应的 Promise,直接返回该 Promise。
- 只有第一次会真正发起请求,后续调用复用同一个 Promise,拿到同一份结果。
示例代码:
const requestCache = new Map()
function singletonRequest (url, options) {
const key = url + JSON.stringify(options || {})
if (requestCache.has(key)) {
return requestCache.get(key)
}
const reqPromise = fetch(url, options).then(res => res.json())
requestCache.set(key, reqPromise)
return reqPromise
}
// 用法
singletonRequest('/api/data').then(console.log)
singletonRequest('/api/data').then(console.log)
// 只会发起一次请求,所有调用拿到同一份结果
说明:
- 这种方式简单高效,适合幂等请求(如 GET)。
- 若需支持请求失败重试或缓存失效,可在 Promise reject 时清理缓存或设置过期策略。
- 也可用闭包变量实现,无需全局变量。
使用 Promise 实现一个异步流量控制的函数(限制并发数)
答案
核心概念 异步流量控制(并发限制器 / Promise Pool)用于在需要批量执行大量异步请求时,将同时在飞(in-flight)的请求数量限制在指定阈值内,以避免压垮后端服务器或耗尽浏览器网络通道连接数(HTTP/1.1 同源最多 6 个 TCP 连接):
- 工作池机制:同时保持最多
limit个任务在运行。 - 腾挪推进:一旦某个任务执行完毕,立即从等待队列中取出下一个任务放入执行池,直到所有任务全部完成。
- 保持返回顺序:虽然完成时间可能有先有后,但最终返回的结果数组顺序必须严格与输入的任务数组顺序对齐。
手写实现(Promise.all + 动态任务调度):
/**
* 异步并发控制器
* @param {Array<() => Promise<any>>} tasks 返回 Promise 的任务函数数组
* @param {number} limit 最大并发数
* @returns {Promise<Array>} 与输入顺序一致的结果数组
*/
function asyncPool (tasks, limit = 3) {
return new Promise((resolve, reject) => {
const results = []
let nextIndex = 0
let finishedCount = 0
let isRejected = false
if (tasks.length === 0) {
return resolve([])
}
function runNext () {
if (nextIndex >= tasks.length || isRejected) {
return
}
const currentIndex = nextIndex++
const taskFn = tasks[currentIndex]
Promise.resolve(taskFn())
.then(res => {
results[currentIndex] = res
finishedCount++
if (finishedCount === tasks.length) {
resolve(results)
} else {
// 当前槽位空出,立即启动下一个
runNext()
}
})
.catch(err => {
isRejected = true
reject(err)
})
}
// 初始启动 limit 个并发执行器
const initialPoolSize = Math.min(limit, tasks.length)
for (let i = 0; i < initialPoolSize; i++) {
runNext()
}
})
}
// 验证
const createMockTask = (id, delay) => () =>
new Promise(res => setTimeout(() => {
console.log(`Task ${id} finished at ${Date.now() % 10000}ms`)
res(`Result ${id}`)
}, delay))
const mockTasks = [
createMockTask(1, 1000),
createMockTask(2, 500),
createMockTask(3, 300),
createMockTask(4, 400),
createMockTask(5, 200)
]
asyncPool(mockTasks, 2).then(results => {
console.log('全部完成,结果与任务顺序对齐:', results)
})
面试官视角
- 高频实战题目!重点考察对 Promise 状态推进时机、槽位复用逻辑以及结果索引保持一致的细致把控。
延伸阅读
集合运算
答案
可以使用 ES6 的 Set 数据结构来实现数组交集。
首先,将一个数组转化为 Set,然后遍历另一个数组,将数组中存在于 Set 中的元素存入结果数组中。
以下是一个示例代码:
function intersection (nums1, nums2) {
const set1 = new Set(nums1)
const res = []
for (const num of nums2) {
if (set1.has(num)) {
res.push(num)
}
}
return res
}
使用示例:
const nums1 = [1, 2, 2, 1]
const nums2 = [2, 2]
console.log(intersection(nums1, nums2)) // [2]
该算法的时间复杂度为 O(m+n),其中 m 和 n 分别为两个数组的长度。
限制并发的 Promise
答案
async function promiseLimit<T> (
promises: (() => Promise<T>)[],
limit: number
): Promise<T[]> {
const results: T[] = []
const executing: Promise<void>[] = []
for (const [index, promiseFunc] of promises.entries()) {
const p = Promise.resolve()
.then(() => promiseFunc())
.then(result => {
results[index] = result
})
.finally(() => {
const i = executing.indexOf(p)
if (i !== -1) executing.splice(i, 1)
})
executing.push(p)
if (executing.length >= limit) {
await Promise.race(executing)
}
}
await Promise.all(executing)
return results
}