前端面试通关指南:30个高频手写JS算法,吃透就能拿高薪(附完整代码)

0 阅读20分钟

聊前端面试,算法永远是绕不开的坎。很多小伙伴项目经验很丰富,框架用得溜,但一上面试场就被手写算法题卡住,直接导致面试失利。

我整理了一份30个高频手写JS算法清单,覆盖ES6语法、数组操作、链表、二叉树、动态规划等核心考点,从简单到进阶,每道题都附完整可复制代码+考点解析,跟着敲一遍,面试时直接手到擒来!

一、基础语法与数组变换(必拿分,入门级)

这类题考察基础语法功底,难度低、频率高,面试时先搞定这类题,稳拿基础分,给面试官留好第一印象。

1. 深度克隆(Deep Clone)

考察点:引用类型传址、循环引用、基本类型与引用类型区别

function deepClone(obj, map = new WeakMap()) {
  // 基本类型直接返回(null也是基本类型范畴)
  if (obj === null || typeof obj !== 'object') return obj;
  // 处理循环引用(避免无限递归)
  if (map.has(obj)) return map.get(obj);
  
  // 区分数组和对象,创建对应克隆容器
  const clone = Array.isArray(obj) ? [] : {};
  map.set(obj, clone); // 存入map,标记已克隆
  
  // 遍历对象/数组,递归克隆每一项
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) { // 只克隆自身属性,不克隆原型链属性
      clone[key] = deepClone(obj[key], map);
    }
  }
  return clone;
}

// 测试
const obj = { a: 1, b: { c: 2 }, d: [3, 4] };
const cloneObj = deepClone(obj);
obj.b.c = 100;
console.log(cloneObj.b.c); // 2(克隆后互不影响)

2. 数组扁平化(Flatten)

考察点:递归、数组方法(forEach、concat)、ES6新特性

// 解法1:递归(兼容性好,易理解)
function flatten(arr) {
  let result = [];
  arr.forEach(item => {
    // 若当前项是数组,递归扁平化,否则直接加入结果
    result = result.concat(Array.isArray(item) ? flatten(item) : item);
  });
  return result;
}

// 解法2:ES6 flat方法(简洁,实际开发常用)
const flatten = arr => arr.flat(Infinity); // Infinity表示无限层级扁平化

// 解法3:reduce实现(更简洁,面试加分)
const flatten = arr => arr.reduce((prev, curr) => {
  return prev.concat(Array.isArray(curr) ? flatten(curr) : curr);
}, []);

// 测试
console.log(flatten([1, [2, [3, 4], 5]])); // [1,2,3,4,5]

3. 防抖(Debounce)

考察点:高频事件控制、定时器、this指向

// 核心:频繁触发时,只在最后一次触发后延迟执行
function debounce(fn, delay = 500) {
  let timer = null; // 定时器标识,闭包保存
  return function(...args) {
    clearTimeout(timer); // 每次触发,清除上一次定时器
    // 重新设置定时器,延迟执行目标函数
    timer = setTimeout(() => {
      fn.apply(this, args); // 绑定this和参数,适配实际场景
    }, delay);
  };
}

// 用法(搜索框输入示例)
const handleSearch = debounce((val) => {
  console.log('请求搜索接口:', val);
}, 500);

4. 节流(Throttle)

考察点:高频事件控制、时间戳/定时器、性能优化

// 解法1:时间戳版(触发时立即执行,之后固定时间内不执行)
function throttle(fn, interval = 1000) {
  let lastTime = 0; // 上一次执行时间
  return function(...args) {
    const nowTime = Date.now(); // 当前时间
    // 若当前时间 - 上一次执行时间 > 间隔,执行函数
    if (nowTime - lastTime >= interval) {
      fn.apply(this, args);
      lastTime = nowTime; // 更新上一次执行时间
    }
  };
}

// 解法2:定时器版(触发后延迟执行,固定时间内只执行一次)
function throttle2(fn, interval = 1000) {
  let timer = null;
  return function(...args) {
    if (!timer) { // 若定时器不存在,执行函数
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null; // 执行后清空定时器,允许下次执行
      }, interval);
    }
  };
}

// 用法(滚动加载示例)
window.addEventListener('scroll', throttle(() => {
  console.log('滚动加载更多');
}, 1000));

5. 数组去重(Unique)

考察点:数组方法、Set数据结构、兼容性

// 解法1:Set实现(最简洁,ES6+常用)
const unique = arr => [...new Set(arr)];

// 解法2:indexOf实现(兼容性好,适合旧项目)
function unique(arr) {
  const result = [];
  arr.forEach(item => {
    // 若结果数组中没有当前项,加入结果
    if (result.indexOf(item) === -1) {
      result.push(item);
    }
  });
  return result;
}

// 解法3:filter+indexOf(简洁,面试常用)
const unique = arr => arr.filter((item, index) => {
  // 只保留第一次出现的元素(indexOf返回第一个匹配的索引)
  return arr.indexOf(item) === index;
});

// 测试
console.log(unique([1, 2, 2, 3, 3, 3])); // [1,2,3]

6. 数组排序(冒泡排序)

考察点:排序原理、循环逻辑、基础算法思维

// 冒泡排序:相邻元素对比,大的往后移,每次循环确定一个最大值
function bubbleSort(arr) {
  const len = arr.length;
  // 外层循环:控制排序轮次(共len-1轮)
  for (let i = 0; i < len - 1; i++) {
    let flag = false; // 优化:标记是否发生交换,若无则排序完成
    // 内层循环:对比相邻元素,交换位置
    for (let j = 0; j < len - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        // 交换两个元素
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        flag = true;
      }
    }
    if (!flag) break; // 无交换,直接退出循环
  }
  return arr;
}

// 测试
console.log(bubbleSort([3, 1, 4, 1, 5, 9])); // [1,1,3,4,5,9]

7. 数组排序(快速排序)

考察点:分治思想、递归、时间复杂度优化(面试高频)

// 快速排序:分治思想,选一个基准值,将数组分成两部分,递归排序
function quickSort(arr) {
  // 终止条件:数组长度<=1,直接返回
  if (arr.length <= 1) return arr;
  // 选基准值(中间项,避免极端情况)
  const pivot = arr[Math.floor(arr.length / 2)];
  // 分治:小于基准值的放左边,等于的放中间,大于的放右边
  const left = arr.filter(x => x < pivot);
  const middle = arr.filter(x => x === pivot);
  const right = arr.filter(x => x > pivot);
  // 递归排序左右两部分,拼接结果
  return [...quickSort(left), ...middle, ...quickSort(right)];
}

// 测试
console.log(quickSort([3, 1, 4, 1, 5, 9])); // [1,1,3,4,5,9]

8. 实现数组forEach方法

考察点:数组方法原理、this绑定、回调函数

// 模拟数组forEach,接收回调函数和this指向
Array.prototype.myForEach = function(callback, thisArg) {
  // 边界判断:回调必须是函数
  if (typeof callback !== 'function') {
    throw new TypeError('callback must be a function');
  }
  // 遍历当前数组(this指向调用myForEach的数组)
  for (let i = 0; i < this.length; i++) {
    // 执行回调,传入三个参数:当前项、索引、原数组,绑定thisArg
    callback.call(thisArg, this[i], i, this);
  }
};

// 用法
[1, 2, 3].myForEach((item, index) => {
  console.log(item, index); // 1 0 | 2 1 | 3 2
});

9. 实现数组map方法

考察点:数组方法原理、返回值处理、回调函数

// 模拟数组map,返回新数组,新数组元素是回调函数的返回值
Array.prototype.myMap = function(callback, thisArg) {
  if (typeof callback !== 'function') {
    throw new TypeError('callback must be a function');
  }
  const result = []; // 存储结果的新数组
  for (let i = 0; i < this.length; i++) {
    // 执行回调,将返回值加入结果数组
    result.push(callback.call(thisArg, this[i], i, this));
  }
  return result;
};

// 用法
const newArr = [1, 2, 3].myMap(item => item * 2);
console.log(newArr); // [2,4,6]

10. 实现数组filter方法

考察点:数组方法原理、条件判断、返回值处理

