Skip to content

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 算法进行隐式类型转换:

核心转换规则(按优先级):

  1. null == undefined → true(且它们不等于其他任何值)
  2. Number vs String → 将 String 转为 Number
  3. Boolean vs 任意 → 将 Boolean 转为 Number(true→1, false→0)
  4. 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 == ""        // true

Object 的 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]]):所有对象都有,指向创建该对象的构造函数的 prototype
  • constructor:原型对象上的属性,指回构造函数本身
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 操作符做了四件事:

  1. 创建一个新的空对象
  2. 将新对象的 __proto__ 指向构造函数的 prototype
  3. 以新对象为 this 执行构造函数
  4. 如果构造函数返回了一个对象,则使用该对象;否则返回新创建的对象
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 Objectfalse
  • 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()) // 3

outer 执行完毕后,按理说 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

深度解答

特性varletconst
作用域函数作用域块级作用域块级作用域
提升提升并初始化为 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 = 2

TDZ 的精确边界:

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() 只是浅冻结,如何实现深冻结?
  • switchcase 中使用 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.nextTick vs queueMicrotask
  • requestAnimationFrame 的回调在事件循环的什么位置执行?
  • queueMicrotaskPromise.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.allSettledPromise.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+ 规范的核心要点:

  1. 状态不可逆:pending → fulfilled 或 pending → rejected,只能转换一次
  2. then 必须返回新 Promise:支持链式调用 p.then(...).then(...)
  3. 回调异步执行:用 queueMicrotask 保证异步(规范要求"异步调用")
  4. 值穿透onFulfilled/onRejected 不是函数时,值要传递下去
  5. resolvePromise:处理 then 回调返回值,递归解析 thenable 对象
  6. 防止重复调用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 多了 catchfinally、静态方法)

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 是什么?异步迭代器的使用场景?
  • awaitfor 循环中是串行的,如何改为并行?什么时候用串行/并行?
  • 顶层 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)) 的局限:无法处理 undefinedfunctionSymbol、循环引用、Date(变成字符串)、RegExp(变成空对象)、MapSet 等。

完整版深拷贝:

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...inObject.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 返回一个新函数,需要注意:

  1. 支持柯里化(预设参数)
  2. 返回的函数用 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==(非 ===),同时匹配 nullundefined
  • Object(context) 处理原始值(如 call(42)
  • bind 返回函数的 prototype 需指向原函数的 prototype

追问延伸

  • 为什么用 Symbol 而不是普通字符串作为临时 key?
  • Function.prototype.bind 返回的函数有 prototype 属性吗?(答:没有)
  • 如何实现一个支持软绑定(softBind)的 bind?即允许后续的隐式/显式绑定覆盖?

16. Proxy 和 Object.defineProperty 的区别?Vue2 和 Vue3 为什么不同?

⭐⭐⭐ 深入 | 考察点:Proxy、响应式

深度解答

特性Object.definePropertyProxy
拦截粒度单个属性整个对象
新增属性❌ 无法检测✅ 自动拦截
删除属性❌ 无法检测deleteProperty trap
数组变化❌ 需要 hack(重写方法)✅ 原生支持
可拦截操作get / set13 种 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:

  1. 不需要遍历所有属性:Proxy 是惰性的,只在访问时拦截
  2. 天然支持新增/删除属性:不需要 Vue.set/Vue.delete
  3. 天然支持数组操作:不需要 hack 数组方法
  4. 更多拦截能力: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 的原因:

  1. 调试困难:栈帧被复用后,Error.stack 丢失调用信息
  2. 性能权衡:检测尾调用本身有开销
  3. 隐式优化:开发者难以确认优化是否生效

实际替代方案——蹦床函数(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 只在严格模式下生效?(argumentscaller 的影响)
  • 除了蹦床函数,还有什么方式避免深递归导致栈溢出?(循环改写、Web Worker)
  • Continuation Passing Style (CPS) 是什么?和尾调用有什么关系?

18. WeakMap/WeakSet 和 Map/Set 的区别?应用场景?

⭐⭐ 进阶 | 考察点:GC、弱引用

深度解答

特性Map / SetWeakMap / WeakSet
Key 类型任意类型只能是对象(非原始值)
GC强引用,key 不会被 GC弱引用,key 可被 GC
可枚举sizeforEach、迭代器不可枚举,无 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
}

追问延伸

  • WeakRefFinalizationRegistry 是什么?ES2021 新增了哪些弱引用能力?
  • 为什么 WeakMap 的 key 不能是原始值?(原始值无法被 GC 追踪)
  • Vue3 的 reactive() 底层用 WeakMap 缓存已代理的对象,为什么?

19. ES Module 和 CommonJS 的核心区别?循环引用时各自如何处理?

⭐⭐⭐ 深入 | 考察点:模块化

深度解答

特性CommonJS (CJS)ES Module (ESM)
加载时机运行时加载编译时静态分析
输出值的拷贝值的引用(live binding)
thismodule.exportsundefined
顶层 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 done

ESM 的循环引用——利用引用绑定:

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 === ba0 时,用 1/a === 1/b 区分 +0-01/+0 = Infinity1/-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 === NaNfalse,会导致无限更新)
  • +0-0 在数学计算中有区别(如 1/+0 = Infinity),应视为不同值

追问延伸

  • React 的 setState(prevState => prevState) 会触发重渲染吗?
  • 为什么 React 只做"浅比较"?如果深比较会有什么问题?
  • Object.isSameValueZero(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 中 voidundefined 的区别?函数返回 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 中哪些地方用到了观察者模式?(useStateuseEffectuseSyncExternalStore
  • 发布订阅模式在微前端通信中如何应用?
  • 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 体现了工厂模式?(createElementcreateContextcreateRoot
  • 单例模式在前端的应用?(全局 Store、Modal 管理器、日志服务)
  • 装饰器模式和 ES 装饰器语法(Stage 3)的关系?HOC 是装饰器模式吗?

用心学习,用代码说话 💻