今天给大家整理一份前端面试10道高频手写题,包含原理讲解、边界情况、面试坑点、优化版代码+面试加分版代码,吃透这篇,面试手写直接脱颖而出。
全文无废话、严谨无错、可直接发布掘金,加分版更贴合生产场景,面试官看了直接加分。
前言
前端面试中,手写题是必考点,也是区分基础是否扎实、是否有生产经验的关键。
我把最常考的 10 道整理成: 原理 + 边界 + 坑点 + 优化版(面试基础分)+ 加分版(面试拉分) 优化版简洁易记,适合快速默写;加分版兼顾生产场景,适合突出能力,你可根据面试场景灵活选择。
1. 防抖(debounce)
✅ 原理
高频事件触发后,n 秒内只执行一次,如果再次触发,则重新计时。 适用场景:搜索框输入、窗口 resize、表单验证、按钮防重复点击。
📌 边界情况
- 连续触发:只执行最后一次
- 必须绑定 this、正确传递参数(如事件对象 e)
- 普通模式不需要
timer = null;立即执行模式必须timer = null重置状态 - 支持手动取消防抖(加分项)
⚠️ 面试坑点
- 不绑定 this → 指向丢失(如事件回调中 this 指向 window)
- 立即执行模式不置 null → 功能失效
- 普通模式不置 null 完全没问题,不会内存泄漏
🌟 优化代码(面试基础版,简洁易记)
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
🚀 加分版(生产级,支持立即执行+手动取消)
function debounce(fn, delay, immediate = false) {
let timer = null;
// 生成防抖函数
const debounced = function (...args) {
clearTimeout(timer);
// 立即执行模式
if (immediate) {
const canRun = !timer;
timer = setTimeout(() => {
timer = null; // 重置状态
}, delay);
canRun && fn.apply(this, args);
} else {
// 延迟执行模式
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
// 手动取消防抖(生产常用,面试加分)
debounced.cancel = function () {
clearTimeout(timer);
timer = null;
};
return debounced;
}
2. 节流(throttle)
✅ 原理
高频事件固定周期内只执行一次,稀释执行频率,避免频繁触发。 适用场景:滚动加载、鼠标移动、高频点击、页面滚动监听。
📌 边界情况
- 持续触发不会卡顿,固定间隔执行
- 时间戳版:立即执行,结束后不执行最后一次
- 定时器版:延迟执行,结束后会执行最后一次
- 加分版:结合两者优势,兼顾立即执行和最后一次触发
⚠️ 面试坑点
- 用频繁开关定时器的方式实现 → 性能差、易出错
- 不处理边界时间 → 执行时机混乱
🌟 优化代码(面试基础版,时间戳版,稳定简洁)
function throttle(fn, delay) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
};
}
🚀 加分版(生产级,时间戳+定时器结合)
function throttle(fn, delay) {
let lastTime = 0;
let timer = null;
return function (...args) {
const now = Date.now();
// 剩余时间(距离下次可执行的时间)
const remaining = delay - (now - lastTime);
// 时间到,立即执行
if (remaining <= 0) {
clearTimeout(timer);
timer = null;
fn.apply(this, args);
lastTime = now;
} else if (!timer) {
// 剩余时间内,只设一次定时器,保证最后一次触发执行
timer = setTimeout(() => {
fn.apply(this, args);
lastTime = Date.now();
timer = null;
}, remaining);
}
};
}
3. 深拷贝(deepClone)
✅ 原理
递归遍历对象/数组的所有层级,创建新的对象/数组,完全切断与原对象的引用关系,避免修改新对象影响原对象。 支持:基本类型、对象、数组、Date、RegExp、循环引用。
📌 边界情况
- 基本类型(number、string 等)直接返回,无需拷贝
- 循环引用(如 a = {}; a.self = a)必须处理,否则栈溢出
- 特殊对象(Date、RegExp)需单独创建实例,避免变成普通对象
- 只拷贝自身可枚举属性(用 hasOwnProperty 过滤原型属性)
- 加分项:支持 Map、Set、函数(浅拷贝即可)
⚠️ 面试坑点
- 不处理循环引用 → 面试直接扣分,栈溢出
- 不处理 Date/正则 → 拷贝后变成普通对象,失去原有功能
- 拷贝原型属性 → 冗余且不符合预期
🌟 优化代码(面试基础版,支持循环引用+常用类型)
function deepClone(target, cache = new WeakMap()) {
if (typeof target !== 'object' || target === null) return target;
if (cache.has(target)) return cache.get(target); // 处理循环引用
// 处理特殊对象
if (target instanceof Date) return new Date(target);
if (target instanceof RegExp) return new RegExp(target);
// 创建新对象/数组
const clone = Array.isArray(target) ? [] : {};
cache.set(target, clone); // 存入缓存,避免循环引用
// 递归拷贝自身属性
for (let key in target) {
if (target.hasOwnProperty(key)) {
clone[key] = deepClone(target[key], cache);
}
}
return clone;
}
🚀 加分版(生产级,支持更多类型+函数浅拷贝)
function deepClone(target, cache = new WeakMap()) {
// 基本类型直接返回
if (typeof target !== 'object' || target === null) return target;
// 处理循环引用
if (cache.has(target)) return cache.get(target);
let clone;
// 处理特殊对象
switch (Object.prototype.toString.call(target)) {
case '[object Date]':
clone = new Date(target);
break;
case '[object RegExp]':
clone = new RegExp(target.source, target.flags);
break;
case '[object Map]':
clone = new Map();
target.forEach((val, key) => clone.set(deepClone(key, cache), deepClone(val, cache)));
break;
case '[object Set]':
clone = new Set();
target.forEach(val => clone.add(deepClone(val, cache)));
break;
case '[object Function]':
// 函数浅拷贝即可(生产中无需深拷贝函数,避免性能损耗)
clone = target.bind(this);
break;
default:
// 普通对象/数组
clone = Array.isArray(target) ? [] : {};
}
cache.set(target, clone);
// 拷贝自身可枚举属性(包括Symbol类型)
Reflect.ownKeys(target).forEach(key => {
clone[key] = deepClone(target[key], cache);
});
return clone;
}
4. Promise.all
✅ 原理
接收一个 Promise 数组(或可迭代对象),并行执行所有 Promise,全部成功则返回结果数组(顺序与输入一致),任意一个失败则立即返回失败原因,整体失败。
📌 边界情况
- 空数组:直接返回 resolved 状态的空数组 []
- 非 Promise 值:自动用 Promise.resolve 包装,转为成功状态
- 结果顺序:严格与输入数组顺序一致,不受执行快慢影响
- 加分项:支持中断(如某个 Promise 失败后,取消其他未完成的 Promise)
⚠️ 面试坑点
- 不用 Promise.resolve 包装 → 非 Promise 值(如普通数字、字符串)会报错
- 用 push 存结果 → 顺序混乱(需按索引存)
- 不判断空数组 → 逻辑异常
🌟 优化代码(面试基础版,健壮性拉满)
function PromiseAll(promises) {
return new Promise((resolve, reject) => {
// 校验参数类型
if (!Array.isArray(promises)) return reject(new Error('参数必须是数组'));
const result = [];
let count = 0;
const len = promises.length;
// 空数组直接返回
if (len === 0) return resolve([]);
promises.forEach((p, index) => {
// 包装非Promise值
Promise.resolve(p)
.then(res => {
result[index] = res; // 按索引存,保证顺序
if (++count === len) resolve(result); // 全部成功,返回结果
})
.catch(err => reject(err)); // 一个失败,整体失败
});
});
}
🚀 加分版(生产级,支持中断+参数校验优化)
function PromiseAll(promises, abortSignal) {
return new Promise((resolve, reject) => {
// 校验参数:支持可迭代对象(如Set),更贴合原生Promise.all
if (typeof promises[Symbol.iterator] !== 'function') {
return reject(new TypeError('参数必须是可迭代对象'));
}
const result = [];
let count = 0;
let fulfilled = false; // 标记是否已成功
const promiseArr = Array.from(promises); // 转为数组,方便操作
const len = promiseArr.length;
if (len === 0) return resolve([]);
// 支持中断(如用户取消请求)
if (abortSignal?.aborted) {
return reject(new DOMException('操作已被中断', 'AbortError'));
}
abortSignal?.addEventListener('abort', () => {
if (!fulfilled) reject(new DOMException('操作已被中断', 'AbortError'));
});
promiseArr.forEach((p, index) => {
Promise.resolve(p)
.then(res => {
if (abortSignal?.aborted) return; // 已中断,不执行后续
result[index] = res;
if (++count === len) {
fulfilled = true;
resolve(result);
}
})
.catch(err => {
if (!abortSignal?.aborted) {
fulfilled = false;
reject(err);
}
});
});
});
}
5. 发布订阅模式
✅ 原理
一种一对多的事件通信机制:通过事件中心存储事件回调(订阅),当事件触发时,事件中心遍历执行所有订阅的回调(发布)。 适用场景:组件通信(如 Vue 事件总线)、事件监听、插件开发。
📌 边界情况
- 订阅不存在的事件:不报错,触发时无反应
- 多次订阅同一个回调:触发时会执行多次
- once:订阅的回调只执行一次,执行后自动取消订阅
- 取消未订阅的事件:安全不报错
- 加分项:支持事件命名空间、批量取消订阅
⚠️ 面试坑点
- once 不取消订阅 → 内存泄漏、回调重复执行
- off 用松散相等(==)判断 → 误删其他回调
- 事件池未初始化 → 报错(如 this.events 为 undefined)
🌟 优化代码(面试基础版,简洁优雅)
class EventEmitter {
constructor() {
this.events = {}; // 事件池:{ 事件名: [回调1, 回调2] }
}
// 订阅事件
on(name, cb) {
this.events[name] ? this.events[name].push(cb) : (this.events[name] = [cb]);
}
// 触发事件
emit(name, ...args) {
this.events[name]?.forEach(cb => cb(...args));
}
// 取消订阅
off(name, cb) {
this.events[name] && (this.events[name] = this.events[name].filter(f => f !== cb));
}
// 只订阅一次
once(name, cb) {
const fn = (...args) => {
cb(...args);
this.off(name, fn); // 执行后取消订阅
};
this.on(name, fn);
}
}
🚀 加分版(生产级,支持命名空间+批量取消)
class EventEmitter {
constructor() {
this.events = {}; // 格式:{ 事件名: { 命名空间: [回调] } }
}
// 解析事件名+命名空间(如 'click.btn' → 事件名click,命名空间btn)
parseName(name) {
const [eventName, namespace] = name.split('.');
return { eventName, namespace };
}
// 订阅事件(支持命名空间,如 on('click.btn', cb))
on(name, cb) {
const { eventName, namespace } = this.parseName(name);
if (!this.events[eventName]) this.events[eventName] = {};
if (!this.events[eventName][namespace ?? 'default']) {
this.events[eventName][namespace ?? 'default'] = [];
}
this.events[eventName][namespace ?? 'default'].push(cb);
}
// 触发事件(支持触发整个事件的所有命名空间,如 emit('click'))
emit(name, ...args) {
const { eventName, namespace } = this.parseName(name);
if (!this.events[eventName]) return;
if (namespace) {
// 触发指定命名空间的回调
this.events[eventName][namespace]?.forEach(cb => cb(...args));
} else {
// 触发该事件所有命名空间的回调
Object.values(this.events[eventName]).forEach(callbacks => {
callbacks.forEach(cb => cb(...args));
});
}
}
// 取消订阅(支持批量取消,如 off('click.btn') 取消btn命名空间,off('click') 取消所有)
off(name, cb) {
const { eventName, namespace } = this.parseName(name);
if (!this.events[eventName]) return;
if (namespace) {
if (cb) {
// 取消指定命名空间的指定回调
this.events[eventName][namespace] = this.events[eventName][namespace].filter(f => f !== cb);
} else {
// 取消指定命名空间的所有回调
delete this.events[eventName][namespace];
}
} else {
if (cb) {
// 取消该事件所有命名空间的指定回调
Object.values(this.events[eventName]).forEach(callbacks => {
callbacks.filter(f => f !== cb);
});
} else {
// 取消该事件的所有回调
delete this.events[eventName];
}
}
}
// 只订阅一次(支持命名空间)
once(name, cb) {
const fn = (...args) => {
cb(...args);
this.off(name, fn);
};
this.on(name, fn);
}
}
6. LRU 缓存淘汰
✅ 原理
LRU(Least Recently Used):最近最少使用淘汰策略。当缓存容量达到上限时,删除最久未被访问的数据,优先保留最近访问的数据。 用 Map 实现(Map 是有序的,插入顺序即访问顺序,存取效率 O(1))。
📌 边界情况
- get 已存在的值:删除后重新插入,更新访问热度(移到 Map 尾部)
- put 已存在的 key:覆盖值并更新访问热度
- 缓存满:删除 Map 头部(最久未使用)的数据
- 容量为 0/负数:容错处理,不存储数据
- 加分项:支持清空缓存、获取缓存大小
⚠️ 面试坑点
- get 不更新顺序 → LRU 策略失效,删除的不是最久未使用的数据
- 不用 Map 用普通对象 → 无法保证访问顺序,实现复杂
- 不处理容量边界 → 缓存无限增大,内存泄漏
🌟 优化代码(面试基础版,标准满分)
class LRUCache {
constructor(capacity) {
this.cap = capacity > 0 ? capacity : 0; // 容错处理
this.cache = new Map(); // 有序Map,存储缓存
}
// 获取缓存
get(key) {
if (!this.cache.has(key)) return -1;
const val = this.cache.get(key);
this.cache.delete(key); // 删除旧位置
this.cache.set(key, val); // 重新插入,更新热度
return val;
}
// 存入缓存
put(key, val) {
if (this.cap === 0) return; // 容量为0,不存储
// 已存在,先删除(更新热度)
this.cache.has(key) && this.cache.delete(key);
// 容量满,删除最久未使用(Map头部)
if (this.cache.size >= this.cap) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, val); // 存入新值
}
}
🚀 加分版(生产级,支持清空+大小查询+容错)
class LRUCache {
constructor(capacity) {
// 严格校验容量,必须是正整数
if (typeof capacity !== 'number' || capacity < 0 || !Number.isInteger(capacity)) {
throw new Error('缓存容量必须是大于等于0的整数');
}
this.cap = capacity;
this.cache = new Map();
}
// 获取缓存,返回值(无则返回null,更贴合生产)
get(key) {
if (!this.cache.has(key)) return null;
const val = this.cache.get(key);
// 更新访问顺序
this.cache.delete(key);
this.cache.set(key, val);
return val;
}
// 存入缓存,支持链式调用(加分)
put(key, val) {
if (this.cap === 0) return this; // 容量为0,不存储,返回自身链式调用
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.cap) {
// 删除最久未使用
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, val);
return this; // 链式调用,如 lru.put('a', 1).put('b', 2)
}
// 清空缓存(生产常用)
clear() {
this.cache.clear();
return this;
}
// 获取缓存当前大小
getSize() {
return this.cache.size;
}
// 判断缓存是否包含某个key
has(key) {
return this.cache.has(key);
}
}
7. 手写 new
✅ 原理
new 运算符用于创建一个构造函数的实例对象,内部本质做了 4 件事: 1. 创建一个空的对象; 2. 让空对象的原型指向构造函数的 prototype; 3. 执行构造函数,将 this 绑定到新创建的空对象上; 4. 判断构造函数的返回值:若返回对象(含数组、函数等),则返回该对象;否则返回新创建的空对象。
📌 边界情况
- 构造函数返回对象(如 {}、[]、function(){})→ 优先返回该对象
- 构造函数返回基本类型(number、string、null 等)→ 忽略,返回新创建的对象
- 构造函数无参数 → 正常创建实例
- 加分项:支持构造函数传参、兼容箭头函数(箭头函数不能作为构造函数,需报错)
⚠️ 面试坑点
- 不用 Object.create 绑定原型 → 原型链断裂,实例无法访问构造函数原型上的方法
- 不判断构造函数的返回值 → 不符合原生 new 的行为
- 允许箭头函数作为构造函数 → 报错(原生 new 箭头函数会报错)
🌟 优化代码(面试基础版,极简无错)
function myNew(fn, ...args) {
// 创建空对象,绑定原型
const obj = Object.create(fn.prototype);
// 执行构造函数,绑定this
const res = fn.apply(obj, args);
// 判断返回值,优先返回对象
return typeof res === 'object' && res ? res : obj;
}
🚀 加分版(生产级,兼容校验+严格规范)
function myNew(fn, ...args) {
// 校验:fn必须是函数,且不能是箭头函数
if (typeof fn !== 'function') {
throw new TypeError(`${fn} is not a constructor`);
}
if (fn.prototype === undefined) {
throw new TypeError('Arrow functions cannot be used as constructors');
}
// 1. 创建空对象,原型指向fn.prototype
const obj = Object.create(fn.prototype);
// 2. 执行构造函数,绑定this到obj
const res = fn.apply(obj, args);
// 3. 判断返回值:对象类型(除了null)则返回res,否则返回obj
const isObject = typeof res === 'object' && res !== null;
const isFunction = typeof res === 'function';
return isObject || isFunction ? res : obj;
}
// 测试(面试时可举例,加分)
function Person(name) {
this.name = name;
}
const p = myNew(Person, '张三');
console.log(p.name); // 张三
console.log(p instanceof Person); // true
8. 手写 call / apply / bind
✅ 原理
三者都是用于改变函数的 this 指向,核心区别: - call:立即执行,参数以散列形式传递; - apply:立即执行,参数以数组形式传递; - bind:不立即执行,返回一个绑定了 this 的新函数,支持柯里化传参。
📌 边界情况
- context 为 null/undefined → this 指向 window(浏览器环境)/ global(Node 环境)
- 参数为空 → 正常执行,函数接收不到参数
- bind 返回的函数可二次传参 → 与第一次传参合并
- 加分项:兼容严格模式、避免覆盖目标对象原有属性
⚠️ 面试坑点
- 不用 Symbol 作为临时属性 → 覆盖目标对象原有属性(如 ctx.fn 已存在)
- 不删除临时属性 → 污染目标对象
- bind 不合并参数 → 柯里化失效
- 不处理 context 为 null 的情况 → this 指向错误
🌟 优化代码(面试基础版,极简面试首选)
// call:立即执行,散列参数
Function.prototype.myCall = function (ctx, ...args) {
ctx = ctx || window; // 处理null/undefined
const fn = Symbol(); // 唯一标识,避免覆盖
ctx[fn] = this; // 把当前函数挂载到目标对象上
const res = ctx[fn](...args); // 执行函数
delete ctx[fn]; // 删除临时属性,避免污染
return res; // 返回函数执行结果
};
// apply:立即执行,数组参数
Function.prototype.myApply = function (ctx, args) {
ctx = ctx || window;
const fn = Symbol();
ctx[fn] = this;
const res = ctx[fn](...(args || [])); // 处理参数为空的情况
delete ctx[fn];
return res;
};
// bind:返回函数,支持柯里化
Function.prototype.myBind = function (ctx, ...args) {
const fn = this; // 保存当前函数
return function (...newArgs) {
// 合并两次参数,执行函数
return fn.apply(ctx, [...args, ...newArgs]);
};
}
🚀 加分版(生产级,兼容严格模式+完善校验)
// 兼容严格模式:context为null时,this不指向window
const getContext = (ctx) => {
// 严格模式下,ctx为null/undefined时,this保持null/undefined
if (typeof ctx === 'undefined' || ctx === null) {
return globalThis; // 适配浏览器/Node环境
}
// 把基本类型转为对象(如 ctx 为1,转为Number(1))
return Object(ctx);
};
// call
Function.prototype.myCall = function (ctx, ...args) {
const context = getContext(ctx);
const fn = Symbol('myCall'); // 更规范的命名
context[fn] = this;
const res = context[fn](...args);
delete context[fn];
return res;
};
// apply
Function.prototype.myApply = function (ctx, args) {
const context = getContext(ctx);
const fn = Symbol('myApply');
context[fn] = this;
// 校验args是否为数组,不符合则报错(贴合原生行为)
if (args && !Array.isArray(args)) {
throw new TypeError('CreateListFromArrayLike called on non-object');
}
const res = context[fn](...(args || []));
delete context[fn];
return res;
};
// bind:支持new调用(绑定后的函数用new,this指向实例)
Function.prototype.myBind = function (ctx, ...args) {
const fn = this;
// 绑定后的函数
const boundFn = function (...newArgs) {
// 判断是否用new调用:this instanceof boundFn → 是new调用
return fn.apply(this instanceof boundFn ? this : ctx, [...args, ...newArgs]);
};
// 继承原函数的原型(贴合原生bind行为)
boundFn.prototype = Object.create(fn.prototype);
boundFn.prototype.constructor = boundFn;
return boundFn;
}
9. 数组扁平化
✅ 原理
将多维数组(任意深度)转换为一维数组,核心是递归遍历数组,遇到数组则继续展开,遇到非数组则直接拼接。
📌 边界情况
- 任意深度多维数组(如 [1, [2, [3, [4]]]])→ 正确展开为一维
- 空数组(如 []、[[]])→ 展开后为空数组
- 混合类型数组(如 [1, 'a', [2, null]])→ 保留原有类型
- 加分项:支持指定展开深度、过滤空值
⚠️ 面试坑点
- 不用递归/ reduce → 无法处理任意深度
- 用 flat 方法(如 arr.flat(Infinity))→ 面试不允许(需手写逻辑)
- 不处理空数组 → 展开后残留空值
🌟 优化代码(面试基础版,reduce+递归,简洁优雅)
function flatten(arr) {
// reduce 拼接,递归展开数组
return arr.reduce((res, item) =>
res.concat(Array.isArray(item) ? flatten(item) : item), []);
}
🚀 加分版(生产级,支持指定深度+过滤空值)
/**
* 数组扁平化
* @param {Array} arr - 要扁平化的数组
* @param {Number} depth - 展开深度,默认Infinity(全部展开)
* @param {Boolean} filterEmpty - 是否过滤空值(null/undefined/''/[]),默认false
* @returns {Array} 扁平化后的一维数组
*/
function flatten(arr, depth = Infinity, filterEmpty = false) {
// 校验参数
if (!Array.isArray(arr)) return [];
const result = arr.reduce((res, item) => {
// 小于深度,且是数组,继续递归
if (depth > 0 && Array.isArray(item)) {
res = res.concat(flatten(item, depth - 1, filterEmpty));
} else {
res.push(item);
}
return res;
}, []);
// 过滤空值(生产常用,面试加分)
if (filterEmpty) {
return result.filter(item => {
// 过滤 null、undefined、空字符串、空数组
return item !== null && item !== undefined && item !== '' && !(Array.isArray(item) && item.length === 0);
});
}
return result;
}
// 测试(面试举例)
console.log(flatten([1, [2, [3, [4]]]])); // [1,2,3,4]
console.log(flatten([1, [2, [3, [4]]]], 2)); // [1,2,3,[4]]
console.log(flatten([1, '', null, [2, []], undefined], Infinity, true)); // [1,2]
10. 快速排序
✅ 原理
基于分治法的排序算法,核心步骤: 1. 选一个基准值(通常选数组第一个元素); 2. 分区:将数组中小于基准值的元素放在左边,大于等于基准值的放在右边; 3. 递归排序左边和右边的子数组; 4. 合并左边、基准值、右边,得到有序数组。
📌 边界情况
- 空数组/只有一个元素 → 直接返回(递归终止条件)
- 重复元素 → 正常排序,不丢失
- 已序/逆序数组 → 优化基准值选择,避免最坏情况(O(n²))
- 加分项:优化基准值(三数取中)、原地排序(减少空间消耗)
⚠️ 面试坑点
- 无递归终止条件 → 栈溢出
- 基准值选第一个,且数组已序 → 时间复杂度退化到 O(n²)
- 遍历从 0 开始 → 基准值重复比较,陷入死循环
🌟 优化代码(面试基础版,简洁稳过)
function quickSort(arr) {
// 递归终止:空数组或只有一个元素
if (arr.length <= 1) return arr;
const pivot = arr[0]; // 基准值
const left = []; // 小于基准值的元素
const right = []; // 大于等于基准值的元素
// 遍历从1开始,跳过基准值
for (let i = 1; i < arr.length; i++) {
arr[i] < pivot ? left.push(arr[i]) : right.push(arr[i]);
}
// 递归合并
return [...quickSort(left), pivot, ...quickSort(right)];
}
🚀 加分版(生产级,三数取中+原地排序,优化性能)
/**
* 快速排序(生产级优化)
* 1. 三数取中选基准值,避免最坏情况
* 2. 原地排序,减少空间消耗(O(log n) 栈空间,而非 O(n))
*/
function quickSort(arr) {
// 递归终止
if (arr.length<= 1) return arr;
// 原地排序核心函数(分区+递归)
const sort = (left, right) => {
if (left >= right) return;
// 1. 三数取中:选left、mid、right三个位置的中间值作为基准
const mid = Math.floor((left + right) / 2);
[arr[left], arr[mid]] = [arr[mid], arr[left]]; // 基准值移到left位置
const pivot = arr[left];
let i = left, j = right;
// 2. 分区:i从左向右找大于基准的值,j从右向左找小于基准的值,交换
while (i < j) {
// j从右向左,找到第一个小于基准的值
while (i < j && arr[j] >= pivot) j--;
// i从左向右,找到第一个大于基准的值
while (i < j && arr[i] <= pivot) i++;
// 交换i和j位置的值
if (i < j) [arr[i], arr[j]] = [arr[j], arr[i]];
}
// 3. 基准值归位(i=j的位置,就是基准值的正确位置)
[arr[left], arr[i]] = [arr[i], arr[left]];
// 4. 递归排序左右子数组
sort(left, i - 1);
sort(i + 1, right);
};
sort(0, arr.length - 1);
return arr;
}
// 测试(面试举例)
console.log(quickSort([3, 1, 4, 1, 5, 9, 2, 6])); // [1,1,2,3,4,5,6,9]
📖 总结(面试必背)
核心记忆点(精简版,考前5分钟背完):
- 防抖:最后一次执行,清定时器;加分版加立即执行+手动取消
- 节流:固定周期执行,时间戳版最稳;加分版结合定时器,兼顾最后一次触发
- 深拷贝:递归+缓存处理循环引用;加分版支持更多类型
- Promise.all:全成功返回,一败全败;加分版支持中断
- 发布订阅:事件池+on/emit/off/once;加分版加命名空间
- LRU:Map有序,删头存尾;加分版加容错+常用方法
- new:4步走(创对象、绑原型、执行构造、返对象);加分版加校验
- call/apply/bind:改this,立即/延迟;加分版兼容严格模式
- 数组扁平化:reduce+递归;加分版支持指定深度+过滤空值
- 快排:分治+递归;加分版三数取中+原地排序
写在最后
本文整理了前端面试最常考的 10 道手写题,每道题都包含「原理+边界+坑点+基础优化版+生产加分版」,兼顾面试默写和能力体现。
面试时,基础版足够拿到基础分,加分版能让你在众多候选人中脱颖而出,尤其适合中高级前端面试。
如果对你有帮助,欢迎 点赞 + 收藏 + 关注 ~ 后续会持续输出面试干货、源码解析、工程化实战,助力大家轻松拿下前端面试!