// 模拟数组filter,返回满足条件的元素组成的新数组
Array.prototype.myFilter = function(callback, thisArg) {
  if (typeof callback !== 'function') {
    throw new TypeError('callback must be a function');
  }
  const result = [];
  for (let i = 0; i < this.length; i++) {
    // 回调返回true,将当前项加入结果数组
    if (callback.call(thisArg, this[i], i, this)) {
      result.push(this[i]);
    }
  }
  return result;
};

// 用法
const evenArr = [1, 2, 3, 4].myFilter(item => item % 2 === 0);
console.log(evenArr); // [2,4]

二、原型与作用域(进阶必问,中层前端考点)

这类题考察对JS底层原理的理解,是区分初级和中级前端的关键,面试时高频出现,必须吃透。

11. 实现new关键字

考察点:原型链、构造函数、this绑定、返回值判断

// myNew:模拟new关键字的作用
function myNew(fn, ...args) {
  // 1. 创建一个空对象,让其原型指向构造函数的prototype
  const obj = Object.create(fn.prototype);
  // 2. 执行构造函数,将this绑定到新创建的对象上
  const result = fn.apply(obj, args);
  // 3. 判断构造函数的返回值:若为对象(非null),则返回该对象;否则返回新创建的obj
  return result instanceof Object ? result : obj;
}

// 测试
function Person(name, age) {
  this.name = name;
  this.age = age;
}
const person = myNew(Person, '张三', 25);
console.log(person.name); // 张三
console.log(person instanceof Person); // true

12. 手写Promise(简易版,支持then链式调用)

考察点:异步编程、状态机、回调队列、链式调用

class MyPromise {
  constructor(exector) {
    // 初始化状态:pending(等待)、fulfilled(成功)、rejected(失败)
    this.status = 'pending';
    this.value = null; // 成功时的返回值
    this.reason = null; // 失败时的原因
    // 存储成功/失败的回调队列(支持多个then绑定)
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    // 成功回调:改变状态,保存值,执行所有成功回调
    const resolve = (value) => {
      if (this.status === 'pending') {
        this.status = 'fulfilled';
        this.value = value;
        // 执行所有缓存的成功回调
        this.onFulfilledCallbacks.forEach(fn => fn());
      }
    };

    // 失败回调:改变状态,保存原因,执行所有失败回调
    const reject = (reason) => {
      if (this.status === 'pending') {
        this.status = 'rejected';
        this.reason = reason;
        // 执行所有缓存的失败回调
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };

    // 执行 executor,捕获异常,异常时调用reject
    try {
      exector(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }

  // then方法:支持链式调用,返回新的Promise
  then(onFulfilled, onRejected) {
    // 兼容:若then未传回调,默认透传值/原因
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };

    // 返回新Promise,实现链式调用
    return new MyPromise((resolve, reject) => {
      // 状态为成功时,执行成功回调
      if (this.status === 'fulfilled') {
        try {
          const result = onFulfilled(this.value);
          // 回调返回值,传递给下一个then的成功回调
          resolve(result);
        } catch (err) {
          // 回调执行失败,传递给下一个then的失败回调
          reject(err);
        }
      }

      // 状态为失败时,执行失败回调
      if (this.status === 'rejected') {
        try {
          const result = onRejected(this.reason);
          resolve(result);
        } catch (err) {
          reject(err);
        }
      }

      // 状态为等待时,缓存回调
      if (this.status === 'pending') {
        this.onFulfilledCallbacks.push(() => {
          try {
            const result = onFulfilled(this.value);
            resolve(result);
          } catch (err) {
            reject(err);
          }
        });
        this.onRejectedCallbacks.push(() => {
          try {
            const result = onRejected(this.reason);
            resolve(result);
          } catch (err) {
            reject(err);
          }
        });
      }
    });
  }
}

// 测试
new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('成功');
  }, 1000);
}).then(res => {
  console.log(res); // 成功
  return '下一个then';
}).then(res => {
  console.log(res); // 下一个then
});

13. 实现Promise.all方法

考察点:Promise并发控制、数组遍历、状态判断

