Handing 手写类
实现一个 instanceof
function myInstanceof(left, right) {
let proto = Object.getPrototypeOf(left)
while (true) {
if (proto === null) return false;
if (proto === right.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
}
实现一个 call 函数
- 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况;
- 判断传入上下文对象是否存在,如果不存在,则设置为 window;
- 处理传入的参数,截取第一个参数后的所有参数;
- 将函数作为上下文对象的一个属性;
- 使用上下文对象来调用这个方法,并保存返回结果;
- 删除刚才新增的属性;
- 返回结果。
Function.prototype.myCall = function (context, ...args) {
if (typeof this !== 'function') {
throw new TypeError('Error');
}
context = context || window;
context.fn = this;
const result = context.fn(...args);
delete context.fn;
return result;
}
实现一个 apply 函数
- 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况;
- 判断传入上下文对象是否存在,如果不存在,则设置为 window;
- 将函数作为上下文对象的一个属性;
- 判断参数值是否传入;
- 使用上下文对象来调用这个方法,并保存返回结果;
- 删除刚才新增的属性;
- 返回结果。
Function.prototype.myApply = function (context, arr) {
if (typeof this !== 'function') {
throw new TypeError('Error');
}
context = context || window;
context.fn = this;
let result;
if (!arr) {
result = context.fn();
} else {
result = context.fn(...arr);
}
delete context.fn;
return result;
}
⭐ 实现一个 bind 函数
- 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况;
- 保存当前函数的引用,获取其余传入参数值;
- 创建一个函数返回;
- 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
Function.prototype.myBind = function (context, ...args) {
if (typeof this !== 'function') {
throw new TypeError('Error');
}
const fn = this;
return function result(...innerArgs) {
return fn.apply(
this instanceof result ? this : context,
args.concat(...innerArgs)
);
}
}
⭐ 实现一个 new 函数
function _new(obj, ...rest) {
const newObj = Object.create(obj.prototype);
const result = obj.apply(newObj, rest);
return result instanceof Object ? result : newObj;
}
实现 Object.create() 函数
一句口述:用空构造函数桥接原型链——Fn.prototype = obj 后 new Fn(),第二参数用 defineProperties 挂;与 new 的差别是它不执行构造函数。这题 2026 基本不考手写,讲清原理即可。
实现 Object.is() 函数
一句口述:就是 === 加两个边界修正——+0 和 -0 判不相等(用 1 / x === 1 / y 比较),两个 NaN 判相等(x !== x && y !== y)。不需背手写。
⭐ 实现数组的 flat 方法
function myFlat(arr, depth = 1) {
if (!Array.isArray(arr) || depth <= 0) {
return arr;
}
let result = [];
arr.forEach(item => {
result = result.concat(myFlat(item, depth - 1));
});
return result;
}
实现数组的 filter 方法
Array.prototype.myFilter = function (callback, thisArg) {
if (typeof callback !== 'function') {
throw new TypeError('callback type error');
}
const result = [];
this.forEach((item, index) => {
if (callback.call(thisArg, item, index, this)) {
result.push(item);
}
});
return result;
}
实现数组的 map 方法
Array.prototype.myMap = function (callback, context) {
if (typeof callback !== 'function') {
throw new TypeError('callback type error');
}
const result = [];
this.forEach((item, index) => {
result.push(callback.call(context, item, index, this));
});
return result;
};
CSS 实现超出一行显示省略号
一句口述:三件套 overflow: hidden + white-space: nowrap + text-overflow: ellipsis,现场可写,不需背。
将对象的 key 值作为数组返回
一句:Object.keys(obj) 一行(ES5 时代用 for in 收集),现场可写,不需背。
将对象的 value 值作为数组返回
一句:Object.values(obj) 一行,现场可写,不需背。
判断对象是否为空
function judge(obj) {
for (let i in obj) {
return false;
}
return true;
}
⭐ 实现深拷贝函数
function deepCopy(obj) {
if (typeof obj !== "object" || obj === null) {
return obj;
}
let result;
if (obj instanceof Array) {
result = [];
} else {
result = {};
}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepCopy(obj[key]);
}
}
return result;
}
实现构造函数的继承
function City() {
this.city = 'Guangzhou'
}
function District(name) {
this.name = name
City.call(this)
}
const district1 = new District('Haizhu') // { name: 'Haizhu', city: 'Guangzhou' }
实现原型链的继承
function City() {
this.city = 'Guangzhou'
}
function District(name) {
this.name = name
}
District.prototype = new City()
const district1 = new District('Haizhu') // 自身 { name: 'Haizhu' },city 挂在原型上
实现 ES6 class 的继承
class City {
constructor(name) {
this.city = 'Guangzhou'
this.district = name
}
}
class District extends City {
constructor(name) {
super(name);
}
}
const district1 = new District("Haizhu") // { city: 'Guangzhou', district: 'Haizhu' }
实现 JavaScript 中的异常捕获
一句口述:try / catch / finally,finally 放无论成败都要执行的收尾逻辑;项目级的错误捕获走错误边界 + 前端监控那套,这题不会单独考。
⭐ 实现函数柯里化
function curry(fn, ...args) {
if (args.length >= fn.length) {
return fn(...args)
}
return (...args2) => curry(fn, ...args, ...args2);
}
const add = (x, y) => {
console.log(x + y)
}
const curriedAdd = curry(add)
curriedAdd(1)(2) // 3
curriedAdd(1, 2) // 3
常见的正则表达式类问题
- 题目
- 答案
// 手写手机号正则表达式
// 手写邮箱正则表达式
// 手写手机号正则表达式
/^1\d{10}$/
// 手写邮箱正则表达式
/^\w+@\w+\.[a-zA-Z]{2,3}(\.[a-zA-Z]{2,3})?$/
实现获取 URL 中参数
const getParam = key => {
const params = new URLSearchParams(window.location.search);
return params.get(key)
}
⭐ 模拟实现 Promise
function MyPromise(fn) {
this.state = "pending";
this.value = null;
this.resolveFns = [];
this.rejectFns = [];
const self = this;
function resolve(value) {
if (value instanceof MyPromise) {
return value.then(resolve, reject);
}
queueMicrotask(() => {
if (self.state === "pending") {
self.state = "fulfilled";
self.value = value;
self.resolveFns.forEach((callback) => callback(value));
}
});
}
function reject(reason) {
queueMicrotask(() => {
if (self.state === "pending") {
self.state = "rejected";
self.value = reason;
self.rejectFns.forEach((callback) => callback(reason));
}
});
}
try {
fn(resolve, reject);
} catch (e) {
reject(e);
}
}
MyPromise.prototype.then = function (onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) => v;
onRejected = typeof onRejected === "function" ? onRejected : (e) => { throw e; };
if (this.state === "pending") {
this.resolveFns.push(onFulfilled);
this.rejectFns.push(onRejected);
} else if (this.state === "fulfilled") {
queueMicrotask(() => onFulfilled(this.value));
} else {
queueMicrotask(() => onRejected(this.value));
}
};
封装 React 受控组件
- 受控钩子的使用
- 受控钩子的封装
import React from 'react';
import useDefault from '../hooks/useDefault';
const Switch = (props) => {
const {
value,
defaultValue = false,
onChange
} = props;
const [innerValue, setInnerValue] = useDefault(value, defaultValue, onChange);
const handleSwitch = () => {
// 注意这里不需要额外调用 onChange,钩子函数中已经做了处理
setInnerValue(!innerValue)
}
return (
<button onClick={handleSwitch}>
{innerValue ? '开' : '关'}
</button>
);
};
import { useState } from 'react';
export default function useDefault(value, defaultValue, onChange) {
const [internalValue, setInternalValue] = useState(defaultValue);
const defaultFn = () => {};
// 受控模式
if (typeof value !== 'undefined') {
return [value, onChange || defaultFn];
}
// 非受控模式
return [internalValue, (newValue, ...args) => {
setInternalValue(newValue);
if (typeof onChange === 'function') {
onChange(newValue, ...args);
}
}];
}
⭐ 实现防抖和节流函数
- 防抖函数
- 节流函数
function debounce(fn, wait) {
let timer = null;
return function () {
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(() => {
fn.apply(this, arguments)
}, wait);
}
}
// 时间戳实现
function throttled(fn, delay = 500) {
let oldTime = Date.now()
return function () {
const newTime = Date.now()
if (newTime - oldTime >= delay) {
fn.apply(this, arguments)
oldTime = Date.now()
}
}
}
// 定时器实现
function throttled(fn, delay = 500) {
let timer = null
return function () {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, delay);
}
}
}
⭐ 手写 useDebounce 防抖 Hook
import { useEffect, useState } from 'react';
function useDebounce(value, wait = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), wait);
return () => clearTimeout(timer);
}, [value, wait]);
return debounced;
}
防抖「值」用这个:等输入停下来再发请求;要防抖「回调」就再包一层用 ref 持住最新函数的 useDebounceFn。经典错误是直接在 JSX 里写 onChange={debounce(fn, 300)}——每次渲染都新建一个防抖实例,闭包里的 timer 也是新的,等于完全没防抖;实例要用 useRef 或 useMemo 稳住,卸载时再 cancel。
Ajax 的使用(现在写 fetch 版本)
XHR 手写已不考,现场直接写 fetch:fetch(url, { method, headers, signal }) 返回 Promise,用 response.json() 取数据;需要取消就 new AbortController(),把 signal 传进去,调用 abort() 中断请求。
const controller = new AbortController();
const res = await fetch(url, { method: "GET", signal: controller.signal });
const data = await res.json();
// 取消:controller.abort()
设计模式在项目里用过哪两个(单例/观察者)
我简历里最常聊的是单例和观察者。单例保证全局只有一个实例,我用它做过全局的请求缓存层;观察者(发布订阅)我在小程序里自己实现过事件总线,跨页面传事件。其余模式不用背手写,能说清思想就行:工厂是把创建逻辑收口(按类型参数返回不同实例)、适配器是封装第三方 SDK 统一对外接口、装饰器是不改原对象动态加能力(React HOC 就是)、代理是套一层拦截(Vue3 的 Proxy)。
// 单例:全局唯一实例
const Singleton = (function () {
let instance = null;
return {
getInstance: function () {
if (!instance) {
instance = new Benz();
}
return instance;
},
};
})();
const benz1 = Singleton.getInstance()
const benz2 = Singleton.getInstance()
console.log(benz1 === benz2) // true
// 观察者:被观察者维护订阅列表,变更时广播
class Subject {
constructor() {
this.observers = [];
}
attach(observer) {
this.observers.push(observer);
}
notify() {
this.observers.forEach((o) => o.update());
}
}
class Observer {
update() {
console.log('通知:被观察者已更新');
}
}
const subject = new Subject();
const o1 = new Observer();
const o2 = new Observer();
subject.attach(o1);
subject.attach(o2);
subject.notify(); // 通知:被观察者已更新 × 2
实现冒泡排序算法
一句话口径:相邻元素两两比较交换,一轮把最大值冒到末尾;加 swapped 标记可提前退出,最好 O(n)、最坏 O(n²),稳定。
实现选择排序算法
一句话口径:每轮从未排序部分选出最小值换到左边,恒为 O(n²);交换会打乱相同值的相对位置,不稳定。
实现插入排序算法
一句话口径:把数组看成已排序和未排序两段,每次从未排序段取一个元素插入已排序段的正确位置;接近有序时趋近 O(n),最坏 O(n²),稳定。
实现归并排序算法
const mergeSort = arr => {
if (arr.length <= 1) {
return arr;
}
const mid = Math.floor(arr.length / 2);
const left = arr.slice(0, mid);
const right = arr.slice(mid);
// 合并成一个有序数组
const merge = (left, right) => {
const result = [];
// 将左右子数组较小的元素放入 result,直到有一个子数组遍历完毕
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
// 此时将另一个子数组剩余的元素放入 result
return result.concat(left).concat(right);
}
return merge(mergeSort(left), mergeSort(right));
}
const arr = [6, 5, 3, 4, 2, 1];
console.log(mergeSort(arr)); // [1, 2, 3, 4, 5, 6]
实现快速排序算法
const quickSort = (arr) => {
const len = arr.length;
if (len < 2) {
return arr;
} else {
const flag = arr[0];
const left = [];
const right = [];
for (let i = 1; i < len; i++) {
const temp = arr[i];
if (temp < flag) {
left.push(temp);
} else {
right.push(temp);
}
}
return quickSort(left).concat(flag, quickSort(right));
}
};
const arr = [6, 5, 3, 4, 2, 1];
console.log(quickSort(arr)); // [1, 2, 3, 4, 5, 6]
实现数组随机排序(Fisher-Yates)
sort(() => Math.random() - 0.5) 写法简单但有偏差且不稳定;面试加分写法是从后往前遍历,每次随机挑一个前面的元素交换。
const shuffle = (arr) => {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
实现数组元素求和
一句:arr.reduce((t, i) => t + i) 一行,现场可写,不需背。
实现数组扁平化
const flatten = (arr) => {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr)
}
return arr
}
实现数组去重(越多越好)
嵌套循环用 ===,去不掉 NaN;Set 能去 NaN,但对对象仍按引用;用普通对象当哈希表时 key 会转成字符串,1 和 '1' 会撞车,不同对象都会变成 '[object Object]'。
// 嵌套循环的方式
const unique = (arr) => {
for (let i = 0, len = arr.length; i < len; i++) {
for (let j = i + 1; j < len; j++) {
if (arr[i] === arr[j]) {
// 如果有重复,就从原数组中删去重复的元素,并修改 len 和 j 的值
arr.splice(j, 1);
len--;
j--;
}
}
}
return arr;
}
// ES6 Set
const unique = (arr) => [...new Set(arr)]
// 对象的方式:key 会转成字符串,1 和 '1' 会冲突
const unique = (arr) => {
const result = [];
const obj = {}; // 用来去重的对象
for (let i = 0; i < arr.length; i++) {
if (!obj[arr[i]]) {
result.push(arr[i]);
obj[arr[i]] = 'x';
}
}
return result;
}
数组从小到大排序(sort 一句话)
排序数组直接 arr.sort((a, b) => a - b) 升序、arr.sort((a, b) => b - a) 降序;注意 sort 默认按字符串字典序排,数字必须传比较函数。要讲复杂度或稳定性再说归并/快排。
实现二分搜索算法 (arr, num) => index
const binarySearch = (arr, num) => {
arr.sort((a, b) => a - b)
let left = 0
let right = arr.length - 1
while (left < right) {
const mid = Math.floor((left + right) / 2)
if (arr[mid] < num) {
left = mid + 1
} else if (arr[mid] > num) {
right = mid - 1
} else {
return mid
}
}
return -1
}
const arr = [6, 5, 3, 4, 2, 1];
binarySearch(arr, 3) // 2
实现 add(1)(2)(3)
// 柯里化函数
function curry(fn, ...args) {
// 当接收的参数数量与原函数形参数量一致时,执行原函数
if (args.length >= fn.length) {
return fn(...args)
}
// 否则递归返回一个 curry 函数接收剩余的参数
return (...args2) => curry(fn, ...args, ...args2);
}
// 使用
const add = (x, y, z) => {
return x + y + z
}
const curriedAdd = curry(add)
curriedAdd(1)(2)(3) // 6
curriedAdd(1, 2)(3) // 6
curriedAdd(1, 2, 3) // 6
循环打印红绿黄问题
- 题目
- 使用递归实现
- 使用 Promise 实现
- 使用 async/await 实现
// 红灯 3s 亮一次,绿灯 2s 亮一次,黄灯 1s 亮一次;如何让三个灯不断交替重复亮灯?
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
// 红灯 3s 亮一次,绿灯 2s 亮一次,黄灯 1s 亮一次;如何让三个灯不断交替重复亮灯?
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
const task = (timer, light, callback) => {
setTimeout(() => {
if (light === 'red') {
red()
} else if (light === 'green') {
green()
} else if (light === 'yellow') {
yellow()
}
callback()
}, timer)
}
const step = () => {
task(3000, 'red', () => {
task(2000, 'green', () => {
task(1000, 'yellow', step)
})
})
}
step()
// 红灯 3s 亮一次,绿灯 2s 亮一次,黄灯 1s 亮一次;如何让三个灯不断交替重复亮灯?
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
const task = (timer, light) => new Promise((resolve, reject) => {
setTimeout(() => {
if (light === 'red') {
red()
} else if (light === 'green') {
green()
} else if (light === 'yellow') {
yellow()
}
resolve()
}, timer)
})
const step = () => {
task(3000, 'red')
.then(() => task(2000, 'green'))
.then(() => task(1000, 'yellow'))
.then(step)
}
step()
// 红灯 3s 亮一次,绿灯 2s 亮一次,黄灯 1s 亮一次;如何让三个灯不断交替重复亮灯?
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
const task = (timer, light) =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (light === 'red') {
red()
} else if (light === 'green') {
green()
} else if (light === 'yellow') {
yellow()
}
resolve()
}, timer)
})
const taskRunner = async () => {
await task(3000, 'red')
await task(2000, 'green')
await task(1000, 'yellow')
taskRunner()
}
taskRunner()
⭐ 实现一个并发控制的请求池
面试关注点:限制最大并发数;结果顺序要按输入顺序保留(用结果数组按下标填);单个失败要能 reject;空数组要能正常 resolve。
function limitConcurrency(tasks, limit) {
return new Promise((resolve, reject) => {
let index = 0, active = 0, done = 0;
const results = new Array(tasks.length);
function runNext() {
if (done === tasks.length) return resolve(results);
while (active < limit && index < tasks.length) {
const cur = index++;
active++;
Promise.resolve()
.then(() => tasks[cur]())
.then((res) => { results[cur] = res; },
(err) => { reject(err); })
.finally(() => { active--; done++; runNext(); });
}
}
runNext();
});
}
实现洋葱模型的 compose(Koa 中间件)
面试关注点:核心是捕获索引的递归闭包;await next() 之前是进入阶段、之后是离开阶段,这是「洋葱」的由来;next() 调用两次要报错;前面中间件不调 next,后面的就不会执行。
function compose(middlewares) {
return function (ctx) {
let lastIndex = -1;
function dispatch(i) {
if (i <= lastIndex) return Promise.reject(new Error('next() called multiple times'));
lastIndex = i;
const fn = middlewares[i];
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
} catch (err) { return Promise.reject(err); }
}
return dispatch(0);
};
}
⭐ 实现 LRU 缓存
面试关注点:用 Map 的插入顺序当链表,不用手写双向链表;get 命中要删除重插(移到队尾);容量满时删 Map 的第一个 key(最久未使用);重复 put 同 key 只更新不扩容。
class LRUCache {
constructor(capacity) { this.capacity = capacity; this.cache = new Map(); }
get(key) {
if (!this.cache.has(key)) return -1;
const value = this.cache.get(key);
this.cache.delete(key); // 命中后挪到队尾 = 最近使用
this.cache.set(key, value);
return value;
}
put(key, value) {
if (this.cache.has(key)) this.cache.delete(key);
else if (this.cache.size >= this.capacity) {
this.cache.delete(this.cache.keys().next().value); // Map 的首个 key 即最久未用
}
this.cache.set(key, value);
}
}
⭐ 实现请求重试(指数退避 + 抖动 + 可取消)
面试关注点:退避公式 base * 2 ** attempt 并封顶;加随机抖动避免服务恢复时所有客户端同时重连造成雪崩;支持 AbortSignal 取消;shouldRetry 区分 4xx(不重试)和 5xx/网络错误(重试)。
function sleepWithSignal(ms, signal) {
return new Promise((resolve, reject) => {
const timer = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => { clearTimeout(timer); reject(signal.reason); }, { once: true });
});
}
const fullJitter = (base, attempt, max) => Math.min(max, base * 2 ** attempt) * Math.random();
async function retry(task, options = {}) {
const { retries = 3, baseDelay = 300, maxDelay = 5000, signal, shouldRetry = () => true } = options;
let lastError;
for (let attempt = 0; attempt <= retries; attempt++) {
if (signal?.aborted) throw signal.reason;
try { return await task(attempt); }
catch (err) {
lastError = err;
if (attempt === retries || !shouldRetry(err)) break;
await sleepWithSignal(fullJitter(baseDelay, attempt, maxDelay), signal);
}
}
throw lastError;
}
⭐ 实现事件总线 EventEmitter
面试关注点:on / once / off / emit 四个方法;emit 时要复制一份监听数组再执行,防止回调里 off 自己导致漏执行;on 返回取消订阅函数(React 里好用)。
class EventEmitter {
constructor() { this.events = new Map(); }
on(name, fn) {
if (!this.events.has(name)) this.events.set(name, []);
this.events.get(name).push(fn);
return () => this.off(name, fn); // 返回取消订阅函数
}
once(name, fn) {
const wrapper = (...args) => { this.off(name, wrapper); fn(...args); };
return this.on(name, wrapper);
}
off(name, fn) {
const list = this.events.get(name);
if (!list) return;
const i = list.indexOf(fn);
if (i > -1) list.splice(i, 1);
if (!list.length) this.events.delete(name);
}
emit(name, ...args) {
// 复制一份再执行,防止回调里 off 自己导致漏执行
[...(this.events.get(name) || [])].forEach((fn) => fn(...args));
}
}
⭐ 实现 Promise.allSettled / any
面试关注点:allSettled 是把每个 Promise 折叠成 {status, value/reason} 永不 reject;any 是返回第一个成功的、全失败才 reject 且抛 AggregateError;都要处理空数组边界。
function allSettled(promises) {
return Promise.all(
[...promises].map((p) =>
Promise.resolve(p).then(
(value) => ({ status: 'fulfilled', value }),
(reason) => ({ status: 'rejected', reason }),
),
),
);
}
function any(promises) {
return new Promise((resolve, reject) => {
const errors = [];
let count = 0;
const list = [...promises];
if (!list.length) return reject(new AggregateError([], 'All promises were rejected'));
list.forEach((p, i) => {
Promise.resolve(p).then(resolve, (err) => {
errors[i] = err;
if (++count === list.length) reject(new AggregateError(errors, 'All promises were rejected'));
});
});
});
}
⭐ 实现带超时和取消的 fetch
面试关注点:内部 AbortController 管超时、外部 AbortSignal 管用户取消,两者要桥接;finally 里清定时器、移除监听;超时 abort 用 DOMException 的 TimeoutError,和用户取消的 AbortError 区分开。
async function fetchWithTimeout(url, options = {}) {
const { timeout = 8000, signal, ...rest } = options;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(new DOMException('timeout', 'TimeoutError')), timeout);
const onOuterAbort = () => controller.abort(signal?.reason);
if (signal) {
if (signal.aborted) onOuterAbort();
else signal.addEventListener('abort', onOuterAbort, { once: true });
}
try {
return await fetch(url, { ...rest, signal: controller.signal });
} finally {
clearTimeout(timer);
signal?.removeEventListener('abort', onOuterAbort);
}
}
实现金额分摊(保证分项之和 = 总额)
面试关注点:电商场景经典题(税费/运费/优惠按权重分摊到分项);全程用「分」做整数计算避免浮点误差;用最大余额法——先按比例向下取整,剩下的差额按小数部分从大到小补 1 分。
function allocate(total, weights) {
const totalCents = Math.round(total * 100); // 全程用「分」计算,避开浮点误差
const sum = weights.reduce((a, b) => a + b, 0);
if (sum === 0) throw new Error('权重之和不能为 0');
const raw = weights.map((w) => (w / sum) * totalCents);
const base = raw.map(Math.floor);
let remainder = totalCents - base.reduce((a, b) => a + b, 0);
const byFrac = raw
.map((v, i) => ({ i, frac: v - Math.floor(v) }))
.sort((a, b) => b.frac - a.frac || a.i - b.i); // 小数部分大的优先多拿 1 分,同分按原序
const result = base.slice();
for (let k = 0; k < remainder; k++) result[byFrac[k % byFrac.length].i] += 1;
return result.map((v) => v / 100);
}
⭐ 实现虚拟列表的核心计算(定高)
面试关注点:只渲染可视区内的 item,用撑高容器(totalHeight)保持滚动条真实;start/end 要做 overscan 和边界 clamp;offsetY 用「起始索引 × 行高」做 translateY 平移。
function getVisibleRange({ scrollTop, viewportHeight, itemHeight, total, overscan = 3 }) {
const first = Math.floor(scrollTop / itemHeight);
const visibleCount = Math.ceil(viewportHeight / itemHeight);
const start = Math.max(0, first - overscan);
const end = Math.min(total, first + visibleCount + overscan);
return { start, end, offsetY: start * itemHeight, totalHeight: total * itemHeight };
}
实现千分位格式化(多语言)
面试关注点:优先用 Intl.NumberFormat,locale 不同分隔符自动不同(zh-CN 是逗号、de-DE 是点);手写版用正则 \B(?=(\d{3})+(?!\d));金额先 toFixed 处理精度,避免 0.1+0.2 的浮点问题。
// 多语言版:Intl 自动适配 locale 的分隔符和小数点
function formatMoney(value, locale = 'zh-CN', options = {}) {
return new Intl.NumberFormat(locale, {
minimumFractionDigits: 2, maximumFractionDigits: 2, ...options,
}).format(value);
}
// 手写版(面试可能要求不依赖 Intl)
function thousands(value, digits = 2, sep = ',', dot = '.') {
const negative = value < 0;
const fixed = Math.abs(Number(value)).toFixed(digits); // 先定精度,规避浮点
let [intPart, fracPart] = fixed.split('.');
intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, sep);
return (negative ? '-' : '') + intPart + (fracPart ? dot + fracPart : '');
}
⭐ 权限树的查找与父子联动
面试关注点:RBAC 项目真实锚点;递归回溯整条路径;父子联动三态(全选/半选/不选);复杂度 O(n)。先讲「我在中后台权限系统里做过三层权限 + 地区只读」,再写代码。
// 查找节点 id 的完整路径(用于回显勾选状态)
function findPath(tree, id, path = []) {
for (const node of tree) {
if (node.id === id) return [...path, node.id];
const found = findPath(node.children || [], id, [...path, node.id]);
if (found) return found;
}
return null;
}
// 父子联动:勾选子节点后,父节点自动全选/半选
function cascadeCheck(tree, checkedIds) {
const result = new Set(checkedIds);
function walk(nodes) {
for (const n of nodes) {
const kids = n.children || [];
walk(kids);
const checkedCount = kids.filter((k) => result.has(k.id)).length;
if (checkedCount === kids.length && kids.length) result.add(n.id); // 全选
else if (checkedCount > 0) result.add('__half__' + n.id); // 半选
}
}
walk(tree);
return result;
}
⭐ 保留最后一次输入的异步合并(竞态)
面试关注点:竞态根因是「请求返回顺序不定」;两种解法——序号标记(只认最新)+ AbortController(取消旧请求);AbortError 要单独处理不算错。这是搜索框、下拉补全的高频手写。
// 方案一:序号标记,只保留最后一次结果
let seq = 0;
async function search(keyword) {
const cur = ++seq; // 每次调用自增
const res = await fetch('/search?q=' + keyword).then((r) => r.json());
if (cur !== seq) return null; // 有更新请求了,丢弃过期结果
render(res);
}
// 方案二:AbortController 取消旧请求(更彻底,省带宽)
let controller = null;
async function search2(keyword) {
controller?.abort();
controller = new AbortController();
try {
const res = await fetch('/search?q=' + keyword, { signal: controller.signal });
render(await res.json());
} catch (e) {
if (e.name !== 'AbortError') throw e; // 取消不算错,其他错误照抛
}
}
⭐ SSE 流式解析手写
面试关注点:半包粘包(按 \n\n 切帧、残帧留在缓冲);TextDecoder 的 stream 模式处理跨包多字节字符;data: 行解析。这是 AI 流式对话岗的高频手写。
let buffer = '';
async function readSSE(response) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true }); // stream: true 保留多字节残字
let sep;
while ((sep = buffer.indexOf('\n\n')) !== -1) { // 按空行切帧
const frame = buffer.slice(0, sep);
buffer = buffer.slice(sep + 2); // 残帧留到下一包
for (const line of frame.split('\n')) {
if (line.startsWith('data:')) onData(line.slice(5).trim());
}
}
}
}