🔥 前端面试必刷10道手写题 | 原理+边界+坑点+优化代码+加分版(掘金精排版)

9 阅读18分钟

今天给大家整理一份前端面试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分钟背完):

  1. 防抖:最后一次执行,清定时器;加分版加立即执行+手动取消
  2. 节流:固定周期执行,时间戳版最稳;加分版结合定时器,兼顾最后一次触发
  3. 深拷贝:递归+缓存处理循环引用;加分版支持更多类型
  4. Promise.all:全成功返回,一败全败;加分版支持中断
  5. 发布订阅:事件池+on/emit/off/once;加分版加命名空间
  6. LRU:Map有序,删头存尾;加分版加容错+常用方法
  7. new:4步走(创对象、绑原型、执行构造、返对象);加分版加校验
  8. call/apply/bind:改this,立即/延迟;加分版兼容严格模式
  9. 数组扁平化:reduce+递归;加分版支持指定深度+过滤空值
  10. 快排:分治+递归;加分版三数取中+原地排序

写在最后

本文整理了前端面试最常考的 10 道手写题,每道题都包含「原理+边界+坑点+基础优化版+生产加分版」,兼顾面试默写和能力体现。

面试时,基础版足够拿到基础分,加分版能让你在众多候选人中脱颖而出,尤其适合中高级前端面试。

如果对你有帮助,欢迎 点赞 + 收藏 + 关注 ~ 后续会持续输出面试干货、源码解析、工程化实战,助力大家轻松拿下前端面试!