前端手撕自用

10 阅读16分钟

🚀 JavaScript 手撕面试题合集

整理了前端开发中最常见的手撕代码面试题,包含详细注释和测试用例


📑 目录


一、数组相关

1.1 数组与树的转换

⭐⭐⭐⭐⭐ 扁平数组转树形结构

题目描述: 将带有 idparentId 的扁平数组转换为树形结构

解题思路:

  1. 第一次遍历:创建哈希表,建立 id 到节点的映射
  2. 第二次遍历:根据 parentId 建立父子关系
/**
 * 数组转树形结构
 * @param {Array} arr - 扁平数组
 * @returns {Array} - 树形结构数组
 */
function arrayToTree(arr) {
  if (!Array.isArray(arr) || !arr?.length) return [];
  
  // 1. 创建哈希表映射
  let map = {};
  let tree = [];
  
  arr.forEach((item) => {
    map[item?.id] = { ...item, children: [] };
  });
  
  // 2. 建立父子关系
  arr.forEach((item) => {
    if (item?.parentId) {
      map[item?.parentId].children.push(map[item.id]);
    } else {
      tree.push(map[item.id]);
    }
  });
  
  return tree;
}

// 测试用例
const arr = [
  { id: 1, parentId: null, name: 'A' },
  { id: 2, parentId: 1, name: 'B' },
  { id: 3, parentId: 1, name: 'C' },
  { id: 4, parentId: 2, name: 'D' }
];
console.log(arrayToTree(arr));
// 结果: [{ id: 1, name: 'A', children: [...] }]

时间复杂度: O(n) 空间复杂度: O(n)


⭐⭐⭐⭐ 树形结构转扁平数组

解题思路: 深度优先遍历,提取节点信息

/**
 * 树形结构转扁平数组
 * @param {Array} tree - 树形结构
 * @returns {Array} - 扁平数组
 */
function treeToFlat(tree) {
  const result = [];
  
  function traverse(node) {
    const { children, ...nodeData } = node;
    result.push(nodeData);
  
    if (children && children.length > 0) {
      children.forEach(child => traverse(child));
    }
  }
  
  if (Array.isArray(tree)) {
    tree.forEach(node => traverse(node));
  } else {
    traverse(tree);
  }
  
  return result;
}

时间复杂度: O(n) 空间复杂度: O(h) - h为树的高度


1.2 数组去重

⭐⭐⭐⭐⭐ 三种去重方法

方法1:双层循环 + splice

function unique(arr) {
  const result = [...arr];
  for (let i = 0; i < result.length; i++) {
    for (let j = i + 1; j < result.length; j++) {
      if (result[i] === result[j]) {
        result.splice(j, 1);
        j--; // 注意:删除后需要j--
      }
    }
  }
  return result;
}
  • 时间复杂度:O(n²)
  • 空间复杂度:O(n)

方法2:filter + indexOf

function unique2(arr) {
  return arr.filter((item, index, self) => {
    return self.indexOf(item) === index;
  });
}
  • 时间复杂度:O(n²)
  • 空间复杂度:O(n)

方法3:Set(推荐)