// Promise.all:接收一个Promise数组,所有Promise成功才成功,有一个失败则失败
Promise.myAll = function(promises) {
  return new Promise((resolve, reject) => {
    // 边界判断:若传入的不是数组,直接reject
    if (!Array.isArray(promises)) {
      return reject(new TypeError('arguments must be an array'));
    }

    const result = []; // 存储所有Promise的成功结果
    let count = 0; // 记录已完成的Promise数量

    // 若数组为空,直接resolve空数组
    if (promises.length === 0) return resolve(result);

    // 遍历每个Promise
    promises.forEach((promise, index) => {
      // 兼容非Promise值(直接视为成功)
      Promise.resolve(promise).then(res => {
        result[index] = res; // 按原顺序存储结果
        count++;
        // 所有Promise都完成,resolve结果数组
        if (count === promises.length) {
          resolve(result);
        }
      }).catch(err => {
        // 有一个失败,直接reject该错误
        reject(err);
      });
    });
  });
};

// 测试
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);
Promise.myAll([p1, p2, p3]).then(res => {
  console.log(res); // [1,2,3]
});

14. 实现Promise.race方法

考察点:Promise并发控制、第一个完成的状态优先

// Promise.race:接收一个Promise数组,第一个完成(成功/失败)的结果作为最终结果
Promise.myRace = function(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('arguments must be an array'));
    }

    // 遍历每个Promise,第一个完成的直接改变状态
    promises.forEach(promise => {
      Promise.resolve(promise).then(res => {
        resolve(res); // 第一个成功,直接resolve
      }).catch(err => {
        reject(err); // 第一个失败,直接reject
      });
    });
  });
};

// 测试
const p1 = new Promise((resolve) => setTimeout(() => resolve(1), 1000));
const p2 = new Promise((resolve, reject) => setTimeout(() => reject('失败'), 500));
Promise.myRace([p1, p2]).catch(err => {
  console.log(err); // 失败(p2先完成,且失败)
});

15. 函数柯里化(Currying)

考察点:闭包、参数复用、函数式编程

// 柯里化:将多参数函数,转化为单参数函数的链式调用
function curry(fn) {
  // 闭包保存已传入的参数
  return function curried(...args) {
    // 若传入的参数数量 >= 原函数的参数数量,执行原函数
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      // 否则,返回一个新函数,接收剩余参数,递归调用curried
      return function(...args2) {
        return curried.apply(this, [...args, ...args2]);
      };
    }
  };
}

// 用法
const add = (a, b, c) => a + b + c; // 原函数,3个参数
const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // 6(链式调用)
console.log(curriedAdd(1, 2)(3)); // 6(支持部分参数)
console.log(curriedAdd(1, 2, 3)); // 6(支持完整参数)

16. 实现call方法

考察点:this绑定、函数执行、参数传递

// 模拟Function.prototype.call:改变函数this指向,立即执行函数
Function.prototype.myCall = function(context, ...args) {
  // 边界判断:若context为null/undefined,指向window(浏览器环境)
  context = context || window;
  // 给context添加一个临时属性,指向当前函数(this就是调用myCall的函数)
  const fnKey = Symbol('tempFn'); // 用Symbol避免属性冲突
  context[fnKey] = this;
  // 执行函数,传入参数,获取返回值
  const result = context[fnKey](...args);
  // 删除临时属性,避免污染context
  delete context[fnKey];
  // 返回函数执行结果
  return result;
};

// 测试
const obj = { name: '张三' };
function sayHello(age) {
  console.log(`我是${this.name},年龄${age}`);
}
sayHello.myCall(obj, 25); // 我是张三,年龄25

17. 实现apply方法

考察点:this绑定、函数执行、参数传递(与call的区别:参数是数组)

// 模拟Function.prototype.apply:改变this指向,参数以数组形式传递,立即执行
Function.prototype.myApply = function(context, args = []) {
  context = context || window;
  const fnKey = Symbol('tempFn');
  context[fnKey] = this;
  // 执行函数,args是数组,用扩展运算符展开
  const result = context[fnKey](...args);
  delete context[fnKey];
  return result;
};

// 测试
const obj = { name: '李四' };
function sayHello(age, gender) {
  console.log(`我是${this.name},年龄${age},性别${gender}`);
}
sayHello.myApply(obj, [28, '男']); // 我是李四,年龄28,性别男

18. 实现bind方法

考察点:this绑定、闭包、函数柯里化、构造函数兼容

// 模拟Function.prototype.bind:改变this指向,返回一个新函数,不立即执行
Function.prototype.myBind = function(context, ...args1) {
  const fn = this; // 保存当前函数(this就是调用myBind的函数)
  // 返回新函数
  const boundFn = function(...args2) {
    // 兼容构造函数:若新函数被new调用,this指向实例,否则指向context
    const isNew = this instanceof boundFn;
    const targetContext = isNew ? this : context;
    // 合并参数,执行原函数
    return fn.apply(targetContext, [...args1, ...args2]);
  };
  // 继承原函数的原型,确保new调用时,实例能访问原函数原型上的属性
  boundFn.prototype = Object.create(fn.prototype);
  return boundFn;
};

// 测试1:普通调用
const obj = { name: '王五' };
function sayHello(age) {
  console.log(`我是${this.name},年龄${age}`);
}
const boundSay = sayHello.myBind(obj, 30);
boundSay(); // 我是王五,年龄30

// 测试2:new调用(构造函数兼容)
function Person(name, age) {
  this.name = name;
  this.age = age;
}
const BoundPerson = Person.myBind(null, '赵六');
const person = new BoundPerson(35);
console.log(person.name); // 赵六
console.log(person.age); // 35

19. 实现防抖+立即执行版

考察点:防抖原理、立即执行逻辑、定时器控制

// 立即执行版防抖:第一次触发立即执行,之后频繁触发不执行,延迟后可再次触发
function debounceImmediate(fn, delay = 500) {
  let timer = null;
  return function(...args) {
    // 若定时器存在,清除定时器(取消延迟执行)
    if (timer) clearTimeout(timer);
    // 判断是否是第一次触发(timer为null)
    const isImmediate = !timer;
    if (isImmediate) {
      fn.apply(this, args); // 立即执行
    }
    // 重置定时器,延迟后清空timer,允许下次立即执行
    timer = setTimeout(() => {
      timer = null;
    }, delay);
  };
}

// 用法(按钮提交示例,避免重复提交,第一次点击立即执行)
const handleSubmit = debounceImmediate(() => {
  console.log('提交表单');
}, 1000);

20. 实现节流+立即执行/延迟执行可选版

考察点:节流原理、参数配置、灵活性优化

// 可选版节流:可配置立即执行(leading)和延迟执行(trailing)
function throttleOpt(fn, interval = 1000, options = { leading: true, trailing: false }) {
  const { leading, trailing } = options;
  let lastTime = 0;
  let timer = null;

  return function(...args) {
    const nowTime = Date.now();
    // 若不允许立即执行,且是第一次触发,重置lastTime
    if (!leading && !lastTime) {
      lastTime = nowTime;
    }

    // 计算剩余时间
    const remainingTime = interval - (nowTime - lastTime);
    // 剩余时间<=0,执行函数
    if (remainingTime <= 0) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      fn.apply(this, args);
      lastTime = nowTime;
    } else if (trailing && !timer) {
      // 允许延迟执行,且无定时器,设置延迟执行
      timer = setTimeout(() => {
        timer = null;
        lastTime = Date.now();
        fn.apply(this, args);
      }, remainingTime);
    }
  };
}

// 用法
// 立即执行,不延迟执行(默认)
const throttle1 = throttleOpt(() => console.log('立即执行'), 1000);
// 不立即执行,延迟执行
const throttle2 = throttleOpt(() => console.log('延迟执行'), 1000, { leading: false, trailing: true });

三、数据结构与算法(大厂高频,高级前端考点)

这类题考察数据结构基础和算法思维,是大厂面试的重点,也是拉开薪资差距的关键,建议重点练习。

21. 两数之和(Two Sum)

考察点:哈希表、时间复杂度优化(从O(n²)优化到O(n))

// 题目:给定一个整数数组和一个目标值,找出数组中和为目标值的两个整数的索引
function twoSum(nums, target) {
  const map = new Map(); // 用Map存储{数值: 索引},快速查找
  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i]; // 互补值
    // 若互补值存在于Map中,返回两个索引
    if (map.has(complement)) {
      return [map.get(complement), i];
    }
    // 否则,将当前数值和索引存入Map
    map.set(nums[i], i);
  }
  return []; // 无匹配项,返回空数组
}

// 测试
console.log(twoSum([2, 7, 11, 15], 9)); // [0,1]

22. LRU缓存机制

考察点:哈希表+双向链表、缓存淘汰策略(Vue3响应式缓存底层类似)

// LRU:最近最少使用,超出容量时,删除最久未使用的元素
class LRUCache {
  constructor(capacity) {
    this.capacity = capacity; // 缓存容量
    this.cache = new Map(); // Map特性:插入顺序就是访问顺序,可快速获取最久未使用的元素
  }

  // 获取元素:访问后,将元素移到最近使用的位置
  get(key) {
    if (!this.cache.has(key)) return -1; // 无该元素,返回-1
    const value = this.cache.get(key);
    this.cache.delete(key); // 删除旧位置
    this.cache.set(key, value); // 重新插入,移到末尾(最近使用)
    return value;
  }

  // 存入元素:超出容量时,删除最久未使用的元素(Map的第一个键)
  put(key, value) {
    // 若元素已存在,先删除(避免覆盖后,位置不变)
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }
    // 超出容量,删除最久未使用的元素
    if (this.cache.size >= this.capacity) {
      const oldestKey = this.cache.keys().next().value; // Map的第一个键是最久未使用的
      this.cache.delete(oldestKey);
    }
    // 存入新元素,移到最近使用的位置
    this.cache.set(key, value);
  }
}

// 测试
const lru = new LRUCache(2);
lru.put(1, 1);
lru.put(2, 2);
console.log(lru.get(1)); // 1(访问后,1变为最近使用)
lru.put(3, 3); // 超出容量,删除最久未使用的2
console.log(lru.get(2)); // -1(已被删除)

23. 发布-订阅模式(EventBus)

考察点:设计模式、组件通信、回调函数管理(Vue EventBus底层原理)

// 发布-订阅模式:实现组件间通信,解耦
class EventEmitter {
  constructor() {
    this.events = new Map(); // 存储事件:{事件名: [回调函数数组]}
  }

  // 订阅事件:绑定回调函数
  on(event, callback) {
    if (!this.events.has(event)) {
      this.events.set(event, []); // 若事件不存在,初始化回调数组
    }
    this.events.get(event).push(callback); // 加入回调数组
  }

  // 发布事件:触发该事件的所有回调函数
  emit(event, ...args) {
    const callbacks = this.events.get(event);
    if (callbacks) {
      // 执行所有回调,传入参数
      callbacks.forEach(cb => cb(...args));
    }
  }

  // 取消订阅:移除指定事件的指定回调
  off(event, callback) {
    const callbacks = this.events.get(event);
    if (callbacks) {
      // 过滤掉要取消的回调,保留其他回调
      this.events.set(event, callbacks.filter(cb => cb !== callback));
      // 若回调数组为空,删除该事件
      if (this.events.get(event).length === 0) {
        this.events.delete(event);
      }
    }
  }

  // 一次性订阅:触发一次后,自动取消订阅
  once(event, callback) {
    // 包装回调,执行后取消订阅
    const wrapCallback = (...args) => {
      callback(...args);
      this.off(event, wrapCallback);
    };
    this.on(event, wrapCallback);
  }
}

// 测试
const bus = new EventEmitter();
const callback = (msg) => console.log('收到消息:', msg);

bus.on('message', callback);
bus.emit('message', 'Hello World'); // 收到消息:Hello World

bus.off('message', callback);
bus.emit('message', 'Hello Again'); // 无输出(已取消订阅)

bus.once('onceEvent', () => console.log('一次性事件'));
bus.emit('onceEvent'); // 一次性事件
bus.emit('onceEvent'); // 无输出(已自动取消)

24. 实现链表反转(单链表)

考察点:链表数据结构、指针操作、递归/迭代思维

// 1. 定义单链表节点
class ListNode {
  constructor(val = 0, next = null) {
    this.val = val;
    this.next = next;
  }
}

// 解法1:迭代法(推荐,空间复杂度O(1))
function reverseList(head) {
  let prev = null; // 前驱节点
  let curr = head; // 当前节点
  while (curr !== null) {
    const next = curr.next; // 保存下一个节点
    curr.next = prev; // 反转当前节点的指针
    prev = curr; // 前驱节点后移
    curr = next; // 当前节点后移
  }
  return prev; // 反转后,prev是新的头节点
}

// 解法2:递归法(易理解,空间复杂度O(n))
function reverseListRecursive(head) {
  // 终止条件:空节点或只有一个节点,直接返回
  if (head === null || head.next === null) return head;
  // 递归反转后续节点
  const newHead = reverseListRecursive(head.next);
  // 反转当前节点和下一个节点的指针
  head.next.next = head;
  head.next = null; // 避免循环
  return newHead;
}

// 测试
const head = new ListNode(1, new ListNode(2, new ListNode(3)));
const reversedHead = reverseList(head);
// 遍历反转后的链表:3 -> 2 -> 1
let curr = reversedHead;
while (curr) {
  console.log(curr.val); // 3 2 1
  curr = curr.next;
}

25. 判断回文链表

考察点:链表操作、双指针、回文判断

// 题目:判断一个单链表是否是回文(正读和反读一样)
// 步骤:1. 找到链表中点;2. 反转后半部分;3. 对比前半部分和反转后的后半部分
function isPalindrome(head) {
  if (head === null || head.next === null) return true; // 空链表或单个节点,是回文

  // 1. 找到链表中点(慢指针走1步,快指针走2步,快指针到末尾时,慢指针到中点)
  let slow = head;
  let fast = head;
  while (fast.next !== null && fast.next.next !== null) {
    slow = slow.next;
    fast = fast.next.next;
  }

  // 2. 反转后半部分链表(从slow.next开始)
  let prev = null;
  let curr = slow.next;
  while (curr !== null) {
    const next = curr.next;
    curr.next = prev;
    prev = curr;
    curr = next;
  }
  slow.next = prev; // 反转后的后半部分链表,头节点是prev

  // 3. 对比前半部分和反转后的后半部分
  let left = head;
  let right = prev;
  while (right !== null) {
    if (left.val !== right.val) return false; // 不相等,不是回文
    left = left.next;
    right = right.next;
  }
  return true; // 全部相等,是回文
}

// 测试
const head1 = new ListNode(1, new ListNode(2, new ListNode(1)));
console.log(isPalindrome(head1)); // true

const head2 = new ListNode(1, new ListNode(2, new ListNode(3)));
console.log(isPalindrome(head2)); // false

26. 二叉树的前序遍历(递归+迭代)

考察点:二叉树数据结构、遍历算法、递归/迭代思维

// 1. 定义二叉树节点
class TreeNode {
  constructor(val = 0, left = null, right = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

// 解法1:递归法(简洁,易理解)
function preorderTraversalRecursive(root, result = []) {
  if (root === null) return result;
  result.push(root.val); // 根节点
  preorderTraversalRecursive(root.left, result); // 左子树
  preorderTraversalRecursive(root.right, result); // 右子树
  return result;
}

// 解法2:迭代法(面试常考,避免递归栈溢出)
function preorderTraversalIterative(root) {
  const result = [];
  if (root === null) return result;
  const stack = [root]; // 用栈存储节点
  while (stack.length > 0) {
    const node = stack.pop(); // 弹出栈顶节点(根节点)
    result.push(node.val);
    // 注意:栈是先进后出,所以先压右子树,再压左子树
    if (node.right !== null) stack.push(node.right);
    if (node.left !== null) stack.push(node.left);
  }
  return result;
}

// 测试
const root = new TreeNode(1, null, new TreeNode(2, new TreeNode(3)));
console.log(preorderTraversalRecursive(root)); // [1,2,3]
console.log(preorderTraversalIterative(root)); // [1,2,3]

27. 二叉树的中序遍历(递归+迭代)

考察点:二叉树遍历、栈的应用

// 解法1:递归法
function inorderTraversalRecursive(root, result = []) {
  if (root === null) return result;
  inorderTraversalRecursive(root.left, result); // 左子树
  result.push(root.val); // 根节点
  inorderTraversalRecursive(root.right, result); // 右子树
  return result;
}

// 解法2:迭代法
function inorderTraversalIterative(root) {
  const result = [];
  const stack = [];
  let curr = root;
  while (curr !== null || stack.length > 0) {
    // 先遍历左子树,所有左节点入栈
    while (curr !== null) {
      stack.push(curr);
      curr = curr.left;
    }
    // 弹出栈顶节点(左子树最底层节点),加入结果
    curr = stack.pop();
    result.push(curr.val);
    // 遍历右子树
    curr = curr.right;
  }
  return result;
}

// 测试
const root = new TreeNode(1, null, new TreeNode(2, new TreeNode(3)));
console.log(inorderTraversalRecursive(root)); // [1,3,2]
console.log(inorderTraversalIterative(root)); // [1,3,2]

28. 斐波那契数列(递归+迭代+优化)

考察点:递归、动态规划、时间/空间复杂度优化

// 题目:求第n个斐波那契数(F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2))

// 解法1:递归法(简单但效率低,时间复杂度O(2ⁿ),有重复计算)
function fibRecursive(n) {
  if (n <= 1) return n;
  return fibRecursive(n - 1) + fibRecursive(n - 2);
}

// 解法2:迭代法(推荐,时间复杂度O(n),空间复杂度O(1))
function fibIterative(n) {
  if (n <= 1) return n;
  let prevPrev = 0; // F(n-2)
  let prev = 1; // F(n-1)
  let curr = 0;
  for (let i = 2; i <= n; i++) {
    curr = prevPrev + prev; // F(n) = F(n-2) + F(n-1)
    prevPrev = prev;
    prev = curr;
  }
  return curr;
}

// 解法3:动态规划(空间复杂度O(n),适合需要保存所有斐波那契数的场景)
function fibDP(n) {
  if (n <= 1) return n;
  const dp = new Array(n + 1);
  dp[0] = 0;
  dp[1] = 1;
  for (let i = 2; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2];
  }
  return dp[n];
}

// 测试
console.log(fibIterative(10)); // 55

29. 最长公共前缀

考察点:字符串操作、遍历对比、边界处理

// 题目:编写一个函数,找出字符串数组中的最长公共前缀
function longestCommonPrefix(strs) {
  // 边界判断:数组为空,返回空字符串
  if (strs.length === 0) return '';
  // 以第一个字符串为基准,逐个字符对比
  let prefix = strs[0];
  for (let i = 1; i < strs.length; i++) {
    // 循环对比当前字符串和基准字符串,直到找到公共前缀
    while (strs[i].indexOf(prefix) !== 0) {
      // 若不匹配,缩短基准字符串(去掉最后一个字符)
      prefix = prefix.slice(0, prefix.length - 1);
      // 若基准字符串为空,说明没有公共前缀,直接返回
      if (prefix === '') return '';
    }
  }
  return prefix;
}

// 测试
console.log(longestCommonPrefix(["flower","flow","flight"])); // "fl"
console.log(longestCommonPrefix(["dog","racecar","car"])); // ""

30. 验证回文串

考察点:字符串处理、正则表达式、双指针

// 题目:验证一个字符串是否是回文串(只考虑字母和数字,忽略大小写)
function isPalindromeStr(s) {
  // 1. 过滤无效字符(只保留字母和数字),并转为小写
  const validStr = s.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
  // 2. 双指针:左指针从开头,右指针从末尾,逐步对比
  let left = 0;
  let right = validStr.length - 1;
  while (left < right) {
    if (validStr[left] !== validStr[right]) {
      return false; // 不相等,不是回文串
    }
    left++;
    right--;
  }
  return true; // 全部相等,是回文串
}

// 测试
console.log(isPalindromeStr("A man, a plan, a canal: Panama")); // true
console.log(isPalindromeStr("race a car")); // false

写在最后

这30个手写JS算法,覆盖了前端面试从基础到进阶的所有高频考点——基础语法、数组操作、原型作用域、数据结构、算法思维,每道题都能直接复制到编辑器调试,吃透这30道题,面试时再遇到手写算法题,就能从容应对。

很多前端同学觉得算法难,其实是没找对方法:不用刷上千道题,重点吃透这些高频题,搞懂每道题的原理和考点,比盲目刷题更有效。


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!