主题
JavaScript / TypeScript 基础
面向 5 年经验的前端/全栈工程师,不是八股文背诵,而是结合原理的深度理解。
每道题均包含:题目 → 考察点 → 深度解答 → 追问延伸
难度分级:⭐ 基础 / ⭐⭐ 进阶 / ⭐⭐⭐ 深入
1. typeof null 为什么是 "object"?
⭐ 基础 | 考察点:JS 历史遗留问题、类型判断
深度解答
这是 JavaScript 诞生之初(1995 年)的一个 bug,但由于向后兼容性一直保留至今。
在早期的 JavaScript 实现中,值在底层以 32 位存储,其中低 3 位用于表示类型标签(type tag):
| 类型标签 | 含义 |
|---|---|
000 | 对象(object) |
1 | 整数(int) |
010 | 浮点数(double) |
100 | 字符串(string) |
110 | 布尔值(boolean) |
而 null 的机器码是全零(0x00),低 3 位自然是 000,被 typeof 误判为对象。
javascript
typeof null // "object" — 历史 bug
typeof undefined // "undefined"
typeof 42 // "number"
typeof "hello" // "string"
typeof true // "boolean"
typeof Symbol() // "symbol"
typeof 42n // "bigint"
typeof function(){} // "function"
typeof {} // "object"正确判断 null 的方式:
javascript
const isNull = (val) => val === null
Object.prototype.toString.call(null) // "[object Null]"最可靠的类型判断函数:
javascript
function getType(val) {
return Object.prototype.toString.call(val).slice(8, -1).toLowerCase()
}
getType(null) // "null"
getType([]) // "array"
getType(new Date()) // "date"
getType(/abc/) // "regexp"追问延伸
- TC39 曾在 ES1 的修订中提议修复
typeof null,为什么最终没有通过? typeof能否区分普通对象和数组?如何实现精确的类型判断?typeof对未声明变量返回什么?在let/const的暂时性死区中呢?
2. == 和 === 的区别?== 的隐式转换规则是什么?
⭐ 基础 | 考察点:类型转换、抽象相等算法
深度解答
===(严格相等)不做类型转换,类型不同直接返回 false。
==(抽象相等)会按 ECMAScript 规范中的 Abstract Equality Comparison 算法进行隐式类型转换:
核心转换规则(按优先级):
- null == undefined → true(且它们不等于其他任何值)
- Number vs String → 将 String 转为 Number
- Boolean vs 任意 → 将 Boolean 转为 Number(true→1, false→0)
- Object vs 原始值 → 调用对象的
[Symbol.toPrimitive]('default')或valueOf()或toString()
javascript
null == undefined // true(特殊规则)
null == 0 // false(null 只等于 undefined)
null == "" // false
"5" == 5 // true → Number("5") === 5
true == 1 // true → Number(true) === 1
true == "1" // true → 1 == "1" → 1 === 1
false == "0" // true → 0 == "0" → 0 === 0
false == "" // true → 0 == "" → 0 === 0
[] == false // true → [] == 0 → "" == 0 → 0 === 0
[] == ![] // true → [] == false → 同上
{} == "[object Object]" // true → ({}).toString() === "[object Object]"经典陷阱题:
javascript
"" == false // true
"0" == false // true
"" == "0" // false — 两个字符串直接比较,不转换
[] == 0 // true
[] == "" // true
0 == "" // trueObject 的 toPrimitive 转换链:
javascript
const obj = {
[Symbol.toPrimitive](hint) {
if (hint === 'number') return 42
if (hint === 'string') return 'hello'
return 'default'
}
}
obj == 42 // false → obj == 42 → "default" == 42 → NaN === 42
obj == 'default' // true追问延伸
Object.is()和===有什么区别?(提示:NaN和±0)- 为什么 ESLint 默认推荐
eqeqeq规则?实际项目中有没有合理使用==的场景? [Symbol.toPrimitive]、valueOf()、toString()的调用优先级是什么?
3. 说说你对原型链的理解,画出 Function/Object 的完整原型关系
⭐⭐ 进阶 | 考察点:原型链、
__proto__、prototype
深度解答
JavaScript 的继承是基于原型链的,每个对象都有一个内部属性 [[Prototype]](通过 __proto__ 访问),指向它的原型对象。属性查找会沿着原型链向上搜索,直到找到属性或到达 null。
三个核心概念:
prototype:只有函数才有的属性,指向该函数的原型对象__proto__(即[[Prototype]]):所有对象都有,指向创建该对象的构造函数的prototypeconstructor:原型对象上的属性,指回构造函数本身
javascript
function Person(name) {
this.name = name
}
const p = new Person('Alice')
p.__proto__ === Person.prototype // true
Person.prototype.constructor === Person // true
p.constructor === Person // true(沿原型链找到的)完整的 Function / Object 原型关系:
null
↑
Object.prototype
↗ ↑
Person.prototype Function.prototype
↑ ↑ ↑
p Person Object
Function用代码验证:
javascript
Object.prototype.__proto__ === null // true — 原型链顶端
Function.prototype.__proto__ === Object.prototype // true
Person.prototype.__proto__ === Object.prototype // true
Person.__proto__ === Function.prototype // true — 函数是 Function 的实例
Object.__proto__ === Function.prototype // true
Function.__proto__ === Function.prototype // true — 鸡生蛋问题
typeof Function.prototype // "function" — 唯一的函数原型"鸡蛋问题" 解析:
Function.__proto__ === Function.prototype 看似"自己创造自己",实际上这是引擎初始化时的特殊处理——Function.prototype 是引擎直接创建的内置函数对象,并非通过 new Function() 产生。
属性查找过程:
javascript
p.toString()
// 查找链:
// 1. p 自身 → 没有 toString
// 2. p.__proto__ (Person.prototype) → 没有 toString
// 3. Person.prototype.__proto__ (Object.prototype) → 找到 toString!追问延伸
Object.create(null)创建的对象有什么特殊之处?适用什么场景?Object.getPrototypeOf()和__proto__的区别?为什么推荐前者?- 如何实现一个不使用
class的继承?(寄生组合继承)
4. 手写 new 操作符
⭐⭐ 进阶 | 考察点:原型链、构造函数
深度解答
new 操作符做了四件事:
- 创建一个新的空对象
- 将新对象的
__proto__指向构造函数的prototype - 以新对象为
this执行构造函数 - 如果构造函数返回了一个对象,则使用该对象;否则返回新创建的对象
javascript
function myNew(Constructor, ...args) {
const obj = Object.create(Constructor.prototype)
const result = Constructor.apply(obj, args)
return result instanceof Object ? result : obj
}验证:
javascript
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function() {
return `Hi, I'm ${this.name}`
}
const p = myNew(Person, 'Alice', 25)
console.log(p.name) // "Alice"
console.log(p.sayHi()) // "Hi, I'm Alice"
console.log(p instanceof Person) // true
console.log(p.__proto__ === Person.prototype) // true构造函数返回对象的情况:
javascript
function Weird() {
this.name = 'normal'
return { name: 'override' }
}
const w1 = new Weird()
console.log(w1.name) // "override" — 返回了显式对象
function Weird2() {
this.name = 'normal'
return 42
}
const w2 = new Weird2()
console.log(w2.name) // "normal" — 返回原始值被忽略追问延伸
- 如果构造函数返回
null会怎样?(null虽然typeof是"object",但null instanceof Object是false) Object.create()和{}.__proto__ = xxx的区别?new操作符和Reflect.construct()有什么区别?
5. 手写 instanceof
⭐⭐ 进阶 | 考察点:原型链遍历
深度解答
instanceof 的原理是沿着左侧对象的原型链向上查找,看能否找到右侧构造函数的 prototype。
javascript
function myInstanceof(left, right) {
if (left === null || (typeof left !== 'object' && typeof left !== 'function')) {
return false
}
let proto = Object.getPrototypeOf(left)
const target = right.prototype
while (proto !== null) {
if (proto === target) return true
proto = Object.getPrototypeOf(proto)
}
return false
}验证:
javascript
function Animal() {}
function Dog() {}
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog
const d = new Dog()
console.log(myInstanceof(d, Dog)) // true
console.log(myInstanceof(d, Animal)) // true
console.log(myInstanceof(d, Object)) // true
console.log(myInstanceof(d, Array)) // false
console.log(myInstanceof(42, Number)) // false — 原始值
console.log(myInstanceof("str", String)) // false — 原始值instanceof 的局限性:
javascript
42 instanceof Number // false — 原始值没有原型链
new Number(42) instanceof Number // true — 包装对象
const iframe = document.createElement('iframe')
document.body.appendChild(iframe)
const IframeArray = iframe.contentWindow.Array
const arr = new IframeArray()
arr instanceof Array // false — 不同执行环境的 Array 不同
Array.isArray(arr) // true — 推荐方式Symbol.hasInstance 自定义:
javascript
class Even {
static [Symbol.hasInstance](num) {
return typeof num === 'number' && num % 2 === 0
}
}
console.log(2 instanceof Even) // true
console.log(3 instanceof Even) // false追问延伸
- 为什么
Array.isArray()比instanceof Array更可靠? - 跨 iframe 的
instanceof为什么会失败? Symbol.hasInstance能用在什么实际场景中?
6. 解释闭包,给出 3 个实际应用场景
⭐⭐ 进阶 | 考察点:闭包、作用域链
深度解答
闭包是指一个函数能够访问其词法作用域中的变量,即使该函数在其词法作用域之外执行。
本质:函数创建时会捕获其所在的词法环境(Lexical Environment),形成一个闭合的变量引用链。
javascript
function outer() {
let count = 0
return function inner() {
return ++count
}
}
const counter = outer()
console.log(counter()) // 1
console.log(counter()) // 2
console.log(counter()) // 3outer 执行完毕后,按理说 count 应该被回收,但因为 inner 引用了它,所以 count 被"关"在了闭包中,只要 counter 存在,count 就不会被 GC。
场景一:模块化(数据私有化)
javascript
const createStore = (initialState) => {
let state = initialState
return {
getState: () => state,
setState: (newState) => {
state = typeof newState === 'function' ? newState(state) : newState
}
}
}
const store = createStore({ count: 0 })
store.setState(prev => ({ count: prev.count + 1 }))
console.log(store.getState()) // { count: 1 }场景二:函数工厂(柯里化 / 偏函数)
javascript
const createMultiplier = (multiplier) => (value) => value * multiplier
const double = createMultiplier(2)
const triple = createMultiplier(3)
console.log(double(5)) // 10
console.log(triple(5)) // 15场景三:缓存(记忆化函数)
javascript
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('computing...')
return n * n
})
expensiveCalc(5) // computing... 25
expensiveCalc(5) // 25 (from cache, no log)经典闭包陷阱:
javascript
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100) // 3, 3, 3
}
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100) // 0, 1, 2 — let 块级作用域
}
for (var i = 0; i < 3; i++) {
;((j) => {
setTimeout(() => console.log(j), 100) // 0, 1, 2 — IIFE 创建闭包
})(i)
}追问延伸
- 闭包会导致内存泄漏吗?什么情况下会?如何排查?
- React 的
useCallback和闭包有什么关系?"过时闭包"(stale closure)是什么? - V8 引擎对闭包做了哪些优化?(提示:闭包只捕获实际使用的变量)
7. var/let/const 的区别?什么是暂时性死区?
⭐ 基础 | 考察点:变量提升、TDZ
深度解答
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 提升 | 提升并初始化为 undefined | 提升但不初始化 | 提升但不初始化 |
| 重复声明 | 允许 | 不允许 | 不允许 |
| 修改绑定 | 允许 | 允许 | 不允许 |
| 全局属性 | 挂到 window 上 | 不挂 | 不挂 |
暂时性死区(TDZ, Temporal Dead Zone):
let/const 虽然也会被提升(hoisting),但在声明语句之前处于"暂时性死区",访问会抛出 ReferenceError。
javascript
console.log(a) // undefined — var 提升并初始化
var a = 1
console.log(b) // ReferenceError: Cannot access 'b' before initialization
let b = 2TDZ 的精确边界:
javascript
{
// --- TDZ 开始 ---
console.log(typeof x) // ReferenceError! (不是 "undefined")
// --- TDZ 结束 ---
let x = 42
}
// 对比 var 的行为
console.log(typeof y) // "undefined" — 即使 y 完全不存在也不报错const 的"不可变"误解:
javascript
const obj = { a: 1 }
obj.a = 2 // OK — 修改属性值
obj.b = 3 // OK — 添加属性
// obj = {} // TypeError — 不能重新绑定
Object.freeze(obj)
obj.a = 999 // 静默失败(严格模式下 TypeError)
console.log(obj.a) // 2
const deepObj = { nested: { x: 1 } }
Object.freeze(deepObj)
deepObj.nested.x = 999 // OK! — freeze 是浅冻结追问延伸
for循环中let为什么每次迭代都有独立的绑定?引擎是如何实现的?Object.freeze()只是浅冻结,如何实现深冻结?- 在
switch的case中使用let/const为什么要加花括号?
8. 说说事件循环,给出一段代码分析输出顺序
⭐⭐⭐ 深入 | 考察点:宏任务/微任务、执行栈
深度解答
JavaScript 是单线程的,通过**事件循环(Event Loop)**实现异步。每一轮事件循环的流程:
┌──────────────────────────────────────────────┐
│ 执行栈(Call Stack) │
│ 同步代码在这里执行 │
└──────────────┬───────────────────────────────┘
↓ 同步代码执行完毕
┌──────────────────────────────────────────────┐
│ 微任务队列(Microtask Queue) │
│ Promise.then / MutationObserver / queueMicrotask │
│ 全部清空后,才进入下一步 │
└──────────────┬───────────────────────────────┘
↓ 微任务队列清空
┌──────────────────────────────────────────────┐
│ (可能触发)渲染更新 │
│ requestAnimationFrame → Style → Layout → Paint │
└──────────────┬───────────────────────────────┘
↓
┌──────────────────────────────────────────────┐
│ 宏任务队列(Macrotask Queue) │
│ setTimeout / setInterval / I/O / UI Events │
│ 取出一个宏任务执行,然后回到顶部 │
└──────────────────────────────────────────────┘关键规则:
- 每执行完一个宏任务,都会清空所有微任务
- 微任务中产生的新微任务也会在本轮清空
requestAnimationFrame在渲染前执行,既不是宏任务也不是微任务
经典面试题:分析输出顺序
javascript
console.log('1')
setTimeout(() => {
console.log('2')
Promise.resolve().then(() => console.log('3'))
}, 0)
Promise.resolve().then(() => {
console.log('4')
setTimeout(() => console.log('5'), 0)
})
Promise.resolve().then(() => console.log('6'))
console.log('7')分析过程:
| 步骤 | 执行栈 | 微任务队列 | 宏任务队列 | 输出 |
|---|---|---|---|---|
| 1 | 同步执行 console.log('1') | 1 | ||
| 2 | 注册 setTimeout 回调 | [setTimeout-2] | ||
| 3 | 注册 Promise.then 回调 | [then-4, then-6] | [setTimeout-2] | |
| 4 | 同步执行 console.log('7') | [then-4, then-6] | [setTimeout-2] | 7 |
| 5 | 清空微任务:执行 then-4 | [then-6] | [setTimeout-2, setTimeout-5] | 4 |
| 6 | 清空微任务:执行 then-6 | [] | [setTimeout-2, setTimeout-5] | 6 |
| 7 | 宏任务:执行 setTimeout-2 | [then-3] | [setTimeout-5] | 2 |
| 8 | 清空微任务:执行 then-3 | [] | [setTimeout-5] | 3 |
| 9 | 宏任务:执行 setTimeout-5 | [] | [] | 5 |
最终输出:1 → 7 → 4 → 6 → 2 → 3 → 5
async/await 的微任务时机:
javascript
async function foo() {
console.log('foo start')
await bar()
console.log('foo end')
}
async function bar() {
console.log('bar')
}
console.log('script start')
foo()
console.log('script end')输出:script start → foo start → bar → script end → foo end
await 之后的代码等价于放入 Promise.then 中,属于微任务。
追问延伸
- Node.js 的事件循环和浏览器有什么区别?(6 个阶段、
process.nextTickvsqueueMicrotask) requestAnimationFrame的回调在事件循环的什么位置执行?queueMicrotask和Promise.resolve().then()有什么区别?
9. Promise.all / Promise.allSettled / Promise.race / Promise.any 的区别?手写 Promise.all
⭐⭐ 进阶 | 考察点:Promise 静态方法
深度解答
四个方法的对比:
| 方法 | 成功条件 | 失败条件 | 返回值 |
|---|---|---|---|
Promise.all | 全部成功 | 任一失败 | 所有结果的数组 / 第一个错误 |
Promise.allSettled | — | — | 所有结果(含状态)的数组,永不 reject |
Promise.race | 第一个 settle | 第一个 settle | 最先完成的结果(无论成功/失败) |
Promise.any | 任一成功 | 全部失败 | 第一个成功的结果 / AggregateError |
javascript
const p1 = Promise.resolve(1)
const p2 = Promise.reject('err')
const p3 = Promise.resolve(3)
Promise.all([p1, p2, p3])
.catch(e => console.log(e)) // "err"
Promise.allSettled([p1, p2, p3])
.then(r => console.log(r))
// [
// { status: "fulfilled", value: 1 },
// { status: "rejected", reason: "err" },
// { status: "fulfilled", value: 3 }
// ]
Promise.race([p1, p2, p3])
.then(r => console.log(r)) // 1(p1 最先 resolve)
Promise.any([p2, p3])
.then(r => console.log(r)) // 3(跳过 rejected,取第一个 resolved)手写 Promise.all:
javascript
function promiseAll(promises) {
return new Promise((resolve, reject) => {
const results = []
let completed = 0
const items = Array.from(promises)
if (items.length === 0) {
resolve(results)
return
}
items.forEach((promise, index) => {
Promise.resolve(promise).then(
(value) => {
results[index] = value
completed++
if (completed === items.length) {
resolve(results)
}
},
(reason) => {
reject(reason)
}
)
})
})
}验证:
javascript
promiseAll([
Promise.resolve(1),
new Promise(r => setTimeout(() => r(2), 100)),
Promise.resolve(3)
]).then(console.log) // [1, 2, 3]
promiseAll([
Promise.resolve(1),
Promise.reject('fail'),
Promise.resolve(3)
]).catch(console.log) // "fail"
promiseAll([]).then(console.log) // []
promiseAll([42, 'hello', Promise.resolve(true)])
.then(console.log) // [42, "hello", true] — 非 Promise 值也能处理关键细节:
results[index] = value而不是results.push(value),保证结果顺序与输入一致- 用
Promise.resolve(promise)包装,兼容非 Promise 的值 completed计数器而不是results.length,避免稀疏数组问题
追问延伸
- 手写
Promise.allSettled和Promise.any? Promise.all传入空数组返回什么?Promise.race传入空数组呢?(提示:后者永远 pending)- 如何实现一个
Promise.all的并发限制版本?(控制最大并发数)
10. 手写完整的 Promise(符合 A+ 规范)
⭐⭐⭐ 深入 | 考察点:Promise 原理
深度解答
符合 Promise/A+ 规范的核心要求:三种状态(pending → fulfilled/rejected,不可逆)、then 返回新 Promise、链式调用、异步执行回调。
javascript
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
constructor(executor) {
this.status = PENDING
this.value = undefined
this.reason = undefined
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value) => {
if (value instanceof MyPromise) {
value.then(resolve, reject)
return
}
if (this.status === PENDING) {
this.status = FULFILLED
this.value = value
this.onFulfilledCallbacks.forEach(fn => fn())
}
}
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED
this.reason = reason
this.onRejectedCallbacks.forEach(fn => fn())
}
}
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 }
const promise2 = new MyPromise((resolve, reject) => {
const handleFulfilled = () => {
queueMicrotask(() => {
try {
const x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
})
}
const handleRejected = () => {
queueMicrotask(() => {
try {
const x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
})
}
if (this.status === FULFILLED) {
handleFulfilled()
} else if (this.status === REJECTED) {
handleRejected()
} else {
this.onFulfilledCallbacks.push(handleFulfilled)
this.onRejectedCallbacks.push(handleRejected)
}
})
return promise2
}
catch(onRejected) {
return this.then(null, onRejected)
}
finally(callback) {
return this.then(
(value) => MyPromise.resolve(callback()).then(() => value),
(reason) => MyPromise.resolve(callback()).then(() => { throw reason })
)
}
static resolve(value) {
if (value instanceof MyPromise) return value
return new MyPromise((resolve) => resolve(value))
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason))
}
}
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected'))
}
if (x instanceof MyPromise) {
x.then(resolve, reject)
return
}
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
let called = false
try {
const then = x.then
if (typeof then === 'function') {
then.call(
x,
(y) => {
if (called) return
called = true
resolvePromise(promise2, y, resolve, reject)
},
(r) => {
if (called) return
called = true
reject(r)
}
)
} else {
resolve(x)
}
} catch (err) {
if (called) return
called = true
reject(err)
}
} else {
resolve(x)
}
}A+ 规范的核心要点:
- 状态不可逆:pending → fulfilled 或 pending → rejected,只能转换一次
- then 必须返回新 Promise:支持链式调用
p.then(...).then(...) - 回调异步执行:用
queueMicrotask保证异步(规范要求"异步调用") - 值穿透:
onFulfilled/onRejected不是函数时,值要传递下去 - resolvePromise:处理 then 回调返回值,递归解析 thenable 对象
- 防止重复调用:
called标志位防止 thenable 的 resolve/reject 被多次调用
验证链式调用:
javascript
new MyPromise((resolve) => {
setTimeout(() => resolve(1), 100)
})
.then(val => {
console.log(val) // 1
return val + 1
})
.then(val => {
console.log(val) // 2
return new MyPromise(r => r(val * 10))
})
.then(val => {
console.log(val) // 20
})追问延伸
resolvePromise中为什么需要called标志位?什么情况下 thenable 会调用多次?- 为什么用
queueMicrotask而不是setTimeout?两者有什么区别? - Promise/A+ 规范和 ES6 Promise 有什么差异?(提示:ES6 多了
catch、finally、静态方法)
11. async/await 的本质是什么?如何用 Generator 模拟?
⭐⭐⭐ 深入 | 考察点:Generator、自动执行器
深度解答
async/await 是 Generator + 自动执行器的语法糖。async 函数返回一个 Promise,await 暂停函数执行,等待 Promise resolve 后继续。
Generator 基础回顾:
javascript
function* gen() {
const a = yield Promise.resolve(1)
const b = yield Promise.resolve(a + 2)
return b
}
const it = gen()
const { value: p1 } = it.next()
p1.then(val1 => {
const { value: p2 } = it.next(val1)
p2.then(val2 => {
const { value: result } = it.next(val2)
console.log(result) // 3
})
})上面的手动调用太繁琐,async/await 本质上就是自动执行这个过程。
手写自动执行器(模拟 async/await):
javascript
function asyncToGenerator(generatorFn) {
return function (...args) {
const gen = generatorFn.apply(this, args)
return new Promise((resolve, reject) => {
function step(key, arg) {
let result
try {
result = gen[key](arg)
} catch (err) {
return reject(err)
}
const { value, done } = result
if (done) {
resolve(value)
} else {
Promise.resolve(value).then(
(val) => step('next', val),
(err) => step('throw', err)
)
}
}
step('next', undefined)
})
}
}使用对比:
javascript
const fetchData = asyncToGenerator(function* () {
const user = yield fetch('/api/user').then(r => r.json())
const posts = yield fetch(`/api/posts?uid=${user.id}`).then(r => r.json())
return { user, posts }
})
async function fetchDataAsync() {
const user = await fetch('/api/user').then(r => r.json())
const posts = await fetch(`/api/posts?uid=${user.id}`).then(r => r.json())
return { user, posts }
}async/await 的错误处理:
javascript
async function riskyOperation() {
try {
const result = await mightFail()
return result
} catch (err) {
console.error('Failed:', err)
return fallbackValue
}
}
async function multipleAwaits() {
const [a, b] = await Promise.all([fetchA(), fetchB()])
return a + b
}await 的微任务开销:
每个 await 都会创建至少一个微任务。在 V8 的优化版本中(V8 7.2+),如果 await 的值已经是一个 resolved 的 Promise,只需一个微任务而不是两个。
javascript
async function foo() {
return 42
}
async function bar() {
return Promise.resolve(42)
}
foo().then(console.log) // 42 — 直接 resolve
bar().then(console.log) // 42 — 多一层 Promise 包装追问延伸
for await...of是什么?异步迭代器的使用场景?await在for循环中是串行的,如何改为并行?什么时候用串行/并行?- 顶层
await(Top-level await)是什么?有什么限制?
12. 手写 debounce 和 throttle
⭐⭐ 进阶 | 考察点:闭包、定时器
深度解答
防抖(debounce):事件触发后延迟执行,如果在延迟期间再次触发,则重新计时。适用于搜索框输入、窗口 resize 等。
节流(throttle):在一段时间内只执行一次。适用于滚动事件、按钮防重复点击等。
完整版 debounce(含 leading/trailing/cancel):
javascript
function debounce(fn, delay, options = {}) {
const { leading = false, trailing = true } = options
let timer = null
let lastCallTime = 0
function debounced(...args) {
const now = Date.now()
const isFirstCall = !timer && leading
if (timer) clearTimeout(timer)
if (isFirstCall) {
fn.apply(this, args)
lastCallTime = now
}
if (trailing) {
timer = setTimeout(() => {
if (!leading || now - lastCallTime >= delay) {
fn.apply(this, args)
}
timer = null
}, delay)
}
}
debounced.cancel = () => {
if (timer) {
clearTimeout(timer)
timer = null
}
}
debounced.flush = (...args) => {
debounced.cancel()
fn.apply(this, args)
}
return debounced
}完整版 throttle(时间戳 + 定时器组合版):
javascript
function throttle(fn, interval, options = {}) {
const { leading = true, trailing = true } = options
let lastTime = 0
let timer = null
function throttled(...args) {
const now = Date.now()
const remaining = interval - (now - lastTime)
if (remaining <= 0 || remaining > interval) {
if (timer) {
clearTimeout(timer)
timer = null
}
if (leading || lastTime !== 0) {
fn.apply(this, args)
}
lastTime = now
} else if (!timer && trailing) {
timer = setTimeout(() => {
lastTime = leading ? Date.now() : 0
timer = null
fn.apply(this, args)
}, remaining)
}
}
throttled.cancel = () => {
if (timer) {
clearTimeout(timer)
timer = null
}
lastTime = 0
}
return throttled
}使用示例:
javascript
const searchInput = document.getElementById('search')
const debouncedSearch = debounce((value) => {
console.log('Searching:', value)
}, 300, { leading: false, trailing: true })
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value)
})
window.addEventListener('scroll', throttle(() => {
console.log('Scroll position:', window.scrollY)
}, 200))追问延伸
requestAnimationFrame可以替代 throttle 吗?什么场景更适合?- React 中如何优雅地使用 debounce?为什么不能直接在组件内
debounce(handler)? - Lodash 的
_.debounce和_.throttle有什么额外功能?(maxWait参数)
13. 手写深拷贝(处理循环引用、Date、RegExp、Map、Set)
⭐⭐⭐ 深入 | 考察点:递归、WeakMap
深度解答
JSON.parse(JSON.stringify(obj)) 的局限:无法处理 undefined、function、Symbol、循环引用、Date(变成字符串)、RegExp(变成空对象)、Map、Set 等。
完整版深拷贝:
javascript
function deepClone(source, cache = new WeakMap()) {
if (source === null || typeof source !== 'object') {
return source
}
if (cache.has(source)) {
return cache.get(source)
}
if (source instanceof Date) {
return new Date(source.getTime())
}
if (source instanceof RegExp) {
return new RegExp(source.source, source.flags)
}
if (source instanceof Map) {
const map = new Map()
cache.set(source, map)
source.forEach((val, key) => {
map.set(deepClone(key, cache), deepClone(val, cache))
})
return map
}
if (source instanceof Set) {
const set = new Set()
cache.set(source, set)
source.forEach((val) => {
set.add(deepClone(val, cache))
})
return set
}
const target = Array.isArray(source) ? [] : Object.create(Object.getPrototypeOf(source))
cache.set(source, target)
const symbols = Object.getOwnPropertySymbols(source)
symbols.forEach((sym) => {
if (Object.prototype.propertyIsEnumerable.call(source, sym)) {
target[sym] = deepClone(source[sym], cache)
}
})
for (const key of Object.keys(source)) {
target[key] = deepClone(source[key], cache)
}
return target
}验证:
javascript
const sym = Symbol('key')
const original = {
number: 42,
string: 'hello',
boolean: true,
nullVal: null,
undefinedVal: undefined,
date: new Date('2025-01-01'),
regex: /abc/gi,
nested: { a: { b: { c: 1 } } },
array: [1, [2, [3]]],
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
[sym]: 'symbol value',
fn: function() { return 42 }
}
original.self = original
const cloned = deepClone(original)
console.log(cloned !== original) // true
console.log(cloned.nested !== original.nested) // true
console.log(cloned.nested.a.b.c) // 1
console.log(cloned.date instanceof Date) // true
console.log(cloned.date.getTime() === original.date.getTime()) // true
console.log(cloned.regex instanceof RegExp) // true
console.log(cloned.regex.flags) // "gi"
console.log(cloned.map.get('key')) // "value"
console.log(cloned.set.has(2)) // true
console.log(cloned.self === cloned) // true(循环引用正确处理)
console.log(cloned[sym]) // "symbol value"关键设计决策:
| 决策 | 方案 | 原因 |
|---|---|---|
| 循环引用检测 | WeakMap 缓存 | 弱引用不会阻止 GC |
| 原型链保留 | Object.create(Object.getPrototypeOf(source)) | 保持继承关系 |
| Symbol 属性 | Object.getOwnPropertySymbols() | for...in 和 Object.keys 无法遍历 Symbol |
| 函数处理 | 直接引用(不拷贝) | 函数通常是无状态的,拷贝没有意义 |
追问延伸
structuredClone()原生 API 支持哪些类型?和手写深拷贝相比有什么优劣?WeakMap为什么比Map更适合做缓存?什么时候会被 GC?- 如何用迭代而不是递归实现深拷贝?(避免栈溢出)
14. this 指向的 5 种绑定规则?箭头函数的 this 有什么不同?
⭐⭐ 进阶 | 考察点:this 绑定
深度解答
JavaScript 中 this 的值取决于函数的调用方式,而不是定义方式。有 5 种绑定规则(优先级从低到高):
1. 默认绑定: 独立函数调用,this 指向全局对象(严格模式下为 undefined)
javascript
function foo() {
console.log(this)
}
foo() // window(非严格模式)/ undefined(严格模式)2. 隐式绑定: 作为对象方法调用,this 指向调用对象
javascript
const obj = {
name: 'Alice',
greet() {
console.log(this.name)
}
}
obj.greet() // "Alice"
const fn = obj.greet
fn() // undefined — 隐式绑定丢失!3. 显式绑定: call/apply/bind 手动指定 this
javascript
function greet(greeting) {
console.log(`${greeting}, ${this.name}`)
}
const user = { name: 'Bob' }
greet.call(user, 'Hello') // "Hello, Bob"
greet.apply(user, ['Hi']) // "Hi, Bob"
const bound = greet.bind(user, 'Hey')
bound() // "Hey, Bob"4. new 绑定: new 调用时,this 指向新创建的对象
javascript
function Person(name) {
this.name = name
}
const p = new Person('Charlie')
console.log(p.name) // "Charlie"5. 箭头函数: 没有自己的 this,捕获定义时外层作用域的 this(词法 this)
javascript
const obj = {
name: 'Diana',
greet: () => {
console.log(this.name)
},
greetDelay() {
setTimeout(() => {
console.log(this.name)
}, 100)
}
}
obj.greet() // undefined — 箭头函数的 this 是外层(全局)
obj.greetDelay() // "Diana" — 箭头函数捕获了 greetDelay 的 this优先级排序:
箭头函数(不可改变) > new 绑定 > 显式绑定(call/apply/bind) > 隐式绑定 > 默认绑定经典面试陷阱:
javascript
const obj = {
name: 'test',
getName: function() {
return this.name
},
getNameArrow: () => {
return this.name
}
}
console.log(obj.getName()) // "test"
console.log(obj.getNameArrow()) // undefined
const { getName } = obj
console.log(getName()) // undefined — 隐式绑定丢失
const boundGetName = obj.getName.bind({ name: 'bound' })
console.log(boundGetName()) // "bound"
new boundGetName() // new 优先级高于 bind,this 指向新对象追问延伸
- 箭头函数能用
call/apply/bind改变this吗?为什么? - class 中的方法为什么推荐在 constructor 里用箭头函数或
bind? - React 的事件处理函数
this丢失问题,有哪几种解决方案?
15. 手写 call/apply/bind
⭐⭐ 进阶 | 考察点:this、函数方法
深度解答
手写 call:
核心思路:将函数作为目标对象的临时方法调用,调用后删除。
javascript
Function.prototype.myCall = function (context, ...args) {
if (typeof this !== 'function') {
throw new TypeError('myCall must be called on a function')
}
context = context == null ? globalThis : Object(context)
const key = Symbol('fn')
context[key] = this
const result = context[key](...args)
delete context[key]
return result
}手写 apply:
和 call 的唯一区别是参数传入方式为数组。
javascript
Function.prototype.myApply = function (context, argsArray) {
if (typeof this !== 'function') {
throw new TypeError('myApply must be called on a function')
}
context = context == null ? globalThis : Object(context)
const key = Symbol('fn')
context[key] = this
const result = argsArray ? context[key](...argsArray) : context[key]()
delete context[key]
return result
}手写 bind:
bind 返回一个新函数,需要注意:
- 支持柯里化(预设参数)
- 返回的函数用
new调用时,this应指向新对象而非绑定的context
javascript
Function.prototype.myBind = function (context, ...outerArgs) {
if (typeof this !== 'function') {
throw new TypeError('myBind must be called on a function')
}
const originalFn = this
const boundFn = function (...innerArgs) {
const isNew = this instanceof boundFn
return originalFn.apply(
isNew ? this : context,
[...outerArgs, ...innerArgs]
)
}
if (originalFn.prototype) {
boundFn.prototype = Object.create(originalFn.prototype)
}
return boundFn
}验证:
javascript
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`
}
const user = { name: 'Alice' }
console.log(greet.myCall(user, 'Hello', '!')) // "Hello, Alice!"
console.log(greet.myApply(user, ['Hi', '?'])) // "Hi, Alice?"
const boundGreet = greet.myBind(user, 'Hey')
console.log(boundGreet('~')) // "Hey, Alice~"
function Person(name) {
this.name = name
}
const BoundPerson = Person.myBind({ name: 'ignored' })
const p = new BoundPerson('Bob')
console.log(p.name) // "Bob" — new 优先级高于 bind
console.log(p instanceof Person) // true — 原型链正确关键细节:
- 用
Symbol作为临时属性 key,避免和对象已有属性冲突 context == null用==(非===),同时匹配null和undefinedObject(context)处理原始值(如call(42))- bind 返回函数的
prototype需指向原函数的prototype
追问延伸
- 为什么用
Symbol而不是普通字符串作为临时 key? Function.prototype.bind返回的函数有prototype属性吗?(答:没有)- 如何实现一个支持软绑定(softBind)的 bind?即允许后续的隐式/显式绑定覆盖?
16. Proxy 和 Object.defineProperty 的区别?Vue2 和 Vue3 为什么不同?
⭐⭐⭐ 深入 | 考察点:Proxy、响应式
深度解答
| 特性 | Object.defineProperty | Proxy |
|---|---|---|
| 拦截粒度 | 单个属性 | 整个对象 |
| 新增属性 | ❌ 无法检测 | ✅ 自动拦截 |
| 删除属性 | ❌ 无法检测 | ✅ deleteProperty trap |
| 数组变化 | ❌ 需要 hack(重写方法) | ✅ 原生支持 |
| 可拦截操作 | get / set | 13 种 trap |
| 性能 | 初始化时遍历所有属性 | 惰性代理,访问时才拦截 |
| 嵌套对象 | 需要递归遍历 | 可以惰性递归(访问时才代理) |
Object.defineProperty(Vue2 方式):
javascript
function defineReactive(obj, key, val) {
const dep = []
Object.defineProperty(obj, key, {
get() {
console.log(`[get] ${key} = ${val}`)
return val
},
set(newVal) {
if (newVal === val) return
console.log(`[set] ${key}: ${val} → ${newVal}`)
val = newVal
dep.forEach(fn => fn())
}
})
}
const data = { name: 'Alice', age: 25 }
Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
data.name // [get] name = Alice
data.name = 'Bob' // [set] name: Alice → Bob
data.email = 'x' // 无法拦截!Vue2 需要 Vue.set()Proxy(Vue3 方式):
javascript
function reactive(target) {
return new Proxy(target, {
get(obj, key, receiver) {
console.log(`[get] ${String(key)}`)
const value = Reflect.get(obj, key, receiver)
if (typeof value === 'object' && value !== null) {
return reactive(value)
}
return value
},
set(obj, key, value, receiver) {
const oldValue = obj[key]
console.log(`[set] ${String(key)}: ${oldValue} → ${value}`)
return Reflect.set(obj, key, value, receiver)
},
deleteProperty(obj, key) {
console.log(`[delete] ${String(key)}`)
return Reflect.deleteProperty(obj, key)
},
has(obj, key) {
console.log(`[has] ${String(key)}`)
return Reflect.has(obj, key)
}
})
}
const state = reactive({ name: 'Alice', nested: { count: 0 } })
state.name // [get] name
state.name = 'Bob' // [set] name: Alice → Bob
state.email = 'new' // [set] email: undefined → new ✅ 新增属性也能拦截
delete state.email // [delete] email ✅
'name' in state // [has] name ✅
state.nested.count = 1 // [get] nested → [set] count: 0 → 1 ✅ 惰性递归Vue2 的 hack 方案:
javascript
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
arrayMethods[method] = function (...args) {
const result = arrayProto[method].apply(this, args)
console.log(`Array ${method} called`)
return result
}
})为什么 Vue3 选择 Proxy:
- 不需要遍历所有属性:Proxy 是惰性的,只在访问时拦截
- 天然支持新增/删除属性:不需要
Vue.set/Vue.delete - 天然支持数组操作:不需要 hack 数组方法
- 更多拦截能力:13 种 trap 覆盖几乎所有操作
追问延伸
Reflect是什么?为什么 Proxy 的 trap 中推荐使用Reflect?- Proxy 的性能开销在哪里?为什么 Vue3 的嵌套对象代理是"惰性"的?
Proxy能代理一个已有的Proxy对象吗?(多层代理)
17. 什么是尾调用优化?JS 引擎支持吗?
⭐⭐ 进阶 | 考察点:调用栈、V8
深度解答
尾调用(Tail Call):函数的最后一步操作是调用另一个函数并直接返回其结果,不做任何额外计算。
javascript
function tailCall() {
return anotherFn()
}
function notTailCall() {
return anotherFn() + 1
}
function alsoNotTailCall() {
const result = anotherFn()
return result
}尾调用优化(TCO, Tail Call Optimization):引擎检测到尾调用后,可以复用当前栈帧而不创建新的,从而避免栈溢出。
经典案例:阶乘
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)
}
factorial(100000) // RangeError: Maximum call stack size exceeded
factorialTCO(100000) // 理论上 TCO 环境下不会栈溢出JS 引擎的支持情况:
| 引擎 | TCO 支持 |
|---|---|
| Safari (JavaScriptCore) | ✅ ES6 严格模式下支持 |
| V8 (Chrome/Node.js) | ❌ 曾实现后移除 |
| SpiderMonkey (Firefox) | ❌ |
V8 团队移除 TCO 的原因:
- 调试困难:栈帧被复用后,
Error.stack丢失调用信息 - 性能权衡:检测尾调用本身有开销
- 隐式优化:开发者难以确认优化是否生效
实际替代方案——蹦床函数(Trampoline):
javascript
function trampoline(fn) {
return function (...args) {
let result = fn(...args)
while (typeof result === 'function') {
result = result()
}
return result
}
}
function factorialTrampoline(n, acc = 1) {
if (n <= 1) return acc
return () => factorialTrampoline(n - 1, n * acc)
}
const factorial = trampoline(factorialTrampoline)
console.log(factorial(100000)) // 不会栈溢出追问延伸
- 为什么 TCO 只在严格模式下生效?(
arguments和caller的影响) - 除了蹦床函数,还有什么方式避免深递归导致栈溢出?(循环改写、Web Worker)
- Continuation Passing Style (CPS) 是什么?和尾调用有什么关系?
18. WeakMap/WeakSet 和 Map/Set 的区别?应用场景?
⭐⭐ 进阶 | 考察点:GC、弱引用
深度解答
| 特性 | Map / Set | WeakMap / WeakSet |
|---|---|---|
| Key 类型 | 任意类型 | 只能是对象(非原始值) |
| GC | 强引用,key 不会被 GC | 弱引用,key 可被 GC |
| 可枚举 | 有 size、forEach、迭代器 | 不可枚举,无 size |
| 序列化 | 可以 | 不可以 |
核心区别:弱引用
javascript
let obj = { name: 'Alice' }
const map = new Map()
map.set(obj, 'data')
const weakMap = new WeakMap()
weakMap.set(obj, 'data')
obj = null
// map 仍然持有 { name: 'Alice' } 的引用 — 不会被 GC
// weakMap 中 { name: 'Alice' } 可以被 GC — 弱引用不阻止回收场景一:DOM 元素关联数据(避免内存泄漏)
javascript
const elementData = new WeakMap()
function bindData(element, data) {
elementData.set(element, data)
}
function getData(element) {
return elementData.get(element)
}
const div = document.createElement('div')
bindData(div, { clicks: 0, timestamp: Date.now() })
getData(div) // { clicks: 0, ... }
div.remove()
// div 被 GC 后,WeakMap 中的关联数据也自动被清理场景二:深拷贝的循环引用检测
javascript
function deepClone(source, cache = new WeakMap()) {
if (source === null || typeof source !== 'object') return source
if (cache.has(source)) return cache.get(source)
const target = Array.isArray(source) ? [] : {}
cache.set(source, target)
for (const key of Object.keys(source)) {
target[key] = deepClone(source[key], cache)
}
return target
}场景三:对象的私有数据
javascript
const _private = new WeakMap()
class User {
constructor(name, password) {
_private.set(this, { password })
this.name = name
}
checkPassword(input) {
return _private.get(this).password === input
}
}
const user = new User('Alice', 'secret123')
console.log(user.name) // "Alice"
console.log(user.checkPassword('secret123')) // true
console.log(Object.keys(user)) // ["name"] — password 不可见WeakSet 的常见用途:标记对象
javascript
const visited = new WeakSet()
function processNode(node) {
if (visited.has(node)) return
visited.add(node)
// ... 处理 node
}追问延伸
WeakRef和FinalizationRegistry是什么?ES2021 新增了哪些弱引用能力?- 为什么
WeakMap的 key 不能是原始值?(原始值无法被 GC 追踪) - Vue3 的
reactive()底层用WeakMap缓存已代理的对象,为什么?
19. ES Module 和 CommonJS 的核心区别?循环引用时各自如何处理?
⭐⭐⭐ 深入 | 考察点:模块化
深度解答
| 特性 | CommonJS (CJS) | ES Module (ESM) |
|---|---|---|
| 加载时机 | 运行时加载 | 编译时静态分析 |
| 输出 | 值的拷贝 | 值的引用(live binding) |
this | module.exports | undefined |
顶层 await | ❌ | ✅ |
| Tree Shaking | ❌(动态结构) | ✅(静态结构) |
| 执行 | 同步 | 异步 |
核心区别:值拷贝 vs 引用
javascript
// counter.cjs
let count = 0
module.exports = {
count,
increment() { count++ }
}
// main.cjs
const counter = require('./counter.cjs')
console.log(counter.count) // 0
counter.increment()
console.log(counter.count) // 0 ← 还是 0!CJS 是值拷贝javascript
// counter.mjs
export let count = 0
export function increment() { count++ }
// main.mjs
import { count, increment } from './counter.mjs'
console.log(count) // 0
increment()
console.log(count) // 1 ← ESM 是引用,获取最新值循环引用处理:
CJS 的循环引用——返回不完整的 exports:
javascript
// a.cjs
console.log('a starts')
exports.done = false
const b = require('./b.cjs')
console.log('in a, b.done =', b.done)
exports.done = true
console.log('a done')
// b.cjs
console.log('b starts')
exports.done = false
const a = require('./a.cjs')
console.log('in b, a.done =', a.done) // false ← 获取到 a 的不完整 exports
exports.done = true
console.log('b done')
// main.cjs
require('./a.cjs')
// 输出:
// a starts
// b starts
// in b, a.done = false ← a 还没执行完,只有部分 exports
// b done
// in a, b.done = true
// a doneESM 的循环引用——利用引用绑定:
javascript
// a.mjs
import { bar } from './b.mjs'
export function foo() {
return 'foo + ' + bar()
}
// b.mjs
import { foo } from './a.mjs'
export function bar() {
return 'bar'
}
// main.mjs
import { foo } from './a.mjs'
console.log(foo()) // "foo + bar" ✅ 因为 ESM 是引用绑定,调用时 bar 已定义ESM 能正确处理是因为 import 是引用绑定(live binding),在实际调用 bar() 时,b.mjs 已经执行完毕。但如果在模块顶层立即使用(而不是在函数里),则可能得到 undefined。
静态分析 vs 动态加载:
javascript
// CJS — 可以动态 require
if (condition) {
const mod = require('./a')
}
// ESM — import 必须在顶层(静态)
import { foo } from './a.mjs' // 只能在顶层
// ESM 的动态导入
const mod = await import('./a.mjs') // 返回 Promise追问延伸
package.json的"type": "module"和"exports"字段的作用?- Webpack 的 Tree Shaking 为什么需要 ESM?
sideEffects字段怎么配合? - Node.js 中 CJS 和 ESM 能互相导入吗?有什么限制?
20. Object.is 和 === 的区别?React 的 Object.is 用在哪里?
⭐⭐ 进阶 | 考察点:值比较、React 更新
深度解答
Object.is 和 === 几乎相同,只有两个例外:
javascript
// === 的"错误"行为
NaN === NaN // false ← NaN 不等于自身
+0 === -0 // true ← 正零等于负零
// Object.is 修正了这两个
Object.is(NaN, NaN) // true ✅
Object.is(+0, -0) // false ✅手写 Object.is:
javascript
function objectIs(a, b) {
if (a === b) {
return a !== 0 || 1 / a === 1 / b
}
return a !== a && b !== b
}解析:
a === b但a是0时,用1/a === 1/b区分+0和-0(1/+0 = Infinity,1/-0 = -Infinity)a !== a只有NaN满足,因此a !== a && b !== b表示两者都是NaN
React 中 Object.is 的使用:
React 在多个地方使用 Object.is(polyfill 版本)来判断"值是否变了":
1. useState 的 bailout(跳过更新):
javascript
const [count, setCount] = useState(0)
setCount(0) // Object.is(0, 0) === true → 不触发重渲染
setCount(NaN)
setCount(NaN) // Object.is(NaN, NaN) === true → 不触发重渲染2. useMemo / useCallback 的依赖比较:
javascript
useMemo(() => expensiveCalc(a, b), [a, b])
// React 用 Object.is 逐一比较 [a, b] 的每个元素
// 如果所有元素都 Object.is 相等,则复用缓存3. React.memo 的浅比较:
javascript
const MemoComp = React.memo(MyComponent)
// 默认的 propsAreEqual 使用浅比较
// 每个 prop 用 Object.is 比较React 的浅比较实现(简化版):
javascript
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
}为什么 React 选择 Object.is 而不是 ===:
NaN值不应该触发重渲染(NaN === NaN是false,会导致无限更新)+0和-0在数学计算中有区别(如1/+0 = Infinity),应视为不同值
追问延伸
- React 的
setState(prevState => prevState)会触发重渲染吗? - 为什么 React 只做"浅比较"?如果深比较会有什么问题?
Object.is和SameValueZero(Map/Set 使用的比较算法)有什么区别?
21. TypeScript 的泛型是什么?写一个类型安全的 pick / omit 工具类型
⭐⭐ 进阶 | 考察点:泛型、映射类型
深度解答
泛型(Generics) 是 TypeScript 中实现"类型参数化"的机制,让类型像变量一样可以被传递和复用,从而在保持类型安全的同时实现代码的通用性。
泛型基础:
typescript
function identity<T>(value: T): T {
return value
}
identity<string>('hello') // 显式指定
identity(42) // 类型推断为 number
function first<T>(arr: T[]): T | undefined {
return arr[0]
}
first([1, 2, 3]) // T 推断为 number
first(['a', 'b', 'c']) // T 推断为 string泛型约束(extends):
typescript
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: 'Alice', age: 25 }
getProperty(user, 'name') // string
getProperty(user, 'age') // number
// getProperty(user, 'email') // Error: 'email' 不存在于 keyof typeof user手写 MyPick:
typescript
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
interface User {
id: number
name: string
email: string
age: number
}
type UserBasic = MyPick<User, 'id' | 'name'>
// { id: number; name: string }手写 MyOmit:
typescript
type MyOmit<T, K extends keyof T> = {
[P in Exclude<keyof T, K>]: T[P]
}
type UserWithoutEmail = MyOmit<User, 'email'>
// { id: number; name: string; age: number }其中 Exclude 的实现:
typescript
type MyExclude<T, U> = T extends U ? never : T
type Result = MyExclude<'a' | 'b' | 'c', 'a'>
// 'b' | 'c'
// 分配条件类型:'a' extends 'a' → never, 'b' extends 'a' → 'b', 'c' extends 'a' → 'c'更多实用工具类型手写:
typescript
type MyPartial<T> = {
[P in keyof T]?: T[P]
}
type MyRequired<T> = {
[P in keyof T]-?: T[P]
}
type MyReadonly<T> = {
readonly [P in keyof T]: T[P]
}
type MyRecord<K extends keyof any, V> = {
[P in K]: V
}泛型在实际项目中的应用:
typescript
function createApi<T>() {
return {
async get(id: string): Promise<T> {
const res = await fetch(`/api/${id}`)
return res.json()
},
async list(): Promise<T[]> {
const res = await fetch('/api')
return res.json()
}
}
}
interface Post {
id: string
title: string
content: string
}
const postApi = createApi<Post>()
const post = await postApi.get('1') // 类型:Post
const posts = await postApi.list() // 类型:Post[]追问延伸
- 映射类型中的
+和-修饰符(如-?、-readonly)是什么意思? keyof any等于什么?(string | number | symbol)- 如何实现一个
DeepPartial<T>递归地将所有嵌套属性变为可选?
22. TypeScript 的条件类型和 infer 关键字?手写 ReturnType / Parameters
⭐⭐⭐ 深入 | 考察点:条件类型、类型推断
深度解答
条件类型(Conditional Types) 的语法类似三元表达式:
typescript
T extends U ? X : Y基础用法:
typescript
type IsString<T> = T extends string ? true : false
type A = IsString<'hello'> // true
type B = IsString<42> // false分配条件类型(Distributive):
当 T 是联合类型时,条件类型会自动分配到每个成员上:
typescript
type ToArray<T> = T extends any ? T[] : never
type Result = ToArray<string | number>
// string[] | number[] (不是 (string | number)[])
type NonDistributive<T> = [T] extends [any] ? T[] : never
type Result2 = NonDistributive<string | number>
// (string | number)[] — 用方括号包裹阻止分配infer 关键字:
infer 在条件类型的 extends 子句中声明一个待推断的类型变量:
typescript
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type R1 = MyReturnType<() => string> // string
type R2 = MyReturnType<(x: number) => boolean> // boolean
type R3 = MyReturnType<string> // never手写 Parameters:
typescript
type MyParameters<T> = T extends (...args: infer P) => any ? P : never
type P1 = MyParameters<(a: string, b: number) => void>
// [a: string, b: number]
type P2 = MyParameters<() => void>
// []更多 infer 的高级用法:
typescript
type FirstElement<T> = T extends [infer F, ...any[]] ? F : never
type F1 = FirstElement<[string, number, boolean]> // string
type LastElement<T> = T extends [...any[], infer L] ? L : never
type L1 = LastElement<[string, number, boolean]> // boolean
type Flatten<T> = T extends Array<infer Item> ? Item : T
type F2 = Flatten<string[]> // string
type F3 = Flatten<number[][]> // number[]
type F4 = Flatten<string> // string
type DeepFlatten<T> = T extends Array<infer Item> ? DeepFlatten<Item> : T
type F5 = DeepFlatten<number[][]> // number实际应用:提取 Promise 的解析值
typescript
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T
type U1 = UnwrapPromise<Promise<string>> // string
type U2 = UnwrapPromise<Promise<Promise<number>>> // number(递归解包)
type U3 = UnwrapPromise<string> // string实际应用:提取组件 Props 类型
typescript
type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never
type ButtonProps = ComponentProps<typeof Button>追问延伸
infer在同一类型中出现多次时如何工作?(协变/逆变位置)- 如何用条件类型实现字符串的
TrimLeft/TrimRight? - TypeScript 4.7 引入的
infer extends语法解决了什么问题?
23. TypeScript 的模板字面量类型能做什么?实现一个类型安全的路由参数提取
⭐⭐⭐ 深入 | 考察点:模板字面量类型
深度解答
模板字面量类型(Template Literal Types) 是 TypeScript 4.1 引入的特性,允许在类型层面进行字符串操作:
typescript
type Greeting = `Hello, ${string}`
const a: Greeting = 'Hello, World' // ✅
const b: Greeting = 'Hi, World' // ❌ Type '"Hi, World"' is not assignable联合类型的组合爆炸:
typescript
type Color = 'red' | 'blue' | 'green'
type Size = 'sm' | 'md' | 'lg'
type ClassName = `${Color}-${Size}`
// 'red-sm' | 'red-md' | 'red-lg' | 'blue-sm' | 'blue-md' | 'blue-lg' | 'green-sm' | 'green-md' | 'green-lg'字符串操作工具类型:
typescript
type EventName<T extends string> = `on${Capitalize<T>}`
type E1 = EventName<'click'> // 'onClick'
type E2 = EventName<'change'> // 'onChange'
type UpperFirst<S extends string> = S extends `${infer F}${infer Rest}`
? `${Uppercase<F>}${Rest}`
: S
type U1 = UpperFirst<'hello'> // 'Hello'实际应用:类型安全的路由参数提取
typescript
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param]: string } & ExtractParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {}
type Params1 = ExtractParams<'/users/:id'>
// { id: string }
type Params2 = ExtractParams<'/users/:userId/posts/:postId'>
// { userId: string } & { postId: string }
type Params3 = ExtractParams<'/about'>
// {}使用路由参数类型的函数:
typescript
function navigate<T extends string>(
path: T,
params: ExtractParams<T>
) {
let url: string = path
for (const [key, value] of Object.entries(params)) {
url = url.replace(`:${key}`, value as string)
}
return url
}
navigate('/users/:id', { id: '123' }) // ✅
navigate('/users/:id/posts/:postId', {
id: '1',
postId: '42'
}) // ✅
// navigate('/users/:id', {}) // ❌ 缺少 id
// navigate('/users/:id', { name: 'test' }) // ❌ 不存在 name实际应用:类型安全的 CSS 变量
typescript
type CSSVar<T extends string> = `var(--${T})`
type ThemeVars = 'primary' | 'secondary' | 'bg' | 'text'
type ThemeCSSVar = CSSVar<ThemeVars>
// 'var(--primary)' | 'var(--secondary)' | 'var(--bg)' | 'var(--text)'
function setThemeVar(name: ThemeVars, value: string) {
document.documentElement.style.setProperty(`--${name}`, value)
}实际应用:类型安全的事件系统
typescript
type EventMap = {
click: { x: number; y: number }
change: { value: string }
submit: { data: Record<string, unknown> }
}
type EventHandler<T extends keyof EventMap> = (event: EventMap[T]) => void
type OnEventName<T extends string> = `on${Capitalize<T>}`
type EventHandlers = {
[K in keyof EventMap as OnEventName<K & string>]: EventHandler<K>
}
// {
// onClick: (event: { x: number; y: number }) => void
// onChange: (event: { value: string }) => void
// onSubmit: (event: { data: Record<string, unknown> }) => void
// }追问延伸
Capitalize/Uncapitalize/Uppercase/Lowercase这四个内置字符串类型工具如何实现?- 如何用模板字面量类型实现一个
Split<S, D>将字符串按分隔符拆分为元组? - 模板字面量类型在 key remapping(
as子句)中的应用?
24. TypeScript 的 unknown vs any vs never?各自的使用场景?
⭐⭐ 进阶 | 考察点:类型系统
深度解答
| 类型 | 含义 | 赋值给其他 | 接收赋值 | 操作限制 |
|---|---|---|---|---|
any | "放弃类型检查" | ✅ 任意类型 | ✅ 任意值 | ❌ 无限制 |
unknown | "安全的顶部类型" | ❌ 只能赋给 unknown/any | ✅ 任意值 | ✅ 必须先收窄 |
never | "底部类型,不可能的值" | ✅ 任意类型 | ❌ 没有值可以赋 | — |
any — 类型系统的逃生舱:
typescript
let a: any = 42
a.foo.bar.baz // 不报错 — 完全绕过类型检查
a() // 不报错
let b: string = a // 不报错
// any 的传染性
function unsafe(x: any) {
return x.data // 返回类型也是 any
}unknown — 类型安全的 any:
typescript
let u: unknown = 42
// u.toFixed() // ❌ Error: Object is of type 'unknown'
// let s: string = u // ❌ Error: Type 'unknown' is not assignable to type 'string'
if (typeof u === 'number') {
u.toFixed(2) // ✅ 类型收窄后可以操作
}
if (u instanceof Date) {
u.getTime() // ✅
}unknown 的实际应用:
typescript
async function fetchData(url: string): Promise<unknown> {
const res = await fetch(url)
return res.json()
}
const data = await fetchData('/api/user')
function isUser(val: unknown): val is User {
return (
typeof val === 'object' &&
val !== null &&
'name' in val &&
'age' in val
)
}
if (isUser(data)) {
console.log(data.name) // ✅ 类型收窄为 User
}never — 不可能存在的值:
typescript
function throwError(msg: string): never {
throw new Error(msg)
}
function infiniteLoop(): never {
while (true) {}
}
type CheckExhaustive<T> = T extends never ? 'empty' : 'not empty'
type A = CheckExhaustive<never> // 'empty'... 实际是 never(分配条件类型,never 不分配)
type B = CheckExhaustive<string> // 'not empty'never 用于穷举检查:
typescript
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'triangle'; base: number; height: number }
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2
case 'square':
return shape.side ** 2
case 'triangle':
return 0.5 * shape.base * shape.height
default: {
const _exhaustive: never = shape
return _exhaustive
}
}
}如果后续新增了 { kind: 'rectangle' } 但忘记处理,_exhaustive: never = shape 就会报错,提醒开发者补充 case。
类型层级关系:
any(顶部类型,特殊)
↑
unknown(真正的顶部类型)
↗ ↑ ↖
string number boolean ...
↖ ↓ ↗
never(底部类型)never是所有类型的子类型(可以赋给任何类型)unknown是所有类型的超类型(任何类型都可以赋给 unknown)any既是顶部又是底部(破坏了类型系统的完备性)
追问延伸
{}类型和object类型和Object类型的区别?- 为什么
never[]是所有数组类型的子类型? - TypeScript 中
void和undefined的区别?函数返回void意味着什么?
25. 前端常用设计模式:观察者 vs 发布订阅的区别?手写实现
⭐⭐ 进阶 | 考察点:设计模式
深度解答
观察者模式(Observer) 和 发布订阅模式(Pub/Sub) 经常被混淆,核心区别在于是否有中间调度层:
| 特性 | 观察者模式 | 发布订阅模式 |
|---|---|---|
| 耦合性 | 目标对象直接通知观察者 | 通过事件中心解耦 |
| 关系 | 一对多(Subject → Observer) | 多对多(Publisher ↔ EventBus ↔ Subscriber) |
| 中间层 | 无 | 有(Event Bus / Message Broker) |
| 典型实现 | DOM 事件监听 | Node.js EventEmitter / Vue 的 $emit |
手写观察者模式:
typescript
interface Observer {
update(data: any): void
}
class Subject {
private observers: Set<Observer> = new Set()
attach(observer: Observer) {
this.observers.add(observer)
}
detach(observer: Observer) {
this.observers.delete(observer)
}
notify(data: any) {
this.observers.forEach(observer => observer.update(data))
}
}
class PriceDisplay implements Observer {
update(price: number) {
console.log(`Price updated: $${price}`)
}
}
class PriceLogger implements Observer {
update(price: number) {
console.log(`[LOG] Price changed to $${price} at ${new Date().toISOString()}`)
}
}
const stock = new Subject()
const display = new PriceDisplay()
const logger = new PriceLogger()
stock.attach(display)
stock.attach(logger)
stock.notify(142.5)
// Price updated: $142.5
// [LOG] Price changed to $142.5 at 2025-...
stock.detach(logger)
stock.notify(145.0)
// Price updated: $145.0手写发布订阅模式(完整的 EventEmitter):
typescript
type EventHandler = (...args: any[]) => void
class EventEmitter {
private events: Map<string, Set<EventHandler>> = new Map()
on(event: string, handler: EventHandler) {
if (!this.events.has(event)) {
this.events.set(event, new Set())
}
this.events.get(event)!.add(handler)
return this
}
off(event: string, handler: EventHandler) {
this.events.get(event)?.delete(handler)
return this
}
once(event: string, handler: EventHandler) {
const wrapper: EventHandler = (...args) => {
handler(...args)
this.off(event, wrapper)
}
this.on(event, wrapper)
return this
}
emit(event: string, ...args: any[]) {
this.events.get(event)?.forEach(handler => {
handler(...args)
})
return this
}
removeAllListeners(event?: string) {
if (event) {
this.events.delete(event)
} else {
this.events.clear()
}
return this
}
}
const bus = new EventEmitter()
bus.on('user:login', (user) => {
console.log(`Welcome, ${user.name}!`)
})
bus.once('user:login', (user) => {
console.log(`First login bonus for ${user.name}!`)
})
bus.emit('user:login', { name: 'Alice' })
// Welcome, Alice!
// First login bonus for Alice!
bus.emit('user:login', { name: 'Bob' })
// Welcome, Bob!
// (once 的回调不会再触发)类型安全的 EventEmitter:
typescript
type EventMap = {
'user:login': [user: { name: string; id: number }]
'user:logout': [userId: number]
'data:update': [key: string, value: unknown]
}
class TypedEmitter<T extends Record<string, any[]>> {
private events = new Map<keyof T, Set<Function>>()
on<K extends keyof T>(event: K, handler: (...args: T[K]) => void) {
if (!this.events.has(event)) {
this.events.set(event, new Set())
}
this.events.get(event)!.add(handler)
return this
}
emit<K extends keyof T>(event: K, ...args: T[K]) {
this.events.get(event)?.forEach(handler => {
(handler as Function)(...args)
})
return this
}
}
const emitter = new TypedEmitter<EventMap>()
emitter.on('user:login', (user) => {
console.log(user.name) // ✅ 类型推断正确
})
// emitter.emit('user:login', 42) // ❌ 类型错误
emitter.emit('user:login', { name: 'Alice', id: 1 }) // ✅追问延伸
- React 中哪些地方用到了观察者模式?(
useState、useEffect、useSyncExternalStore) - 发布订阅模式在微前端通信中如何应用?
- Vue 的响应式系统用的是观察者还是发布订阅?(观察者模式:Dep/Watcher)
26. 策略模式、工厂模式、代理模式在前端的实际应用场景?
⭐⭐ 进阶 | 考察点:设计模式
深度解答
策略模式(Strategy Pattern)
将算法/策略封装为独立对象,运行时可以互换。消除 if-else 的利器。
场景:表单校验
typescript
type Validator = (value: string) => string | null
const validators: Record<string, Validator> = {
required: (value) => value.trim() ? null : '此字段必填',
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : '邮箱格式不正确',
minLength: (value) => value.length >= 6 ? null : '最少 6 个字符',
phone: (value) => /^1[3-9]\d{9}$/.test(value) ? null : '手机号格式不正确',
}
function validate(value: string, rules: string[]): string[] {
return rules
.map(rule => validators[rule]?.(value))
.filter((error): error is string => error !== null)
}
validate('', ['required', 'email'])
// ['此字段必填', '邮箱格式不正确']
validate('alice@example.com', ['required', 'email'])
// []场景:不同支付方式
typescript
interface PayStrategy {
pay(amount: number): Promise<{ success: boolean; transactionId: string }>
}
const payStrategies: Record<string, PayStrategy> = {
alipay: {
async pay(amount) {
return { success: true, transactionId: `ALI_${Date.now()}` }
}
},
wechat: {
async pay(amount) {
return { success: true, transactionId: `WX_${Date.now()}` }
}
},
creditCard: {
async pay(amount) {
return { success: true, transactionId: `CC_${Date.now()}` }
}
}
}
async function checkout(method: string, amount: number) {
const strategy = payStrategies[method]
if (!strategy) throw new Error(`Unsupported payment method: ${method}`)
return strategy.pay(amount)
}工厂模式(Factory Pattern)
将对象创建逻辑封装起来,调用者不需要知道具体类。
场景:创建不同类型的弹窗组件
typescript
interface Dialog {
title: string
render(): string
}
class ConfirmDialog implements Dialog {
title = '确认'
constructor(private message: string) {}
render() {
return `<div class="confirm">${this.message}<button>确认</button><button>取消</button></div>`
}
}
class AlertDialog implements Dialog {
title = '提示'
constructor(private message: string) {}
render() {
return `<div class="alert">${this.message}<button>知道了</button></div>`
}
}
class PromptDialog implements Dialog {
title = '输入'
constructor(private message: string) {}
render() {
return `<div class="prompt">${this.message}<input /><button>提交</button></div>`
}
}
function createDialog(type: 'confirm' | 'alert' | 'prompt', message: string): Dialog {
const dialogs = {
confirm: ConfirmDialog,
alert: AlertDialog,
prompt: PromptDialog
}
return new dialogs[type](message)
}
const dialog = createDialog('confirm', '确定删除吗?')
console.log(dialog.render())场景:Axios 实例工厂
typescript
function createApiClient(baseURL: string, token?: string) {
const instance = axios.create({
baseURL,
timeout: 10000,
headers: token ? { Authorization: `Bearer ${token}` } : {}
})
instance.interceptors.response.use(
(res) => res.data,
(err) => {
if (err.response?.status === 401) {
window.location.href = '/login'
}
return Promise.reject(err)
}
)
return instance
}
const mainApi = createApiClient('https://api.example.com', getToken())
const analyticsApi = createApiClient('https://analytics.example.com')代理模式(Proxy Pattern)
为对象提供一个替身,控制对目标对象的访问。
场景:图片懒加载代理
typescript
class ImageProxy {
private realImage: HTMLImageElement | null = null
constructor(private src: string, private placeholder: string) {}
display(container: HTMLElement) {
const img = document.createElement('img')
img.src = this.placeholder
container.appendChild(img)
const realImg = new Image()
realImg.onload = () => {
img.src = this.src
this.realImage = realImg
}
realImg.src = this.src
}
}场景:缓存代理
typescript
function createCachedFetcher<T>(fetcher: (key: string) => Promise<T>) {
const cache = new Map<string, { data: T; timestamp: number }>()
const TTL = 5 * 60 * 1000
return async (key: string): Promise<T> => {
const cached = cache.get(key)
if (cached && Date.now() - cached.timestamp < TTL) {
return cached.data
}
const data = await fetcher(key)
cache.set(key, { data, timestamp: Date.now() })
return data
}
}
const cachedGetUser = createCachedFetcher(async (id: string) => {
const res = await fetch(`/api/users/${id}`)
return res.json()
})
await cachedGetUser('123') // 请求 API
await cachedGetUser('123') // 命中缓存场景:Proxy 实现响应式(简化版 Vue3)
typescript
function reactive<T extends object>(target: T): T {
return new Proxy(target, {
get(obj, key, receiver) {
track(obj, key)
return Reflect.get(obj, key, receiver)
},
set(obj, key, value, receiver) {
const result = Reflect.set(obj, key, value, receiver)
trigger(obj, key)
return result
}
})
}追问延伸
- React 中的哪些 API 体现了工厂模式?(
createElement、createContext、createRoot) - 单例模式在前端的应用?(全局 Store、Modal 管理器、日志服务)
- 装饰器模式和 ES 装饰器语法(Stage 3)的关系?HOC 是装饰器模式吗?