function unique3(arr) {
  return Array.from(new Set(arr));
  // 或者:return [...new Set(arr)];
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
  • 最简洁高效的方法

1.3 数组扁平化

⭐⭐⭐⭐⭐ 三种扁平化方法

方法1:递归 + reduce

function flatten(arr) {
  return arr.reduce((acc, curr) => {
    return acc.concat(Array.isArray(curr) ? flatten(curr) : curr);
  }, []);
}

方法2:flat方法(ES2019)

function flatten2(arr) {
  return arr.flat(Infinity);
}

方法3:toString(仅适用于数字)

function flatten3(arr) {
  return arr.toString().split(',').map(Number);
}

1.4 栈与队列

⭐⭐⭐⭐ 括号匹配检查
function isBalanced(expression) {
  const stack = [];
  const brackets = { '(': ')', '[': ']', '{': '}' };
  
  for (let char of expression) {
    if (brackets[char]) {
      // 左括号入栈
      stack.push(char);
    } else if (Object.values(brackets).includes(char)) {
      // 右括号检查匹配
      if (stack.length === 0 || brackets[stack.pop()] !== char) {
        return false;
      }
    }
  }
  
  return stack.length === 0;
}

console.log(isBalanced('({[]})')); // true
console.log(isBalanced('({[})')); // false

⭐⭐⭐⭐ 实现Undo/Redo功能
class Editor {
  constructor() {
    this.content = '';
    this.undoStack = [];
    this.redoStack = [];
  }
  
  type(text) {
    this.undoStack.push(this.content);
    this.redoStack = [];
    this.content += text;
  }
  
  undo() {
    if (this.undoStack.length > 0) {
      this.redoStack.push(this.content);
      this.content = this.undoStack.pop();
    }
  }
  
  redo() {
    if (this.redoStack.length > 0) {
      this.undoStack.push(this.content);
      this.content = this.redoStack.pop();
    }
  }
}

二、字符串相关

2.1 URL解析

⭐⭐⭐⭐⭐ 解析URL参数
function parseUrl(url) {
  const str = url.split("?")[1];
  if (!str) return {};
  
  const result = {};
  str.split("&").forEach(item => { 
    const [key, value] = item.split("=");
    const decodedValue = decodeURIComponent(value || '');
  
    if (Object.hasOwn(result, key)) {
      // 处理重复key,转为数组
      result[key] = [].concat(result[key], decodedValue);
    } else if (decodedValue === "undefined") {
      result[key] = true;
    } else {
      result[key] = decodedValue;
    }
  });
  
  return result;
}

const url = "https://example.com?name=John&age=30&tag=js&tag=html";
console.log(parseUrl(url));
// { name: 'John', age: '30', tag: ['js', 'html'] }

2.2 命名转换

⭐⭐⭐⭐ 下划线转驼峰
function toCamelCase(str) {
  return str.replace(/_(\w)/g, (_, letter) => letter.toUpperCase());
}

console.log(toCamelCase('hello_world_test')); // helloWorldTest
⭐⭐⭐⭐ 驼峰转下划线
function toSnakeCase(str) {
  return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
}

console.log(toSnakeCase('helloWorldTest')); // hello_world_test

2.3 字符串反转与回文

⭐⭐⭐ 字符串反转
function reverseString(str) {
  return str.split('').reverse().join('');
}
⭐⭐⭐⭐ 判断回文字符串
function isPalindrome(str) {
  const cleaned = str.toLowerCase().replace(/[^a-z0-9]/g, '');
  return cleaned === cleaned.split('').reverse().join('');
}

console.log(isPalindrome('A man, a plan, a canal: Panama')); // true

2.4 字符串压缩

⭐⭐⭐⭐ 连续字符计数压缩
function compressString(str) {
  if (!str) return str;
  
  let result = '';
  let count = 1;
  
  for (let i = 0; i < str.length; i++) {
    if (str[i] === str[i + 1]) {
      count++;
    } else {
      result += str[i] + (count > 1 ? count : '');
      count = 1;
    }
  }
  
  return result.length < str.length ? result : str;
}

console.log(compressString('aabcccccaaa')); // a2bc5a3

2.5 千分位格式化

⭐⭐⭐⭐⭐ 支持小数的千分位格式化
// 方法1: 正则(支持小数)
function formatNumberWithDecimal(num) {
  const [integer, decimal] = num.toString().split('.');
  const formattedInteger = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  return decimal ? `${formattedInteger}.${decimal}` : formattedInteger;
}

// 方法2: 支持负数和指定精度
function formatNumberComplete(num, decimals = 2) {
  const isNegative = num < 0;
  const absNum = Math.abs(num);
  const rounded = absNum.toFixed(decimals);
  const [integer, decimal] = rounded.split('.');
  const formattedInteger = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  let result = decimal ? `${formattedInteger}.${decimal}` : formattedInteger;
  return isNegative ? `-${result}` : result;
}

console.log(formatNumberComplete(1234567.89, 2)); // 1,234,567.89
console.log(formatNumberComplete(-1234567.89, 2)); // -1,234,567.89

正则解释:

  • \B:非单词边界
  • (?=(\d{3})+(?!\d)):正向预查,匹配后面是3的倍数个数字且后面不是数字的位置

2.6 模板引擎

⭐⭐⭐⭐ 简易模板引擎
function templateEngine(template, data) {
  return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
    return data[key] !== undefined ? data[key] : match;
  });
}

const template = 'Hello {{name}}, you are {{age}} years old!';
const data = { name: 'Alice', age: 25 };
console.log(templateEngine(template, data));
// Hello Alice, you are 25 years old!

2.7 版本号比较

⭐⭐⭐⭐ 版本号大小比较
function compareVersion(v1, v2) {
  const arr1 = v1.split('.').map(Number);
  const arr2 = v2.split('.').map(Number);
  const maxLen = Math.max(arr1.length, arr2.length);
  
  for (let i = 0; i < maxLen; i++) {
    const num1 = arr1[i] || 0;
    const num2 = arr2[i] || 0;
  
    if (num1 > num2) return 1;
    if (num1 < num2) return -1;
  }
  
  return 0;
}

console.log(compareVersion('1.2.3', '1.2.4')); // -1
console.log(compareVersion('1.3.0', '1.2.9')); // 1

2.8 大数相加

⭐⭐⭐⭐⭐ 字符串模拟大数相加
function addBigNumbers(num1, num2) {
  let i = num1.length - 1;
  let j = num2.length - 1;
  let carry = 0;
  let result = '';
  
  while (i >= 0 || j >= 0 || carry > 0) {
    const digit1 = i >= 0 ? parseInt(num1[i]) : 0;
    const digit2 = j >= 0 ? parseInt(num2[j]) : 0;
    const sum = digit1 + digit2 + carry;
  
    result = (sum % 10) + result;
    carry = Math.floor(sum / 10);
  
    i--;
    j--;
  }
  
  return result;
}

console.log(addBigNumbers('123456789', '987654321')); // 1111111110

三、Promise相关

3.1 手写Promise方法

⭐⭐⭐⭐⭐ Promise.all
function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    if (promises.length === 0) {
      resolve([]);
      return;
    }
  
    const result = new Array(promises.length);
    let count = 0;
  
    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then(res => {
          result[index] = res;
          count++;
    
          if (count === promises.length) {
            resolve(result);
          }
        })
        .catch(reject);
    });
  });
}

特点:

  • 所有成功才成功
  • 一个失败就失败
  • 保持结果顺序

⭐⭐⭐⭐⭐ Promise.race
function promiseRace(promises) {
  return new Promise((resolve, reject) => {
    if (promises.length === 0) return;
  
    promises.forEach(promise => {
      Promise.resolve(promise)
        .then(resolve)
        .catch(reject);
    });
  });
}

特点: 第一个完成的结果(成功或失败)


⭐⭐⭐⭐ Promise.allSettled
function promiseAllSettled(promises) {
  return new Promise((resolve) => {
    if (promises.length === 0) {
      resolve([]);
      return;
    }
  
    const result = new Array(promises.length);
    let count = 0;
  
    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then(value => {
          result[index] = { status: 'fulfilled', value };
        })
        .catch(reason => {
          result[index] = { status: 'rejected', reason };
        })
        .finally(() => {
          count++;
          if (count === promises.length) {
            resolve(result);
          }
        });
    });
  });
}

特点: 等待所有完成,返回每个的状态


3.2 Promise并发控制

⭐⭐⭐⭐⭐ 限制并发数量
function promiseLimit(promises, limit) {
  return new Promise((resolve, reject) => {
    let index = 0;
    let running = 0;
    let finished = 0;
    const result = [];
  
    function run() {
      while (running < limit && index < promises.length) {
        const currentIndex = index;
        const promise = promises[index++];
        running++;
  
        Promise.resolve(promise)
          .then(res => {
            result[currentIndex] = res;
          })
          .catch(err => {
            result[currentIndex] = err;
          })
          .finally(() => {
            running--;
            finished++;
      
            if (finished === promises.length) {
              resolve(result);
            } else {
              run();
            }
          });
      }
    }
  
    run();
  });
}

应用场景:

  • 批量上传文件(限制并发数)
  • 批量请求(避免服务器压力过大)
  • 爬虫控制并发

3.3 Promise重试机制

⭐⭐⭐⭐ 请求失败自动重试
function promiseRetry(fn, times = 3, delay = 1000) {
  return new Promise((resolve, reject) => {
    function attempt(remainingTimes) {
      fn()
        .then(resolve)
        .catch(error => {
          if (remainingTimes === 0) {
            reject(error);
          } else {
            console.log(`重试中... 剩余次数: ${remainingTimes}`);
            setTimeout(() => {
              attempt(remainingTimes - 1);
            }, delay);
          }
        });
    }
  
    attempt(times);
  });
}

3.4 手写Promise类

⭐⭐⭐⭐⭐ 简易版Promise实现
class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
  
    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        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(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };
  
    const promise2 = new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            resolve(x);
          } catch (error) {
            reject(error);
          }
        });
      }
  
      if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            resolve(x);
          } catch (error) {
            reject(error);
          }
        });
      }
  
      if (this.state === 'pending') {
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              resolve(x);
            } catch (error) {
              reject(error);
            }
          });
        });
  
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              resolve(x);
            } catch (error) {
              reject(error);
            }
          });
        });
      }
    });
  
    return promise2;
  }
  
  catch(onRejected) {
    return this.then(null, onRejected);
  }
}

四、定时器相关

4.1 setTimeout实现setInterval

⭐⭐⭐⭐⭐ 用setTimeout模拟setInterval
function mySetInterval(fn, delay) {
  let timerId = null;
  let stopped = false;
  
  function interval() {
    if (stopped) return;
  
    fn();
    timerId = setTimeout(interval, delay);
  }
  
  timerId = setTimeout(interval, delay);
  
  // 返回清除函数
  return () => {
    stopped = true;
    clearTimeout(timerId);
  };
}

// 使用
const clear = mySetInterval(() => {
  console.log('执行');
}, 1000);

// 清除
// clear();

优势:

  • 避免setInterval的任务堆积问题
  • 更精确的时间控制

4.2 防抖与节流

⭐⭐⭐⭐⭐ 防抖(Debounce)
function debounce(fn, delay) {
  let timerId = null;
  
  return function(...args) {
    if (timerId) {
      clearTimeout(timerId);
    }
  
    timerId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 使用场景:搜索框输入、窗口resize
const debouncedSearch = debounce((keyword) => {
  console.log('搜索:', keyword);
}, 300);

原理: 等待delay时间后执行,期间有新调用则重新计时


⭐⭐⭐⭐⭐ 节流(Throttle)
// 方法1: 时间戳版本(立即执行)
function throttle(fn, delay) {
  let lastTime = 0;
  
  return function(...args) {
    const now = Date.now();
  
    if (now - lastTime >= delay) {
      fn.apply(this, args);
      lastTime = now;
    }
  };
}

// 方法2: 定时器版本(延迟执行)
function throttle2(fn, delay) {
  let timerId = null;
  
  return function(...args) {
    if (timerId) return;
  
    timerId = setTimeout(() => {
      fn.apply(this, args);
      timerId = null;
    }, delay);
  };
}

// 使用场景:滚动事件、鼠标移动
const throttledScroll = throttle(() => {
  console.log('滚动事件');
}, 200);

原理: 每隔delay时间最多执行一次


4.3 定时打印

⭐⭐⭐⭐⭐ 每隔一秒打印1,2,3,4

方法1:使用let(块级作用域)

function printNumbers1() {
  for (let i = 1; i <= 4; i++) {
    setTimeout(() => {
      console.log(i);
    }, i * 1000);
  }
}

方法2:使用闭包

function printNumbers2() {
  for (var i = 1; i <= 4; i++) {
    (function(num) {
      setTimeout(() => {
        console.log(num);
      }, num * 1000);
    })(i);
  }
}

方法3:使用setTimeout的第三个参数

function printNumbers3() {
  for (var i = 1; i <= 4; i++) {
    setTimeout((num) => {
      console.log(num);
    }, i * 1000, i);
  }
}

五、设计模式

5.1 发布-订阅模式

⭐⭐⭐⭐⭐ EventEmitter实现
class EventEmitter {
  constructor() {
    this.events = {};
  }
  
  // 订阅事件
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  
    // 返回取消订阅函数
    return () => this.off(event, callback);
  }
  
  // 订阅一次
  once(event, callback) {
    const wrapper = (...args) => {
      callback(...args);
      this.off(event, wrapper);
    };
    this.on(event, wrapper);
  }
  
  // 发布事件
  emit(event, ...args) {
    if (!this.events[event]) return;
  
    this.events[event].forEach(callback => {
      callback(...args);
    });
  }
  
  // 取消订阅
  off(event, callback) {
    if (!this.events[event]) return;
  
    if (!callback) {
      delete this.events[event];
    } else {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
  }
  
  // 清空所有订阅
  clear() {
    this.events = {};
  }
}

// 使用
const eventBus = new EventEmitter();
eventBus.on('login', (user) => console.log(`${user} 登录了`));
eventBus.emit('login', 'Alice');

应用场景:

  • Vue的EventBus
  • Node.js的EventEmitter
  • 跨组件通信

5.2 观察者模式

⭐⭐⭐⭐ Subject-Observer
// 主题(被观察者)
class Subject {
  constructor() {
    this.observers = [];
    this.state = null;
  }
  
  attach(observer) {
    this.observers.push(observer);
  }
  
  detach(observer) {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }
  
  notify() {
    this.observers.forEach(observer => {
      observer.update(this.state);
    });
  }
  
  setState(state) {
    this.state = state;
    this.notify();
  }
}

// 观察者
class Observer {
  constructor(name) {
    this.name = name;
  }
  
  update(state) {
    console.log(`${this.name} 收到更新: ${state}`);
  }
}

// 使用
const subject = new Subject();
const observer1 = new Observer('观察者1');
subject.attach(observer1);
subject.setState('新状态');

发布订阅 vs 观察者:

  • 观察者:Subject直接通知Observer(紧耦合)
  • 发布订阅:通过事件中心解耦(松耦合)

5.3 单例模式

⭐⭐⭐⭐⭐ 三种单例实现

方法1:闭包

const Singleton = (function() {
  let instance;
  
  class Singleton {
    constructor(name) {
      if (instance) {
        return instance;
      }
      this.name = name;
      instance = this;
    }
  }
  
  return Singleton;
})();

方法2:静态属性

class Singleton {
  constructor(name) {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    this.name = name;
    Singleton.instance = this;
  }
  
  static getInstance(name) {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton(name);
    }
    return Singleton.instance;
  }
}

方法3:Proxy

function proxySingleton(className) {
  let instance;
  return new Proxy(className, {
    construct(target, args) {
      if (!instance) {
        instance = new target(...args);
      }
      return instance;
    }
  });
}

5.4 工厂模式

⭐⭐⭐⭐ 简单工厂
class SimpleFactory {
  static createProduct(type) {
    switch (type) {
      case 'A':
        return new ProductA();
      case 'B':
        return new ProductB();
      default:
        throw new Error('未知产品类型');
    }
  }
}

class ProductA {
  constructor() {
    this.type = 'A';
  }
}

class ProductB {
  constructor() {
    this.type = 'B';
  }
}

// 使用
const productA = SimpleFactory.createProduct('A');

5.5 代理模式

⭐⭐⭐⭐⭐ 缓存代理
function cacheProxy(fn) {
  const cache = new Map();
  
  return function(...args) {
    const key = JSON.stringify(args);
  
    if (cache.has(key)) {
      console.log('从缓存获取');
      return cache.get(key);
    }
  
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// 使用
const fibonacci = (n) => {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
};

const cachedFib = cacheProxy(fibonacci);
console.log(cachedFib(10)); // 计算
console.log(cachedFib(10)); // 从缓存获取

六、数学相关

6.1 斐波那契数列

⭐⭐⭐⭐⭐ 三种实现方式

方法1:动态规划(推荐)

function fibonacciDP(n) {
  if (n <= 1) return n;
  
  let prev = 0, curr = 1;
  for (let i = 2; i <= n; i++) {
    [prev, curr] = [curr, prev + curr];
  }
  return curr;
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

方法2:递归(效率低)

function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}
  • 时间复杂度:O(2^n)

方法3:记忆化递归

function fibonacciMemo(n, memo = {}) {
  if (n in memo) return memo[n];
  if (n <= 1) return n;
  
  memo[n] = fibonacciMemo(n - 1, memo) + fibonacciMemo(n - 2, memo);
  return memo[n];
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

6.2 质数判断

⭐⭐⭐⭐ 判断是否为质数
function isPrime(n) {
  if (n <= 1) return false;
  if (n <= 3) return true;
  if (n % 2 === 0 || n % 3 === 0) return false;
  
  // 只需检查到 sqrt(n)
  for (let i = 5; i * i <= n; i += 6) {
    if (n % i === 0 || n % (i + 2) === 0) {
      return false;
    }
  }
  return true;
}

优化点:

  • 排除2和3的倍数
  • 只检查到√n
  • 步长为6(质数分布规律)

6.3 最大公约数与最小公倍数

⭐⭐⭐⭐⭐ 辗转相除法(欧几里得算法)

最大公约数(GCD)

function gcd(a, b) {
  return b === 0 ? a : gcd(b, a % b);
}

// 迭代版本
function gcdIterative(a, b) {
  while (b !== 0) {
    [a, b] = [b, a % b];
  }
  return a;
}

最小公倍数(LCM)

function lcm(a, b) {
  return Math.abs(a * b) / gcd(a, b);
}

console.log(gcd(48, 18)); // 6
console.log(lcm(12, 18)); // 36

6.4 快速幂算法

⭐⭐⭐⭐ 二进制优化幂运算
function power(base, exp) {
  if (exp === 0) return 1;
  if (exp < 0) return 1 / power(base, -exp);
  
  let result = 1;
  while (exp > 0) {
    if (exp % 2 === 1) {
      result *= base;
    }
    base *= base;
    exp = Math.floor(exp / 2);
  }
  return result;
}

console.log(power(2, 10)); // 1024

优势: O(log n) 时间复杂度,比直接连乘快得多


七、排序算法

7.1 冒泡排序

⭐⭐⭐ 基础排序算法
function bubbleSort(arr) {
  const len = arr.length;
  
  for (let i = 0; i < len - 1; i++) {
    let swapped = 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]];
        swapped = true;
      }
    }
  
    // 优化:如果没有交换,提前结束
    if (!swapped) break;
  }
  
  return arr;
}
  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)
  • 稳定性:稳定

7.2 快速排序

⭐⭐⭐⭐⭐ 最常用的排序算法
function quickSort(arr, left = 0, right = arr.length - 1) {
  if (left < right) {
    const pivotIndex = partition(arr, left, right);
    quickSort(arr, left, pivotIndex - 1);
    quickSort(arr, pivotIndex + 1, right);
  }
  return arr;
}

function partition(arr, left, right) {
  const pivot = arr[right];
  let i = left - 1;
  
  for (let j = left; j < right; j++) {
    if (arr[j] <= pivot) {
      i++;
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
  }
  
  [arr[i + 1], arr[right]] = [arr[right], arr[i + 1]];
  return i + 1;
}
  • 时间复杂度:平均O(nlogn),最坏O(n²)
  • 空间复杂度:O(logn)
  • 稳定性:不稳定
  • 实际应用中最快的排序算法

7.3 归并排序

⭐⭐⭐⭐⭐ 稳定的O(nlogn)排序
function mergeSort(arr) {
  if (arr.length <= 1) return arr;
  
  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));
  
  return merge(left, right);
}

function merge(left, right) {
  const result = [];
  let i = 0, j = 0;
  
  while (i < left.length && j < right.length) {
    if (left[i] <= right[j]) {
      result.push(left[i++]);
    } else {
      result.push(right[j++]);
    }
  }
  
  return result.concat(left.slice(i)).concat(right.slice(j));
}
  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(n)
  • 稳定性:稳定
  • 需要稳定排序时的首选

7.4 堆排序

⭐⭐⭐⭐ 原地排序
function heapSort(arr) {
  const len = arr.length;
  
  // 1. 构建最大堆
  for (let i = Math.floor(len / 2) - 1; i >= 0; i--) {
    heapify(arr, len, i);
  }
  
  // 2. 依次取出堆顶元素
  for (let i = len - 1; i > 0; i--) {
    [arr[0], arr[i]] = [arr[i], arr[0]];
    heapify(arr, i, 0);
  }
  
  return arr;
}

function heapify(arr, len, i) {
  let largest = i;
  const left = 2 * i + 1;
  const right = 2 * i + 2;
  
  if (left < len && arr[left] > arr[largest]) {
    largest = left;
  }
  
  if (right < len && arr[right] > arr[largest]) {
    largest = right;
  }
  
  if (largest !== i) {
    [arr[i], arr[largest]] = [arr[largest], arr[i]];
    heapify(arr, len, largest);
  }
}
  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(1)
  • 稳定性:不稳定
  • 空间受限时的最佳选择

八、JS基础

8.1 深拷贝

⭐⭐⭐⭐⭐ 完整的深拷贝实现
function deepClone(obj, map = new WeakMap()) {
  // 基本类型直接返回
  if (obj === null || typeof obj !== 'object') return obj;
  
  // 处理循环引用
  if (map.has(obj)) return map.get(obj);
  
  // 特殊对象处理
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  
  // Set
  if (obj instanceof Set) {
    const cloneSet = new Set();
    map.set(obj, cloneSet);
    obj.forEach(value => {
      cloneSet.add(deepClone(value, map));
    });
    return cloneSet;
  }
  
  // Map
  if (obj instanceof Map) {
    const cloneMap = new Map();
    map.set(obj, cloneMap);
    obj.forEach((value, key) => {
      cloneMap.set(deepClone(key, map), deepClone(value, map));
    });
    return cloneMap;
  }
  
  // 数组或对象
  const clone = Array.isArray(obj) ? [] : {};
  map.set(obj, clone);
  
  // 包括 Symbol 和不可枚举属性
  Reflect.ownKeys(obj).forEach(key => {
    clone[key] = deepClone(obj[key], map);
  });
  
  return clone;
}

处理的场景:

  • ✅ 基本类型
  • ✅ 普通对象和数组
  • ✅ 循环引用
  • ✅ Date、RegExp
  • ✅ Set、Map
  • ✅ Symbol属性

8.2 手写new

⭐⭐⭐⭐⭐ new操作符的实现
function myNew(constructor, ...args) {
  // 1. 创建一个新对象,继承构造函数的原型
  const obj = Object.create(constructor.prototype);
  
  // 2. 执行构造函数,将this绑定到新对象
  const result = constructor.apply(obj, args);
  
  // 3. 如果构造函数返回对象,则返回该对象,否则返回新对象
  return result !== null && (typeof result === 'object' || typeof result === 'function') 
    ? result 
    : obj;
}

// 测试
function Person(name, age) {
  this.name = name;
  this.age = age;
}

const person = myNew(Person, 'John', 20);
console.log(person); // { name: 'John', age: 20 }

new的四个步骤:

  1. 创建新对象
  2. 绑定原型
  3. 绑定this并执行构造函数
  4. 返回新对象(或构造函数返回的对象)

8.3 手写instanceof

⭐⭐⭐⭐⭐ instanceof操作符的实现
function myInstanceOf(obj, constructor) {
  let proto = Object.getPrototypeOf(obj);
  let prototype = constructor.prototype;
  
  while (true) {
    if (proto === null) return false;
    if (proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
}

// 测试
console.log(myInstanceOf([], Array)); // true
console.log(myInstanceOf({}, Array)); // false

原理: 沿着原型链查找,判断对象的原型链上是否存在构造函数的prototype


8.4 this指向

⭐⭐⭐⭐⭐ 经典this指向问题
var name = 'window';

const obj = {
  name: 'obj',
  sayName: function() {
    console.log(this.name);
  },
  sayArrow: () => {
    console.log(this.name);
  }
};

// 情况1:直接调用
obj.sayName(); // 'obj' - 隐式绑定

// 情况2:赋值后调用
const fn = obj.sayName;
fn(); // 'window' - 默认绑定

// 情况3:setTimeout
setTimeout(obj.sayName, 100); // 'window' - 回调函数默认绑定
setTimeout(() => obj.sayName(), 100); // 'obj' - 箭头函数内调用

// 情况4:箭头函数
obj.sayArrow(); // 'window' - 箭头函数继承外层this

// 情况5:构造函数
function Person(name) {
  this.name = name;
}
const person = new Person('person');
console.log(person.name); // 'person' - new绑定

this绑定规则优先级:

  1. new绑定 > 显式绑定(call/apply/bind)
  2. 显式绑定 > 隐式绑定(obj.method())
  3. 隐式绑定 > 默认绑定(独立函数调用)
  4. 箭头函数不绑定this,继承外层

8.5 手写call

⭐⭐⭐⭐⭐ 改变this指向并立即执行
Function.prototype.myCall = function(context, ...args) {
  // 1. 判断调用者是否为函数
  if (typeof this !== 'function') {
    throw new TypeError('Error');
  }
  
  // 2. 处理context(null/undefined指向window)
  context = context || window;
  
  // 3. 将函数作为context的属性
  const fn = Symbol('fn'); // 使用Symbol避免属性名冲突
  context[fn] = this;
  
  // 4. 执行函数并获取结果
  const result = context[fn](...args);
  
  // 5. 删除临时属性
  delete context[fn];
  
  // 6. 返回结果
  return result;
};

// 测试
function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`;
}

const person = { name: 'Alice' };
console.log(greet.myCall(person, 'Hello', '!')); // "Hello, Alice!"

核心思路:

  1. 将函数设为对象的方法
  2. 执行该方法
  3. 删除该方法
  4. 返回结果

8.6 手写apply

⭐⭐⭐⭐⭐ call的数组版本
Function.prototype.myApply = function(context, args = []) {
  // 1. 判断调用者是否为函数
  if (typeof this !== 'function') {
    throw new TypeError('Error');
  }
  
  // 2. 处理context
  context = context || window;
  
  // 3. 将函数作为context的属性
  const fn = Symbol('fn');
  context[fn] = this;
  
  // 4. 执行函数(参数以数组形式传入)
  const result = context[fn](...args);
  
  // 5. 删除临时属性
  delete context[fn];
  
  // 6. 返回结果
  return result;
};

// 测试
function sum(a, b, c) {
  console.log(this.prefix);
  return a + b + c;
}

const obj = { prefix: '结果是:' };
console.log(sum.myApply(obj, [1, 2, 3])); // "结果是:" 6

call vs apply:

  • call(context, arg1, arg2, ...) - 参数列表
  • apply(context, [arg1, arg2, ...]) - 参数数组

8.7 手写bind

⭐⭐⭐⭐⭐ 返回一个绑定this的新函数
Function.prototype.myBind = function(context, ...args) {
  // 1. 判断调用者是否为函数
  if (typeof this !== 'function') {
    throw new TypeError('Error');
  }
  
  // 2. 保存原函数
  const self = this;
  
  // 3. 返回一个新函数
  const fBound = function(...newArgs) {
    // 4. 如果作为构造函数调用(new fBound()),this指向实例
    // 否则this指向绑定的context
    return self.apply(
      this instanceof fBound ? this : context,
      args.concat(newArgs)
    );
  };
  
  // 5. 维护原型链
  if (this.prototype) {
    fBound.prototype = Object.create(this.prototype);
  }
  
  return fBound;
};

// 测试1:普通函数
function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`;
}

const person = { name: 'Bob' };
const boundGreet = greet.myBind(person, 'Hi');
console.log(boundGreet('!')); // "Hi, Bob!"
console.log(boundGreet('?')); // "Hi, Bob?"

// 测试2:构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}

const BoundPerson = Person.myBind(null, 'Charlie');
const p = new BoundPerson(25);
console.log(p.name); // "Charlie"
console.log(p.age); // 25

bind的特点:

  1. 返回新函数,不立即执行
  2. 可以分步传参(柯里化)
  3. 作为构造函数时,绑定的this无效
  4. 维护原型链

简化版(不支持new):

Function.prototype.myBindSimple = function(context, ...args) {
  const self = this;
  return function(...newArgs) {
    return self.apply(context, args.concat(newArgs));
  };
};

8.8 函数柯里化

⭐⭐⭐⭐⭐ 将多参数函数转换为单参数函数序列

基础版本:

function curry(fn) {
  return function curried(...args) {
    // 如果参数够了,直接执行
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
  
    // 参数不够,返回一个新函数继续接收参数
    return function(...newArgs) {
      return curried.apply(this, args.concat(newArgs));
    };
  };
}

// 测试
function add(a, b, c) {
  return a + b + c;
}

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

高级版本(支持占位符):

function curry(fn, placeholder = '_') {
  return function curried(...args) {
    // 检查是否有占位符
    const hasPlaceholder = args.some(arg => arg === placeholder);
  
    // 没有占位符且参数够了,直接执行
    if (!hasPlaceholder && args.length >= fn.length) {
      return fn.apply(this, args);
    }
  
    // 返回新函数
    return function(...newArgs) {
      // 替换占位符
      let index = 0;
      const mergedArgs = args.map(arg => {
        if (arg === placeholder && index < newArgs.length) {
          return newArgs[index++];
        }
        return arg;
      });
    
      // 添加剩余参数
      while (index < newArgs.length) {
        mergedArgs.push(newArgs[index++]);
      }
    
      return curried.apply(this, mergedArgs);
    };
  };
}

// 测试
function sum(a, b, c, d) {
  return a + b + c + d;
}

const curriedSum = curry(sum);
console.log(curriedSum(1, 2, 3, 4)); // 10
console.log(curriedSum('_', 2)(1, 3, 4)); // 10
console.log(curriedSum('_', '_', 3)(1)(2, 4)); // 10

无限参数柯里化:

function curryInfinity(fn) {
  const args = [];
  
  function curried(...newArgs) {
    if (newArgs.length === 0) {
      // 没有参数时,执行函数
      return fn.apply(this, args);
    }
  
    // 收集参数
    args.push(...newArgs);
    return curried;
  }
  
  return curried;
}

// 测试
function sumAll(...nums) {
  return nums.reduce((a, b) => a + b, 0);
}

const curriedSumAll = curryInfinity(sumAll);
console.log(curriedSumAll(1)(2)(3)(4)()); // 10
console.log(curriedSumAll(1, 2)(3, 4)()); // 10

应用场景:

  1. 参数复用
  2. 延迟执行
  3. 函数组合
  4. 事件处理

实际应用例子:

// 1. 正则验证
const match = curry((pattern, str) => pattern.test(str));
const hasNumber = match(/[0-9]+/);
const hasLetter = match(/[a-zA-Z]+/);

console.log(hasNumber('abc123')); // true
console.log(hasLetter('123')); // false

// 2. 日志记录
const log = curry((level, time, message) => {
  console.log(`[${level}] ${time}: ${message}`);
});

const errorLog = log('ERROR');
const errorLogNow = errorLog(new Date().toISOString());

errorLogNow('Something went wrong'); // [ERROR] 2026-01-29...: Something went wrong

8.9 手写Object.create

⭐⭐⭐⭐⭐ 创建新对象并设置原型
function myCreate(proto, propertiesObject) {
  // 1. 参数校验
  if (typeof proto !== 'object' && typeof proto !== 'function') {
    throw new TypeError('Object prototype may only be an Object or null');
  }
  
  // 2. 创建一个空的构造函数
  function F() {}
  
  // 3. 将构造函数的原型指向proto
  F.prototype = proto;
  
  // 4. 创建实例
  const obj = new F();
  
  // 5. 处理第二个参数(属性描述符)
  if (propertiesObject !== undefined) {
    Object.defineProperties(obj, propertiesObject);
  }
  
  return obj;
}

// 测试1:基本使用
const person = {
  isHuman: true,
  greet: function() {
    console.log(`Hello, I'm ${this.name}`);
  }
};

const john = myCreate(person);
john.name = 'John';
john.greet(); // "Hello, I'm John"
console.log(john.isHuman); // true

// 测试2:使用属性描述符
const obj = myCreate(Object.prototype, {
  foo: {
    value: 'bar',
    writable: true,
    enumerable: true,
    configurable: true
  },
  baz: {
    value: 42,
    writable: false
  }
});

console.log(obj.foo); // "bar"
console.log(obj.baz); // 42

简化版本:

function myCreateSimple(proto) {
  function F() {}
  F.prototype = proto;
  return new F();
}

Object.create的特点:

  1. 创建一个新对象
  2. 新对象的 __proto__ 指向传入的原型对象
  3. 可以传入 null 创建纯净对象(没有原型)
  4. 第二个参数可以定义属性

使用场景:

// 1. 实现继承
function Parent(name) {
  this.name = name;
}

Parent.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}

// 使用Object.create实现原型继承
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

// 2. 创建纯净对象(没有Object.prototype的方法)
const pureObj = Object.create(null);
pureObj.name = 'test';
console.log(pureObj.toString); // undefined(没有继承Object.prototype)

// 3. 克隆对象(保持原型链)
const original = { x: 1, y: 2 };
const clone = Object.create(
  Object.getPrototypeOf(original),
  Object.getOwnPropertyDescriptors(original)
);

📊 复杂度对比表

排序算法对比

算法平均时间最坏时间空间稳定性适用场景
冒泡排序O(n²)O(n²)O(1)稳定教学
选择排序O(n²)O(n²)O(1)不稳定交换次数少
插入排序O(n²)O(n²)O(1)稳定小规模/近乎有序
快速排序O(nlogn)O(n²)O(logn)不稳定大规模数据
归并排序O(nlogn)O(nlogn)O(n)稳定需要稳定排序
堆排序O(nlogn)O(nlogn)O(1)不稳定空间受限

🎯 面试高频 TOP 15

  1. ⭐⭐⭐⭐⭐ Promise.all - 几乎必考
  2. ⭐⭐⭐⭐⭐ 深拷贝 - 基础必会
  3. ⭐⭐⭐⭐⭐ 手写call/apply/bind - 原理必会
  4. ⭐⭐⭐⭐⭐ 防抖节流 - 性能优化
  5. ⭐⭐⭐⭐⭐ 函数柯里化 - 函数式编程
  6. ⭐⭐⭐⭐⭐ 数组转树 - 业务常见
  7. ⭐⭐⭐⭐⭐ 快速排序 - 算法基础
  8. ⭐⭐⭐⭐⭐ EventEmitter - 设计模式
  9. ⭐⭐⭐⭐⭐ 手写new - 原理理解
  10. ⭐⭐⭐⭐⭐ 千分位格式化 - 字符串处理
  11. ⭐⭐⭐⭐⭐ 并发控制 - 异步编程
  12. ⭐⭐⭐⭐⭐ this指向 - 基础必会
  13. ⭐⭐⭐⭐⭐ 手写instanceof - 原型链理解
  14. ⭐⭐⭐⭐⭐ Object.create - 对象创建
  15. ⭐⭐⭐⭐⭐ Promise并发控制 - 高级异步

💡 学习建议

按优先级学习

  1. 必须掌握(⭐⭐⭐⭐⭐)

    • Promise相关(all、race、并发控制)
    • 深拷贝
    • 手写call/apply/bind - 新增
    • 函数柯里化 - 新增
    • 防抖节流
    • 数组转树
    • EventEmitter
    • Object.create - 新增
  2. 重点掌握(⭐⭐⭐⭐)

    • 快速排序、归并排序
    • 千分位格式化
    • URL解析
    • 手写new/instanceof
    • 单例模式
    • this指向问题
  3. 了解即可(⭐⭐⭐)

    • 其他排序算法
    • 数学算法
    • 其他设计模式