面试-常考经典2
> Last Format Time:6/12/2026 20:34:58
啥样的题目都有哈,没分类。这里是我自己总结的一些题目 🔥为常考,⌚️为我面试的时候考过
异步加法⌚️
实现 sum 函数
// 异步加法
function asyncAdd(a, b, cb) {
setTimeout(() => {
cb(null, a + b)
}, Math.random() * 1000)
}
async function total() {
const res1 = await sum(1, 3, 5, 7, 9, 1, 3, 5, 7, 9);
const res2 = await sum(1, 3, 5, 7, 9, 1, 3, 5, 7, 9);
console.log([res1, res2])
return [res1, res2]
}
total()
// 实现 sum 函数
async function sum(...args) {
// 边界条件:没有参数返回0,只有一个参数直接返回该参数
if (args.length === 0) return 0;
if (args.length === 1) return args[0];
// 边界条件:如果只有两个参数,直接调用 asyncAdd 相加
if (args.length === 2) {
return new Promise((resolve) => {
asyncAdd(args[0], args[1], (_, res) => resolve(res));
});
}
// 二分法:将数组从中间劈开,分成左右两部分
const mid = Math.floor(args.length / 2);
const leftArgs = args.slice(0, mid);
const rightArgs = args.slice(mid);
// 并行计算左右两部分的和(递归调用)
// Promise.all 会让左右两边的计算同时开始,而不是等左边算完再算右边
const [leftSum, rightSum] = await Promise.all([
sum(...leftArgs),
sum(...rightArgs)
]);
// 最后将左右两边的结果相加
return new Promise((resolve) => {
asyncAdd(leftSum, rightSum, (_, res) => resolve(res));
});
}
防抖与节流 ⌚️
- 考察点:性能优化、闭包、定时器。
- 场景:搜索框输入(防抖)、滚动加载/按钮防重复点击(节流)。
- 关键点:
setTimeout的使用、this指向和参数传递。
// 防抖
function debounce(fn, time) {
// 闭包
let timer = null
// 返回一个经过包装的函数
return function () {
// 获取函数的context与参数
let _this = this
let arg = arguments
// 移除定时器,再设置一个新的
if (timer) {
clearTimeout(timer)
timer = null
}
timer = setTimeout(() => {
fn.apply(_this, arg)
// fn.apply(this, arguments) 这样写也是一样的,没有区别
}, time)
}
}
// 节流
function throttle(fn, time) {
let timer = null
return function () {
let _this = this
let arg = arguments
// 如果存在定时器,就什么也不做,定时器会自动的清除自己
if (!timer) {
timer = setTimeout(() => {
fn.apply(_this, arg)
// 时间到了后,将timer清除
// 写 clearTimeout(timer)是一样的,清除后的timer也是null
timer = null
}, time)
}
}
}
深拷贝
- 考察点:递归、引用类型处理、循环引用。
- 场景:复杂对象的数据隔离。
- 关键点:处理数组/对象/日期/正则,以及如何处理循环引用(使用
WeakMap)。
function deepClone(obj, map = new WeakMap()) {
// typeof 是object的只有object与null
if (typeof obj !== 'object' || obj === null) {
return obj
}
if (obj instanceof Date) {
return new Date(obj.getTime())
}
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags)
}
// 如果在缓存中存在拷贝出的对象,说明这里出现了循环引用
if (map.has(obj)) {
return map.get(obj)
}
if (Array.isArray(obj)) {
let newArr = []
// 建立起被拷贝对象与拷贝出的对象之间的映射
map.set(obj, newArr)
obj.forEach((item, index) => {
newArr[index] = deepClone(item, map)
})
return newArr
}
let newObj = {}
map.set(obj, newObj)
Object.keys(obj).forEach((key) => {
newObj[key] = deepClone(obj[key], map)
})
return newObj
}
test = {
a: 1,
b: 2,
c: {
d: 4,
e: 5,
},
}
obj = {
a: 1,
b: test,
c: {
d: 4,
e: 5,
},
}
let newObj = deepClone(obj)
obj.b.a = 100
console.log(newObj, obj)
手写 Promise(promise、all、allSettled、race、any与map)⌚️🔥
- 考察点:异步编程、状态机模式、发布订阅。
- 场景:理解异步控制流。
- 关键点:三种状态(Pending/Fulfilled/Rejected)的转换、
then的链式调用、resolve和reject的处理。
promise
class MyPromise {
constructor(executor) {
this.state = 'pending'
this.value = null
this.reason = null
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfiled'
this.value = value
this.onFulfilledCallbacks.forEach((fn) => fn())
}
}
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = reason
this.onRejectedCallbacks.forEach((fn) => fn())
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
// then 这里的两个函数实际上可以直接填两个函数的名字,其参数会自动在内部被填上
then(onFulfilled, onRejected) {
if (this.state === 'fulfiled') {
onFulfilled(this.value)
} else if (this.state === 'rejected') {
onRejected(this.reason)
} else if (this.state === 'pending') {
this.onFulfilledCallbacks.push(() => onFulfilled(this.value))
this.onRejectedCallbacks.push(() => onRejected(this.reason))
}
}
catch(onRejected) {
return this.then(null, onRejected)
}
finally(onFinally) {
return this.then(
(value) => {
onFinally()
return value
},
(reason) => {
onFinally()
throw reason
},
)
}
}
上面的是不能链式调用的Promise,将其中的成员方法then改造为下面的就可以实现链式调用了:
// 核心改造:then 方法
then(onFulfilled, onRejected) {
// 处理未传入回调的情况 - 需要将函数设为默认或透传
onFulfilled =
typeof onFulfilled === 'function' ? onFulfilled : (value) => value
onRejected =
typeof onRejected === 'function'
? onRejected
: (reason) => {
throw reason
}
// 1. 关键!每次调用 .then() 都必须返回一个新的 Promise
return new ChainablePromise((resolve, reject) => {
// 2. 定义一个函数来处理 onFulfilled 回调的执行和结果
const handleFulfillment = () => {
try {
// 执行用户传入的成功回调
const result = onFulfilled(this.value)
// 检查回调的返回值
if (result instanceof ChainablePromise) {
// 如果返回的是一个 Promise,就等待它完成
result.then(resolve, reject)
} else {
// 如果返回的是普通值,就用这个值 resolve 新的 Promise
resolve(result)
}
} catch (error) {
// 如果回调函数内部抛出异常,则 reject 新的 Promise
reject(error)
}
}
// 3. 定义一个函数来处理 onRejected 回调,和上面的逻辑很像
const handleRejection = () => {
try {
const result = onRejected(this.reason)
if (result instanceof ChainablePromise) {
result.then(resolve, reject)
} else {
resolve(result)
}
} catch (error) {
reject(error)
}
}
// 4. 根据当前 Promise 的状态,决定何时执行回调
if (this.state === 'fulfilled') {
handleFulfillment()
}
if (this.state === 'rejected') {
handleRejection()
}
if (this.state === 'pending') {
// 如果还是 pending 状态,就将回调函数存入队列,等待未来执行
this.onFulfilledCallbacks.push(handleFulfillment)
this.onRejectedCallbacks.push(handleRejection)
}
})
}
all
// 这是我面试的时候写的,没有返回一个Promise,没有提供resolve与reject
// 另外这里也不是写在原型上的,而是挂载在构造函数上
Promise.prototype.myAll=function(promises){
let n = promises.length
let settledCount =0
// 这里也是后加上去的
let res = []
promises.forEach((promise,index)=>{
Promise.resolve(promise).then((value)=>{
settledCount++
if(settledCount===n){
res[index] = value
resolve(res)
}
}).catch(reject(
new Error('error!!!')
))
})
}
- 挂载在构造函数上 :不需要创建实例,直接用 Promise.myAll() 调用,类似 Promise.all() 和 Promise.race() 这种静态方法
- 挂载在原型上 :需要先 new Promise() 创建实例,然后在实例上调用 当前代码挂载在构造函数上是正确的做法,因为 Promise.myAll 的语义和原生的 Promise.all() 一致,都是直接传入一组 promise,而不是在某个 promise 实例上调用。
Promise.myAll = (promises) => {
return new Promise((resolve, reject) => {
let total = promises.length
let results = []
let finishedCount = 0
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then((value) => {
results[index] = value
finishedCount++
if (finishedCount === total) {
resolve(results)
}
})
.catch(reject)
})
})
}
const promises = [
Promise.resolve('成功1'),
// Promise.reject('错误'),
Promise.resolve('成功2'),
new Promise((resolve) => setTimeout(() => resolve('延时成功'), 2000)),
]
Promise.myAll(promises)
.then((values) => {
console.log(values)
})
.catch((error) => {
console.log(error)
})
// [ '成功1', '成功2', '延时成功' ]
allSettled
和all很像,不过是将处理返回结果的代码移动到了finally中
Promise.myAllSettled = function (promises) {
return new Promise((resolve, reject) => {
const results = new Array(promises.length)
let settledCount = 0
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then((value) => {
results[index] = { status: 'fulfilled', value }
})
.catch((reason) => {
results[index] = { status: 'rejected', reason }
})
.finally(() => {
settledCount++
if (settledCount === promises.length) {
resolve(results)
}
})
})
})
}
const promises = [
Promise.resolve('成功1'),
Promise.reject('错误'),
Promise.resolve('成功2'),
new Promise((resolve) => setTimeout(() => resolve('延时成功'), 2000)),
]
Promise.MyAllSettled(promises).then((results) => {
console.log(results)
})
// [
// ({ status: 'fulfilled', value: '成功1' },
// { status: 'rejected', reason: '错误' },
// { status: 'fulfilled', value: '成功2' },
// { status: 'fulfilled', value: '延时成功' })
// ]
race
Promise.myRace = function(promises) {
return new Promise((resolve, reject) => {
// 1. 遍历所有 Promise
promises.forEach(promise => {
// 2. 使用 Promise.resolve 包装,确保处理非 Promise 值
Promise.resolve(promise)
.then(resolve) // 3. 任何一个 Promise 成功,立即 resolve
.catch(reject); // 4. 任何一个 Promise 失败,立即 reject
});
});
};
const promises = [
// Promise.resolve('成功1'),
Promise.reject('错误'),
// Promise.resolve('成功2'),
new Promise((resolve) => setTimeout(() => resolve('延时成功'), 2000)),
]
Promise.myRace(promises)
.then((result) => {
console.log(result)
})
.catch((error) => {
console.log(error)
})
// 错误
any
和all很像
Promise.myAny = function(promises) {
return new Promise((resolve, reject) => {
const errors = []; // 1. 收集所有失败的原因
let rejectedCount = 0; // 2. 计数器,记录已失败的 Promise 数量
promises.forEach(promise => {
Promise.resolve(promise)
.then(resolve) // 3. 任何一个 Promise 成功,立即 resolve
.catch(error => {
// 4. 记录失败原因
errors.push(error);
rejectedCount++;
// 5. 只有当所有 Promise 都失败时,才 reject
if (rejectedCount === promises.length) {
// AggregateError 是 ES2021 内置的错误类型
reject(new AggregateError(errors, 'All promises were rejected'));
}
});
});
});
};
map
实际上是带有并发限制的请求:
/**
* 实现一个带并发限制的 Promise.map
* @param {Array} list - 需要处理的数组
* @param {Function} iteratee - 映射函数
* @param {Object} options - 选项,例如 { concurrency: 2 } 表示最多同时执行2个
* @returns {Promise}
*/
Promise.myMapWithConcurrency = function(list, iteratee, options = {}) {
const concurrency = options.concurrency || Infinity; // 默认无限制
const results = new Array(list.length);
let nextIndexToProcess = 0; // 下一个要处理的任务索引
let completedCount = 0;
return new Promise((resolve, reject) => {
// 启动第一批任务,数量不超过并发限制
function startNext() {
// 这里会跑满
while (nextIndexToProcess < list.length && activeCount < concurrency) {
const index = nextIndexToProcess++;
activeCount++;
// 执行任务
Promise.resolve(iteratee(list[index], index))
.then(value => {
results[index] = value;
completedCount++;
activeCount--;
if (completedCount === list.length) {
resolve(results);
}
startNext(); // 当前任务完成,尝试启动下一个,第一批任务完成一个,就空了一个,就再调一个
})
.catch(reject); // 任何一个任务失败,整个 Promise.map 失败
}
}
let activeCount = 0;
// 开始执行
startNext();
});
};
数组扁平化
- 考察点:数组操作、递归、迭代。
- 场景:处理树形结构数据(如菜单、目录)。
- 关键点:
flat()的模拟实现、reduce+concat、正则方法。
Array.prototype.myFlat = function (depth = 1) {
// console.log(this) ← 普通函数的 this 指向调用者(即数组本身)我最开始是使用的箭头函数,由于箭头函数没有this,会有问题
if (depth === 0) return this
return this.reduce((acc, cur) => {
if (Array.isArray(cur)) {
return acc.concat(cur.myFlat(depth - 1))
} else {
// 注意concat只能“去皮”一层数组
// 在直接concat原始值的时候,就会直接加入数组
return acc.concat(cur)
}
}, [])
}
console.log(arr.myFlat(1))
// [ 1, 3, 4, 6, 1, 3, [ 1, 2 ] ]
🔥函数柯里化
- 考察点:闭包、函数式编程思想。
- 场景:参数复用、延迟执行。
- 关键点:判断参数个数是否满足,返回新函数继续接收参数。
核心思想:将一个多参数函数转换为一系列单参数函数的链式调用 这这这,能考吗?
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args)
}
return function (nextArgs) {
return curried.apply(this, args.concat(nextArgs))
}
}
}
const fn = (a, b) => {
console.log(a, b)
}
// 这里的1是第一次返回的curried的...args收集进去的,2是第二次返回的匿名函数的nextArgs收集进去的
let curriedFn = curry(fn)(1)(2)
// 1 2
工作流程:
curried(1)- 收集到 1 个参数,不足 3 个,返回新函数- 新函数被调用时
curried(1)(2),继续收集得到[1, 2] - 最终
curried(1)(2)(3)- 参数达到 3 个,调用原函数
手写 call / apply / bind
- 考察点:
this指向改变、函数执行上下文。 - 场景:借用其他对象的方法。
- 关键点:将函数绑定到指定对象上执行,处理参数列表,
bind需要返回一个函数。
// ...args 将参数收集到了args中
Function.prototype.myCall = function (thisArg, ...args) {
thisArg = thisArg || window
// 将被call的函数(this)绑定到thisArg上,这样在调用fn时,this指向thisArg
thisArg.fn = this
let res = thisArg.fn(args)
delete thisArg.fn
return res
}
// apply的实现和这个几乎一样,就是参数不同,apply接收一个数组,call接收多个参数
Function.prototype.myApply = function (thisArg, args) {
thisArg = thisArg || window
// 借助临时的属性fn,将被apply的函数(this)绑定到thisArg上,这样在调用fn时,this指向thisArg
thisArg.fn = this
let res = thisArg.fn(args)
delete thisArg.fn
return res
}
// 返回一个被call过的函数就可以了
Function.prototype.myBind = function (thisArg, ...args) {
let fn = this
// 这里会返回一个新的函数,如果在bind的时候args没有传,可以在调用新函数时传
return function (...rest) {
// 如果在args的位置传了值,在调用新函数时可以不用传了
fn.call(thisArg, ...args, ...rest)
}
}
事件发布订阅模式
- 考察点:设计模式、解耦。
- 场景:Vue/React 的底层原理、组件通信。
- 关键点:维护一个事件中心(对象),包含
on,emit,off方法。
class EventBus {
constructor() {
// 创建一个空对象,用于存储事件监听函数
this.events = {}
}
on(event, callback) {
// 如果事件不存在,创建一个空数组
if (!this.events[event]) {
this.events[event] = []
}
// 如果存在存储事件的数组,就将回调函数添加到数组中
this.events[event].push(callback)
}
off(event, callback) {
// 如果事件不存在,直接返回
if (!this.events[event]) return
// 如果没有传入回调函数,就删除事件监听函数
if (!callback) {
delete this.events[event]
return
}
// 过滤,将传入的回调函数从数组中移除
this.events[event] =
this.events[event].filter((cb) => cb !== callback)
}
emit(event, ...args) {
// 如果事件不存在,直接返回
if (!this.events[event]) return
// 遍历事件监听函数数组,调用每个回调函数
this.events[event].forEach((callback) => callback(...args))
}
once(event, callback) {
const wrapper = (...args) => {
callback(...args)
this.off(event, wrapper)
}
// 订阅了事件,将处理的函数中添加了off方法,用于移除事件监听函数
this.on(event, wrapper)
}
}
const bus = new EventBus()
function handleUserClick(data) {
console.log('用户点击:', data)
}
function handleUserLogin(data) {
console.log('用户登录:', data)
}
function handleUserLoginOnce(data) {
console.log('仅触发一次:', data)
}
bus.on('click', handleUserClick)
bus.on('login', handleUserLogin)
bus.once('login', handleUserLoginOnce)
//
// EventBus {
// events: {
// click: [ [Function: handleUserClick] ],
// login: [ [Function: handleUserLogin], [Function: wrapper] ]
// }
// }
console.log('=== 触发 click 事件 ===')
bus.emit('click', { x: 100, y: 200 })
bus.emit('click', { x: 150, y: 250 })
// === 触发 click 事件 ===
// 用户点击: { x: 100, y: 200 }
// 用户点击: { x: 150, y: 250 }
console.log('\n=== 触发 login 事件 ===')
bus.emit('login', { username: '张三', time: '2024-01-01' })
bus.emit('login', { username: '李四', time: '2024-01-02' })
// === 触发 login 事件 ===
// 用户登录: { username: '张三', time: '2024-01-01' }
// 仅触发一次: { username: '张三', time: '2024-01-01' }
// 用户登录: { username: '李四', time: '2024-01-02' }
console.log('\n=== 移除 click 监听 ===')
bus.off('click', handleUserClick)
bus.emit('click', { x: 300, y: 300 })
console.log('\n=== 移除所有 login 监听 ===')
bus.off('login')
bus.emit('login', { username: '王五', time: '2024-01-03' })
数组去重
- 考察点:ES6 新特性、数据结构。
- 场景:数据处理。
- 关键点:
Set的使用、filter、reduce、Map、includes
const arr = [1, 2, 2, 3, 3, 3, 4, 4, 5, '1', '2', 1, 2]
function unique1(arr) {
// 将数组转为set,再展开,优雅实在是优雅
return [...new Set(arr)]
// return Array.from(new Set(arr)) 也是一样的
}
function unique2(arr) {
const result = []
for (const item of arr) {
if (!result.includes(item)) {
result.push(item)
}
}
return result
}
// 就是换了一个存储的数据结构,本质与上面的没有什么区别
function unique3(arr) {
const map = new Map()
for (const item of arr) {
if (!map.has(item)) {
map.set(item, item)
}
}
return [...map.values()]
}
function unique4(arr) {
return arr.reduce((acc, item) => {
if (!acc.includes(item)) {
acc.push(item)
}
return acc
}, [])
}
console.log('原始数组:', arr)
console.log('\n方法1 - Set:', unique1(arr))
console.log('方法2 - includes:', unique2(arr))
console.log('方法3 - Map:', unique3(arr))
console.log('方法4 - reduce:', unique4(arr))
手写 new 操作符
考察点:原型链、构造函数机制。
场景:理解对象创建过程。
关键点:创建一个空对象、链接原型、绑定
this、判断返回值。创建空对象
将对象的隐式原型链接到函数的原型上
将空对象作为调用者,调用构造函数
判断构造出来的结果,根据结果的类型返回值
function myNew(constructor, ...args) {
const obj = {}
// 使用 Object.setPrototypeOf() 方法设置原型链。
// 将obj的__proto__设置为constructor
Object.setPrototypeOf(obj, constructor.prototype)
// 将构造函数的 this 指向设置为 obj,等于obj.constructor(args)
// 也就是初始化一个空的对象
const result = constructor.apply(obj, args)
return result instanceof Object ? result : obj
}
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHello = function () {
return `你好,我是${this.name},今年${this.age}岁`
}
function Animal(type, sound) {
this.type = type
this.sound = sound
return { custom: 'object' }
}
const p1 = new Person('张三', 25)
const p2 = myNew(Person, '李四', 30)
console.log('=== 使用 new ===')
console.log('p1:', p1)
console.log('p1.sayHello():', p1.sayHello())
// === 使用 new ===
// p1: Person { name: '张三', age: 25 }
// p1.sayHello(): 你好,我是张三,今年25岁
console.log('\n=== 使用 myNew ===')
console.log('p2:', p2)
console.log('p2.sayHello():', p2.sayHello())
// === 使用 myNew ===
// p2: { custom: 'object' }
// p2.sayHello(): 你好,我是李四,今年30岁
console.log('\n=== 构造函数返回对象的情况 ===')
const a1 = new Animal('狗', '汪汪')
const a2 = myNew(Animal, '猫', '喵喵')
console.log('a1:', a1)
console.log('a2:', a2)
// === 构造函数返回对象的情况 ===
// a1: { custom: 'object' }
// a2: { custom: 'object' }
console.log('\n=== 原型链验证 ===')
console.log('p2 instanceof Person:', p2 instanceof Person)
console.log(
'Object.getPrototypeOf(p2) === Person.prototype:',
Object.getPrototypeOf(p2) === Person.prototype,
)
// === 原型链验证 ===
// p2 instanceof Person: true
// Object.getPrototypeOf(p2) === Person.prototype: true
配置项深度合并
在插件或组件库开发中,需将用户配置与默认配置进行深度合并,确保嵌套属性也被正确覆盖。
function deepMerge(target, source) {
for (let key in source) {
if (source[key] instanceof Object && key in target) {
Object.assign(source[key], deepMerge(target[key], source[key]));
}
}
return Object.assign(target || {}, source);
}
sleep/delay
基于promise实现的
const sleep = (time) => {
return new Promise((resolve) => {
setTimeout(resolve(`sleep: ${time}ms`), time);
});
};
sleep(23).then((message) => {
console.log(message); // sleep: 23ms
})
isArray
// [补充说明]:兼容的数组检测方法
function myIsArray(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
}
手写reduce
可以看另一个的实现,更加的简单 关键考点解析:
- 初始值处理:必须判断
initialValue是否为undefined,而非简单使用== null,以区分null和undefined。 - 空数组防御:当数组为空且无初始值时,应抛出类型错误,模拟原生行为。
- 稀疏数组兼容:使用
i in this检查索引是否存在,确保跳过空位,符合规范要求。
Array.prototype.fakeReduce =function(callback, init) {
const arr = this
function next(acc, index) {
if (index >= arr.length) return acc
let result = callback(acc, arr[index], index, arr)
return next(result, index + 1)
}
let res
if (init !== undefined) {
res = next(init, 0)
} else {
res = next(arr[0], 1)
}
return res
}
let arr = [1, 3, 4, 5]
console.log(arr.fakeReduce((sum, item) => sum + item))
// 13
Array.prototype.myReduce = function (callback, init) {
let arr = this
function next(res, index) {
if (index >= arr.length) return res
return next(callback(res, arr[index], index, arr), index + 1)
}
if (init === undefined) {
return next(arr[0], 1)
} else {
return next(init, 0)
}
}
let arr = [1, 3, 2]
console.log(
arr.myReduce((sum, item) =>
sum + item
,0),
)