技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

22 道高频 JavaScript 手写题:修正版与现代实现

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/05-22 道高频 JavaScript 手写面试题及答案
本文目录24 个章节
  1. 1. 实现防抖(debounce)
  2. 2. 实现节流(throttle)
  3. 3. 深克隆(deep clone)
  4. 4. 实现 Event Bus
  5. 5. 实现 instanceof
  6. 6. 模拟 new
  7. 7. 实现 call
  8. 8. 实现 apply
  9. 9. 实现 bind
  10. 10. 模拟 Object.create
  11. 11. 实现继承
  12. 12. 解析 JSON
  13. 13. 实现 Promise 的关键部分
  14. 14. 解析 URL 参数
  15. 15. 实现简单模板替换
  16. 16. 转换为驼峰命名
  17. 17. 查找出现次数最多的字符
  18. 18. 用遍历查找字符串
  19. 19. 实现千位分隔符
  20. 20. 判断手机号格式
  21. 21. 验证邮箱格式
  22. 22. 验证身份证号码格式
  23. 总结
  24. 参考资料

22 道高频 JavaScript 手写题:修正版与现代实现

Category(分类): JavaScript Status: 已更新

手写题适合用来理解语言机制,但示例代码不应直接复制到生产环境。本文保留原文的 22 个练习方向,修正了防抖/节流中箭头函数的 this 错误、EventEmitter 只能保存一个监听器、call 重复执行、深拷贝遗漏循环引用、参数解析未编码、正则过时和 eval 解析 JSON 等问题。

1. 实现防抖(debounce)

防抖表示:连续触发时不断重新计时,停止触发一段时间后执行。下面支持 leadingtrailingcancel

function debounce(fn, wait, { leading = false, trailing = true } = {}) {
  let timer = null
  let lastArgs
  let lastThis
  let result

  const invoke = () => {
    const args = lastArgs
    const context = lastThis
    lastArgs = lastThis = undefined
    result = fn.apply(context, args)
    return result
  }

  function debounced(...args) {
    const callNow = leading && timer === null
    lastArgs = args
    lastThis = this

    clearTimeout(timer)
    timer = setTimeout(() => {
      timer = null
      if (trailing && lastArgs) invoke()
      else lastArgs = lastThis = undefined
    }, wait)

    if (callNow) invoke()
    return result
  }

  debounced.cancel = () => {
    clearTimeout(timer)
    timer = null
    lastArgs = lastThis = undefined
  }

  return debounced
}

适合搜索联想、输入校验、窗口变化后的重新计算。它不能取消已经发出的请求,网络请求应配合 AbortController

2. 实现节流(throttle)

节流表示:在一个时间窗口内最多执行一次,可选择执行窗口开始时的 leading 调用或窗口结束时的 trailing 调用:

function throttle(fn, wait, { leading = true, trailing = true } = {}) {
  let lastTime = 0
  let timer = null
  let lastArgs
  let lastThis

  const invoke = () => {
    lastTime = Date.now()
    const args = lastArgs
    const context = lastThis
    lastArgs = lastThis = undefined
    fn.apply(context, args)
  }

  function throttled(...args) {
    const now = Date.now()
    if (!lastTime && !leading) lastTime = now

    const remaining = wait - (now - lastTime)
    lastArgs = args
    lastThis = this

    if (remaining <= 0 || remaining > wait) {
      clearTimeout(timer)
      timer = null
      invoke()
    } else if (!timer && trailing) {
      timer = setTimeout(() => {
        timer = null
        if (lastArgs) invoke()
      }, remaining)
    }
  }

  throttled.cancel = () => {
    clearTimeout(timer)
    timer = null
    lastTime = 0
    lastArgs = lastThis = undefined
  }

  return throttled
}

滚动、拖拽、指针移动等视觉更新还可以使用 requestAnimationFrame 合并到浏览器绘制节奏中。节流间隔不是固定答案,应根据交互和性能指标测试。

普通调用、防抖和节流的时间关系

3. 深克隆(deep clone)

3.1 JSON 简化版的边界

const copy = JSON.parse(JSON.stringify(source))

它只适合无循环、只包含 JSON 数据的简单对象,会丢失或改变 undefined、函数、Symbol、BigInt、Date、RegExp、Map、Set、TypedArray、特殊数值和原型信息。

3.2 推荐使用 structuredClone

const copy = structuredClone(source)

structuredClone() 支持循环引用和许多内置对象,但不能克隆函数、DOM 节点和所有宿主对象;需要转移 ArrayBuffer 时还应明确使用 transfer 选项。

3.3 教学版递归克隆

function clone(value, seen = new WeakMap()) {
  if (value === null || typeof value !== 'object') return value
  if (seen.has(value)) return seen.get(value)

  if (value instanceof Date) return new Date(value.getTime())
  if (value instanceof RegExp) {
    const copy = new RegExp(value.source, value.flags)
    copy.lastIndex = value.lastIndex
    return copy
  }

  if (value instanceof Map) {
    const copy = new Map()
    seen.set(value, copy)
    for (const [key, item] of value) {
      copy.set(clone(key, seen), clone(item, seen))
    }
    return copy
  }

  if (value instanceof Set) {
    const copy = new Set()
    seen.set(value, copy)
    for (const item of value) copy.add(clone(item, seen))
    return copy
  }

  const copy = Array.isArray(value)
    ? []
    : Object.create(Object.getPrototypeOf(value))
  seen.set(value, copy)

  for (const key of Reflect.ownKeys(value)) {
    const descriptor = Object.getOwnPropertyDescriptor(value, key)
    if ('value' in descriptor) descriptor.value = clone(descriptor.value, seen)
    Object.defineProperty(copy, key, descriptor)
  }

  return copy
}

这个版本仍未覆盖 Error、Promise、各种 TypedArray、跨 realm 类型和所有宿主对象;面试时应主动说明边界。

4. 实现 Event Bus

事件总线需要支持一个事件对应多个监听器,并且应该提供移除接口:

class EventEmitter {
  #events = new Map()

  on(type, listener) {
    const listeners = this.#events.get(type) ?? new Set()
    listeners.add(listener)
    this.#events.set(type, listeners)
    return () => this.off(type, listener)
  }

  once(type, listener) {
    const off = this.on(type, (...args) => {
      off()
      listener(...args)
    })
    return off
  }

  off(type, listener) {
    const listeners = this.#events.get(type)
    if (!listeners) return false

    const removed = listeners.delete(listener)
    if (listeners.size === 0) this.#events.delete(type)
    return removed
  }

  emit(type, ...args) {
    const listeners = this.#events.get(type)
    if (!listeners) return false

    for (const listener of [...listeners]) listener(...args)
    return true
  }
}

原文的 EventEmeitter 拼写错误、同一个事件只能保留一个函数,且没有安全处理无监听器的情况。真实应用还应决定监听器抛错时是否继续通知其他监听器,并在组件销毁时取消订阅。

5. 实现 instanceof

function myInstanceOf(value, Constructor) {
  if (typeof Constructor !== 'function') {
    throw new TypeError('右侧必须是构造函数或可调用对象')
  }

  if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
    return false
  }

  const target = Constructor.prototype
  if (
    target === null ||
    (typeof target !== 'object' && typeof target !== 'function')
  ) {
    throw new TypeError('prototype 必须是对象')
  }

  let current = Object.getPrototypeOf(value)

  while (current !== null) {
    if (current === target) return true
    current = Object.getPrototypeOf(current)
  }

  return false
}

这是原型链检查的简化版。原生 instanceof 还会遵循右值的 Symbol.hasInstance,跨 iframe 判断数组时优先使用 Array.isArray()

6. 模拟 new

function objectFactory(Constructor, ...args) {
  if (typeof Constructor !== 'function') {
    throw new TypeError('Constructor 必须是函数')
  }

  if (
    /^class\s/.test(Function.prototype.toString.call(Constructor)) ||
    Constructor.prototype === undefined
  ) {
    // class 和无 prototype 的可构造函数交给规范级构造调用处理;箭头函数会抛 TypeError。
    return Reflect.construct(Constructor, args)
  }

  const prototype = Constructor.prototype
  const instance = Object.create(
    prototype !== null &&
      (typeof prototype === 'object' || typeof prototype === 'function')
      ? prototype
      : Object.prototype
  )
  const result = Reflect.apply(Constructor, instance, args)

  return result !== null && (
    typeof result === 'object' || typeof result === 'function'
  )
    ? result
    : instance
}

function Person(name, age) {
  this.name = name
  this.age = age
}

const person = objectFactory(Person, 'Ada', 18)

原文只判断 typeof ret === 'object',会漏掉构造函数返回函数的情况,也会把 null 当成对象返回;上面的实现补足了这两个边界。

7. 实现 call

Function.prototype.myCall = function (context, ...args) {
  if (typeof this !== 'function') {
    throw new TypeError('目标不是函数')
  }

  const receiver = context == null ? globalThis : Object(context)
  const key = Symbol('call')
  Object.defineProperty(receiver, key, {
    value: this,
    configurable: true
  })

  try {
    return receiver[key](...args)
  } finally {
    delete receiver[key]
  }
}

原文的 context.fn(...args) 调用了两次目标函数,必须删除其中一次。临时属性法只是教学近似实现,不能完整复制严格模式下的原始 this 语义。

8. 实现 apply

Function.prototype.myApply = function (context, argsList) {
  if (typeof this !== 'function') {
    throw new TypeError('目标不是函数')
  }

  const args = argsList == null ? [] : Array.from(argsList)
  return this.myCall(context, ...args)
}

不建议使用 eval 拼接参数;标准的 Reflect.apply(fn, thisArg, args) 更可靠。

9. 实现 bind

Function.prototype.myBind = function (context, ...boundArgs) {
  if (typeof this !== 'function') {
    throw new TypeError('目标不是函数')
  }

  const target = this
  function bound(...args) {
    const allArgs = [...boundArgs, ...args]
    if (new.target) {
      return Reflect.construct(target, allArgs, new.target)
    }
    return target.apply(context, allArgs)
  }

  if (target.prototype) {
    bound.prototype = Object.create(target.prototype)
  }
  return bound
}

完整的原生 bind 还会维护 lengthname、构造目标和更多原型细节;面试时重点解释绑定参数与 new 调用的区别。

10. 模拟 Object.create

function myCreate(proto, descriptors) {
  if (
    proto !== null &&
    typeof proto !== 'object' &&
    typeof proto !== 'function'
  ) {
    throw new TypeError('原型必须是对象、函数或 null')
  }

  const result = {}
  Object.setPrototypeOf(result, proto)
  if (descriptors !== undefined) {
    Object.defineProperties(result, descriptors)
  }
  return result
}

实际开发直接使用 Object.create(),它能更清楚地表达意图并由引擎优化。

11. 实现继承

function Parent(name) {
  this.parent = name
}

Parent.prototype.say = function () {
  return `父类:${this.parent}`
}

function Child(name, parent) {
  Parent.call(this, parent)
  this.child = name
}

Child.prototype = Object.create(Parent.prototype, {
  constructor: {
    value: Child,
    configurable: true,
    writable: true
  }
})

Child.prototype.say = function () {
  return `${Parent.prototype.say.call(this)},子类:${this.child}`
}

Child.prototype = Parent.prototype 会共享原型;Child.prototype = new Parent() 会额外调用父构造函数。Object.create(Parent.prototype) 是经典寄生组合继承方案。现代代码优先使用 class Child extends Parent

12. 解析 JSON

JSON 数据必须使用标准解析器:

const text = '{"name":"Ada","age":25}'
const object = JSON.parse(text)
const output = JSON.stringify(object)

原文使用 eval('(' + json + ')'),这会执行输入中的任意 JavaScript,不能用于解析不可信字符串,也不是 JSON 语法的正确实现。new Function() 同样不安全。若要理解解析器原理,应实现 tokenizer、递归下降解析和错误位置报告,而不是执行字符串。

13. 实现 Promise 的关键部分

Promise 的完整实现较长,面试时可以先说明四个核心:状态只能改变一次、executor 同步执行、then 回调异步执行、then 返回新的 Promise 并采用 thenable。一个可读的教学骨架如下:

class SimplePromise {
  constructor(executor) {
    this.state = 'pending'
    this.value = undefined
    this.handlers = []

    let locked = false
    const fulfill = value => {
      if (this.state !== 'pending') return
      this.state = 'fulfilled'
      this.value = value
      this.flush()
    }
    const rejectInternal = reason => {
      if (this.state !== 'pending') return
      this.state = 'rejected'
      this.value = reason
      this.flush()
    }
    const reject = reason => {
      if (locked) return
      locked = true
      rejectInternal(reason)
    }
    const resolve = value => {
      if (locked) return
      locked = true

      if (value === this) {
        rejectInternal(new TypeError('循环解析'))
        return
      }

      if (value && (typeof value === 'object' || typeof value === 'function')) {
        let then
        try {
          then = value.then
        } catch (error) {
          rejectInternal(error)
          return
        }

        if (typeof then !== 'function') {
          fulfill(value)
          return
        }

        let called = false
        try {
          then.call(
            value,
            nextValue => {
              if (called) return
              called = true
              locked = false
              resolve(nextValue)
            },
            reason => {
              if (called) return
              called = true
              rejectInternal(reason)
            }
          )
        } catch (error) {
          if (!called) rejectInternal(error)
        }
        return
      }

      fulfill(value)
    }

    try {
      executor(resolve, reject)
    } catch (error) {
      reject(error)
    }
  }

  flush() {
    if (this.state === 'pending') return
    queueMicrotask(() => {
      for (const handler of this.handlers.splice(0)) {
        const callback = this.state === 'fulfilled'
          ? handler.onFulfilled
          : handler.onRejected
        if (typeof callback !== 'function') {
          ;(this.state === 'fulfilled' ? handler.resolve : handler.reject)(this.value)
          continue
        }
        try {
          handler.resolve(callback(this.value))
        } catch (error) {
          handler.reject(error)
        }
      }
    })
  }

  then(onFulfilled, onRejected) {
    return new SimplePromise((resolve, reject) => {
      this.handlers.push({ onFulfilled, onRejected, resolve, reject })
      this.flush()
    })
  }

  catch(onRejected) {
    return this.then(undefined, onRejected)
  }
}

这个骨架还需要补足 thenable 多次调用保护、静态组合方法、finally 和更全面的 Promise/A+ 测试,不能替换原生 Promise。

14. 解析 URL 参数

现代实现优先使用 URLURLSearchParams。如果业务约定“没有等号的参数为 true”,需要保留原始查询字符串来区分 ?enabled?enabled=

function decodePart(value) {
  return decodeURIComponent(value.replace(/\+/g, ' '))
}

function parseParam(input) {
  const url = new URL(input, window.location.href)
  const result = Object.create(null)
  const query = url.search.slice(1)

  if (!query) return result

  for (const part of query.split('&')) {
    if (!part) continue

    const equalIndex = part.indexOf('=')
    const rawKey = equalIndex === -1 ? part : part.slice(0, equalIndex)
    const rawValue = equalIndex === -1 ? null : part.slice(equalIndex + 1)
    const key = decodePart(rawKey)
    const value = rawValue === null
      ? true
      : decodePart(rawValue)

    const normalized = typeof value === 'string' && /^-?(?:\d+\.?\d*|\.\d+)$/.test(value)
      ? Number(value)
      : value

    if (Object.hasOwn(result, key)) {
      result[key] = Array.isArray(result[key])
        ? [...result[key], normalized]
        : [result[key], normalized]
    } else {
      result[key] = normalized
    }
  }

  return result
}

parseParam('https://example.com/?user=anonymous&id=123&id=456&enabled')

不可信参数不能直接当作 HTML 或 SQL 使用;解析和验证是两件事。

15. 实现简单模板替换

只做纯文本替换时可以使用正则,不要把模板字符串拼成 JavaScript 再 eval

function render(template, data) {
  return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, path) => {
    const value = path.split('.').reduce((current, key) => current?.[key], data)
    return value == null ? '' : String(value)
  })
}

render('你好,{{user.name}}', { user: { name: 'Ada' } })

如果输出到 HTML,需要对 &<>"' 做上下文相关转义;真正的模板引擎还需要处理表达式、循环、条件、原始 HTML 和 XSS 防护。

16. 转换为驼峰命名

function toCamelCase(value) {
  return value
    .trim()
    .replace(/[-_]+([a-zA-Z0-9])/g, (_, char) => char.toUpperCase())
}

toCamelCase('get-element-by-id') // getElementById

是否保留首字母大写、连续分隔符、Unicode 字母和数字,需要按业务定义。

17. 查找出现次数最多的字符

原文使用 \w,只能覆盖有限的 ASCII 字符。用 for…of 可以按 Unicode 码点统计:

function mostFrequentChar(text) {
  const counts = new Map()
  let result = ''
  let max = 0

  for (const char of text) {
    const count = (counts.get(char) ?? 0) + 1
    counts.set(char, count)
    if (count > max) {
      max = count
      result = char
    }
  }

  return { char: result, count: max }
}

mostFrequentChar('abcabcabcbbccccc')

“字符”与“用户感知的字素簇”不完全相同,若需要处理组合字符可以结合 Intl.Segmenter

18. 用遍历查找字符串

function isContain(needle, haystack) {
  if (needle.length === 0) return 0

  for (let start = 0; start <= haystack.length - needle.length; start += 1) {
    let matched = true
    for (let offset = 0; offset < needle.length; offset += 1) {
      if (haystack[start + offset] !== needle[offset]) {
        matched = false
        break
      }
    }
    if (matched) return start
  }

  return -1
}

生产代码直接使用 indexOf()includes();如果按 Unicode 码点或本地化规则搜索,应先明确字符边界和比较规则。

19. 实现千位分隔符

国际化展示优先使用 Intl.NumberFormat

const moneyFormatter = new Intl.NumberFormat('en-US', {
  useGrouping: true,
  maximumFractionDigits: 3
})

function parseToMoney(value) {
  const number = Number(value)
  if (!Number.isFinite(number)) throw new TypeError('必须是有限数值')
  return moneyFormatter.format(number)
}

parseToMoney(1087654.321) // '1,087,654.321'

如果只是对已格式化的字符串加逗号,不能直接用 Number(),因为它会丢失前导零、超大整数和精确小数语义。货币金额还应明确币种、舍入方式、负数和 Intl.NumberFormat 的 locale。

20. 判断手机号格式

正则只能检查格式,不能证明号码真实存在。中国大陆手机号号段也会变化,旧表达式 ^1[34578]\d{9}$ 已经漏掉一些号段:

function looksLikeMainlandMobile(value) {
  return /^1[3-9]\d{9}$/.test(String(value))
}

实际注册仍需要服务端校验、验证码、频率限制和隐私保护。

21. 验证邮箱格式

邮箱完整语法非常复杂,应用通常只做宽松格式检查,再通过验证邮件确认:

function looksLikeEmail(value) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value))
}

不要声称一个短正则覆盖 RFC 所有合法邮箱;还要限制长度并防止把输入用于 HTML、日志或查询时产生注入。

22. 验证身份证号码格式

简单正则只能检查 15 位或 18 位结构,不能校验日期、地址码和校验码。可以先做形状检查:

function looksLikeIdCard(value) {
  return /^(?:\d{15}|\d{17}[\dXx])$/.test(String(value))
}

如果业务确实需要校验 18 位身份证,应另外实现出生日期合法性、地区码和最后一位校验码,并在服务端再次验证。不要把身份证号写入日志或前端长期存储。

总结

  • 防抖和节流要正确处理 this、参数、leading/trailing 和取消;
  • 深克隆优先 structuredClone(),JSON 技巧只适用于受限数据;
  • Event Bus 要支持多监听器和清理;
  • callapplybindnewinstanceof 都受原型、严格模式和构造调用影响;
  • eval 不是 JSON 解析器,模板渲染也不能执行不可信代码;
  • URL 参数使用 URLSearchParams,文本输出使用安全转义;
  • 电话、邮箱和身份证正则只能做格式检查,不能替代服务端验证。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS