跳到主要内容

业务场景代码✅

统计前端请求耗时​

答案
方法类别技术手段/工具实现方式简述代码示例/要点优缺点说明
初级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 获取结果不阻塞主线程,适合高并发或复杂场景

说明:

  • 初级手段适合简单场景,易于实现,但统计结果可能受主线程阻塞影响。
  • 进阶和高级手段能获得更准确或更详细的性能数据,适合对性能要求较高的业务场景。
答案

核心概念 HTTP Cookie 在前端通过 document.cookie 获取,表现为由分号和空格分隔的键值对字符串(如 "user=john; token=abc%3D123; logged_in=true"):

  1. 键值拆分:按 ; 切分出各个独立的条目。
  2. 解码处理:Cookie 的键与值可能经过 URL 编码,必须使用 decodeURIComponent 正确解码。
  3. 边界防御:处理值中包含等号(如 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 连接):

  1. 工作池机制:同时保持最多 limit 个任务在运行。
  2. 腾挪推进:一旦某个任务执行完毕,立即从等待队列中取出下一个任务放入执行池,直到所有任务全部完成。
  3. 保持返回顺序:虽然完成时间可能有先有后,但最终返回的结果数组顺序必须严格与输入的任务数组顺序对齐。

手写实现(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
}