JavaScript 高级面试题
1. 闭包是什么?有什么应用场景?
闭包(Closure) 是指函数可以记住并访问其定义时的词法作用域 ,即使函数在其词法作用域之外执行。简单说:函数 + 它能访问的外部变量 = 闭包。
闭包产生的条件:
- 存在函数嵌套
- 内部函数引用了外部函数的变量
- 内部函数被返回或传递到外部
javascript
// ✅ 基本闭包:makeCounter 返回的函数"记住"了 count 变量
function makeCounter() {
let count = 0 // 这个变量被闭包捕获,不会被垃圾回收
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count,
}
}
const counter = makeCounter()
counter.increment() // 1
counter.increment() // 2
counter.getCount() // 2
// count 对外部不可见,只能通过返回的方法访问
// ✅ 应用1:数据私有化(模拟私有变量)
function createPerson(name) {
let _age = 0 // "私有"变量
return {
getName: () => name,
getAge: () => _age,
setAge: (age) => {
if (typeof age !== 'number' || age < 0) throw new Error('无效年龄')
_age = age
},
}
}
const person = createPerson('张三')
person.setAge(25)
person.getAge() // 25
// person._age // undefined(无法直接访问)
// ✅ 应用2:函数柯里化
const multiply = (a) => (b) => a * b
const double = multiply(2)
const triple = multiply(3)
double(5) // 10
triple(5) // 15
// ✅ 应用3:模块模式(IIFE + 闭包)
const module = (() => {
let privateData = []
const add = (item) => privateData.push(item)
const getAll = () => [...privateData] // 返回副本,防止外部修改
const clear = () => { privateData = [] }
return { add, getAll, clear }
})()
// ✅ 应用4:缓存(记忆化函数)
function memoize(fn) {
const cache = new Map()
return function(...args) {
const key = JSON.stringify(args)
if (cache.has(key)) return cache.get(key)
const result = fn.apply(this, args)
cache.set(key, result)
return result
}
}
const expensiveCalc = memoize((n) => {
console.log('计算中...')
return n * n
})
expensiveCalc(10) // '计算中...' → 100
expensiveCalc(10) // 100(直接从缓存返回)
⚠️ 闭包的注意事项:
javascript
// ❌ 经典坑:循环中的闭包(var 没有块级作用域)
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0) // 输出 3 3 3
}
// ✅ 解决方案1:使用 let(推荐)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0) // 输出 0 1 2
}
// ✅ 解决方案2:使用 IIFE 创建新作用域
for (var i = 0; i < 3; i++) {
((j) => setTimeout(() => console.log(j), 0))(i)
}
// ⚠️ 闭包可能导致内存泄漏(闭包引用的变量不会被 GC 回收)
function createLeak() {
const hugeData = new Array(1000000).fill('data')
return () => hugeData.length // hugeData 一直被引用,无法回收
}
// 解决:不再使用时将引用设为 null
💡 面试加分点: 闭包不是 JS 特有的,它是函数式编程中的基本概念。React Hooks(如 useState、useEffect)底层大量使用闭包。闭包的"陷阱"在于它会延长变量的生命周期------被闭包引用的变量不会被垃圾回收,因此需要注意内存管理。
2. 变量提升和函数提升是什么?
变量提升(Hoisting) 是指在代码执行前,JS 引擎会将变量声明和函数声明"提升"到当前作用域的顶部。这是执行上下文创建阶段的行为。
javascript
// ========== var 变量提升 ==========
console.log(a) // undefined(声明提升,赋值未提升)
var a = 10
console.log(a) // 10
// 等价于引擎处理后:
// var a = undefined ← 提升
// console.log(a) // undefined
// a = 10 ← 赋值在原位置
// console.log(a) // 10
// ========== 函数提升(整体提升,优先级高于 var)==========
foo() // ✅ 'foo'(函数声明整体提升,可以在声明前调用)
function foo() { console.log('foo') }
bar() // ❌ TypeError: bar is not a function
var bar = function() { console.log('bar') }
// bar 被当做变量提升为 undefined,所以调用会报错
// ========== let/const 暂时性死区(TDZ)==========
console.log(b) // ❌ ReferenceError: Cannot access 'b' before initialization
let b = 20
// let/const 也会"提升"(分配内存),但在声明前处于暂时性死区,不可访问
// ========== 面试经典题 ==========
var x = 1
function test() {
console.log(x) // undefined(不是 1!函数内 var x 被提升了)
var x = 2
console.log(x) // 2
}
test()
// ========== 函数声明 vs var 同名时 ==========
console.log(typeof fn) // 'function'(函数声明优先级高于 var)
var fn = 10
function fn() {}
console.log(typeof fn) // 'number'(赋值覆盖了函数)
💡 面试加分点:
let/const实际上也会被"提升",但处于暂时性死区(Temporal Dead Zone),这个设计是为了让开发者养成"先声明后使用"的好习惯。函数声明的提升优先级高于var,如果同名,函数声明会在变量赋值之前覆盖var。
3. 事件循环(Event Loop)是什么?
事件循环 是 JavaScript 实现异步非阻塞的核心机制。JS 是单线程语言,通过事件循环在一个线程中处理异步操作。
执行顺序:同步代码 → 微任务 → 宏任务(每轮循环:执行一个宏任务 → 清空所有微任务 → 渲染 → 下一个宏任务)
| 类型 | 常见任务 | 优先级 |
|---|---|---|
| 同步代码 | 普通代码、console.log |
最高 |
| 微任务(Microtask) | Promise.then/catch/finally、MutationObserver、queueMicrotask |
高 |
| 宏任务(Macrotask) | setTimeout、setInterval、I/O、requestAnimationFrame、MessageChannel |
低 |
javascript
事件循环流程:
┌───────────────────────────┐
│ 执行同步代码(调用栈) │
└────────────┬──────────────┘
↓
┌───────────────────────────┐
│ 清空所有微任务队列 │ ← Promise.then / queueMicrotask
│ (如果微任务中产生新的微 │
│ 任务,继续执行,直到清空) │
└────────────┬──────────────┘
↓
┌───────────────────────────┐
│ 浏览器渲染(如有需要) │ ← requestAnimationFrame 在此执行
└────────────┬──────────────┘
↓
┌───────────────────────────┐
│ 取出一个宏任务执行 │ ← setTimeout / setInterval
└────────────┬──────────────┘
↓
回到第一步...
javascript
// ✅ 基础示例
console.log('1') // 同步
setTimeout(() => console.log('2'), 0) // 宏任务
Promise.resolve().then(() => console.log('3')) // 微任务
.then(() => console.log('4')) // 微任务
console.log('5') // 同步
// 输出顺序:1 → 5 → 3 → 4 → 2
// ✅ 复杂示例(高频面试题)
async function async1() {
console.log('async1 start') // 2 同步
await async2() // await 后面的代码变成微任务
console.log('async1 end') // 6 微任务
}
async function async2() {
console.log('async2') // 3 同步(await 的表达式会立即执行)
}
console.log('script start') // 1 同步
setTimeout(() => console.log('setTimeout'), 0) // 8 宏任务
async1()
new Promise(resolve => {
console.log('promise1') // 4 同步(Promise 构造函数是同步的!)
resolve()
}).then(() => console.log('promise2')) // 7 微任务
console.log('script end') // 5 同步
// 输出:script start → async1 start → async2 → promise1 → script end
// → async1 end → promise2 → setTimeout
宏任务与微任务的关键区别:
- 微任务 在当前宏任务执行完后立即执行,不需要等待渲染
- 宏任务需要等到下一轮事件循环才执行
- 微任务中产生的微任务会在当轮清空,不会等到下一轮
- 如果微任务队列无限增长,会阻塞渲染和宏任务的执行
💡 面试加分点:
await本质是Promise.then的语法糖。Node.js 中还有process.nextTick(优先级高于 Promise.then)和setImmediate(在 I/O 回调后执行)。
4. 防抖(debounce)和节流(throttle)的区别?
| 对比 | 防抖(debounce) | 节流(throttle) |
|---|---|---|
| 核心思想 | 事件停止触发后 n 秒才执行 | n 秒内只执行一次 |
| 比喻 | 电梯等人:有人进来就重新等 | 技能 CD:冷却好了才能放 |
| 适用场景 | 搜索框输入、窗口 resize | 滚动事件、鼠标移动、按钮点击 |
javascript
// ✅ 防抖:事件触发后等待 n 秒,若期间再次触发则重新计时
function debounce(fn, delay, immediate = false) {
let timer = null
return function(...args) {
const callNow = immediate && !timer
clearTimeout(timer)
timer = setTimeout(() => {
timer = null
if (!immediate) fn.apply(this, args)
}, delay)
if (callNow) fn.apply(this, args) // 立即执行模式
}
}
// ✅ 节流:n 秒内只执行一次(时间戳版)
function throttle(fn, interval) {
let lastTime = 0
return function(...args) {
const now = Date.now()
if (now - lastTime >= interval) {
lastTime = now
fn.apply(this, args)
}
}
}
// ✅ 节流(定时器版,最后一次也能执行)
function throttleTimer(fn, interval) {
let timer = null
return function(...args) {
if (timer) return
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, interval)
}
}
// ========== 使用示例 ==========
// 搜索框输入(防抖):用户停止输入 500ms 后才发请求
const handleSearch = debounce((value) => {
console.log('搜索:', value)
}, 500)
// 页面滚动(节流):每 200ms 最多执行一次
const handleScroll = throttle(() => {
console.log('滚动位置:', window.scrollY)
}, 200)
// 按钮提交(防抖 + 立即执行):点击立即执行,短时间内重复点击无效
const handleSubmit = debounce(() => {
console.log('提交表单')
}, 1000, true)
window.addEventListener('scroll', handleScroll)
💡 面试加分点: Lodash 的
_.debounce和_.throttle提供了leading(前缘触发)和trailing(后缘触发)选项。React 中推荐使用useDeferredValue或useTransition(React 18+)来替代手写防抖处理搜索场景。
5. 深拷贝和浅拷贝的区别?如何实现深拷贝?
| 对比 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 定义 | 只拷贝第一层,嵌套对象共享引用 | 递归拷贝所有层级,完全独立 |
| 嵌套对象 | 修改会互相影响 | 修改互不影响 |
深拷贝方案对比:
| 方案 | 循环引用 | 函数 | Symbol | Date/RegExp | 性能 |
|---|---|---|---|---|---|
JSON.parse/stringify |
❌ 报错 | ❌ 丢失 | ❌ 丢失 | ❌ 变字符串 | 快 |
structuredClone |
✅ | ❌ 报错 | ❌ | ✅ | 快 |
| 手写递归 | ✅ | ✅ | ✅ | ✅ | 自定义 |
Lodash _.cloneDeep |
✅ | ✅ | ✅ | ✅ | 最完善 |
javascript
// ========== 浅拷贝 ==========
const obj = { a: 1, b: { c: 2 } }
const shallow1 = Object.assign({}, obj)
const shallow2 = { ...obj }
shallow1.b.c = 99 // ⚠️ 会影响原对象!obj.b.c 也变成 99
// ========== 深拷贝方法1:JSON(最简单,但有局限) ==========
const deep1 = JSON.parse(JSON.stringify(obj))
// ========== 深拷贝方法2:structuredClone(现代浏览器推荐 ✅) ==========
const deep2 = structuredClone(obj)
// ========== 深拷贝方法3:手写递归(面试重点 ✅) ==========
function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj
if (map.has(obj)) return map.get(obj) // 处理循环引用
if (obj instanceof Date) return new Date(obj.getTime())
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags)
if (obj instanceof Map) {
const mapClone = new Map()
map.set(obj, mapClone)
obj.forEach((val, key) => mapClone.set(deepClone(key, map), deepClone(val, map)))
return mapClone
}
if (obj instanceof Set) {
const setClone = new Set()
map.set(obj, setClone)
obj.forEach(val => setClone.add(deepClone(val, map)))
return setClone
}
const clone = Array.isArray(obj) ? [] : {}
map.set(obj, clone)
for (const key of Reflect.ownKeys(obj)) {
clone[key] = deepClone(obj[key], map)
}
return clone
}
// 测试
const original = { a: 1, b: { c: 2 }, d: new Date(), e: /test/g }
original.self = original // 循环引用
const cloned = deepClone(original)
cloned.b.c = 99
console.log(original.b.c) // 2(互不影响)
💡 面试加分点: 使用
WeakMap而不是Map来存储已拷贝对象------WeakMap的键是弱引用,拷贝完成后不会阻止垃圾回收。structuredClone是 2022 年新增的全局方法,生产环境推荐使用。
6. Promise 的用法和原理?
Promise 是异步编程的核心方案,解决了回调地狱问题。它代表一个异步操作的最终结果。
三种状态(不可逆):
pending(等待中)→fulfilled(已成功):通过resolve(value)触发pending(等待中)→rejected(已失败):通过reject(reason)触发
javascript
// ✅ 基本用法
const fetchUser = (id) => new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) resolve({ id, name: '张三' })
else reject(new Error('Invalid ID'))
}, 1000)
})
// ✅ 链式调用
fetchUser(1)
.then(user => user.name)
.then(name => console.log(name)) // '张三'
.catch(err => console.error(err))
.finally(() => console.log('完成'))
// ========== Promise 静态方法 ==========
// ✅ Promise.all:全部成功才成功,任一失败则失败
Promise.all([fetchUser(1), fetchUser(2)])
.then(([user1, user2]) => console.log(user1, user2))
// ✅ Promise.allSettled:等待所有完成(不管成功失败)
Promise.allSettled([fetchUser(1), fetchUser(-1)])
.then(results => {
results.forEach(r => {
if (r.status === 'fulfilled') console.log('成功:', r.value)
if (r.status === 'rejected') console.log('失败:', r.reason)
})
})
// ✅ Promise.race:第一个完成的(请求超时处理)
function fetchWithTimeout(promise, timeout) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
)
])
}
// ✅ Promise.any:第一个成功的(全部失败才失败)
Promise.any([fetchUser(-1), fetchUser(2)])
.then(first => console.log('第一个成功:', first))
| 方法 | 成功条件 | 失败条件 | 结果类型 |
|---|---|---|---|
all |
全部成功 | 任一失败 | 成功值数组 |
allSettled |
全部完成 | 不会失败 | {status, value/reason} 数组 |
race |
第一个完成 | 第一个完成(如果是失败) | 第一个的结果 |
any |
第一个成功 | 全部失败 | 第一个成功的值 |
7. async/await 的用法?
javascript
async function fetchUserData(userId) {
try {
const user = await fetchUser(userId)
const posts = await fetchPosts(user.id)
return { user, posts }
} catch (error) {
console.error('请求失败:', error)
throw error
}
}
// 并行请求(推荐,比串行快)
async function fetchAll() {
const [user, posts] = await Promise.all([
fetchUser(1),
fetchPosts(1),
])
return { user, posts }
}
// 错误处理封装(Go 风格)
const to = (promise) => promise.then(data => [null, data]).catch(err => [err, null])
async function main() {
const [err, user] = await to(fetchUser(1))
if (err) return console.error(err)
console.log(user)
}
async/await 的本质:
async函数总是返回一个 Promiseawait相当于Promise.then的语法糖await后面的代码相当于在.then()的回调中执行(微任务)
8. 垃圾回收机制是什么?
JavaScript 的内存管理是自动的。垃圾回收器(GC)会自动找出不再使用的内存并释放。
两种主要算法:
| 算法 | 原理 | 状态 |
|---|---|---|
| 标记清除(Mark-and-Sweep) | 从根对象出发,标记所有可达对象,清除不可达的 | ✅ 现代浏览器使用 |
| 引用计数 | 记录每个对象被引用的次数,为 0 时回收 | ❌ 已废弃 |
V8 引擎的分代回收策略:
- 新生代(Young Generation): 存活时间短的对象(Scavenge 算法)
- 老生代(Old Generation): 存活时间长的对象(Mark-Sweep + Mark-Compact)
javascript
// ========== 常见内存泄漏场景 ==========
// ❌ 1. 意外的全局变量
function leak() {
leakedVar = '我是全局变量' // 没有 var/let/const
}
// ✅ 修复:使用 'use strict' 或始终用 let/const
// ❌ 2. 未清除的定时器
const timer = setInterval(() => { /* ... */ }, 1000)
// ✅ 修复:clearInterval(timer)
// ❌ 3. 未移除的事件监听
element.addEventListener('click', handler)
// ✅ 修复:element.removeEventListener('click', handler)
// 或使用 AbortController 统一管理
const controller = new AbortController()
element.addEventListener('click', handler, { signal: controller.signal })
controller.abort() // 一次性移除所有关联监听
// ❌ 4. 闭包持有大对象引用
function createLeak() {
const bigData = new Array(1000000).fill('data')
return () => bigData.length
}
// ✅ 修复:只保留需要的值
function createFixed() {
const bigData = new Array(1000000).fill('data')
const length = bigData.length
return () => length
}
// ❌ 5. DOM 引用未清除
const elements = { button: document.getElementById('btn') }
document.body.removeChild(document.getElementById('btn'))
// ✅ 修复:elements.button = null
// ❌ 6. console.log 在生产环境
console.log(hugeObject) // DevTools 开启时对象不会被回收
💡 面试加分点: Chrome DevTools 的 Memory 面板可以拍摄堆快照查找内存泄漏。
WeakRef和FinalizationRegistry(ES2021)允许创建弱引用和注册垃圾回收回调。
9. 内存泄漏的原因及处理方式?
内存泄漏是指不再需要的内存没有被及时释放,导致内存占用持续增长,最终可能导致页面卡顿甚至崩溃。
javascript
// ========== 常见内存泄漏原因及解决方案 ==========
// 1️⃣ 意外的全局变量
function foo() {
bar = 'hello' // ❌ 未声明的变量自动成为全局变量
this.baz = 'world' // ❌ this 指向 window
}
// ✅ 解决:使用严格模式 'use strict',始终使用 let/const
// 2️⃣ 被遗忘的定时器和回调
const data = fetchHugeData()
const timerId = setInterval(() => {
// ❌ data 被闭包引用,无法回收
process(data)
}, 1000)
// ✅ 解决:组件销毁时清除定时器
// clearInterval(timerId)
// 3️⃣ 脱离 DOM 的引用
const btn = document.getElementById('button')
document.body.removeChild(btn)
// ❌ btn 变量仍然引用着已删除的 DOM 节点
// ✅ 解决:btn = null
// 4️⃣ 不合理的闭包
function outer() {
const largeObj = { data: new Array(100000).fill('x') }
return function inner() {
// ❌ 即使 inner 不使用 largeObj,某些引擎仍可能保留整个闭包作用域
console.log('hello')
}
}
// ✅ 解决:确保闭包只引用必要的变量
// 5️⃣ Map/Set 存储对象引用
const cache = new Map()
function process(obj) {
cache.set(obj, computeResult(obj))
// ❌ obj 被 Map 强引用,即使外部不再需要也不会被回收
}
// ✅ 解决:使用 WeakMap/WeakSet
const weakCache = new WeakMap()
排查内存泄漏的工具:
- Chrome DevTools → Performance → 观察内存曲线是否持续上升
- Chrome DevTools → Memory → 拍摄堆快照(Heap Snapshot)对比
performance.memoryAPI(仅 Chrome)
10. 箭头函数和普通函数的区别?
| 特性 | 普通函数 | 箭头函数 |
|---|---|---|
| this | 调用时动态确定 | 定义时继承外层 this |
| arguments | ✅ 有 | ❌ 没有(用 ...rest 替代) |
| new 调用 | ✅ 可以 | ❌ 不可以(没有 \[Construct]) |
| prototype | ✅ 有 | ❌ 没有 |
| yield | ✅ 可以做生成器 | ❌ 不可以 |
javascript
// ========== this 指向差异(最核心的区别)==========
const obj = {
name: '张三',
// 普通函数:this 指向调用者
greet() {
console.log(this.name) // '张三'
},
// 箭头函数:this 继承定义时的外层作用域
greetArrow: () => {
console.log(this.name) // undefined(外层是全局/模块作用域)
}
}
// ========== 回调场景中的优势 ==========
const timer = {
seconds: 0,
start() {
// ❌ 普通函数:this 指向 window
setInterval(function() {
this.seconds++ // NaN(this 不是 timer)
}, 1000)
// ✅ 箭头函数:this 继承 start 的 this
setInterval(() => {
this.seconds++ // 正常(this 是 timer)
}, 1000)
}
}
// ========== arguments 差异 ==========
function normalFn() {
console.log(arguments) // ✅ Arguments 对象
}
const arrowFn = () => {
// console.log(arguments) // ❌ ReferenceError
}
const arrowWithRest = (...args) => {
console.log(args) // ✅ 使用 rest 参数替代
}
// ========== 不能作为构造函数 ==========
const Foo = () => {}
// new Foo() // ❌ TypeError: Foo is not a constructor
// ========== 不能用作生成器 ==========
// const gen = *() => {} // ❌ SyntaxError
💡 面试加分点: 箭头函数的 this 不能通过
call/apply/bind修改。在 React 类组件中,事件处理推荐用箭头函数避免 this 绑定问题。在对象字面量的方法中,不建议使用箭头函数(this 不会指向对象)。
11. 手写 call、apply、bind?
javascript
// 手写 call
Function.prototype.myCall = function(context, ...args) {
context = context ?? globalThis
const key = Symbol('fn')
context[key] = this
const result = context[key](...args)
delete context[key]
return result
}
// 手写 apply
Function.prototype.myApply = function(context, args = []) {
context = context ?? globalThis
const key = Symbol('fn')
context[key] = this
const result = context[key](...args)
delete context[key]
return result
}
// 手写 bind
Function.prototype.myBind = function(context, ...outerArgs) {
const fn = this
return function(...innerArgs) {
return fn.apply(context, [...outerArgs, ...innerArgs])
}
}
// 测试
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`
}
greet.myCall({ name: '张三' }, 'Hello', '!') // 'Hello, 张三!'
greet.myApply({ name: '李四' }, ['Hi', '?']) // 'Hi, 李四?'
const bound = greet.myBind({ name: '王五' }, 'Hey')
bound('.') // 'Hey, 王五.'
12. 手写 new 操作符?
javascript
function myNew(Constructor, ...args) {
// 1. 创建新对象,原型指向构造函数的 prototype
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
}
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}`
}
const p = myNew(Person, '张三', 25)
p.greet() // 'Hello, I'm 张三'
p instanceof Person // true
13. 什么是柯里化(Currying)?
javascript
// 柯里化:将多参数函数转换为一系列单参数函数
// 基本柯里化
const add = (a) => (b) => (c) => a + b + c
add(1)(2)(3) // 6
// 通用柯里化函数
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args)
}
return function(...moreArgs) {
return curried.apply(this, [...args, ...moreArgs])
}
}
}
const sum = (a, b, c) => a + b + c
const curriedSum = curry(sum)
curriedSum(1)(2)(3) // 6
curriedSum(1, 2)(3) // 6
curriedSum(1)(2, 3) // 6
// 实际应用:参数复用
const multiply = curry((a, b) => a * b)
const double = multiply(2)
const triple = multiply(3)
double(5) // 10
triple(5) // 15
14. 什么是函数式编程?常用方法有哪些?
javascript
// 纯函数:相同输入总是返回相同输出,无副作用
const add = (a, b) => a + b // 纯函数
let count = 0
const increment = () => ++count // 非纯函数(有副作用)
// 不可变性
const original = [1, 2, 3]
const newArr = [...original, 4] // 不修改原数组
// 高阶函数:接受或返回函数
const numbers = [1, 2, 3, 4, 5, 6]
const doubled = numbers.map(n => n * 2) // [2, 4, 6, 8, 10, 12]
const evens = numbers.filter(n => n % 2 === 0) // [2, 4, 6]
const sum = numbers.reduce((acc, n) => acc + n, 0) // 21
// 组合(compose)与管道(pipe)
const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x)
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x)
const double = x => x * 2
const addOne = x => x + 1
const square = x => x * x
const transform = pipe(double, addOne, square)
transform(3) // ((3*2)+1)^2 = 49
15. 什么是 WeakMap 和 WeakSet?
javascript
// WeakMap:键必须是对象,弱引用(不阻止垃圾回收)
const weakMap = new WeakMap()
let obj = { name: '张三' }
weakMap.set(obj, '关联数据')
weakMap.get(obj) // '关联数据'
obj = null // obj 被垃圾回收,weakMap 中的条目也自动删除
// 应用:存储私有数据
const _private = new WeakMap()
class Person {
constructor(name, age) {
_private.set(this, { age })
this.name = name
}
getAge() { return _private.get(this).age }
}
// WeakSet:存储对象的弱引用集合
const weakSet = new WeakSet()
let element = document.querySelector('.btn')
weakSet.add(element)
weakSet.has(element) // true
element = null // 自动从 weakSet 中移除
// 应用:标记已处理的对象(防止重复处理)
const processed = new WeakSet()
function processOnce(obj) {
if (processed.has(obj)) return
processed.add(obj)
// 处理逻辑...
}
WeakMap/WeakSet vs Map/Set 对比:
| 特性 | Map/Set | WeakMap/WeakSet |
|---|---|---|
| 键类型 | 任意 | 只能是对象 |
| 引用类型 | 强引用 | 弱引用 |
| 可遍历 | ✅ | ❌(不可迭代) |
| size 属性 | ✅ | ❌ |
| GC 回收 | 不回收 | 键对象无其他引用时自动回收 |
16. 手写 Promise?
javascript
class MyPromise {
static PENDING = 'pending'
static FULFILLED = 'fulfilled'
static REJECTED = 'rejected'
constructor(executor) {
this.status = MyPromise.PENDING
this.value = undefined
this.reason = undefined
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value) => {
if (this.status === MyPromise.PENDING) {
this.status = MyPromise.FULFILLED
this.value = value
this.onFulfilledCallbacks.forEach(fn => fn(value))
}
}
const reject = (reason) => {
if (this.status === MyPromise.PENDING) {
this.status = MyPromise.REJECTED
this.reason = reason
this.onRejectedCallbacks.forEach(fn => fn(reason))
}
}
try {
executor(resolve, reject)
} catch (err) {
reject(err)
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected = typeof onRejected === 'function' ? onRejected : e => { throw e }
return new MyPromise((resolve, reject) => {
const handle = (fn, value) => {
try {
const result = fn(value)
result instanceof MyPromise ? result.then(resolve, reject) : resolve(result)
} catch (err) {
reject(err)
}
}
if (this.status === MyPromise.FULFILLED) {
setTimeout(() => handle(onFulfilled, this.value))
} else if (this.status === MyPromise.REJECTED) {
setTimeout(() => handle(onRejected, this.reason))
} else {
this.onFulfilledCallbacks.push((value) => setTimeout(() => handle(onFulfilled, value)))
this.onRejectedCallbacks.push((reason) => setTimeout(() => handle(onRejected, reason)))
}
})
}
catch(onRejected) { return this.then(null, onRejected) }
static resolve(value) { return new MyPromise(resolve => resolve(value)) }
static reject(reason) { return new MyPromise((_, reject) => reject(reason)) }
static all(promises) {
return new MyPromise((resolve, reject) => {
const results = []
let count = 0
promises.forEach((p, i) => {
MyPromise.resolve(p).then(value => {
results[i] = value
if (++count === promises.length) resolve(results)
}, reject)
})
})
}
}
17. 什么是尾调用优化?
javascript
// 普通递归:每次调用都会创建新的栈帧,可能导致栈溢出
function factorial(n) {
if (n <= 1) return 1
return n * factorial(n - 1) // 不是尾调用(还需要乘法)
}
// 尾调用优化:函数的最后一步是调用另一个函数
function factorialTCO(n, acc = 1) {
if (n <= 1) return acc
return factorialTCO(n - 1, n * acc) // 尾调用,可以复用栈帧
}
// 斐波那契数列(尾递归)
function fibonacci(n, a = 0, b = 1) {
if (n === 0) return a
return fibonacci(n - 1, b, a + b)
}
fibonacci(10) // 55
💡 面试加分点: 尾调用优化(TCO)目前只有 Safari 完全支持,V8 引擎尚未实现。实际开发中可用循环代替递归来避免栈溢出。
18. 什么是 Proxy 和 Reflect?
javascript
// Proxy:拦截对象操作(Vue3 响应式的核心)
const handler = {
get(target, key) {
console.log(`读取属性: ${key}`)
return Reflect.get(target, key)
},
set(target, key, value) {
if (typeof value !== 'number') throw new TypeError('只能设置数字')
return Reflect.set(target, key, value)
},
has(target, key) {
console.log(`检查属性: ${key}`)
return Reflect.has(target, key)
},
deleteProperty(target, key) {
console.log(`删除属性: ${key}`)
return Reflect.deleteProperty(target, key)
}
}
const obj = new Proxy({ count: 0 }, handler)
obj.count // 读取属性: count → 0
obj.count = 10 // 设置数字成功
'count' in obj // 检查属性: count → true
// ✅ 实用示例:数据验证
function createValidator(schema) {
return new Proxy({}, {
set(target, key, value) {
if (schema[key] && !schema[key](value)) {
throw new Error(`${key} 验证失败`)
}
return Reflect.set(target, key, value)
}
})
}
const user = createValidator({
name: (v) => typeof v === 'string' && v.length > 0,
age: (v) => typeof v === 'number' && v >= 0 && v <= 150,
})
user.name = '张三' // ✅
user.age = 25 // ✅
// user.age = -1 // ❌ Error: age 验证失败
💡 面试加分点: Proxy 可以拦截 13 种操作(get/set/has/deleteProperty/apply/construct 等),比
Object.defineProperty更强大。Vue3 用 Proxy 替代了 Vue2 的Object.defineProperty来实现响应式,解决了无法检测新增属性和数组索引修改的问题。
19. 浅比较和 Object.is()?
javascript
// ========== Object.is():比 === 更精确 ==========
Object.is(NaN, NaN) // true(=== 返回 false)
Object.is(+0, -0) // false(=== 返回 true)
Object.is(1, 1) // true(与 === 相同)
// ========== 浅比较(Shallow Equal)==========
// React 中 PureComponent / React.memo 使用的比较策略
function shallowEqual(objA, objB) {
if (Object.is(objA, objB)) return true
if (typeof objA !== 'object' || objA === null ||
typeof objB !== 'object' || objB === null) return false
const keysA = Object.keys(objA)
const keysB = Object.keys(objB)
if (keysA.length !== keysB.length) return false
for (const key of keysA) {
if (!Object.hasOwn(objB, key) || !Object.is(objA[key], objB[key])) {
return false
}
}
return true
}
shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2 }) // true
shallowEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } }) // false(嵌套对象不同引用)
💡 面试加分点: React 的性能优化核心就是浅比较------
React.memo、useMemo、useCallback都依赖浅比较。这也是为什么 React 强调不可变数据。
20. 手写数组扁平化(flat)?
javascript
// ✅ 方法1:递归
function flatten(arr, depth = 1) {
if (depth <= 0) return arr.slice()
return arr.reduce((acc, item) => {
return acc.concat(Array.isArray(item) ? flatten(item, depth - 1) : item)
}, [])
}
// ✅ 方法2:迭代(使用栈)
function flattenIterative(arr) {
const stack = [...arr]
const result = []
while (stack.length) {
const item = stack.pop()
Array.isArray(item) ? stack.push(...item) : result.unshift(item)
}
return result
}
// ✅ 方法3:Generator
function* flattenGen(arr, depth = Infinity) {
for (const item of arr) {
if (Array.isArray(item) && depth > 0) {
yield* flattenGen(item, depth - 1)
} else {
yield item
}
}
}
// 测试
const nested = [1, [2, [3, [4, [5]]]]]
flatten(nested, Infinity) // [1, 2, 3, 4, 5]
flattenIterative(nested) // [1, 2, 3, 4, 5]
[...flattenGen(nested)] // [1, 2, 3, 4, 5]
nested.flat(Infinity) // [1, 2, 3, 4, 5](原生方法)
21. 手写数组去重的方法?
javascript
const arr = [1, 2, 3, 2, 1, 4, 3, 5, 4]
// ✅ 方法1:Set(最简洁,推荐)
const unique1 = [...new Set(arr)] // [1, 2, 3, 4, 5]
// ✅ 方法2:filter + indexOf
const unique2 = arr.filter((item, index) => arr.indexOf(item) === index)
// ✅ 方法3:reduce
const unique3 = arr.reduce((acc, item) => {
return acc.includes(item) ? acc : [...acc, item]
}, [])
// ✅ 方法4:Map(适合对象数组按某个字段去重)
function uniqueByKey(arr, key) {
const map = new Map()
return arr.filter(item => {
const k = item[key]
if (map.has(k)) return false
map.set(k, true)
return true
})
}
const users = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 1, name: '张三(重复)' },
]
uniqueByKey(users, 'id') // [{ id: 1, name: '张三' }, { id: 2, name: '李四' }]
// ⚠️ 注意:Set 使用 === 比较(引用类型去重无效)
22. 继承的实现方式有哪些?
javascript
// ========== 1. 原型链继承 ==========
function Parent1() { this.colors = ['red', 'blue'] }
Parent1.prototype.say = function() { return 'hello' }
function Child1() {}
Child1.prototype = new Parent1()
// ❌ 缺点:所有子实例共享父类引用属性
const c1 = new Child1()
const c2 = new Child1()
c1.colors.push('green')
console.log(c2.colors) // ['red', 'blue', 'green'] ← 被影响了
// ========== 2. 借用构造函数继承 ==========
function Parent2(name) { this.name = name; this.colors = ['red'] }
function Child2(name) { Parent2.call(this, name) }
// ❌ 缺点:无法继承原型上的方法
// ========== 3. 组合继承(最常用) ==========
function Parent3(name) { this.name = name }
Parent3.prototype.say = function() { return `I'm ${this.name}` }
function Child3(name, age) {
Parent3.call(this, name) // 继承实例属性
this.age = age
}
Child3.prototype = new Parent3() // 继承原型方法
Child3.prototype.constructor = Child3
// ❌ 缺点:Parent3 构造函数被调用了两次
// ========== 4. 寄生组合继承(最优方案)==========
function Parent4(name) { this.name = name }
Parent4.prototype.say = function() { return `I'm ${this.name}` }
function Child4(name, age) {
Parent4.call(this, name)
this.age = age
}
Child4.prototype = Object.create(Parent4.prototype) // ✅ 不会调用 Parent4
Child4.prototype.constructor = Child4
const child = new Child4('张三', 25)
child.say() // "I'm 张三"
child instanceof Parent4 // true
// ========== 5. ES6 class 继承(语法糖,底层是寄生组合继承)==========
class Parent5 {
constructor(name) { this.name = name }
say() { return `I'm ${this.name}` }
}
class Child5 extends Parent5 {
constructor(name, age) {
super(name) // 必须先调用 super
this.age = age
}
}
// ========== 6. 混入(Mixin)继承 ==========
const Serializable = {
serialize() { return JSON.stringify(this) }
}
const Loggable = {
log() { console.log(this.toString()) }
}
class Base {}
Object.assign(Base.prototype, Serializable, Loggable)
💡 面试加分点: ES6
class的extends底层就是寄生组合继承。ES6 中子类必须在constructor中调用super()后才能使用this。
23. ES6 class 的核心知识点?
ES6 class 是基于原型继承的语法糖,提供了更清晰、更面向对象的写法,但本质仍然是构造函数 + 原型链。
23.1 基本语法与构造函数
javascript
// ========== 基本 class 声明 ==========
class Person {
// 构造函数:new 时自动调用
constructor(name, age) {
// 实例属性:每个实例独有
this.name = name
this.age = age
}
// 原型方法:定义在 Person.prototype 上,所有实例共享
greet() {
return `你好,我是 ${this.name},今年 ${this.age} 岁`
}
// 原型方法
toString() {
return `[Person: ${this.name}]`
}
}
const p = new Person('张三', 25)
p.greet() // '你好,我是 张三,今年 25 岁'
// ⚠️ class 不存在变量提升(与函数声明不同)
// new Foo() // ❌ ReferenceError
// class Foo {}
// ⚠️ class 内部默认严格模式
// ⚠️ class 必须使用 new 调用,不能当普通函数用
// Person() // ❌ TypeError: Class constructor Person cannot be invoked without 'new'
// ========== class 的本质 ==========
typeof Person // 'function'
Person === Person.prototype.constructor // true
// class 本质上就是构造函数
23.2 实例属性、静态属性与静态方法
javascript
class Counter {
// ✅ 实例属性(新写法,直接在类体中声明)
count = 0
name = 'default'
// ✅ 静态属性:属于类本身,不属于实例
static total = 0
static MAX_COUNT = 100
constructor(name) {
this.name = name
Counter.total++
}
// 实例方法
increment() {
if (this.count >= Counter.MAX_COUNT) {
throw new Error('超过最大计数')
}
this.count++
}
// ✅ 静态方法:通过类名调用,不能通过实例调用
static getTotal() {
return Counter.total
}
// 静态方法常用于工厂方法
static create(name) {
return new Counter(name)
}
// 静态方法中的 this 指向类本身
static reset() {
this.total = 0 // this === Counter
}
}
const c1 = new Counter('A')
const c2 = new Counter('B')
Counter.getTotal() // 2
Counter.total // 2
c1.increment()
c1.count // 1
c2.count // 0(实例属性互不影响)
// c1.getTotal() // ❌ TypeError: c1.getTotal is not a function
// 静态方法只能通过类名调用
23.3 getter 和 setter
javascript
class Temperature {
#celsius = 0 // 私有属性
constructor(celsius) {
this.#celsius = celsius
}
// ✅ getter:访问属性时自动调用
get fahrenheit() {
return this.#celsius * 9 / 5 + 32
}
// ✅ setter:设置属性时自动调用(可加验证)
set fahrenheit(value) {
this.#celsius = (value - 32) * 5 / 9
}
get celsius() {
return this.#celsius
}
set celsius(value) {
if (typeof value !== 'number') throw new TypeError('必须是数字')
if (value < -273.15) throw new RangeError('不能低于绝对零度')
this.#celsius = value
}
}
const temp = new Temperature(100)
temp.fahrenheit // 212(调用 getter,像访问属性一样)
temp.fahrenheit = 32 // 调用 setter
temp.celsius // 0
// temp.celsius = 'abc' // ❌ TypeError: 必须是数字
// temp.celsius = -300 // ❌ RangeError: 不能低于绝对零度
23.4 私有属性和私有方法(#)
javascript
class BankAccount {
// ✅ 私有属性:以 # 开头,类外部完全不可访问
#balance = 0
#owner
// ✅ 私有静态属性
static #bankName = '中国银行'
constructor(owner, initialBalance) {
this.#owner = owner
this.#balance = initialBalance
}
// ✅ 私有方法
#validate(amount) {
if (typeof amount !== 'number' || amount <= 0) {
throw new Error('金额无效')
}
}
deposit(amount) {
this.#validate(amount)
this.#balance += amount
return this
}
withdraw(amount) {
this.#validate(amount)
if (amount > this.#balance) throw new Error('余额不足')
this.#balance -= amount
return this
}
get balance() {
return this.#balance
}
// ✅ 私有静态方法
static #log(message) {
console.log(`[${this.#bankName}] ${message}`)
}
static createAccount(owner, balance) {
this.#log(`创建账户: ${owner}`)
return new BankAccount(owner, balance)
}
}
const account = BankAccount.createAccount('张三', 1000)
account.deposit(500).withdraw(200)
account.balance // 1300
// account.#balance // ❌ SyntaxError: Private field
// account.#validate // ❌ SyntaxError: Private field
// 私有成员在类外部完全不可访问,甚至子类也不能访问
// ✅ 检查对象是否拥有某个私有属性
class MyClass {
#value
static hasValue(obj) {
return #value in obj // in 运算符可检测私有属性
}
}
23.5 继承(extends 和 super)
javascript
class Animal {
#name
constructor(name) {
this.#name = name
}
get name() { return this.#name }
speak() {
return `${this.#name} 发出声音`
}
// 静态方法也会被继承
static create(name) {
return new this(name) // this 指向调用的类
}
}
class Dog extends Animal {
#breed
constructor(name, breed) {
// ⚠️ 子类 constructor 中必须先调用 super(),才能使用 this
super(name) // 调用父类构造函数
this.#breed = breed
}
// 方法重写(Override)
speak() {
return `${this.name} 汪汪叫`
}
// 调用父类方法
speakLoud() {
return super.speak() + '(很大声)' // super.method() 调用父类方法
}
get info() {
return `${this.name} (${this.#breed})`
}
}
class GuideDog extends Dog {
#handler
constructor(name, breed, handler) {
super(name, breed) // 调用 Dog 的 constructor
this.#handler = handler
}
speak() {
return `${this.name} 安静地引导 ${this.#handler}`
}
}
const dog = new Dog('旺财', '柴犬')
dog.speak() // '旺财 汪汪叫'
dog.speakLoud() // '旺财 发出声音(很大声)'
dog.info // '旺财 (柴犬)'
dog instanceof Dog // true
dog instanceof Animal // true
// 静态方法继承
const cat = Animal.create('小猫') // Animal 实例
const puppy = Dog.create('小狗') // Dog 实例(this 指向 Dog)
// ========== super 的两种用法 ==========
// 1. super() 作为函数:在子类 constructor 中调用父类构造函数
// 2. super.method():在子类方法中调用父类的同名方法
23.6 抽象类模式与多态
javascript
// JS 没有原生 abstract 关键字,但可以模拟
class Shape {
constructor(color = 'black') {
// 模拟抽象类:不允许直接实例化
if (new.target === Shape) {
throw new Error('Shape 是抽象类,不能直接实例化')
}
this.color = color
}
// 模拟抽象方法:子类必须实现
area() {
throw new Error('子类必须实现 area() 方法')
}
// 模板方法模式:定义通用逻辑,具体步骤由子类实现
describe() {
return `这是一个 ${this.color} 的图形,面积为 ${this.area().toFixed(2)}`
}
}
class Circle extends Shape {
constructor(radius, color) {
super(color)
this.radius = radius
}
area() {
return Math.PI * this.radius ** 2
}
}
class Rectangle extends Shape {
constructor(width, height, color) {
super(color)
this.width = width
this.height = height
}
area() {
return this.width * this.height
}
}
// const s = new Shape() // ❌ Error: Shape 是抽象类
const circle = new Circle(5, 'red')
const rect = new Rectangle(3, 4, 'blue')
// ✅ 多态:相同的方法调用,不同的行为
const shapes = [circle, rect]
shapes.forEach(shape => {
console.log(shape.describe())
})
// '这是一个 red 的图形,面积为 78.54'
// '这是一个 blue 的图形,面积为 12.00'
23.7 Mixin 模式(多继承的替代方案)
javascript
// JS 不支持多继承,但可以用 Mixin 实现类似效果
// 定义 Mixin 为"接受一个基类并返回扩展类"的函数
const Serializable = (Base) => class extends Base {
toJSON() {
const obj = {}
for (const key of Object.keys(this)) {
obj[key] = this[key]
}
return obj
}
serialize() {
return JSON.stringify(this.toJSON())
}
}
const Validatable = (Base) => class extends Base {
validate() {
for (const [key, value] of Object.entries(this)) {
if (value === null || value === undefined) {
throw new Error(`${key} 不能为空`)
}
}
return true
}
}
const Timestamped = (Base) => class extends Base {
constructor(...args) {
super(...args)
this.createdAt = new Date()
this.updatedAt = new Date()
}
touch() {
this.updatedAt = new Date()
}
}
// ✅ 组合多个 Mixin
class User extends Timestamped(Validatable(Serializable(class {}))) {
constructor(name, email) {
super()
this.name = name
this.email = email
}
}
const user = new User('张三', 'zhangsan@example.com')
user.validate() // true
user.serialize() // '{"name":"张三","email":"zhangsan@example.com","createdAt":"...","updatedAt":"..."}'
user.touch() // 更新时间戳
23.8 class vs 构造函数 对比
javascript
// ========== 构造函数写法 ==========
function PersonFn(name) {
this.name = name
}
PersonFn.prototype.greet = function() {
return `Hello, ${this.name}`
}
PersonFn.create = function(name) {
return new PersonFn(name)
}
// ========== class 写法 ==========
class PersonClass {
constructor(name) {
this.name = name
}
greet() {
return `Hello, ${this.name}`
}
static create(name) {
return new PersonClass(name)
}
}
| 对比项 | 构造函数 | class |
|---|---|---|
| 语法 | 分散(原型方法需要单独定义) | 集中(所有成员在一个代码块内) |
| 提升 | ✅ 函数声明会提升 | ❌ 不提升(暂时性死区) |
| 严格模式 | 需要手动开启 | 默认严格模式 |
| new 调用 | 可以不用 new(但结果可能不对) | 必须使用 new |
| 方法可枚举 | prototype 上的方法可枚举 |
方法不可枚举 |
| 私有成员 | 只能通过闭包/WeakMap 模拟 | 原生 # 私有属性和方法 |
| 继承 | 手动实现(寄生组合继承) | extends + super |
💡 面试加分点: ① class 只是语法糖,
typeof一个 class 仍然是'function';② 私有属性#是真正的硬私有(hard private),不同于 TypeScript 的private(编译后消失);③new.target可以检测是否通过new调用,以及具体是哪个类在被实例化(实现抽象类);④ 静态方法中的this指向类本身,且静态方法会被子类继承;⑤ 实际开发中 React 已从 class 组件转向函数组件 + Hooks,但理解 class 仍是面试必考。
24. ES6 新增了哪些主要特性?
javascript
// 1. let/const 块级作用域
let x = 1
const PI = 3.14
// 2. 箭头函数
const add = (a, b) => a + b
// 3. 模板字符串
const name = '张三'
console.log(`Hello, ${name}!`)
// 4. 解构赋值
const { a, b, ...rest } = { a: 1, b: 2, c: 3, d: 4 }
const [first, ...others] = [1, 2, 3, 4]
// 5. 展开运算符
const arr1 = [1, 2]; const arr2 = [...arr1, 3, 4]
const obj1 = { a: 1 }; const obj2 = { ...obj1, b: 2 }
// 6. Promise
const p = new Promise((resolve, reject) => { resolve('ok') })
// 7. class 类
class Animal {
constructor(name) { this.name = name }
speak() { return `${this.name} speaks` }
}
// 8. 模块化(import/export)
// export default function add(a, b) { return a + b }
// import add from './add.js'
// 9. Symbol
const sym = Symbol('description')
// 10. Map/Set/WeakMap/WeakSet
const map = new Map([['key', 'value']])
const set = new Set([1, 2, 3])
// 11. for...of 循环
for (const item of [1, 2, 3]) { console.log(item) }
// 12. 默认参数
function greet(name = '世界') { return `Hello, ${name}!` }
// 13. Proxy/Reflect
const proxy = new Proxy({}, { get: (t, k) => `访问了 ${k}` })
// 14. Generator
function* gen() { yield 1; yield 2; yield 3 }
// 15. 可选链 ?.(ES2020)和空值合并 ??(ES2020)
const value = obj?.a?.b ?? '默认值'
25. forEach 和 map 的区别?
| 对比 | forEach | map |
|---|---|---|
| 返回值 | undefined |
新数组 |
| 是否改变原数组 | 不改变(但回调中可以修改) | 不改变 |
| 可否中断 | ❌ 不能用 break | ❌ 不能用 break |
| 链式调用 | ❌ | ✅(返回数组可继续 .filter/.reduce) |
| 使用场景 | 遍历执行副作用 | 转换数据生成新数组 |
javascript
const numbers = [1, 2, 3, 4, 5]
// forEach:遍历,执行操作,不返回新数组
numbers.forEach((num, index) => {
console.log(`索引 ${index}: ${num}`)
})
// map:转换数据,返回新数组
const doubled = numbers.map(num => num * 2)
console.log(doubled) // [2, 4, 6, 8, 10]
// ✅ map 可链式调用
const result = numbers
.map(n => n * 2)
.filter(n => n > 5)
.reduce((sum, n) => sum + n, 0)
console.log(result) // 24 (6+8+10)
// ❌ forEach 的返回值是 undefined,不能链式调用
// numbers.forEach(n => n * 2).filter(...) // TypeError
26. split() 和 join() 的区别?
javascript
// split():字符串 → 数组(按分隔符拆分)
'hello world'.split(' ') // ['hello', 'world']
'a,b,c'.split(',') // ['a', 'b', 'c']
'hello'.split('') // ['h', 'e', 'l', 'l', 'o']
'a-b-c'.split('-', 2) // ['a', 'b'](限制返回数量)
// join():数组 → 字符串(用连接符合并)
['hello', 'world'].join(' ') // 'hello world'
['a', 'b', 'c'].join(',') // 'a,b,c'
['a', 'b', 'c'].join('') // 'abc'
[1, 2, 3].join('-') // '1-2-3'
// ✅ 经典用法:字符串反转
function reverseString(str) {
return str.split('').reverse().join('')
}
reverseString('hello') // 'olleh'
// ✅ URL 参数处理
const params = { name: '张三', age: 25 }
const queryString = Object.entries(params)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join('&')
// 'name=%E5%BC%A0%E4%B8%89&age=25'
27. 数组和字符串的常用方法?
javascript
// ========== 数组常用方法 ==========
// 增删
const arr = [1, 2, 3]
arr.push(4) // [1, 2, 3, 4](末尾添加,返回新长度)
arr.pop() // [1, 2, 3](末尾删除,返回删除元素)
arr.unshift(0) // [0, 1, 2, 3](头部添加)
arr.shift() // [1, 2, 3](头部删除)
arr.splice(1, 1, 'a') // [1, 'a', 3](从索引1删除1个,插入'a')
// 查找
[1, 2, 3].indexOf(2) // 1(找不到返回 -1)
[1, 2, 3].includes(2) // true
[1, 2, 3].find(n => n > 1) // 2(找到第一个满足条件的元素)
[1, 2, 3].findIndex(n => n > 1) // 1
// 转换
[1, 2, 3].map(n => n * 2) // [2, 4, 6]
[1, 2, 3].filter(n => n > 1) // [2, 3]
[1, 2, 3].reduce((a, b) => a + b, 0) // 6
// 排序
[3, 1, 2].sort((a, b) => a - b) // [1, 2, 3](升序)
[3, 1, 2].sort((a, b) => b - a) // [3, 2, 1](降序)
// 其他
[1, [2, [3]]].flat(Infinity) // [1, 2, 3]
[1, 2, 3].every(n => n > 0) // true
[1, 2, 3].some(n => n > 2) // true
Array.from({ length: 3 }, (_, i) => i) // [0, 1, 2]
// ========== 字符串常用方法 ==========
const str = 'Hello World'
str.charAt(0) // 'H'
str.indexOf('World') // 6
str.includes('World') // true
str.slice(0, 5) // 'Hello'
str.substring(6) // 'World'
str.toUpperCase() // 'HELLO WORLD'
str.toLowerCase() // 'hello world'
str.trim() // 去除两端空格
str.replace('World', 'JS') // 'Hello JS'
str.startsWith('Hello') // true
str.endsWith('World') // true
str.padStart(15, '*') // '****Hello World'
str.repeat(2) // 'Hello WorldHello World'
28. 迭代器协议(Iterator Protocol)是什么?
javascript
// ✅ 手动实现一个可迭代对象
class Range {
constructor(start, end) {
this.start = start
this.end = end
}
[Symbol.iterator]() {
let current = this.start
const end = this.end
return {
next() {
return current <= end
? { value: current++, done: false }
: { done: true }
}
}
}
}
for (const num of new Range(1, 5)) {
console.log(num) // 1, 2, 3, 4, 5
}
[...new Range(1, 5)] // [1, 2, 3, 4, 5]
// ✅ 内置可迭代对象:Array、String、Map、Set、arguments、NodeList
// ✅ 普通对象默认不可迭代,但可以手动实现
const iterableObj = {
a: 1, b: 2, c: 3,
[Symbol.iterator]() {
const entries = Object.entries(this)
let index = 0
return {
next: () => index < entries.length
? { value: entries[index++], done: false }
: { done: true }
}
}
}
for (const [key, value] of iterableObj) {
console.log(key, value) // a 1, b 2, c 3
}
29. 错误处理的最佳实践?
javascript
// ========== try/catch/finally ==========
try {
const data = JSON.parse(invalidJson)
} catch (error) {
console.error(`解析失败: ${error.message}`)
} finally {
console.log('清理完成')
}
// ✅ 自定义错误类型
class ValidationError extends Error {
constructor(field, message) {
super(message)
this.name = 'ValidationError'
this.field = field
}
}
class NotFoundError extends Error {
constructor(resource) {
super(`${resource} 未找到`)
this.name = 'NotFoundError'
this.statusCode = 404
}
}
// ✅ 按错误类型分别处理
try {
validateAge('abc')
} catch (error) {
if (error instanceof ValidationError) {
console.log(`字段 ${error.field} 验证失败: ${error.message}`)
} else if (error instanceof NotFoundError) {
console.log(`404: ${error.message}`)
} else {
throw error // 未知错误继续抛出
}
}
// ✅ 全局错误捕获
window.addEventListener('error', (event) => {
console.error('全局错误:', event.error)
})
window.addEventListener('unhandledrejection', (event) => {
console.error('未捕获的 Promise 错误:', event.reason)
event.preventDefault()
})
💡 面试加分点: 生产环境应配置全局错误捕获并上报到监控平台(如 Sentry)。
async/await中的错误必须用try/catch捕获。
30. cookie、sessionStorage、localStorage 的区别?
| 特性 | cookie | sessionStorage | localStorage |
|---|---|---|---|
| 存储大小 | 约 4KB | 约 5MB | 约 5MB |
| 生命周期 | 可设置过期时间 | 页面会话期间(关闭标签页清除) | 永久(手动清除) |
| 服务端通信 | ✅ 每次请求自动携带 | ❌ | ❌ |
| 作用域 | 同域名下所有标签页 | 当前标签页 | 同域名下所有标签页 |
| API | document.cookie |
sessionStorage.getItem/setItem |
localStorage.getItem/setItem |
javascript
// ========== cookie ==========
// 设置 cookie
document.cookie = 'username=张三; max-age=3600; path=/' // 1小时后过期
document.cookie = 'token=abc123; secure; samesite=strict' // 安全设置
// 读取所有 cookie(返回字符串,需要手动解析)
console.log(document.cookie) // 'username=张三; token=abc123'
// 封装 cookie 操作
function getCookie(name) {
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`))
return match ? decodeURIComponent(match[1]) : null
}
// ========== localStorage ==========
localStorage.setItem('user', JSON.stringify({ name: '张三', age: 25 }))
const user = JSON.parse(localStorage.getItem('user'))
localStorage.removeItem('user')
localStorage.clear() // 清除所有
// ========== sessionStorage ==========
sessionStorage.setItem('tempData', '临时数据')
sessionStorage.getItem('tempData') // '临时数据'
// ✅ 监听 storage 变化(跨标签页通信)
window.addEventListener('storage', (event) => {
console.log(`键 ${event.key} 从 ${event.oldValue} 变为 ${event.newValue}`)
})
💡 面试加分点: cookie 的
HttpOnly属性可以防止 XSS 攻击读取 cookie;SameSite属性可以防止 CSRF 攻击。localStorage的storage事件可以实现跨标签页通信。
31. 栈与队列、栈与堆的区别?
javascript
// ========== 栈(Stack)vs 队列(Queue)==========
// 栈:后进先出(LIFO)--- 如浏览器的后退按钮、函数调用栈
// 队列:先进先出(FIFO)--- 如事件队列、消息队列
// 用数组模拟栈
const stack = []
stack.push(1) // 入栈 [1]
stack.push(2) // 入栈 [1, 2]
stack.push(3) // 入栈 [1, 2, 3]
stack.pop() // 出栈 3 → [1, 2]
// 用数组模拟队列
const queue = []
queue.push(1) // 入队 [1]
queue.push(2) // 入队 [1, 2]
queue.push(3) // 入队 [1, 2, 3]
queue.shift() // 出队 1 → [2, 3]
// ========== 栈内存 vs 堆内存 ==========
// 栈内存:存储基本类型值和引用地址,自动分配释放,速度快
// 堆内存:存储引用类型的实际数据,手动分配(GC 回收),速度较慢
let a = 10 // 基本类型 → 栈中直接存储值 10
let b = a // 值拷贝 → 栈中新建一个 10
b = 20 // 修改 b 不影响 a
console.log(a) // 10
let obj1 = { x: 1 } // 引用类型 → 堆中存对象,栈中存地址
let obj2 = obj1 // 地址拷贝 → obj2 和 obj1 指向同一个堆内存
obj2.x = 99 // 修改 obj2 影响 obj1(同一对象)
console.log(obj1.x) // 99
32. 进程与线程的关系?
| 对比 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 定义 | 系统资源分配的最小单位 | CPU 调度执行的最小单位 |
| 关系 | 一个进程包含多个线程 | 线程属于某个进程 |
| 内存 | 独立内存空间 | 共享进程的内存 |
| 通信 | IPC(进程间通信) | 直接读写共享内存 |
| 开销 | 创建/销毁开销大 | 开销小 |
javascript
// ========== 浏览器的多进程架构 ==========
// 浏览器主进程:UI、用户交互
// 渲染进程:每个标签页一个(页面渲染、JS 执行)
// GPU 进程:图形绘制
// 网络进程:网络请求
// 插件进程:浏览器插件
// ========== JS 是单线程的 ==========
// JS 主线程负责:DOM 操作、事件处理、定时器、网络回调等
// 通过事件循环(Event Loop)实现异步非阻塞
// ========== Web Worker:多线程方案 ==========
// 主线程
const worker = new Worker('worker.js')
worker.postMessage({ type: 'calculate', data: [1, 2, 3] })
worker.onmessage = (e) => {
console.log('Worker 返回:', e.data)
}
// worker.js(独立线程,不能操作 DOM)
// self.onmessage = (e) => {
// const result = heavyComputation(e.data)
// self.postMessage(result)
// }
💡 面试加分点: 每个浏览器标签页通常是一个独立的渲染进程(进程隔离,一个页面崩溃不会影响其他页面)。JS 是单线程的原因是为了避免多线程操作 DOM 的复杂性。Web Worker 可以在后台线程执行耗时计算,但不能访问 DOM。
33. 数组随机排序(洗牌算法)?
javascript
// ❌ 不推荐:sort + Math.random(不均匀)
const arr = [1, 2, 3, 4, 5]
arr.sort(() => Math.random() - 0.5)
// 这种方式的排序结果不是均匀分布的!
// ✅ Fisher-Yates 洗牌算法(均匀随机排列)
function shuffle(arr) {
const result = [...arr] // 不修改原数组
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[result[i], result[j]] = [result[j], result[i]] // 交换
}
return result
}
const original = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(shuffle(original)) // 每次都是随机顺序
console.log(original) // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10](不变)
// ✅ 验证均匀性
function testShuffle(fn, arr, times = 100000) {
const counts = {}
for (let i = 0; i < times; i++) {
const key = fn([...arr]).join(',')
counts[key] = (counts[key] || 0) + 1
}
return counts
}
💡 面试加分点: Fisher-Yates 洗牌算法的时间复杂度是 O(n),空间复杂度 O(1)(原地交换)。
sort(() => Math.random() - 0.5)之所以不均匀,是因为排序算法(如快排)的比较次数和顺序会影响最终分布。
34. JavaScript 中常见的设计模式有哪些?
javascript
// ========== 1. 单例模式(Singleton)==========
// 保证一个类只有一个实例
class Singleton {
static instance = null
constructor() {
if (Singleton.instance) return Singleton.instance
this.data = {}
Singleton.instance = this
}
static getInstance() {
if (!Singleton.instance) Singleton.instance = new Singleton()
return Singleton.instance
}
}
const a = Singleton.getInstance()
const b = Singleton.getInstance()
console.log(a === b) // true
// 实际应用:全局状态管理、弹窗实例、日志记录器
// ========== 2. 观察者模式(Observer)==========
// 当对象状态变化时,自动通知所有依赖它的对象
class EventEmitter {
constructor() {
this.events = new Map()
}
on(event, callback) {
if (!this.events.has(event)) this.events.set(event, [])
this.events.get(event).push(callback)
return this // 支持链式调用
}
off(event, callback) {
const callbacks = this.events.get(event)
if (callbacks) {
this.events.set(event, callbacks.filter(cb => cb !== callback))
}
return this
}
emit(event, ...args) {
const callbacks = this.events.get(event)
if (callbacks) callbacks.forEach(cb => cb(...args))
return this
}
once(event, callback) {
const wrapper = (...args) => {
callback(...args)
this.off(event, wrapper)
}
return this.on(event, wrapper)
}
}
const emitter = new EventEmitter()
emitter.on('login', (user) => console.log(`${user} 登录了`))
emitter.emit('login', '张三') // '张三 登录了'
// 实际应用:Vue 的事件系统、Node.js 的 EventEmitter、自定义事件总线
// ========== 3. 发布-订阅模式 ==========
// 与观察者模式类似,但有一个中间的"消息中心"解耦
class PubSub {
constructor() { this.channels = {} }
subscribe(channel, callback) {
if (!this.channels[channel]) this.channels[channel] = []
this.channels[channel].push(callback)
return () => this.unsubscribe(channel, callback) // 返回取消订阅函数
}
unsubscribe(channel, callback) {
this.channels[channel] = this.channels[channel]?.filter(cb => cb !== callback)
}
publish(channel, data) {
this.channels[channel]?.forEach(cb => cb(data))
}
}
// 实际应用:Redux、Vuex、消息队列
// ========== 4. 策略模式(Strategy)==========
// 定义一系列算法,让它们可以互相替换
const strategies = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
}
function calculate(strategy, a, b) {
return strategies[strategy]?.(a, b) ?? null
}
calculate('add', 5, 3) // 8
calculate('multiply', 5, 3) // 15
// 实际应用:表单验证规则、价格计算策略、权限验证
// ========== 5. 代理模式(Proxy Pattern)==========
// 通过代理对象控制对目标对象的访问
function createCachedFetch(fetcher) {
const cache = new Map()
return new Proxy(fetcher, {
apply(target, thisArg, args) {
const key = JSON.stringify(args)
if (cache.has(key)) return Promise.resolve(cache.get(key))
return target.apply(thisArg, args).then(result => {
cache.set(key, result)
return result
})
}
})
}
// ========== 6. 装饰器模式(Decorator)==========
// 动态地给对象添加功能,不修改原有代码
function withLogging(fn, name) {
return function(...args) {
console.log(`[${name}] 调用参数:`, args)
const result = fn.apply(this, args)
console.log(`[${name}] 返回结果:`, result)
return result
}
}
const add = (a, b) => a + b
const loggedAdd = withLogging(add, 'add')
loggedAdd(1, 2)
// [add] 调用参数: [1, 2]
// [add] 返回结果: 3
// 实际应用:日志记录、性能监控、权限校验、React 高阶组件(HOC)
💡 面试加分点: 观察者模式和发布-订阅模式的区别------观察者模式中主体直接通知观察者 (耦合),发布-订阅模式有事件中心作为中间层(解耦)。Vue2 的响应式系统用的是观察者模式(Dep/Watcher),Vue 组件间通信用 EventBus 是发布-订阅模式。
35. 手写 instanceof?
instanceof 检测构造函数的 prototype 是否出现在对象的原型链上。
javascript
// ========== 手写 instanceof ==========
function myInstanceof(obj, Constructor) {
// 基本类型直接返回 false
if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
return false
}
// 获取对象的原型
let proto = Object.getPrototypeOf(obj)
const prototype = Constructor.prototype
// 沿原型链向上查找
while (proto !== null) {
if (proto === prototype) return true
proto = Object.getPrototypeOf(proto)
}
return false
}
// 测试
function Animal(name) { this.name = name }
function Dog(name) {
Animal.call(this, name)
}
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog
const dog = new Dog('Rex')
console.log(myInstanceof(dog, Dog)) // true
console.log(myInstanceof(dog, Animal)) // true
console.log(myInstanceof(dog, Object)) // true
console.log(myInstanceof(dog, Array)) // false
console.log(myInstanceof([], Array)) // true
console.log(myInstanceof([], Object)) // true
console.log(myInstanceof(null, Object)) // false
console.log(myInstanceof(1, Number)) // false(基本类型)
// ========== instanceof 的注意事项 ==========
// 1. 基本类型无法使用(除非是包装对象)
42 instanceof Number // false
new Number(42) instanceof Number // true
// 2. 跨 iframe 失效(不同 window 的构造函数不同)
// iframe 中的 [] instanceof Array → false
// 3. 可以被 Symbol.hasInstance 修改
class EvenChecker {
static [Symbol.hasInstance](num) {
return typeof num === 'number' && num % 2 === 0
}
}
console.log(4 instanceof EvenChecker) // true
console.log(5 instanceof EvenChecker) // false
💡 面试加分点:
instanceof的本质就是沿原型链查找------obj.__proto__.__proto__...直到找到Constructor.prototype或到null。Symbol.hasInstance可以自定义instanceof的行为。
36. 手写 EventEmitter(发布-订阅模式)?
javascript
class EventEmitter {
constructor() {
this._events = Object.create(null) // 无原型的纯净对象
}
// 监听事件
on(event, listener) {
if (!this._events[event]) this._events[event] = []
this._events[event].push(listener)
return this
}
// 只监听一次
once(event, listener) {
const wrapper = (...args) => {
listener.apply(this, args)
this.off(event, wrapper)
}
wrapper._original = listener // 保存原始引用,方便 off 移除
return this.on(event, wrapper)
}
// 触发事件
emit(event, ...args) {
const listeners = this._events[event]
if (!listeners || listeners.length === 0) return false
// 拷贝一份,防止在回调中修改数组影响遍历
const copy = [...listeners]
copy.forEach(listener => listener.apply(this, args))
return true
}
// 移除事件监听
off(event, listener) {
if (!listener) {
// 不传 listener 则移除该事件的所有监听
delete this._events[event]
} else {
this._events[event] = this._events[event]?.filter(
fn => fn !== listener && fn._original !== listener
)
}
return this
}
// 获取监听器数量
listenerCount(event) {
return this._events[event]?.length ?? 0
}
// 移除所有事件
removeAllListeners() {
this._events = Object.create(null)
return this
}
}
// ========== 测试 ==========
const bus = new EventEmitter()
function onLogin(user) {
console.log(`${user} 登录了`)
}
// 基本使用
bus.on('login', onLogin)
bus.on('login', (user) => console.log(`欢迎 ${user}!`))
bus.emit('login', '张三')
// 张三 登录了
// 欢迎 张三!
// once:只触发一次
bus.once('init', () => console.log('初始化完成'))
bus.emit('init') // '初始化完成'
bus.emit('init') // 无输出(已移除)
// off:移除监听
bus.off('login', onLogin)
bus.emit('login', '李四')
// 欢迎 李四!(只有第二个监听器了)
// 链式调用
bus.on('a', () => {}).on('b', () => {}).emit('a')
// ========== 实际应用:组件间通信(事件总线)==========
// Vue2 中常用 EventBus
const EventBus = new EventEmitter()
// 组件 A 发送
EventBus.emit('data-updated', { id: 1, name: '新数据' })
// 组件 B 监听
EventBus.on('data-updated', (data) => console.log('收到更新:', data))
💡 面试加分点: Node.js 的
events.EventEmitter是很多核心模块(如fs、http、stream)的基础。Vue2 实例本身就是一个 EventEmitter( on/emit/$off)。内存泄漏风险:组件销毁时必须调用off移除监听器。
37. 如何实现并发控制?(控制 Promise 并发数量)
javascript
// ========== 方法1:手写并发控制池 ==========
async function concurrentPool(tasks, maxConcurrency = 3) {
const results = []
const executing = new Set()
for (const [index, task] of tasks.entries()) {
const promise = Promise.resolve().then(() => task())
results[index] = promise
executing.add(promise)
const cleanup = () => executing.delete(promise)
promise.then(cleanup, cleanup)
// 达到最大并发数时,等待其中一个完成
if (executing.size >= maxConcurrency) {
await Promise.race(executing)
}
}
return Promise.all(results)
}
// 使用
const urls = Array.from({ length: 10 }, (_, i) => `/api/data/${i}`)
const tasks = urls.map(url => () => fetch(url).then(r => r.json()))
const results = await concurrentPool(tasks, 3)
// 最多同时 3 个请求在执行
// ========== 方法2:基于类的并发调度器 ==========
class TaskScheduler {
constructor(maxConcurrency) {
this.maxConcurrency = maxConcurrency
this.running = 0
this.queue = []
}
add(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject })
this._run()
})
}
_run() {
while (this.running < this.maxConcurrency && this.queue.length > 0) {
const { task, resolve, reject } = this.queue.shift()
this.running++
Promise.resolve(task())
.then(resolve, reject)
.finally(() => {
this.running--
this._run()
})
}
}
}
// 使用
const scheduler = new TaskScheduler(2)
function createTask(id, time) {
return () => new Promise(resolve => {
console.log(`任务 ${id} 开始`)
setTimeout(() => {
console.log(`任务 ${id} 完成`)
resolve(id)
}, time)
})
}
scheduler.add(createTask(1, 1000))
scheduler.add(createTask(2, 500))
scheduler.add(createTask(3, 300))
scheduler.add(createTask(4, 400))
// 同时运行任务 1 和 2
// 任务 2 完成后开始任务 3
// 任务 3 完成后开始任务 4
// ========== 方法3:简洁实现(面试常用)==========
async function limitConcurrency(tasks, limit) {
const results = new Array(tasks.length)
let index = 0
async function worker() {
while (index < tasks.length) {
const i = index++
results[i] = await tasks[i]()
}
}
// 创建 limit 个 worker 并行执行
await Promise.all(Array.from({ length: limit }, worker))
return results
}
// ========== 实用场景 ==========
// 1. 批量上传文件(限制同时上传数量)
// 2. 爬虫请求频率控制
// 3. 批量数据处理(避免内存溢出)
// 4. API 接口限流
💡 面试加分点: 浏览器对同一域名有并发连接限制(Chrome 是 6 个),但 JS 层面的 Promise 并发不受此限制(只是 HTTP 请求排队)。Promise.all 会同时启动所有 Promise,而并发池可以控制实际执行的数量。
38. 手写 JSON.stringify 和 JSON.parse?
javascript
// ========== 手写 JSON.stringify ==========
function jsonStringify(value) {
// null
if (value === null) return 'null'
// undefined、函数、Symbol → 返回 undefined(顶层)或被忽略(对象属性)
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
return undefined
}
// 布尔值
if (typeof value === 'boolean') return value.toString()
// 数字
if (typeof value === 'number') {
if (Number.isNaN(value) || !Number.isFinite(value)) return 'null'
return value.toString()
}
// 字符串
if (typeof value === 'string') {
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t')}"`
}
// BigInt → 报错
if (typeof value === 'bigint') {
throw new TypeError('BigInt value can\'t be serialized in JSON')
}
// 日期
if (value instanceof Date) {
return `"${value.toISOString()}"`
}
// 正则 → 空对象
if (value instanceof RegExp) return '{}'
// 数组
if (Array.isArray(value)) {
const items = value.map(item => {
const result = jsonStringify(item)
return result === undefined ? 'null' : result
})
return `[${items.join(',')}]`
}
// 对象
if (typeof value === 'object') {
// 如果有 toJSON 方法,使用它
if (typeof value.toJSON === 'function') {
return jsonStringify(value.toJSON())
}
const pairs = []
for (const key of Object.keys(value)) {
const result = jsonStringify(value[key])
if (result !== undefined) {
pairs.push(`${jsonStringify(key)}:${result}`)
}
}
return `{${pairs.join(',')}}`
}
}
// 测试
jsonStringify({ name: '张三', age: 25, fn: () => {} })
// '{"name":"张三","age":25}'(函数被忽略)
jsonStringify([1, undefined, null, '4'])
// '[1,null,null,"4"]'(undefined 变为 null)
// ========== 手写简易 JSON.parse ==========
// 方法1:使用 eval(不推荐,有安全风险)
function jsonParse1(str) {
return eval(`(${str})`)
}
// 方法2:使用 Function 构造函数(稍安全)
function jsonParse2(str) {
return new Function(`return ${str}`)()
}
// 测试
jsonParse2('{"name":"张三","age":25}')
// { name: '张三', age: 25 }
💡 面试加分点:
JSON.stringify会忽略undefined、function、Symbol类型的属性。对于循环引用会报错。toJSON()方法可以自定义序列化行为------Date 对象就是通过toJSON返回 ISO 字符串的。实际手写 JSON.parse 非常复杂(需要词法分析器),面试中说明思路即可。
39. 手写深比较(isEqual)?
javascript
// ========== 深比较:递归比较两个值是否相等 ==========
function isEqual(a, b) {
// 1. 严格相等(处理基本类型、引用相同的情况)
if (a === b) return true
// 2. 处理 NaN(NaN !== NaN,但我们认为它们相等)
if (Number.isNaN(a) && Number.isNaN(b)) return true
// 3. 如果不是对象类型,且不严格相等,则不等
if (typeof a !== 'object' || typeof b !== 'object') return false
if (a === null || b === null) return false
// 4. 类型必须一致
const classA = Object.prototype.toString.call(a)
const classB = Object.prototype.toString.call(b)
if (classA !== classB) return false
// 5. Date 对象
if (a instanceof Date) return a.getTime() === b.getTime()
// 6. RegExp 对象
if (a instanceof RegExp) return a.source === b.source && a.flags === b.flags
// 7. Map
if (a instanceof Map) {
if (a.size !== b.size) return false
for (const [key, val] of a) {
if (!b.has(key) || !isEqual(val, b.get(key))) return false
}
return true
}
// 8. Set
if (a instanceof Set) {
if (a.size !== b.size) return false
for (const val of a) {
if (!b.has(val)) return false
}
return true
}
// 9. 数组和普通对象
const keysA = Object.keys(a)
const keysB = Object.keys(b)
if (keysA.length !== keysB.length) return false
for (const key of keysA) {
if (!Object.hasOwn(b, key) || !isEqual(a[key], b[key])) {
return false
}
}
return true
}
// ========== 测试 ==========
// 基本类型
isEqual(1, 1) // true
isEqual('hello', 'hello') // true
isEqual(NaN, NaN) // true
// 数组
isEqual([1, 2, 3], [1, 2, 3]) // true
isEqual([1, 2], [1, 2, 3]) // false
// 嵌套对象
isEqual(
{ a: 1, b: { c: 2, d: [3, 4] } },
{ a: 1, b: { c: 2, d: [3, 4] } }
) // true
isEqual(
{ a: 1, b: { c: 2 } },
{ a: 1, b: { c: 3 } }
) // false
// 特殊对象
isEqual(new Date('2026-01-01'), new Date('2026-01-01')) // true
isEqual(/abc/gi, /abc/gi) // true
isEqual(new Map([['a', 1]]), new Map([['a', 1]])) // true
isEqual(new Set([1, 2, 3]), new Set([1, 2, 3])) // true
// ========== 浅比较 vs 深比较 ==========
// 浅比较:只比较第一层(React.memo、PureComponent 使用)
function shallowEqual(a, b) {
if (Object.is(a, b)) return true
if (typeof a !== 'object' || typeof b !== 'object' || !a || !b) return false
const keysA = Object.keys(a)
if (keysA.length !== Object.keys(b).length) return false
return keysA.every(key => Object.is(a[key], b[key]))
}
💡 面试加分点: 深比较在大型对象上性能较差,React 中推荐浅比较 + 不可变数据 的方案。Lodash 的
_.isEqual还处理了 Buffer、ArrayBuffer、Error 等更多类型。面试中需要注意 NaN 和循环引用的处理。
40. Generator 生成器和异步迭代器是什么?
javascript
// ========== Generator 基础 ==========
// Generator 函数使用 function* 声明,可以暂停执行并通过 yield 产出值
function* counter(start = 0) {
let count = start
while (true) {
const reset = yield count++
if (reset) count = start // 可以通过 next() 传值给 yield
}
}
const gen = counter(1)
gen.next() // { value: 1, done: false }
gen.next() // { value: 2, done: false }
gen.next() // { value: 3, done: false }
gen.next(true) // { value: 1, done: false }(重置了)
// ========== Generator 作为可迭代对象 ==========
function* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i
}
}
for (const n of range(1, 10, 2)) {
console.log(n) // 1, 3, 5, 7, 9
}
[...range(1, 5)] // [1, 2, 3, 4, 5]
// ========== yield* 委托(遍历其他可迭代对象)==========
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item) // 委托给另一个 Generator
} else {
yield item
}
}
}
[...flatten([1, [2, [3, [4]]]])] // [1, 2, 3, 4]
// ========== Generator 实现异步控制流(co 模式)==========
// 这就是 async/await 的前身
function co(generatorFn) {
return function(...args) {
const gen = generatorFn.apply(this, args)
return new Promise((resolve, reject) => {
function step(key, value) {
try {
const { value: result, done } = gen[key](value)
if (done) return resolve(result)
Promise.resolve(result).then(
val => step('next', val),
err => step('throw', err)
)
} catch (err) {
reject(err)
}
}
step('next')
})
}
}
// 使用 co + Generator 实现类似 async/await 的效果
const fetchData = co(function*() {
const user = yield fetch('/api/user').then(r => r.json())
const posts = yield fetch(`/api/posts/${user.id}`).then(r => r.json())
return { user, posts }
})
fetchData().then(console.log)
// ========== 异步迭代器(Async Iterator)==========
// 使用 for await...of 遍历异步数据流
async function* asyncRange(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 100))
yield i
}
}
// 消费异步迭代器
for await (const num of asyncRange(1, 5)) {
console.log(num) // 每 100ms 输出一个:1, 2, 3, 4, 5
}
// ========== 实用:分页获取所有数据 ==========
async function* fetchAllPages(url) {
let page = 1
let hasMore = true
while (hasMore) {
const response = await fetch(`${url}?page=${page}`)
const data = await response.json()
yield* data.items // 逐个 yield 每一项
hasMore = data.hasMore
page++
}
}
// 使用
for await (const item of fetchAllPages('/api/users')) {
console.log(item)
// 自动翻页,逐条处理,内存友好
}
// ========== 实用:流式读取响应体 ==========
async function* readStream(response) {
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
yield decoder.decode(value, { stream: true })
}
}
const response = await fetch('/api/stream')
for await (const chunk of readStream(response)) {
console.log('收到数据块:', chunk)
}
💡 面试加分点:
async/await的底层实现就是 Generator + 自动执行器(co)。异步迭代器for await...of在 Node.js 中处理文件流、数据库查询结果非常常见。yield*可以将控制权委托给另一个 Generator 或可迭代对象。
41. 手写 Promise 限流与重试?
javascript
// ========== 请求重试(带指数退避)==========
async function retry(fn, options = {}) {
const { maxRetries = 3, baseDelay = 1000, maxDelay = 30000 } = options
let lastError
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn()
} catch (err) {
lastError = err
if (attempt === maxRetries) break
// 指数退避 + 随机抖动
const delay = Math.min(
baseDelay * Math.pow(2, attempt) + Math.random() * 1000,
maxDelay
)
console.log(`第 ${attempt + 1} 次重试,等待 ${delay.toFixed(0)}ms...`)
await new Promise(resolve => setTimeout(resolve, delay))
}
}
throw lastError
}
// 使用
const data = await retry(
() => fetch('/api/unstable').then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
}),
{ maxRetries: 3, baseDelay: 1000 }
)
// ========== 批量请求(并发限流 + 重试)==========
async function batchRequest(urls, { concurrency = 5, retries = 2 } = {}) {
const results = new Array(urls.length)
const errors = []
let index = 0
async function worker() {
while (index < urls.length) {
const i = index++
try {
results[i] = await retry(
() => fetch(urls[i]).then(r => r.json()),
{ maxRetries: retries }
)
} catch (err) {
errors.push({ index: i, url: urls[i], error: err })
results[i] = null
}
}
}
await Promise.all(
Array.from({ length: Math.min(concurrency, urls.length) }, worker)
)
return { results, errors }
}
// 使用
const urls = Array.from({ length: 100 }, (_, i) => `/api/item/${i}`)
const { results, errors } = await batchRequest(urls, {
concurrency: 5,
retries: 2,
})
console.log(`成功: ${results.filter(Boolean).length}, 失败: ${errors.length}`)
// ========== Promise 串行执行 ==========
async function serial(tasks) {
const results = []
for (const task of tasks) {
results.push(await task())
}
return results
}
// 或用 reduce 实现
function serialReduce(tasks) {
return tasks.reduce(
(chain, task) => chain.then(results =>
task().then(result => [...results, result])
),
Promise.resolve([])
)
}
// ========== Promise 超时包装 ==========
function withTimeout(promise, ms, message = '操作超时') {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(message)), ms)
)
return Promise.race([promise, timeout])
}
// 使用
const result = await withTimeout(
fetch('/api/slow-endpoint'),
5000,
'请求超时,请稍后重试'
)
// ========== Promise 缓存(避免重复请求)==========
function createCachedRequest(fn, ttl = 60000) {
const cache = new Map()
return async function(...args) {
const key = JSON.stringify(args)
const cached = cache.get(key)
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data // 缓存未过期,直接返回
}
// 避免并发时重复请求(缓存 Promise 而非结果)
if (cached?.pending) return cached.pending
const pending = fn(...args)
cache.set(key, { pending, timestamp: Date.now() })
try {
const data = await pending
cache.set(key, { data, timestamp: Date.now() })
return data
} catch (err) {
cache.delete(key)
throw err
}
}
}
const cachedFetch = createCachedRequest(
(id) => fetch(`/api/users/${id}`).then(r => r.json()),
30000 // 30 秒缓存
)
await cachedFetch(1) // 发起请求
await cachedFetch(1) // 使用缓存(30秒内)
💡 面试加分点: 指数退避(Exponential Backoff)是重试的最佳实践------避免短时间内大量重试导致服务端雪崩。加上随机抖动(Jitter)可以防止多个客户端同时重试。缓存 Promise 而非结果可以避免竞态条件(多个相同请求同时发起时只执行一次)。