Skip to content

前端手写题专题

前端面试必考手写题,重点掌握 异步编程深拷贝函数增强

📊 题目总览

题目难度梯队关键技巧
Promise.all / race / allSettled⭐⭐⭐第一梯队计数/竞态
深拷贝⭐⭐⭐第一梯队WeakMap/类型判断
防抖节流⭐⭐第二梯队定时器/闭包
call / apply / bind⭐⭐第二梯队this 绑定
LRU 缓存⭐⭐第二梯队Map/双向链表
柯里化⭐⭐第三梯队闭包/递归
Promise 完整实现⭐⭐⭐进阶状态机/微任务
事件发布订阅⭐⭐第三梯队观察者模式

1. Promise.all / race / allSettled

难度: ⭐⭐⭐ | 梯队: 第一梯队 ✅ | 标签: 异步, Promise

题目描述

实现 Promise.allPromise.racePromise.allSettled 三个静态方法。

  • Promise.all: 所有 Promise 都成功才 resolve,任一失败立即 reject
  • Promise.race: 任意一个 Promise 完成(成功或失败)就返回
  • Promise.allSettled: 等待所有 Promise 完成,无论成功失败

代码实现

javascript
/**
 * Promise.all 实现
 * 所有成功才 resolve,任一失败就 reject
 * @param {Iterable} promises
 * @return {Promise}
 */
Promise.myAll = function (promises) {
  return new Promise((resolve, reject) => {
    // 处理非数组输入(可迭代对象)
    const promiseArray = Array.from(promises);
    const results = new Array(promiseArray.length);
    let resolvedCount = 0;

    // 空数组直接返回
    if (promiseArray.length === 0) {
      resolve(results);
      return;
    }

    promiseArray.forEach((promise, index) => {
      // 使用 Promise.resolve 包装,处理非 Promise 值
      Promise.resolve(promise)
        .then((value) => {
          results[index] = value;  // 保持顺序
          resolvedCount++;

          if (resolvedCount === promiseArray.length) {
            resolve(results);
          }
        })
        .catch(reject);  // 任一失败立即 reject
    });
  });
};

/**
 * Promise.race 实现
 * 任意一个完成就返回(成功或失败)
 * @param {Iterable} promises
 * @return {Promise}
 */
Promise.myRace = function (promises) {
  return new Promise((resolve, reject) => {
    const promiseArray = Array.from(promises);

    // 空数组永远 pending(符合规范)
    for (const promise of promiseArray) {
      Promise.resolve(promise).then(resolve, reject);
    }
  });
};

/**
 * Promise.allSettled 实现
 * 等待所有 Promise 完成(无论成功失败)
 * @param {Iterable} promises
 * @return {Promise}
 */
Promise.myAllSettled = function (promises) {
  return new Promise((resolve) => {
    const promiseArray = Array.from(promises);
    const results = new Array(promiseArray.length);
    let settledCount = 0;

    if (promiseArray.length === 0) {
      resolve(results);
      return;
    }

    promiseArray.forEach((promise, index) => {
      Promise.resolve(promise)
        .then(
          (value) => {
            results[index] = { status: 'fulfilled', value };
          },
          (reason) => {
            results[index] = { status: 'rejected', reason };
          }
        )
        .finally(() => {
          settledCount++;
          if (settledCount === promiseArray.length) {
            resolve(results);
          }
        });
    });
  });
};

/**
 * Promise.any 实现(补充)
 * 任意一个成功就返回,全部失败才 reject
 */
Promise.myAny = function (promises) {
  return new Promise((resolve, reject) => {
    const promiseArray = Array.from(promises);
    const errors = new Array(promiseArray.length);
    let rejectedCount = 0;

    if (promiseArray.length === 0) {
      reject(new AggregateError([], 'All promises were rejected'));
      return;
    }

    promiseArray.forEach((promise, index) => {
      Promise.resolve(promise).then(resolve, (reason) => {
        errors[index] = reason;
        rejectedCount++;

        if (rejectedCount === promiseArray.length) {
          reject(new AggregateError(errors, 'All promises were rejected'));
        }
      });
    });
  });
};

示例调用

javascript
// ========== Promise.all ==========
// 基础示例:全部成功
const allResult = await Promise.myAll([
  Promise.resolve(1),
  Promise.resolve(2),
  new Promise((r) => setTimeout(() => r(3), 100)),
]);
console.log(allResult); // [1, 2, 3]

// 边界条件:空数组
console.log(await Promise.myAll([])); // []

// 边界条件:包含非 Promise 值
console.log(await Promise.myAll([1, 2, 3])); // [1, 2, 3]

// 边界条件:任一失败
try {
  await Promise.myAll([
    Promise.resolve(1),
    Promise.reject('error'),
    Promise.resolve(3),
  ]);
} catch (e) {
  console.log(e); // 'error'
}

// ========== Promise.race ==========
// 基础示例
const raceResult = await Promise.myRace([
  new Promise((r) => setTimeout(() => r('slow'), 200)),
  new Promise((r) => setTimeout(() => r('fast'), 100)),
]);
console.log(raceResult); // 'fast'

// 边界条件:有 reject
const raceReject = await Promise.myRace([
  new Promise((r) => setTimeout(() => r('slow'), 200)),
  Promise.reject('error'),
]).catch((e) => e);
console.log(raceReject); // 'error'

// ========== Promise.allSettled ==========
const settledResult = await Promise.myAllSettled([
  Promise.resolve(1),
  Promise.reject('error'),
  Promise.resolve(3),
]);
console.log(settledResult);
// [
//   { status: 'fulfilled', value: 1 },
//   { status: 'rejected', reason: 'error' },
//   { status: 'fulfilled', value: 3 }
// ]

复杂度分析

  • 时间: O(n)
  • 空间: O(n)

2. 深拷贝

难度: ⭐⭐⭐ | 梯队: 第一梯队 ✅ | 标签: 递归, WeakMap

题目描述

实现一个深拷贝函数,支持:

  1. 基本类型
  2. 对象和数组
  3. 循环引用
  4. 特殊对象(Date、RegExp、Map、Set)
  5. Symbol 键

代码实现

javascript
/**
 * 深拷贝 - 完整版
 * @param {any} obj - 要拷贝的对象
 * @param {WeakMap} hash - 用于处理循环引用
 * @return {any}
 */
function deepClone(obj, hash = new WeakMap()) {
  // 处理 null 和非对象类型
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 处理循环引用
  if (hash.has(obj)) {
    return hash.get(obj);
  }

  // 处理 Date
  if (obj instanceof Date) {
    return new Date(obj.getTime());
  }

  // 处理 RegExp
  if (obj instanceof RegExp) {
    return new RegExp(obj.source, obj.flags);
  }

  // 处理 Map
  if (obj instanceof Map) {
    const clone = new Map();
    hash.set(obj, clone);
    obj.forEach((value, key) => {
      clone.set(deepClone(key, hash), deepClone(value, hash));
    });
    return clone;
  }

  // 处理 Set
  if (obj instanceof Set) {
    const clone = new Set();
    hash.set(obj, clone);
    obj.forEach((value) => {
      clone.add(deepClone(value, hash));
    });
    return clone;
  }

  // 处理数组和普通对象
  const clone = Array.isArray(obj) ? [] : {};
  hash.set(obj, clone);

  // 获取所有键(包括 Symbol)
  const keys = [
    ...Object.keys(obj),
    ...Object.getOwnPropertySymbols(obj)
  ];

  for (const key of keys) {
    clone[key] = deepClone(obj[key], hash);
  }

  return clone;
}

/**
 * 简易版深拷贝(面试快速手写)
 * 不支持:循环引用、特殊对象、Symbol
 */
function deepCloneSimple(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  const clone = Array.isArray(obj) ? [] : {};

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepCloneSimple(obj[key]);
    }
  }

  return clone;
}

/**
 * JSON 方法(最简单但有局限)
 * 不支持:函数、undefined、Symbol、循环引用、Date、RegExp
 */
function deepCloneJSON(obj) {
  return JSON.parse(JSON.stringify(obj));
}

示例调用

javascript
// 基础示例
const obj1 = { a: 1, b: { c: 2 } };
const cloned1 = deepClone(obj1);
console.log(cloned1);           // { a: 1, b: { c: 2 } }
console.log(cloned1 !== obj1);  // true
console.log(cloned1.b !== obj1.b); // true

// 边界条件:null 和 undefined
console.log(deepClone(null));       // null
console.log(deepClone(undefined));  // undefined

// 边界条件:基本类型
console.log(deepClone(42));         // 42
console.log(deepClone('hello'));    // 'hello'
console.log(deepClone(true));       // true

// 边界条件:数组
const arr = [1, [2, 3], { a: 4 }];
const clonedArr = deepClone(arr);
console.log(clonedArr);             // [1, [2, 3], { a: 4 }]
console.log(clonedArr[1] !== arr[1]); // true

// 边界条件:循环引用
const circular = { a: 1 };
circular.self = circular;
const clonedCircular = deepClone(circular);
console.log(clonedCircular.self === clonedCircular); // true

// 边界条件:特殊对象
const special = {
  date: new Date('2024-01-01'),
  regex: /hello/gi,
  map: new Map([['key', 'value']]),
  set: new Set([1, 2, 3]),
};
const clonedSpecial = deepClone(special);
console.log(clonedSpecial.date instanceof Date); // true
console.log(clonedSpecial.date !== special.date); // true
console.log(clonedSpecial.regex.test('hello'));  // true

// 边界条件:Symbol 键
const sym = Symbol('test');
const withSymbol = { [sym]: 'symbol value', a: 1 };
const clonedSymbol = deepClone(withSymbol);
console.log(clonedSymbol[sym]); // 'symbol value'

// 边界条件:空对象
console.log(deepClone({}));  // {}
console.log(deepClone([]));  // []

复杂度分析

  • 时间: O(n),n 为对象中的属性总数
  • 空间: O(n),递归栈 + WeakMap

3. 防抖节流

难度: ⭐⭐ | 梯队: 第二梯队 | 标签: 闭包, 定时器

题目描述

  • 防抖 (debounce): 事件触发后延迟执行,期间再次触发则重新计时
  • 节流 (throttle): 事件触发后在一定时间内只执行一次

代码实现

javascript
/**
 * 防抖 - 基础版
 * @param {Function} fn - 要执行的函数
 * @param {number} delay - 延迟时间(ms)
 * @return {Function}
 */
function debounce(fn, delay) {
  let timer = null;

  return function (...args) {
    // 清除之前的定时器
    if (timer) clearTimeout(timer);

    // 设置新的定时器
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

/**
 * 防抖 - 完整版
 * 支持:立即执行、取消、返回值
 */
function debounceAdvanced(fn, delay, immediate = false) {
  let timer = null;
  let result;

  const debounced = function (...args) {
    if (timer) clearTimeout(timer);

    if (immediate) {
      // 立即执行模式
      const callNow = !timer;
      timer = setTimeout(() => {
        timer = null;
      }, delay);

      if (callNow) {
        result = fn.apply(this, args);
      }
    } else {
      // 延迟执行模式
      timer = setTimeout(() => {
        fn.apply(this, args);
      }, delay);
    }

    return result;
  };

  // 取消方法
  debounced.cancel = function () {
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
  };

  return debounced;
}

/**
 * 节流 - 时间戳版(首次立即执行)
 */
function throttleTimestamp(fn, interval) {
  let lastTime = 0;

  return function (...args) {
    const now = Date.now();

    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

/**
 * 节流 - 定时器版(延迟执行)
 */
function throttleTimer(fn, interval) {
  let timer = null;

  return function (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, interval);
    }
  };
}

/**
 * 节流 - 完整版
 * 支持:首次执行、尾部执行、取消
 */
function throttle(fn, interval, options = {}) {
  const { leading = true, trailing = true } = options;
  let lastTime = 0;
  let timer = null;

  const throttled = function (...args) {
    const now = Date.now();

    // 首次不执行
    if (!leading && lastTime === 0) {
      lastTime = now;
    }

    const remaining = interval - (now - lastTime);

    if (remaining <= 0) {
      // 时间到了,立即执行
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      lastTime = now;
      fn.apply(this, args);
    } else if (!timer && trailing) {
      // 设置尾部执行
      timer = setTimeout(() => {
        lastTime = leading ? Date.now() : 0;
        timer = null;
        fn.apply(this, args);
      }, remaining);
    }
  };

  throttled.cancel = function () {
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    lastTime = 0;
  };

  return throttled;
}

示例调用

javascript
// ========== 防抖示例 ==========
// 基础用法
const debouncedLog = debounce((msg) => console.log(msg), 300);
debouncedLog('a');
debouncedLog('b');
debouncedLog('c'); // 只输出 'c'(300ms 后)

// 立即执行版
const debouncedImmediate = debounceAdvanced(
  (msg) => console.log(msg),
  300,
  true
);
debouncedImmediate('first'); // 立即输出
debouncedImmediate('second'); // 被忽略
// 300ms 后可以再次触发

// 取消防抖
const cancellable = debounceAdvanced((msg) => console.log(msg), 300);
cancellable('test');
cancellable.cancel(); // 取消,不会输出

// ========== 节流示例 ==========
// 基础用法
const throttledLog = throttle((msg) => console.log(msg), 1000);
throttledLog('a'); // 立即输出
throttledLog('b'); // 1秒内被忽略
throttledLog('c'); // 1秒内被忽略
// 1秒后再调用会立即输出

// 配置选项
const throttledNoLeading = throttle(
  (msg) => console.log(msg),
  1000,
  { leading: false, trailing: true }
);
throttledNoLeading('a'); // 不立即执行
// 1秒后输出 'a'

// 边界条件:delay 为 0
const debouncedZero = debounce((x) => x * 2, 0);
// 仍然会异步执行(进入事件循环)

// 边界条件:this 绑定
const obj = {
  value: 42,
  getValue: debounce(function () {
    console.log(this.value);
  }, 100),
};
obj.getValue(); // 100ms 后输出 42

复杂度分析

  • 时间: O(1)
  • 空间: O(1)

4. call / apply / bind

难度: ⭐⭐ | 梯队: 第二梯队 | 标签: this, 原型链

题目描述

实现 Function.prototype 上的 call、apply、bind 方法。

代码实现

javascript
/**
 * call 实现
 * 以指定 this 调用函数,参数逐个传入
 */
Function.prototype.myCall = function (context, ...args) {
  // 处理 null/undefined,默认指向全局对象
  context = context == null ? globalThis : Object(context);

  // 创建唯一属性名,避免覆盖原有属性
  const fn = Symbol('fn');
  context[fn] = this;

  // 调用并获取结果
  const result = context[fn](...args);

  // 删除临时属性
  delete context[fn];

  return result;
};

/**
 * apply 实现
 * 以指定 this 调用函数,参数以数组形式传入
 */
Function.prototype.myApply = function (context, args = []) {
  context = context == null ? globalThis : Object(context);

  const fn = Symbol('fn');
  context[fn] = this;

  const result = context[fn](...args);

  delete context[fn];

  return result;
};

/**
 * bind 实现
 * 返回一个绑定了 this 的新函数,支持 new 调用
 */
Function.prototype.myBind = function (context, ...args) {
  const self = this;

  const boundFunc = function (...newArgs) {
    // 判断是否是 new 调用
    // new 调用时 this 是 boundFunc 的实例
    const isNew = this instanceof boundFunc;
    return self.apply(isNew ? this : context, [...args, ...newArgs]);
  };

  // 维护原型关系,支持 new 调用
  if (this.prototype) {
    boundFunc.prototype = Object.create(this.prototype);
  }

  return boundFunc;
};

示例调用

javascript
// ========== call 示例 ==========
function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`;
}

const person = { name: 'Alice' };

// 基础用法
console.log(greet.myCall(person, 'Hello', '!')); // 'Hello, Alice!'

// 边界条件:null/undefined
function getName() {
  return this === globalThis;
}
console.log(getName.myCall(null));      // true
console.log(getName.myCall(undefined)); // true

// 边界条件:原始类型会被包装
function getThis() {
  return this;
}
console.log(typeof getThis.myCall(42)); // 'object' (Number)

// 边界条件:无参数
function noArgs() {
  return this.value;
}
console.log(noArgs.myCall({ value: 100 })); // 100

// ========== apply 示例 ==========
console.log(greet.myApply(person, ['Hi', '?'])); // 'Hi, Alice?'

// 边界条件:空数组
console.log(noArgs.myApply({ value: 200 }, [])); // 200

// 边界条件:undefined 作为参数
console.log(noArgs.myApply({ value: 300 }, undefined)); // 300

// ========== bind 示例 ==========
const boundGreet = greet.myBind(person, 'Hey');
console.log(boundGreet('~')); // 'Hey, Alice~'

// 边界条件:多次 bind
const doubleBound = boundGreet.myBind({ name: 'Bob' });
console.log(doubleBound('!')); // 'Hey, Alice!' (第一次 bind 的 this 生效)

// 边界条件:new 调用
function Person(name) {
  this.name = name;
}
const BoundPerson = Person.myBind({ name: 'ignored' });
const instance = new BoundPerson('Charlie');
console.log(instance.name); // 'Charlie' (new 调用时忽略绑定的 this)
console.log(instance instanceof Person); // true

// 边界条件:无参数 bind
const bound = greet.myBind(person);
console.log(bound('Welcome', '!!')); // 'Welcome, Alice!!'

复杂度分析

  • 时间: O(1)
  • 空间: O(1)

5. LRU 缓存

难度: ⭐⭐ | 梯队: 第二梯队 | 标签: 数据结构, Map LeetCode: 146. LRU 缓存

题目描述

设计一个 LRU (最近最少使用) 缓存,支持 getput 操作,都是 O(1) 时间复杂度。

代码实现

javascript
/**
 * LRU Cache - 使用 Map(推荐)
 * JavaScript Map 自动维护插入顺序
 */
class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map();
  }

  /**
   * 获取键值,访问后移到最新位置
   * @param {number} key
   * @return {number}
   */
  get(key) {
    if (!this.cache.has(key)) return -1;

    // 访问后移到最新位置
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);
    return value;
  }

  /**
   * 设置键值,超出容量时淘汰最老的
   * @param {number} key
   * @param {number} value
   */
  put(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else if (this.cache.size >= this.capacity) {
      // 删除最老的(Map 第一个元素)
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }
    this.cache.set(key, value);
  }
}

/**
 * LRU Cache - 双向链表实现(面试常考)
 * get/put 都是 O(1)
 */
class Node {
  constructor(key, value) {
    this.key = key;
    this.value = value;
    this.prev = null;
    this.next = null;
  }
}

class LRUCacheLinkedList {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map(); // key -> Node

    // 虚拟头尾节点
    this.head = new Node(0, 0);
    this.tail = new Node(0, 0);
    this.head.next = this.tail;
    this.tail.prev = this.head;
  }

  /**
   * 从链表中移除节点
   */
  _remove(node) {
    node.prev.next = node.next;
    node.next.prev = node.prev;
  }

  /**
   * 插入到头部(最新位置)
   */
  _insertToHead(node) {
    node.next = this.head.next;
    node.prev = this.head;
    this.head.next.prev = node;
    this.head.next = node;
  }

  get(key) {
    if (!this.cache.has(key)) return -1;

    const node = this.cache.get(key);
    // 移到头部
    this._remove(node);
    this._insertToHead(node);
    return node.value;
  }

  put(key, value) {
    if (this.cache.has(key)) {
      const node = this.cache.get(key);
      node.value = value;
      this._remove(node);
      this._insertToHead(node);
    } else {
      if (this.cache.size >= this.capacity) {
        // 删除尾部(最老)
        const oldest = this.tail.prev;
        this._remove(oldest);
        this.cache.delete(oldest.key);
      }
      const newNode = new Node(key, value);
      this.cache.set(key, newNode);
      this._insertToHead(newNode);
    }
  }
}

示例调用

javascript
// 基础示例
const cache = new LRUCache(2);
cache.put(1, 1);
cache.put(2, 2);
console.log(cache.get(1));      // 1
cache.put(3, 3);                // 淘汰 key 2
console.log(cache.get(2));      // -1 (已淘汰)
cache.put(4, 4);                // 淘汰 key 1
console.log(cache.get(1));      // -1 (已淘汰)
console.log(cache.get(3));      // 3
console.log(cache.get(4));      // 4

// 边界条件:容量为 1
const cache1 = new LRUCache(1);
cache1.put(1, 1);
cache1.put(2, 2);
console.log(cache1.get(1));     // -1
console.log(cache1.get(2));     // 2

// 边界条件:更新已存在的键
const cache2 = new LRUCache(2);
cache2.put(1, 1);
cache2.put(1, 10);              // 更新
console.log(cache2.get(1));     // 10

// 边界条件:get 不存在的键
console.log(cache2.get(999));   // -1

// 边界条件:连续 put 相同的键
const cache3 = new LRUCache(2);
cache3.put(1, 1);
cache3.put(1, 2);
cache3.put(1, 3);
console.log(cache3.get(1));     // 3

// 边界条件:get 后再 put
const cache4 = new LRUCache(2);
cache4.put(1, 1);
cache4.put(2, 2);
cache4.get(1);                  // 1 变成最新
cache4.put(3, 3);               // 淘汰 2
console.log(cache4.get(2));     // -1
console.log(cache4.get(1));     // 1

复杂度分析

  • 时间: O(1) get/put
  • 空间: O(capacity)

6. 柯里化

难度: ⭐⭐ | 梯队: 第三梯队 | 标签: 闭包, 递归

题目描述

实现函数柯里化,将多参数函数转换为一系列单参数函数。

代码实现

javascript
/**
 * 通用柯里化函数
 * @param {Function} fn - 要柯里化的函数
 * @return {Function}
 */
function curry(fn) {
  return function curried(...args) {
    // 如果参数足够,直接调用
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    // 否则返回新函数,继续收集参数
    return function (...moreArgs) {
      return curried.apply(this, [...args, ...moreArgs]);
    };
  };
}

/**
 * 柯里化 - 支持占位符
 * 使用 _ 作为占位符
 */
function curryWithPlaceholder(fn, placeholder = curry.placeholder) {
  return function curried(...args) {
    // 检查是否有占位符
    const hasPlaceholder = args.some(arg => arg === placeholder);

    // 如果参数足够且没有占位符,直接调用
    if (args.length >= fn.length && !hasPlaceholder) {
      return fn.apply(this, args);
    }

    return function (...moreArgs) {
      // 用新参数替换占位符
      const newArgs = args.map(arg => 
        arg === placeholder && moreArgs.length ? moreArgs.shift() : arg
      );
      // 追加剩余的新参数
      return curried.apply(this, [...newArgs, ...moreArgs]);
    };
  };
}
curryWithPlaceholder.placeholder = Symbol('_');

/**
 * 链式调用的 add 函数
 * add(1)(2)(3) == 6
 */
function add(num) {
  let sum = num;

  function innerAdd(n) {
    sum += n;
    return innerAdd;
  }

  // 隐式类型转换
  innerAdd.toString = innerAdd.valueOf = () => sum;

  return innerAdd;
}

/**
 * 支持多参数的 add
 * add(1, 2)(3)(4, 5) == 15
 */
function addMulti(...args) {
  let sum = args.reduce((a, b) => a + b, 0);

  function innerAdd(...nextArgs) {
    sum += nextArgs.reduce((a, b) => a + b, 0);
    return innerAdd;
  }

  innerAdd.toString = innerAdd.valueOf = () => sum;

  return innerAdd;
}

示例调用

javascript
// ========== 通用柯里化 ==========
function add3(a, b, c) {
  return a + b + c;
}

const curriedAdd = curry(add3);

// 基础用法
console.log(curriedAdd(1)(2)(3));       // 6
console.log(curriedAdd(1, 2)(3));       // 6
console.log(curriedAdd(1)(2, 3));       // 6
console.log(curriedAdd(1, 2, 3));       // 6

// 边界条件:无参数函数
const noArgsFn = curry(() => 42);
console.log(noArgsFn());                 // 42

// 边界条件:单参数函数
const singleArg = curry((x) => x * 2);
console.log(singleArg(5));               // 10

// 边界条件:保持 this 绑定
const obj = {
  value: 10,
  add: curry(function(a, b) {
    return this.value + a + b;
  })
};
console.log(obj.add(1)(2));              // 13

// ========== 占位符柯里化 ==========
const _ = curryWithPlaceholder.placeholder;
const curriedWithPlaceholder = curryWithPlaceholder(add3);

console.log(curriedWithPlaceholder(_, 2)(1)(3));  // 6 (a=1, b=2, c=3)
console.log(curriedWithPlaceholder(_, _, 3)(1)(2)); // 6

// ========== 链式 add ==========
console.log(add(1)(2)(3) == 6);          // true
console.log(+add(1)(2)(3)(4));           // 10

console.log(addMulti(1, 2)(3)(4, 5) == 15); // true
console.log(+addMulti(1)(2)(3)(4)(5));      // 15

// 边界条件:只调用一次
console.log(+add(5));                    // 5
console.log(+addMulti(1, 2, 3));         // 6

复杂度分析

  • 时间: O(n),n 为参数个数
  • 空间: O(n),闭包存储参数

7. Promise 完整实现

难度: ⭐⭐⭐ | 梯队: 进阶 | 标签: 状态机, 微任务

题目描述

实现一个符合 Promises/A+ 规范的 Promise。

代码实现

javascript
/**
 * Promise 完整实现
 */
class MyPromise {
  static PENDING = 'pending';
  static FULFILLED = 'fulfilled';
  static REJECTED = 'rejected';

  constructor(executor) {
    this.status = MyPromise.PENDING;
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
      if (this.status === MyPromise.PENDING) {
        this.status = MyPromise.FULFILLED;
        this.value = value;
        this.onFulfilledCallbacks.forEach((fn) => fn());
      }
    };

    const reject = (reason) => {
      if (this.status === MyPromise.PENDING) {
        this.status = MyPromise.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) => {
      const fulfilledMicrotask = () => {
        queueMicrotask(() => {
          try {
            const x = onFulfilled(this.value);
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        });
      };

      const rejectedMicrotask = () => {
        queueMicrotask(() => {
          try {
            const x = onRejected(this.reason);
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        });
      };

      if (this.status === MyPromise.FULFILLED) {
        fulfilledMicrotask();
      } else if (this.status === MyPromise.REJECTED) {
        rejectedMicrotask();
      } else {
        this.onFulfilledCallbacks.push(fulfilledMicrotask);
        this.onRejectedCallbacks.push(rejectedMicrotask);
      }
    });

    return promise2;
  }

  /**
   * 处理 then 返回值
   */
  resolvePromise(promise2, x, resolve, reject) {
    // 防止循环引用
    if (promise2 === x) {
      return reject(new TypeError('Chaining cycle detected'));
    }

    if (x instanceof MyPromise) {
      x.then(resolve, reject);
    } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
      let called = false;
      try {
        const then = x.then;
        if (typeof then === 'function') {
          then.call(
            x,
            (y) => {
              if (called) return;
              called = true;
              this.resolvePromise(promise2, y, resolve, reject);
            },
            (r) => {
              if (called) return;
              called = true;
              reject(r);
            }
          );
        } else {
          resolve(x);
        }
      } catch (error) {
        if (called) return;
        called = true;
        reject(error);
      }
    } else {
      resolve(x);
    }
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }

  finally(callback) {
    return this.then(
      (value) => MyPromise.resolve(callback()).then(() => value),
      (reason) => MyPromise.resolve(callback()).then(() => { throw reason; })
    );
  }

  static resolve(value) {
    if (value instanceof MyPromise) return value;
    return new MyPromise((resolve) => resolve(value));
  }

  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason));
  }
}

示例调用

javascript
// 基础用法
new MyPromise((resolve) => {
  resolve(1);
}).then((value) => {
  console.log(value); // 1
  return value + 1;
}).then((value) => {
  console.log(value); // 2
});

// 边界条件:异步 resolve
new MyPromise((resolve) => {
  setTimeout(() => resolve('async'), 100);
}).then((value) => {
  console.log(value); // 'async'
});

// 边界条件:链式调用返回 Promise
new MyPromise((resolve) => {
  resolve(1);
}).then((value) => {
  return new MyPromise((r) => r(value * 2));
}).then((value) => {
  console.log(value); // 2
});

// 边界条件:错误处理
new MyPromise((_, reject) => {
  reject('error');
}).catch((reason) => {
  console.log(reason); // 'error'
});

// 边界条件:then 中抛出错误
new MyPromise((resolve) => {
  resolve(1);
}).then(() => {
  throw new Error('oops');
}).catch((err) => {
  console.log(err.message); // 'oops'
});

// 边界条件:finally
new MyPromise((resolve) => {
  resolve(1);
}).finally(() => {
  console.log('cleanup'); // 'cleanup'
}).then((value) => {
  console.log(value); // 1
});

复杂度分析

  • 时间: O(1) 单次操作
  • 空间: O(n) 回调队列

8. 事件发布订阅

难度: ⭐⭐ | 梯队: 第三梯队 | 标签: 设计模式, EventEmitter

题目描述

实现一个事件发布订阅系统,支持 on、off、emit、once 方法。

代码实现

javascript
/**
 * EventEmitter 事件发布订阅
 */
class EventEmitter {
  constructor() {
    this.events = new Map();
  }

  /**
   * 订阅事件
   * @param {string} event - 事件名
   * @param {Function} listener - 监听函数
   * @return {EventEmitter}
   */
  on(event, listener) {
    if (!this.events.has(event)) {
      this.events.set(event, []);
    }
    this.events.get(event).push(listener);
    return this;
  }

  /**
   * 取消订阅
   * @param {string} event - 事件名
   * @param {Function} listener - 要移除的监听函数
   * @return {EventEmitter}
   */
  off(event, listener) {
    if (!this.events.has(event)) return this;

    if (!listener) {
      // 没有指定 listener,移除该事件所有监听
      this.events.delete(event);
    } else {
      const listeners = this.events.get(event);
      const index = listeners.indexOf(listener);
      if (index !== -1) {
        listeners.splice(index, 1);
      }
      if (listeners.length === 0) {
        this.events.delete(event);
      }
    }

    return this;
  }

  /**
   * 触发事件
   * @param {string} event - 事件名
   * @param {...any} args - 传递给监听函数的参数
   * @return {boolean}
   */
  emit(event, ...args) {
    if (!this.events.has(event)) return false;

    const listeners = this.events.get(event).slice(); // 复制防止修改
    listeners.forEach((listener) => {
      listener.apply(this, args);
    });

    return true;
  }

  /**
   * 只订阅一次
   * @param {string} event - 事件名
   * @param {Function} listener - 监听函数
   * @return {EventEmitter}
   */
  once(event, listener) {
    const wrapper = (...args) => {
      listener.apply(this, args);
      this.off(event, wrapper);
    };
    // 保存原始函数引用,方便 off 时匹配
    wrapper.originalListener = listener;

    this.on(event, wrapper);
    return this;
  }

  /**
   * 获取事件的监听器数量
   */
  listenerCount(event) {
    return this.events.has(event) ? this.events.get(event).length : 0;
  }

  /**
   * 移除所有监听器
   */
  removeAllListeners(event) {
    if (event) {
      this.events.delete(event);
    } else {
      this.events.clear();
    }
    return this;
  }
}

示例调用

javascript
const emitter = new EventEmitter();

// 基础用法:on 和 emit
const handler = (msg) => console.log(`Received: ${msg}`);
emitter.on('message', handler);
emitter.emit('message', 'Hello'); // 'Received: Hello'

// 多个监听器
emitter.on('message', (msg) => console.log(`Also got: ${msg}`));
emitter.emit('message', 'World');
// 'Received: World'
// 'Also got: World'

// off 移除监听器
emitter.off('message', handler);
emitter.emit('message', 'Test');
// 只输出 'Also got: Test'

// once 只执行一次
emitter.once('init', () => console.log('Initialized!'));
emitter.emit('init'); // 'Initialized!'
emitter.emit('init'); // 无输出

// 边界条件:emit 不存在的事件
console.log(emitter.emit('unknown')); // false

// 边界条件:off 不存在的事件
emitter.off('nonexistent'); // 不报错

// 边界条件:多参数
emitter.on('data', (a, b, c) => console.log(a, b, c));
emitter.emit('data', 1, 2, 3); // 1 2 3

// 边界条件:链式调用
new EventEmitter()
  .on('a', () => console.log('a'))
  .on('b', () => console.log('b'))
  .emit('a')
  .emit('b');

// 边界条件:listenerCount
const ee = new EventEmitter();
ee.on('test', () => {});
ee.on('test', () => {});
console.log(ee.listenerCount('test')); // 2

// 边界条件:removeAllListeners
ee.removeAllListeners('test');
console.log(ee.listenerCount('test')); // 0

复杂度分析

  • 时间:
    • on/off: O(n) n 为该事件的监听器数
    • emit: O(n)
    • once: O(1)
  • 空间: O(m × n),m 为事件数,n 为平均监听器数

💡 前端手写题技巧总结

考点分类

分类题目核心知识点
异步Promise 系列状态机、微任务、链式调用
函数call/apply/bind、柯里化this 绑定、闭包
数据深拷贝、LRU类型判断、数据结构
工具防抖节流定时器、闭包
模式EventEmitter发布订阅模式

面试技巧

  1. 先说思路:边界条件、核心逻辑
  2. 写核心代码:不要纠结完美
  3. 补充边界:null、空数组、特殊类型
  4. 说复杂度:时间和空间

快速回忆清单

javascript
// Promise.all: 计数器 + forEach + 保持顺序
// 深拷贝: WeakMap + 类型判断 + 递归
// 防抖: clearTimeout + setTimeout
// 节流: 时间戳 或 定时器
// bind: 返回函数 + new 判断 + 参数合并
// LRU: Map 顺序 或 双向链表
// 柯里化: args.length >= fn.length
// EventEmitter: Map<event, listeners[]>

前端面试知识库