「一劳永逸」送你 21 道高频 JavaScript 手写面试题(现代修正版)
本文保留原文的面试题路线和“先写出核心,再讨论边界”的方式。原文来自早期前端面试文章,其中有抓取残留、函数名拼写错误、死循环、
instanceof Promise判断、new返回值判断等问题;下面统一改成可运行的教学版本,并标出浏览器/Node.js 环境边界。手写原生方法适合学习规范和边界,不建议在生产环境覆盖
Array.prototype、Function.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. 实现一个可拖拽的元素
原文使用 mousedown、mousemove、mouseup,但没有处理鼠标移出窗口、触摸屏、释放捕获和元素定位方式。现代浏览器可以使用 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 判断相等:NaN 与 NaN 相同,+0 和 -0 也相同;两个内容相同的对象仍然是不同引用,不会被合并。
4.2 filter 和 indexOf
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. 手写 call 和 apply
两者都改变调用时的 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 还有 length、name、构造函数原型和严格模式等细节;生产代码直接使用 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.all 和 Promise.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) 常用于无原型字典,但它没有 toString、hasOwnProperty 等继承方法,读取外部键时仍应使用 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或字符串键; - 深拷贝要考虑循环引用、内置对象和不可克隆值;
call、bind、new、instanceof都涉及严格模式、原型和构造语义;- 数组方法要处理空数组、稀疏数组和异常;
- 并发限制器只能限制尚未启动的任务;
- 浏览器 API 题要标出运行环境,Node.js 代码不能直接当作浏览器代码运行。
参考资料: