22 道高频 JavaScript 手写题:修正版与现代实现
Category(分类): JavaScript Status: 已更新
手写题适合用来理解语言机制,但示例代码不应直接复制到生产环境。本文保留原文的 22 个练习方向,修正了防抖/节流中箭头函数的 this 错误、EventEmitter 只能保存一个监听器、call 重复执行、深拷贝遗漏循环引用、参数解析未编码、正则过时和 eval 解析 JSON 等问题。
1. 实现防抖(debounce)
防抖表示:连续触发时不断重新计时,停止触发一段时间后执行。下面支持 leading、trailing 和 cancel:
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 还会维护 length、name、构造目标和更多原型细节;面试时重点解释绑定参数与 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 参数
现代实现优先使用 URL 和 URLSearchParams。如果业务约定“没有等号的参数为 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 要支持多监听器和清理;
call、apply、bind、new、instanceof都受原型、严格模式和构造调用影响;eval不是 JSON 解析器,模板渲染也不能执行不可信代码;- URL 参数使用
URLSearchParams,文本输出使用安全转义; - 电话、邮箱和身份证正则只能做格式检查,不能替代服务端验证。