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

显示模式

登录
ARCHIVE DOCUMENTJS

「一劳永逸」送你 21 道高频 JavaScript 手写面试题(现代修正版)

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/63-「一劳永逸」送你21道高频JavaScript手写面试题
本文目录23 个章节
  1. 1. 实现事件委托
  2. 2. 实现一个可拖拽的元素
  3. 3. 手写节流和防抖
  4. 4. 实现数组去重
  5. 5. 实现柯里化(currying)
  6. 6. 实现数组 flat
  7. 7. 实现深拷贝
  8. 8. 实现对象类型判断函数
  9. 9. 手写 call 和 apply
  10. 10. 手写 bind
  11. 11. 实现 new
  12. 12. 实现 instanceof
  13. 13. 实现 sleep
  14. 14. 实现 Array.prototype.reduce
  15. 15. 实现 Promise.all 和 Promise.race
  16. 16. 手写继承
  17. 17. 手写 AJAX
  18. 18. 实现 trim
  19. 19. 实现 Object.create
  20. 20. 限制并发任务数量
  21. 21. 十进制转换为 2~16 进制
  22. 补充:数字转字符串千分位
  23. 总结

「一劳永逸」送你 21 道高频 JavaScript 手写面试题(现代修正版)

本文保留原文的面试题路线和“先写出核心,再讨论边界”的方式。原文来自早期前端面试文章,其中有抓取残留、函数名拼写错误、死循环、instanceof Promise 判断、new 返回值判断等问题;下面统一改成可运行的教学版本,并标出浏览器/Node.js 环境边界。

手写原生方法适合学习规范和边界,不建议在生产环境覆盖 Array.prototypeFunction.prototype 或替换全局 Promise

1. 实现事件委托

事件委托把监听器放到共同祖先上,再根据事件目标找到匹配元素。直接判断 event.target.tagName 会漏掉点击 li 内部 span 的情况。

<ul id="menu">
  <li>第一项 <span>详情</span></li>
  <li>第二项 <span>详情</span></li>
</ul>
function delegate(element, eventType, selector, handler, options) {
  element.addEventListener(eventType, event => {
    const target = event.target
    if (!(target instanceof Element)) return

    const matched = target.closest(selector)
    if (!matched || !element.contains(matched)) return

    handler.call(matched, event, matched)
  }, options)
}

delegate(document.querySelector('#menu'), 'click', 'li', (event, li) => {
  console.log('点击了:', li.textContent.trim())
})

closest() 找到的元素必须属于委托容器,否则嵌套容器可能误触发。Shadow DOM、composedPath()、指针事件和键盘可访问性还需要根据组件场景额外设计;事件委托不应代替按钮的语义和键盘行为。

2. 实现一个可拖拽的元素

原文使用 mousedownmousemovemouseup,但没有处理鼠标移出窗口、触摸屏、释放捕获和元素定位方式。现代浏览器可以使用 Pointer Events:

<div id="drag-box" class="drag-box">拖动我</div>
.drag-box {
  position: fixed;
  left: 20px;
  top: 20px;
  touch-action: none;
  user-select: none;
  cursor: grab;
}

.drag-box.dragging {
  cursor: grabbing;
}
function makeDraggable(element) {
  let pointerId = null
  let offsetX = 0
  let offsetY = 0

  element.addEventListener('pointerdown', event => {
    pointerId = event.pointerId
    const rect = element.getBoundingClientRect()
    offsetX = event.clientX - rect.left
    offsetY = event.clientY - rect.top
    element.setPointerCapture(pointerId)
    element.classList.add('dragging')
  })

  element.addEventListener('pointermove', event => {
    if (event.pointerId !== pointerId) return
    element.style.left = `${event.clientX - offsetX}px`
    element.style.top = `${event.clientY - offsetY}px`
  })

  const stop = event => {
    if (event.pointerId !== pointerId) return
    pointerId = null
    element.classList.remove('dragging')
    if (element.hasPointerCapture(event.pointerId)) {
      element.releasePointerCapture(event.pointerId)
    }
  }

  element.addEventListener('pointerup', stop)
  element.addEventListener('pointercancel', stop)
}

makeDraggable(document.querySelector('#drag-box'))

如果元素需要拖动图片、文本或复杂组件,还要处理 preventDefault()、拖拽手柄、无障碍替代操作和边界限制。原来的 parseInt(element.style.left || 0) 只能读取内联样式,不能可靠反映 CSS 布局后的坐标。

3. 手写节流和防抖

3.1 节流(throttle)

节流保证一段时间内最多执行一次,适合滚动、拖动和高频输入。下面的版本支持前导执行、尾部执行、取消和立即刷新:

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

  function invoke() {
    lastInvokeTime = Date.now()
    result = fn.apply(lastThis, lastArgs)
    lastThis = undefined
    lastArgs = undefined
    return result
  }

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

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

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

    return result
  }

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

  throttled.flush = () => {
    if (!timer) return result
    clearTimeout(timer)
    timer = null
    if (lastArgs) return invoke()
    return result
  }

  return throttled
}

节流没有唯一实现。必须先约定“时间窗口从第一次调用开始还是从执行结束开始”,以及是否保留最后一次调用;面试时应说明这个选择。

3.2 防抖(debounce)

防抖会在最后一次触发后等待一段时间再执行,适合搜索输入、窗口大小调整和自动保存:

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

  function invoke() {
    result = fn.apply(lastThis, lastArgs)
    lastThis = undefined
    lastArgs = undefined
    return result
  }

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

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

    if (shouldCallLeading) return invoke()
    return result
  }

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

  debounced.flush = () => {
    if (!timer) return result
    clearTimeout(timer)
    timer = null
    if (lastArgs) return invoke()
    return result
  }

  return debounced
}

清除计时器只是防抖的核心之一。真实项目还要考虑组件卸载时调用 cancel()、异步请求的取消和过期响应覆盖新结果。

4. 实现数组去重

原文的测试数据包含 NaN、对象、正则和包装对象:

const array = [
  1, 1, '1', '1', null, null,
  undefined, undefined,
  new String('1'), new String('1'),
  /a/, /a/,
  NaN, NaN
]

4.1 使用 Set

const uniqueBySet = values => [...new Set(values)]

console.log(uniqueBySet([1, 1, NaN, NaN])) // [1, NaN]

Set 使用 SameValueZero 判断相等:NaNNaN 相同,+0-0 也相同;两个内容相同的对象仍然是不同引用,不会被合并。

4.2 filterindexOf

function uniqueByIndexOf(values) {
  return values.filter((value, index) => values.indexOf(value) === index)
}

这个版本使用严格相等搜索,indexOf(NaN) 返回 -1,所以会错误地保留多个 NaN。它适合解释旧代码,不是现代通用去重方案。

4.3 reduce 和自定义键

function uniqueByReduce(values) {
  return values.reduce((result, value) => {
    return result.includes(value) ? result : [...result, value]
  }, [])
}

includes 可以识别 NaN,但对象仍按引用判断,且每次复制数组可能带来额外成本。不要使用 typeof value + value 作为通用键:不同对象、Symbol、逗号和字符串拼接都可能造成碰撞,并且普通对象还涉及原型污染风险。

如果业务要按某个字段去重,应明确键策略:

function uniqueBy(values, getKey) {
  const seen = new Set()
  const result = []

  for (const value of values) {
    const key = getKey(value)
    if (seen.has(key)) continue
    seen.add(key)
    result.push(value)
  }

  return result
}

const users = [
  { id: 1, name: 'A' },
  { id: 1, name: 'A latest' },
  { id: 2, name: 'B' }
]
console.log(uniqueBy(users, user => user.id))

5. 实现柯里化(currying)

柯里化把一个多参数函数转换为逐步接收参数的函数。原文测试使用了 curry,但前面定义的是 currying;下面统一命名:

function curry(fn, collected = []) {
  return (...args) => {
    const allArgs = [...collected, ...args]
    return allArgs.length >= fn.length
      ? fn(...allArgs)
      : curry(fn, allArgs)
  }
}

const addSum = (a, b, c) => a + b + c
const add = curry(addSum)

console.log(add(1)(2)(3)) // 6
console.log(add(1, 2)(3)) // 6
console.log(add(1, 2, 3)) // 6

这个教学版依赖 fn.length,而带默认参数、剩余参数的函数可能有意外的 length

console.log(((a, b = 1, c) => a + b + c).length) // 1

生产级 curry 通常需要显式指定参数个数、占位符、this 绑定和多余参数策略。

6. 实现数组 flat

现代环境优先使用原生方法:

const nested = [1, [2, [3, 4]]]
console.log(nested.flat()) // [1, 2, [3, 4]]
console.log(nested.flat(Infinity)) // [1, 2, 3, 4]

递归教学实现如下,修复原文递归函数名拼写错误:

function flatDeep(array, depth = 1) {
  if (depth < 1) return array.slice()

  return array.reduce((result, value) => {
    return result.concat(
      Array.isArray(value)
        ? flatDeep(value, depth - 1)
        : value
    )
  }, [])
}

const array = [1, 2, [3, [4, 5]]]
console.log(flatDeep(array, 1)) // [1, 2, 3, [4, 5]]
console.log(flatDeep(array, Infinity)) // [1, 2, 3, 4, 5]

原生 flat 对稀疏数组、数组子类和 Symbol.isConcatSpreadable 等细节有规范行为;上面的实现主要用于理解递归,不是完整 polyfill。

7. 实现深拷贝

现代浏览器和 Node.js 可以先考虑 structuredClone()

const source = {
  date: new Date(),
  nested: { value: 1 }
}
source.self = source

const copy = structuredClone(source)
copy.nested.value = 2

console.log(source.nested.value) // 1
console.log(copy.self === copy) // true

如果面试要求理解递归和循环引用,可以写一个有限范围的版本:

function deepClone(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)
  if (value instanceof RegExp) return new RegExp(value.source, value.flags)

  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 = deepClone(descriptor.value, seen)
    }
    Object.defineProperty(copy, key, descriptor)
  }

  return copy
}

不要把这个示例称作覆盖所有对象的 polyfill。函数、DOM 节点、WeakMap、私有字段、资源句柄和自定义类的内部状态都可能需要领域专用复制逻辑。

8. 实现对象类型判断函数

const objectToString = Object.prototype.toString

const isType = type => value =>
  objectToString.call(value) === `[object ${type}]`

const isArray = isType('Array')
const isFunction = isType('Function')

console.log(isArray([1, 2, 3])) // true
console.log(isFunction(Map)) // true

Array.isArray() 对数组更直接;Object.prototype.toString.call() 也可能受到 Symbol.toStringTag 影响,跨 Realm、代理对象和伪造标签时要明确需求。

9. 手写 callapply

两者都改变调用时的 this,区别在于参数传递形式。下面的临时 Symbol 属性方式适合说明原理,但不能完全模拟严格模式、代理、不可扩展对象和原生内置函数的所有语义:

Function.prototype.myCall = function (thisArg, ...args) {
  if (typeof this !== 'function') {
    throw new TypeError('myCall must be called on a function')
  }

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

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

Function.prototype.myApply = function (thisArg, args) {
  if (typeof this !== 'function') {
    throw new TypeError('myApply must be called on a function')
  }
  if (args != null && typeof args[Symbol.iterator] !== 'function') {
    throw new TypeError('CreateListFromArrayLike is simplified here')
  }

  const receiver = thisArg == null ? globalThis : Object(thisArg)
  const key = Symbol('temporary-apply')
  Object.defineProperty(receiver, key, {
    value: this,
    configurable: true
  })

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

const context = { value: 10 }
function sum(a, b) {
  return this.value + a + b
}

console.log(sum.myCall(context, 1, 2)) // 13
console.log(sum.myApply(context, [1, 2])) // 13

真正的调用应直接使用 Reflect.apply(fn, thisArg, args)。原文 delete thisArg.fn 删除的是字面量属性 fn,不是 Symbol 临时属性;另外 Object(null)、严格模式下 this 的规则不能靠简单的 Object() 完整还原。

10. 手写 bind

bind 返回新函数,不会立即调用。只用箭头函数实现会丢失构造调用能力;下面的版本同时演示普通调用和 new 调用:

Function.prototype.myBind = function (thisArg, ...boundArgs) {
  if (typeof this !== 'function') {
    throw new TypeError('myBind must be called on a function')
  }

  const target = this

  function boundFunction(...callArgs) {
    const args = [...boundArgs, ...callArgs]
    if (new.target) {
      return Reflect.construct(target, args, new.target)
    }
    return Reflect.apply(target, thisArg, args)
  }

  if (target.prototype && typeof target.prototype === 'object') {
    boundFunction.prototype = Object.create(target.prototype, {
      constructor: {
        value: boundFunction,
        configurable: true,
        writable: true
      }
    })
  }

  return boundFunction
}

const context = { name: 'TianTian' }
function say(prefix, suffix) {
  return `${prefix}${this.name}${suffix}`
}

const boundSay = say.myBind(context, 'Hello ')
console.log(boundSay('!')) // Hello TianTian!

原生 bind 还有 lengthname、构造函数原型和严格模式等细节;生产代码直接使用 Function.prototype.bind()

11. 实现 new

new Constructor(...args) 的核心步骤是:创建一个以 Constructor.prototype 为原型的新对象、调用构造函数、如果构造函数返回对象则使用该返回值,否则使用新对象。

function myNew(Constructor, ...args) {
  if (typeof Constructor !== 'function') {
    throw new TypeError('Constructor must be a function')
  }

  const prototype = Constructor.prototype
  const instance = Object.create(
    prototype && (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) {
  this.name = name
}
Person.prototype.say = function () {
  return this.name
}

const person = myNew(Person, 'Ada')
console.log(person.say()) // Ada

原文条件 result && typeof result === 'function' || typeof result === 'object' 存在运算符优先级问题,null 也可能被错误返回。箭头函数没有 [[Construct]],不能作为真正的 new 构造器;上面的教学函数也没有完全复现原生 new 的全部内部语义。

12. 实现 instanceof

value instanceof Constructor 默认检查 Constructor.prototype 是否出现在 value 的原型链上。它不是通用类型判断,也受跨 Realm 和 Symbol.hasInstance 影响。

function myInstanceOf(value, Constructor) {
  if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
    return false
  }
  if (typeof Constructor !== 'function') {
    throw new TypeError('Right-hand side is not callable')
  }

  const prototype = Constructor.prototype
  if (prototype === null || (typeof prototype !== 'object' && typeof prototype !== 'function')) {
    throw new TypeError('Constructor.prototype is not an object')
  }

  let current = Object.getPrototypeOf(value)
  while (current !== null) {
    if (current === prototype) return true
    current = Object.getPrototypeOf(current)
  }
  return false
}

console.log(myInstanceOf([], Array)) // true
console.log(myInstanceOf([], Object)) // true
console.log(myInstanceOf(null, Object)) // false

不要使用 __proto__ 遍历原型链;标准 API 是 Object.getPrototypeOf()。数组应使用 Array.isArray(),跨窗口数组也能正确判断。

13. 实现 sleep

原文的 sleep(saySomething('TianTian'), 1000) 会先立即执行 saySomething,传给 sleep 的只是返回值,并没有延迟调用函数。更清晰的接口是让 sleep 只等待时间:

function sleep(milliseconds) {
  return new Promise(resolve => {
    setTimeout(resolve, milliseconds)
  })
}

const saySomething = name => console.log(`hello, ${name}`)

async function autoPlay() {
  await sleep(1000)
  saySomething('TianTian')
  await sleep(1000)
  saySomething('李磊')
  await sleep(1000)
  saySomething('掘金的好友们')
}

autoPlay()

如果确实需要延迟调用函数,可以写成:

function delayCall(fn, milliseconds, ...args) {
  return sleep(milliseconds).then(() => fn(...args))
}

delayCall(console.log, 1000, 'one second later')

setTimeout 只保证至少等待一段时间,不保证精确执行时刻;取消需求应保存并调用 clearTimeout

14. 实现 Array.prototype.reduce

原文缺少 i++,会在非空数组上造成死循环;也没有处理空数组、稀疏数组和未提供初始值的情况。下面是接近规范行为的教学版:

function reduce(arrayLike, callback, initialValue) {
  if (arrayLike == null) {
    throw new TypeError('reduce called on null or undefined')
  }
  if (typeof callback !== 'function') {
    throw new TypeError('callback must be a function')
  }

  const object = Object(arrayLike)
  const length = Number(object.length) >>> 0
  let index = 0
  let accumulator

  if (arguments.length >= 3) {
    accumulator = initialValue
  } else {
    while (index < length && !(index in object)) index += 1
    if (index >= length) {
      throw new TypeError('Reduce of empty array with no initial value')
    }
    accumulator = object[index]
    index += 1
  }

  for (; index < length; index += 1) {
    if (index in object) {
      accumulator = callback(accumulator, object[index], index, object)
    }
  }

  return accumulator
}

console.log(reduce([1, 2, 3], (sum, value) => sum + value, 0)) // 6
console.log(reduce([, 2, , 4], (sum, value) => sum + value, 0)) // 6

如果要模拟原型方法,可在实验环境中写 Array.prototype.myReduce,但不要无条件修改全局原型。原生 reduce 还会处理继承属性、超大长度和 ToLength 等规范细节。

15. 实现 Promise.allPromise.race

不要使用 instanceof Promise 判断元素:普通值、跨 Realm Promise、thenable 和其他 Promise 实现都应通过 Promise.resolve() 处理。也不要把自定义实现挂到全局 Promise 上。

function promiseAll(iterable) {
  return new Promise((resolve, reject) => {
    const results = []
    let pending = 1
    let index = 0

    try {
      for (const value of iterable) {
        const currentIndex = index
        index += 1
        pending += 1

        Promise.resolve(value).then(
          result => {
            results[currentIndex] = result
            pending -= 1
            if (pending === 0) resolve(results)
          },
          reject
        )
      }
    } catch (error) {
      reject(error)
      return
    }

    pending -= 1
    if (pending === 0) resolve(results)
  })
}

function promiseRace(iterable) {
  return new Promise((resolve, reject) => {
    try {
      for (const value of iterable) {
        Promise.resolve(value).then(resolve, reject)
      }
    } catch (error) {
      reject(error)
    }
    // 空 iterable 会保持 pending,符合 Promise.race([]) 的行为
  })
}

const p1 = new Promise(resolve => setTimeout(() => resolve(11), 20))
const p2 = new Promise(resolve => setTimeout(() => resolve(22), 5))

promiseAll([p1, p2, 3]).then(values => {
  console.log(values) // [11, 22, 3],按输入顺序
})

promiseRace([p1, p2]).then(value => {
  console.log(value) // 22,先完成的结果
})

原生 Promise.all 还涉及构造器、迭代器关闭、子类化和标准的 thenable 解析。上面代码用于理解“保持输入索引”和“先完成者获胜”,不能作为完整 polyfill。

16. 手写继承

16.1 寄生组合式继承

function inheritPrototype(SubType, SuperType) {
  const prototype = Object.create(SuperType.prototype, {
    constructor: {
      value: SubType,
      configurable: true,
      writable: true
    }
  })
  SubType.prototype = prototype
  Object.setPrototypeOf(SubType, SuperType)
}

function Father(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
Father.prototype.sayName = function () {
  return this.name
}

function Son(name, age) {
  Father.call(this, name)
  this.age = age
}

inheritPrototype(Son, Father)
Son.prototype.sayAge = function () {
  return this.age
}

const demo1 = new Son('TianTian', 21)
const demo2 = new Son('TianTianUp', 20)
demo1.colors.push('extra')

console.log(demo1.colors) // ['red', 'blue', 'green', 'extra']
console.log(demo2.colors) // ['red', 'blue', 'green']

16.2 class extends

现代 JavaScript 通常使用 class 表达继承:

class Rectangle {
  constructor(height, width) {
    this.height = height
    this.width = width
  }

  get area() {
    return this.height * this.width
  }
}

class Square extends Rectangle {
  constructor(length) {
    super(length, length)
    this.name = 'Square'
  }
}

const square = new Square(20)
console.log(square.area) // 400

派生类构造函数在访问 this 前必须调用 super()class 方法默认运行在严格模式下,继承还涉及 super、静态方法、私有字段和 new.target,不能简单等同于一行 prototype 赋值。

17. 手写 AJAX

原文的 XMLHttpRequest 示例只处理了状态码 200 和成功分支。现代浏览器新代码可以优先使用 fetch

async function requestJson(url, options) {
  const response = await fetch(url, options)
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`)
  }
  return response.json()
}

requestJson('/api/user')
  .then(console.log)
  .catch(console.error)

fetch 遇到 HTTP 404/500 时不会自动 reject,必须检查 response.ok;网络错误、超时和取消可以结合 AbortController

const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 5000)

fetch('/api/data', { signal: controller.signal })
  .finally(() => clearTimeout(timer))

需要兼容旧环境或控制上传进度时仍可使用 XMLHttpRequest

function requestText(url) {
  return new Promise((resolve, reject) => {
    const request = new XMLHttpRequest()
    request.open('GET', url)
    request.onload = () => {
      if (request.status >= 200 && request.status < 300) {
        resolve(request.responseText)
      } else {
        reject(new Error(`HTTP ${request.status}`))
      }
    }
    request.onerror = () => reject(new Error('Network error'))
    request.send()
  })
}

18. 实现 trim

现代环境直接使用原生 String.prototype.trim()。不建议覆盖原型,教学时可以写独立函数:

function trim(value) {
  return String(value).replace(/^\s+|\s+$/gu, '')
}

console.log(trim('  hello  ')) // hello
console.log('  hello  '.trim()) // hello

trim() 的空白定义由 ECMAScript 规范决定,不只是 ASCII 空格;正则实现应明确兼容范围。

19. 实现 Object.create

原文版本通过空构造函数模拟原型链接,但会把 constructor 混入原型,而且不支持属性描述符和完整错误边界。有限教学版本可以这样写:

function createWithPrototype(proto) {
  if (proto !== null && typeof proto !== 'object') {
    throw new TypeError('Object prototype may only be an Object or null')
  }

  function Empty() {}
  Empty.prototype = proto
  const object = new Empty()
  // new Empty() 对 null 原型会回退到 Object.prototype,显式设置以覆盖这一点
  return Object.setPrototypeOf(object, proto)
}

const prototype = { kind: 'demo' }
const object = createWithPrototype(prototype)
console.log(Object.getPrototypeOf(object) === prototype) // true

生产代码使用 Object.create(proto, properties)Object.create(null) 常用于无原型字典,但它没有 toStringhasOwnProperty 等继承方法,读取外部键时仍应使用 Object.hasOwn()Object.prototype.hasOwnProperty.call()

20. 限制并发任务数量

任务数组应该是“返回 Promise 或普通值的函数”,而不是已经启动的 Promise;如果 Promise 已经创建,限制器无法阻止它们提前执行。下面的版本处理空数组、同步抛错和 reject:

function limitRunTask(tasks, limit) {
  if (!Number.isInteger(limit) || limit < 1) {
    return Promise.reject(new RangeError('limit must be a positive integer'))
  }

  return new Promise((resolve, reject) => {
    const results = new Array(tasks.length)
    let nextIndex = 0
    let active = 0
    let completed = 0

    if (tasks.length === 0) {
      resolve(results)
      return
    }

    const runNext = () => {
      if (completed === tasks.length) {
        resolve(results)
        return
      }

      while (active < limit && nextIndex < tasks.length) {
        const currentIndex = nextIndex
        const task = tasks[nextIndex]
        nextIndex += 1
        active += 1

        Promise.resolve()
          .then(() => task())
          .then(value => {
            results[currentIndex] = value
            active -= 1
            completed += 1
            runNext()
          }, reject)
      }
    }

    runNext()
  })
}

const tasks = [
  () => new Promise(resolve => setTimeout(() => resolve('A'), 20)),
  () => new Promise(resolve => setTimeout(() => resolve('B'), 5)),
  () => 'C'
]

limitRunTask(tasks, 2).then(console.log) // ['A', 'B', 'C']

如果一个任务失败,当前版本会 reject 外层 Promise;生产实现还可以增加取消、超时、重试和“继续完成其它任务”的策略。

21. 十进制转换为 2~16 进制

原文的循环在输入 0 时返回空字符串,也没有检查进制范围和负数。修正后的整数教学实现:

function convertInteger(number, base = 2) {
  if (!Number.isInteger(number)) {
    throw new TypeError('number must be an integer')
  }
  if (!Number.isInteger(base) || base < 2 || base > 16) {
    throw new RangeError('base must be an integer from 2 to 16')
  }
  if (number === 0) return '0'

  const digits = '0123456789ABCDEF'
  const sign = number < 0 ? '-' : ''
  let value = Math.abs(number)
  let result = ''

  while (value > 0) {
    result = digits[value % base] + result
    value = Math.floor(value / base)
  }

  return sign + result
}

console.log(convertInteger(10, 2)) // 1010
console.log(convertInteger(255, 16)) // FF
console.log(convertInteger(0, 8)) // 0
console.log(convertInteger(-10, 2)) // -1010

如果只需要把安全范围内的整数转成字符串,应直接使用 number.toString(base);超出 Number.MAX_SAFE_INTEGER 时,先使用 BigInt,并注意 BigInt 不能与 Number 直接混合运算:

console.log((255).toString(16)) // ff
console.log((255n).toString(16)) // ff

补充:数字转字符串千分位

原文最后的正则版本只覆盖了部分十进制格式。下面的版本处理负号和小数部分,输入仍应是普通十进制字符串:

function formatThousands(input) {
  const text = String(input)
  const sign = text.startsWith('-') ? '-' : ''
  const unsigned = sign ? text.slice(1) : text
  const [integerPart, fractionPart] = unsigned.split('.')
  const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',')

  return sign + formattedInteger + (
    fractionPart === undefined ? '' : `.${fractionPart}`
  )
}

console.log(formatThousands('1234567.89')) // 1,234,567.89
console.log(formatThousands('-1000000')) // -1,000,000

金额展示还要考虑货币、小数位、舍入、地区和 Intl.NumberFormat

console.log(new Intl.NumberFormat('zh-CN').format(1234567.89)) // 1,234,567.89

总结

手写题真正考查的是边界意识,而不是背诵一段短代码:

  • 事件委托要处理嵌套目标和事件环境;
  • 节流、防抖要说明 leading/trailing、取消和异步请求策略;
  • 去重、Promise.all 和类型判断不能依赖过窄的 instanceof 或字符串键;
  • 深拷贝要考虑循环引用、内置对象和不可克隆值;
  • callbindnewinstanceof 都涉及严格模式、原型和构造语义;
  • 数组方法要处理空数组、稀疏数组和异常;
  • 并发限制器只能限制尚未启动的任务;
  • 浏览器 API 题要标出运行环境,Node.js 代码不能直接当作浏览器代码运行。

参考资料:

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS