Skip to content

前端高频手写面试题大全

一、Promise 系列

Promise 是前端异步编程的核心,相关手写题出现频率极高。

1. 手写 Promise(完整版)

思路:实现一个符合 Promise/A+ 规范的 Promise,包含状态管理、then 链式调用、值穿透、错误捕获等 。

javascript
class MyPromise {
  constructor(executor) {
    this.state = 'pending'; // pending/fulfilled/rejected
    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 (err) {
      reject(err);
    }
  }

  then(onFulfilled, onRejected) {
    // 处理值穿透:如果 onFulfilled/onRejected 不是函数,则创建一个默认函数
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };

    const promise2 = new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        }, 0);
      }

      if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        }, 0);
      }

      if (this.state === 'pending') {
        // 将回调存入队列
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (err) {
              reject(err);
            }
          }, 0);
        });
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (err) {
              reject(err);
            }
          }, 0);
        });
      }
    });
    return promise2;
  }

  resolvePromise(promise2, x, resolve, reject) {
    // 防止循环引用
    if (promise2 === x) {
      return reject(new TypeError('Chaining cycle detected for promise'));
    }

    let called = false; // 防止多次调用
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
      try {
        const then = x.then;
        if (typeof then === 'function') {
          // x 是 thenable 对象或 Promise
          then.call(
            x,
            y => {
              if (called) return;
              called = true;
              this.resolvePromise(promise2, y, resolve, reject);
            },
            err => {
              if (called) return;
              called = true;
              reject(err);
            }
          );
        } else {
          // x 是普通对象
          resolve(x);
        }
      } catch (err) {
        if (called) return;
        called = true;
        reject(err);
      }
    } else {
      // x 是普通值
      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));
  }

  static all(promises) {
    return new MyPromise((resolve, reject) => {
      const results = [];
      let count = 0;
      for (let i = 0; i < promises.length; i++) {
        MyPromise.resolve(promises[i]).then(
          value => {
            results[i] = value;
            count++;
            if (count === promises.length) resolve(results);
          },
          reject
        );
      }
    });
  }

  static race(promises) {
    return new MyPromise((resolve, reject) => {
      for (const p of promises) {
        MyPromise.resolve(p).then(resolve, reject);
      }
    });
  }

  static allSettled(promises) {
    return new MyPromise(resolve => {
      const results = [];
      let count = 0;
      for (let i = 0; i < promises.length; i++) {
        MyPromise.resolve(promises[i]).then(
          value => {
            results[i] = { status: 'fulfilled', value };
            count++;
            if (count === promises.length) resolve(results);
          },
          reason => {
            results[i] = { status: 'rejected', reason };
            count++;
            if (count === promises.length) resolve(results);
          }
        );
      }
    });
  }
}

2. 手写 Promise.all

思路:接收一个 Promise 数组,全部成功时返回结果数组,任一失败则立即失败 。

javascript
Promise.myAll = function(promises) {
  return new Promise((resolve, reject) => {
    const results = [];
    let count = 0;
    if (promises.length === 0) {
      resolve(results);
      return;
    }
    for (let i = 0; i < promises.length; i++) {
      Promise.resolve(promises[i]).then(
        value => {
          results[i] = value;
          count++;
          if (count === promises.length) resolve(results);
        },
        reject
      );
    }
  });
};

3. 手写 Promise.race

思路:接收一个 Promise 数组,返回第一个改变状态的结果 。

javascript
Promise.myRace = function(promises) {
  return new Promise((resolve, reject) => {
    for (const p of promises) {
      Promise.resolve(p).then(resolve, reject);
    }
  });
};

4. 手写 Promise.allSettled

思路:等待所有 Promise 完成,返回每个 Promise 的结果状态 。

javascript
Promise.myAllSettled = function(promises) {
  return new Promise(resolve => {
    const results = [];
    let count = 0;
    if (promises.length === 0) {
      resolve(results);
      return;
    }
    for (let i = 0; i < promises.length; i++) {
      Promise.resolve(promises[i]).then(
        value => {
          results[i] = { status: 'fulfilled', value };
          count++;
          if (count === promises.length) resolve(results);
        },
        reason => {
          results[i] = { status: 'rejected', reason };
          count++;
          if (count === promises.length) resolve(results);
        }
      );
    }
  });
};

5. 手写 Promise.any

思路:接收一个 Promise 数组,返回第一个成功的 Promise。如果全部失败,则返回 AggregateError。

javascript
Promise.myAny = function(promises) {
  return new Promise((resolve, reject) => {
    const errors = [];
    let count = 0;
    if (promises.length === 0) {
      reject(new AggregateError('All promises were rejected'));
      return;
    }
    for (let i = 0; i < promises.length; i++) {
      Promise.resolve(promises[i]).then(
        resolve,
        err => {
          errors[i] = err;
          count++;
          if (count === promises.length) {
            reject(new AggregateError(errors, 'All promises were rejected'));
          }
        }
      );
    }
  });
};

6. 手写 Promise 并发控制(async-pool)

思路:控制同时执行的 Promise 数量,常用于并发请求限制 。

javascript
async function asyncPool(poolLimit, array, iteratorFn) {
  const ret = [];
  const executing = [];
  for (const item of array) {
    const p = Promise.resolve().then(() => iteratorFn(item, array));
    ret.push(p);

    if (poolLimit <= array.length) {
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      if (executing.length >= poolLimit) {
        await Promise.race(executing);
      }
    }
  }
  return Promise.all(ret);
}

// 使用示例
const timeout = (i) => new Promise(resolve => setTimeout(() => resolve(i), i));
asyncPool(2, [1000, 2000, 3000, 1000], timeout).then(console.log);

二、防抖与节流

7. 手写防抖(debounce)

思路:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时 。

javascript
function debounce(func, wait = 300, immediate = false) {
  let timer = null;
  return function(...args) {
    const context = this;
    if (timer) clearTimeout(timer);
    
    if (immediate && !timer) {
      // 立即执行一次
      func.apply(context, args);
      timer = setTimeout(() => {
        timer = null;
      }, wait);
    } else {
      timer = setTimeout(() => {
        func.apply(context, args);
        timer = null;
      }, wait);
    }
  };
}

8. 手写节流(throttle)

思路:规定一个单位时间,在这个单位时间内最多只能触发一次函数执行 。

javascript
// 时间戳版本(立即执行)
function throttle(func, wait) {
  let previous = 0;
  return function(...args) {
    const now = Date.now();
    if (now - previous >= wait) {
      func.apply(this, args);
      previous = now;
    }
  };
}

// 定时器版本(延迟执行)
function throttle(func, wait) {
  let timer = null;
  return function(...args) {
    if (!timer) {
      timer = setTimeout(() => {
        func.apply(this, args);
        timer = null;
      }, wait);
    }
  };
}

// 综合版:支持首次和末次执行
function throttle(func, wait, options = { leading: true, trailing: true }) {
  let timer = null;
  let previous = 0;
  return function(...args) {
    const now = Date.now();
    if (!previous && options.leading === false) previous = now;
    const remaining = wait - (now - previous);
    if (remaining <= 0 || remaining > wait) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      previous = now;
      func.apply(this, args);
    } else if (!timer && options.trailing !== false) {
      timer = setTimeout(() => {
        previous = options.leading === false ? 0 : Date.now();
        timer = null;
        func.apply(this, args);
      }, remaining);
    }
  };
}

三、this 指向相关(call/apply/bind)

9. 手写 call 函数

思路:将函数设为对象的属性,执行该函数,删除该属性 。

javascript
Function.prototype.myCall = function(context = window, ...args) {
  if (typeof this !== 'function') {
    throw new TypeError('Error');
  }
  // 使用 Symbol 创建唯一键,避免覆盖原有属性
  const fn = Symbol('fn');
  context[fn] = this;
  const result = context[fn](...args);
  delete context[fn];
  return result;
};

10. 手写 apply 函数

思路:与 call 类似,参数以数组形式传入 。

javascript
Function.prototype.myApply = function(context = window, args = []) {
  if (typeof this !== 'function') {
    throw new TypeError('Error');
  }
  const fn = Symbol('fn');
  context[fn] = this;
  const result = context[fn](...args);
  delete context[fn];
  return result;
};

11. 手写 bind 函数

思路:返回一个绑定 this 的新函数,支持预设参数,且需要考虑 new 调用的优先级 。

javascript
Function.prototype.myBind = function(context, ...bindArgs) {
  if (typeof this !== 'function') {
    throw new TypeError('Error');
  }
  const self = this;
  return function F(...callArgs) {
    const args = [...bindArgs, ...callArgs];
    // 如果使用 new 调用,this 指向实例,需要忽略传入的 context
    if (this instanceof F) {
      return new self(...args);
    }
    return self.apply(context, args);
  };
};

四、对象与数组工具函数

12. 手写深拷贝(deepClone)

思路:处理循环引用、特殊对象(Date、RegExp)、Symbol 等 。

javascript
function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  if (obj instanceof Error) return new Error(obj.message);

  if (hash.has(obj)) return hash.get(obj);

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

  // 遍历所有属性,包括 Symbol 属性
  [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)].forEach(key => {
    clone[key] = deepClone(obj[key], hash);
  });

  return clone;
}

13. 手写 instanceof

思路:沿着原型链查找,判断右侧构造函数的 prototype 是否在左侧的原型链上 。

javascript
function myInstanceof(left, right) {
  if (left === null || typeof left !== 'object') return false;
  let proto = Object.getPrototypeOf(left);
  while (true) {
    if (proto === null) return false;
    if (proto === right.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
}

14. 手写 new 操作符

思路:创建新对象,链接原型,绑定 this,返回对象 。

javascript
function myNew(ctor, ...args) {
  if (typeof ctor !== 'function') {
    throw new TypeError('ctor must be a function');
  }
  // 创建新对象,原型指向构造函数的 prototype
  const obj = Object.create(ctor.prototype);
  // 执行构造函数,绑定 this
  const result = ctor.apply(obj, args);
  // 如果构造函数返回对象,则返回该对象;否则返回创建的新对象
  return (result !== null && typeof result === 'object') ? result : obj;
}

15. 手写 Object.create

思路:创建一个新对象,使用现有对象作为新对象的原型 。

javascript
function myCreate(proto) {
  if (typeof proto !== 'object' || proto === null) {
    throw new TypeError('Object prototype may only be an Object or null');
  }
  function F() {}
  F.prototype = proto;
  return new F();
}

16. 手写类数组转数组

思路:将 arguments 或 NodeList 等类数组对象转换为真正的数组 。

javascript
function toArray(arrLike) {
  // 方法1:Array.from
  return Array.from(arrLike);
  // 方法2:扩展运算符(需支持迭代器)
  return [...arrLike];
  // 方法3:Array.prototype.slice
  return Array.prototype.slice.call(arrLike);
  // 方法4:Array.prototype.concat
  return Array.prototype.concat.apply([], arrLike);
}

17. 手写数组去重

思路:多种方式实现数组去重 。

javascript
function unique(arr) {
  // 方法1:Set
  return [...new Set(arr)];
  
  // 方法2:filter + indexOf
  return arr.filter((item, index) => arr.indexOf(item) === index);
  
  // 方法3:Map
  const map = new Map();
  return arr.filter(item => !map.has(item) && map.set(item, true));
  
  // 方法4:reduce
  return arr.reduce((acc, cur) => {
    if (!acc.includes(cur)) acc.push(cur);
    return acc;
  }, []);
}

18. 手写数组扁平化(flatten)

思路:将多维数组转换为一维数组 。

javascript
function flatten(arr, depth = Infinity) {
  // 方法1:递归
  if (depth === 0) return arr.slice();
  return arr.reduce((acc, cur) => {
    if (Array.isArray(cur)) {
      acc.push(...flatten(cur, depth - 1));
    } else {
      acc.push(cur);
    }
    return acc;
  }, []);

  // 方法2:ES6 flat
  return arr.flat(depth);

  // 方法3:while + some
  while (arr.some(item => Array.isArray(item))) {
    arr = [].concat(...arr);
  }
  return arr;
}

19. 手写数组 map 方法

思路:对数组每个元素执行回调,返回新数组 。

javascript
Array.prototype.myMap = function(callback, thisArg) {
  if (this === null || this === undefined) {
    throw new TypeError('Cannot read property \'map\' of null or undefined');
  }
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }
  const arr = Object(this);
  const len = arr.length >>> 0;
  const result = new Array(len);
  for (let i = 0; i < len; i++) {
    if (i in arr) {
      result[i] = callback.call(thisArg, arr[i], i, arr);
    }
  }
  return result;
};

20. 手写数组 filter 方法

思路:返回满足回调条件的元素组成的新数组 。

javascript
Array.prototype.myFilter = function(callback, thisArg) {
  if (this === null || this === undefined) {
    throw new TypeError('Cannot read property \'filter\' of null or undefined');
  }
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }
  const arr = Object(this);
  const len = arr.length >>> 0;
  const result = [];
  for (let i = 0; i < len; i++) {
    if (i in arr) {
      if (callback.call(thisArg, arr[i], i, arr)) {
        result.push(arr[i]);
      }
    }
  }
  return result;
};

21. 手写数组 reduce 方法

思路:累加器,对数组每个元素执行回调,返回累积结果 。

javascript
Array.prototype.myReduce = function(callback, initialValue) {
  if (this === null || this === undefined) {
    throw new TypeError('Cannot read property \'reduce\' of null or undefined');
  }
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }
  const arr = Object(this);
  const len = arr.length >>> 0;
  let accumulator = initialValue;
  let startIndex = 0;

  if (arguments.length < 2) {
    // 没有传入初始值,取数组第一个有效元素
    while (startIndex < len && !(startIndex in arr)) {
      startIndex++;
    }
    if (startIndex >= len) {
      throw new TypeError('Reduce of empty array with no initial value');
    }
    accumulator = arr[startIndex++];
  }

  for (let i = startIndex; i < len; i++) {
    if (i in arr) {
      accumulator = callback(accumulator, arr[i], i, arr);
    }
  }
  return accumulator;
};

22. 手写数组 flatMap 方法

思路:map 之后 flatten 一层。

javascript
Array.prototype.myFlatMap = function(callback, thisArg) {
  const arr = this;
  return arr.map(callback, thisArg).reduce((acc, cur) => acc.concat(cur), []);
};

23. 手写函数柯里化(curry)

思路:将接受多个参数的函数转换为接受单一参数的函数序列 。

javascript
function curry(func, arity = func.length) {
  return function curried(...args) {
    if (args.length >= arity) {
      return func.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

// 使用示例
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6

24. 手写组合函数(compose)

思路:从右到左组合多个函数 。

javascript
function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg;
  }
  if (funcs.length === 1) {
    return funcs[0];
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

// 使用示例
const add1 = x => x + 1;
const double = x => x * 2;
const add1ThenDouble = compose(double, add1);
console.log(add1ThenDouble(3)); // (3+1)*2 = 8

25. 手写管道函数(pipe)

思路:从左到右组合多个函数。

javascript
function pipe(...funcs) {
  if (funcs.length === 0) {
    return arg => arg;
  }
  if (funcs.length === 1) {
    return funcs[0];
  }
  return funcs.reduce((a, b) => (...args) => b(a(...args)));
}

五、数据处理与格式化

26. 手写 JSON.stringify

思路:将 JavaScript 对象转换为 JSON 字符串 。

javascript
function myStringify(obj) {
  const type = typeof obj;
  if (type === 'string') return `"${obj}"`;
  if (type === 'number' || type === 'boolean' || obj === null) return String(obj);
  if (type === 'undefined' || type === 'function' || type === 'symbol') return undefined;
  
  if (Array.isArray(obj)) {
    const arr = obj.map(item => myStringify(item) || 'null');
    return `[${arr.join(',')}]`;
  }
  
  if (type === 'object') {
    const keys = Object.keys(obj).filter(key => myStringify(obj[key]) !== undefined);
    const items = keys.map(key => `"${key}":${myStringify(obj[key])}`);
    return `{${items.join(',')}}`;
  }
}

27. 手写 JSON.parse

思路:将 JSON 字符串解析为 JavaScript 对象 。可以使用 eval 或 Function,注意安全性。

javascript
function myParse(jsonStr) {
  return (new Function('return ' + jsonStr))();
}

// 更安全的实现需要自行解析字符串,这里只做演示

28. 手写 URL 参数解析(parseParams)

思路:将 URL 查询字符串解析为对象 。

javascript
function parseParams(url) {
  const paramsStr = url.split('?')[1];
  if (!paramsStr) return {};
  return paramsStr.split('&').reduce((acc, cur) => {
    const [key, value] = cur.split('=');
    if (key in acc) {
      acc[key] = Array.isArray(acc[key]) ? [...acc[key], value] : [acc[key], value];
    } else {
      acc[key] = decodeURIComponent(value || '');
    }
    return acc;
  }, {});
}

// 处理复杂情况:数组、中文等
function parseParamsPro(url) {
  const search = url.split('?')[1] || '';
  return search.split('&').reduce((acc, param) => {
    if (!param) return acc;
    const [key, value] = param.split('=').map(decodeURIComponent);
    const match = key.match(/(.+)\[\]$/);
    if (match) {
      // 处理数组:a[]=1&a[]=2
      const arrKey = match[1];
      acc[arrKey] = acc[arrKey] ? [...acc[arrKey], value] : [value];
    } else {
      acc[key] = value;
    }
    return acc;
  }, {});
}

29. 手写字符串模板解析(render)

思路:实现类似 ${name} 的模板字符串替换 。

javascript
function render(template, data) {
  const reg = /\$\{(\w+)\}/g;
  return template.replace(reg, (match, key) => {
    return data[key] !== undefined ? data[key] : '';
  });
}

// 支持深层属性
function renderDeep(template, data) {
  return template.replace(/\$\{([^}]+)\}/g, (match, path) => {
    const props = path.split('.');
    let value = data;
    for (const prop of props) {
      value = value?.[prop];
      if (value === undefined) break;
    }
    return value !== undefined ? value : '';
  });
}

// 使用示例
const tpl = '我的名字是${name},年龄${age}岁';
console.log(render(tpl, { name: '张三', age: 18 })); // 我的名字是张三,年龄18岁

30. 手写千分位分隔符(toThousands)

思路:将数字每隔三位加一个逗号 。

javascript
function toThousands(num) {
  // 方法1:正则
  return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  
  // 方法2:循环
  const parts = num.toString().split('.');
  parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  return parts.join('.');
  
  // 方法3:toLocaleString
  return num.toLocaleString('en-US');
}

// 支持负数和小数
function formatNumber(num) {
  const [int, decimal] = num.toString().split('.');
  const formattedInt = int.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  return decimal ? `${formattedInt}.${decimal}` : formattedInt;
}

31. 手写大数相加(bigNumberAdd)

思路:处理超出 Number 安全范围的整数相加 。

javascript
function bigNumberAdd(a, b) {
  let i = a.length - 1;
  let j = b.length - 1;
  let carry = 0;
  let result = [];
  
  while (i >= 0 || j >= 0 || carry) {
    const x = i >= 0 ? parseInt(a[i--]) : 0;
    const y = j >= 0 ? parseInt(b[j--]) : 0;
    const sum = x + y + carry;
    result.unshift(sum % 10);
    carry = Math.floor(sum / 10);
  }
  
  return result.join('');
}

// 支持小数
function bigDecimalAdd(a, b) {
  const [int1, dec1 = ''] = a.split('.');
  const [int2, dec2 = ''] = b.split('.');
  const maxDecLen = Math.max(dec1.length, dec2.length);
  const dec1Padded = dec1.padEnd(maxDecLen, '0');
  const dec2Padded = dec2.padEnd(maxDecLen, '0');
  
  // 小数部分相加
  let decSum = bigNumberAdd(dec1Padded, dec2Padded);
  const decCarry = decSum.length > maxDecLen ? 1 : 0;
  if (decCarry) {
    decSum = decSum.slice(1);
  }
  
  // 整数部分相加
  const intSum = bigNumberAdd(int1, int2) + (decCarry ? 1 : 0);
  
  return `${intSum}.${decSum}`;
}

32. 手写列表转树结构(listToTree)

思路:将扁平数组(有 id 和 pid)转换为树形结构 。

javascript
function listToTree(list, idKey = 'id', parentKey = 'parentId', childrenKey = 'children') {
  const map = {};
  const roots = [];
  
  // 先建立 id 到节点的映射
  list.forEach(item => {
    map[item[idKey]] = { ...item, [childrenKey]: [] };
  });
  
  // 建立父子关系
  list.forEach(item => {
    const node = map[item[idKey]];
    const parentId = item[parentKey];
    if (parentId === null || parentId === undefined || parentId === 0) {
      roots.push(node);
    } else {
      const parent = map[parentId];
      if (parent) {
        parent[childrenKey].push(node);
      }
    }
  });
  
  return roots;
}

// 支持动态 id 类型
function listToTreePro(list, idKey = 'id', parentKey = 'parentId') {
  const map = new Map(list.map(item => [item[idKey], { ...item, children: [] }]));
  const tree = [];
  for (const item of map.values()) {
    const parent = map.get(item[parentKey]);
    if (parent) {
      parent.children.push(item);
    } else {
      tree.push(item);
    }
  }
  return tree;
}

33. 手写树转列表结构(treeToList)

思路:将树形结构展开为扁平数组 。

javascript
function treeToList(tree, childrenKey = 'children') {
  const result = [];
  const stack = [...tree];
  
  while (stack.length) {
    const node = stack.pop();
    const children = node[childrenKey];
    if (children) {
      stack.push(...children);
    }
    const { [childrenKey]: _, ...rest } = node;
    result.push(rest);
  }
  
  return result;
}

// 递归版本(保留顺序)
function treeToListRecursive(tree, childrenKey = 'children') {
  return tree.reduce((acc, node) => {
    const children = node[childrenKey] || [];
    const { [childrenKey]: _, ...rest } = node;
    return [...acc, rest, ...treeToListRecursive(children, childrenKey)];
  }, []);
}

34. 手写深比较(isEqual)

思路:比较两个对象是否相等(深度比较)。

javascript
function isEqual(a, b, visited = new Set()) {
  // 处理相同引用
  if (a === b) return true;
  
  // 处理基本类型
  if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') {
    return a === b;
  }
  
  // 防止循环引用
  if (visited.has(a) || visited.has(b)) return false;
  visited.add(a);
  visited.add(b);
  
  // 比较构造函数
  if (a.constructor !== b.constructor) return false;
  
  // 比较数组
  if (Array.isArray(a)) {
    if (a.length !== b.length) return false;
    for (let i = 0; i < a.length; i++) {
      if (!isEqual(a[i], b[i], visited)) return false;
    }
    return true;
  }
  
  // 比较对象
  const keysA = Object.keys(a);
  const keysB = Object.keys(b);
  if (keysA.length !== keysB.length) return false;
  
  for (const key of keysA) {
    if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
    if (!isEqual(a[key], b[key], visited)) return false;
  }
  
  return true;
}

六、设计模式

35. 手写发布订阅(EventEmitter)

思路:实现事件的 on、off、emit、once 方法 。

javascript
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) {
    if (!this.events[event]) return this;
    if (!callback) {
      delete this.events[event];
    } else {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
    return this;
  }

  emit(event, ...args) {
    if (!this.events[event]) return false;
    this.events[event].forEach(callback => {
      callback.apply(this, args);
    });
    return true;
  }

  once(event, callback) {
    const onceWrapper = (...args) => {
      callback.apply(this, args);
      this.off(event, onceWrapper);
    };
    this.on(event, onceWrapper);
    return this;
  }
}

// 使用示例
const emitter = new EventEmitter();
emitter.on('test', (data) => console.log('test:', data));
emitter.emit('test', 123);

36. 手写观察者模式(Observer)

思路:被观察者维护观察者列表,状态变化时通知观察者 。

javascript
// 被观察者
class Subject {
  constructor() {
    this.observers = [];
    this.state = null;
  }

  getState() {
    return this.state;
  }

  setState(state) {
    this.state = state;
    this.notifyAll();
  }

  attach(observer) {
    this.observers.push(observer);
  }

  detach(observer) {
    const index = this.observers.indexOf(observer);
    if (index !== -1) {
      this.observers.splice(index, 1);
    }
  }

  notifyAll() {
    this.observers.forEach(observer => observer.update(this));
  }
}

// 观察者
class Observer {
  constructor(name) {
    this.name = name;
  }

  update(subject) {
    console.log(`${this.name} 收到通知,新状态为:${subject.getState()}`);
  }
}

// 使用示例
const subject = new Subject();
const observer1 = new Observer('观察者1');
const observer2 = new Observer('观察者2');
subject.attach(observer1);
subject.attach(observer2);
subject.setState('开心');

37. 手写单例模式(Singleton)

思路:确保一个类只有一个实例 。

javascript
class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    this.data = [];
    Singleton.instance = this;
  }
}

// 使用闭包实现
const Singleton = (function() {
  let instance = null;
  function createInstance() {
    return { data: [] };
  }
  return {
    getInstance() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();

// 使用示例
const a = new Singleton();
const b = new Singleton();
console.log(a === b); // true

38. 手写惰性单例

思路:在需要时才创建实例。

javascript
function getSingleton(fn) {
  let instance = null;
  return function(...args) {
    return instance || (instance = fn.apply(this, args));
  };
}

// 使用示例
const createModal = getSingleton(() => {
  const div = document.createElement('div');
  div.innerHTML = '我是一个弹窗';
  div.style.display = 'none';
  document.body.appendChild(div);
  return div;
});
document.getElementById('btn').onclick = () => {
  const modal = createModal();
  modal.style.display = 'block';
};

七、原理模拟

39. 手写简易 MVVM(双向绑定)

思路:基于 Object.defineProperty 实现数据劫持和发布订阅 。

javascript
class Vue {
  constructor(options) {
    this.$el = document.querySelector(options.el);
    this.$data = options.data || {};
    this._directives = [];
    this.observe(this.$data);
    this.compile(this.$el);
  }

  observe(data) {
    Object.keys(data).forEach(key => {
      let value = data[key];
      const directives = [];
      Object.defineProperty(data, key, {
        get() {
          return value;
        },
        set(newVal) {
          if (newVal === value) return;
          value = newVal;
          directives.forEach(directive => directive.update());
        }
      });
      this._directives.push({ key, directives });
    });
  }

  compile(el) {
    const nodes = el.children;
    for (let i = 0; i < nodes.length; i++) {
      const node = nodes[i];
      if (node.children.length) {
        this.compile(node);
      }
      if (node.hasAttribute('v-model')) {
        const attr = node.getAttribute('v-model');
        node.addEventListener('input', () => {
          this.$data[attr] = node.value;
        });
        const directive = {
          update: () => {
            node.value = this.$data[attr];
          }
        };
        directive.update();
        this._directives.find(d => d.key === attr).directives.push(directive);
      }
      if (node.hasAttribute('v-bind')) {
        const attr = node.getAttribute('v-bind');
        const directive = {
          update: () => {
            node.innerText = this.$data[attr];
          }
        };
        directive.update();
        this._directives.find(d => d.key === attr).directives.push(directive);
      }
    }
  }
}

40. 手写 JSONP

思路:利用 script 标签实现跨域请求 。

javascript
function jsonp({ url, params = {}, callbackKey = 'callback', timeout = 60000 }) {
  return new Promise((resolve, reject) => {
    // 生成唯一的回调函数名
    const callbackName = `jsonp_${Date.now()}_${Math.random().toString(36).substr(2)}`;
    
    // 组装参数
    const query = Object.keys(params)
      .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
      .join('&');
    
    // 创建 script 标签
    const script = document.createElement('script');
    script.src = `${url}?${query}&${callbackKey}=${callbackName}`;
    
    // 定义回调函数
    window[callbackName] = (data) => {
      resolve(data);
      clearTimeout(timer);
      document.body.removeChild(script);
      delete window[callbackName];
    };
    
    // 超时处理
    const timer = setTimeout(() => {
      reject(new Error('JSONP request timeout'));
      document.body.removeChild(script);
      delete window[callbackName];
    }, timeout);
    
    // 错误处理
    script.onerror = () => {
      reject(new Error('JSONP request failed'));
      clearTimeout(timer);
      document.body.removeChild(script);
      delete window[callbackName];
    };
    
    // 插入 script 标签
    document.body.appendChild(script);
  });
}

// 使用示例
jsonp({
  url: 'https://api.example.com/data',
  params: { id: 123 },
  callbackKey: 'callback'
}).then(data => console.log(data));

41. 手写 Ajax

思路:使用 XMLHttpRequest 封装 AJAX 请求 。

javascript
function ajax(options) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    const method = options.method?.toUpperCase() || 'GET';
    const url = options.url;
    const data = options.data || null;
    const headers = options.headers || {};
    const timeout = options.timeout || 30000;
    const withCredentials = options.withCredentials || false;

    xhr.open(method, url, true);
    xhr.timeout = timeout;
    xhr.withCredentials = withCredentials;

    // 设置请求头
    Object.keys(headers).forEach(key => {
      xhr.setRequestHeader(key, headers[key]);
    });

    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve({
          status: xhr.status,
          statusText: xhr.statusText,
          data: xhr.response,
          headers: xhr.getAllResponseHeaders()
        });
      } else {
        reject(new Error(`Request failed with status ${xhr.status}`));
      }
    };

    xhr.onerror = () => {
      reject(new Error('Network error'));
    };

    xhr.ontimeout = () => {
      reject(new Error('Request timeout'));
    };

    xhr.send(data);
  });
}

// 使用示例
ajax({
  url: '/api/user',
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  data: JSON.stringify({ name: '张三' })
}).then(res => console.log(res));

42. 手写 setTimeout 模拟 setInterval

思路:使用递归 setTimeout 实现 interval 。

javascript
function mySetInterval(fn, delay, ...args) {
  let timer = null;
  const loop = () => {
    timer = setTimeout(() => {
      fn.apply(this, args);
      loop();
    }, delay);
  };
  loop();
  return {
    clear: () => clearTimeout(timer)
  };
}

// 增强版:支持动态调整间隔
function createInterval(fn, delay, ...args) {
  let timer = null;
  let running = true;
  
  const loop = () => {
    if (!running) return;
    timer = setTimeout(() => {
      fn.apply(this, args);
      loop();
    }, delay);
  };
  
  loop();
  
  return {
    clear: () => {
      running = false;
      clearTimeout(timer);
    },
    setDelay: (newDelay) => {
      delay = newDelay;
    }
  };
}

43. 手写 setInterval 模拟 setTimeout

思路:使用 setInterval 并在执行后清除 。

javascript
function mySetTimeout(fn, delay, ...args) {
  const timer = setInterval(() => {
    clearInterval(timer);
    fn.apply(this, args);
  }, delay);
  return timer;
}

// 或者使用 requestAnimationFrame
function rafTimeout(fn, delay) {
  const start = performance.now();
  let frame;
  const loop = (now) => {
    if (now - start >= delay) {
      fn();
      cancelAnimationFrame(frame);
      return;
    }
    frame = requestAnimationFrame(loop);
  };
  frame = requestAnimationFrame(loop);
  return () => cancelAnimationFrame(frame);
}

44. 手写 sleep 函数

思路:实现延迟执行 。

javascript
// 基于 Promise
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 基于 async/await 使用
async function test() {
  console.log('开始');
  await sleep(2000);
  console.log('2秒后');
}

// 同步方式(不推荐,会阻塞线程)
function sleepSync(ms) {
  const start = Date.now();
  while (Date.now() - start < ms) {}
}

八、算法类手写题

45. 手写 LRU 缓存

思路:使用 Map 或 哈希表 + 双向链表实现 。

javascript
class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map();
  }

  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;
  }

  put(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else if (this.cache.size >= this.capacity) {
      // 删除最久未使用的(Map 的第一个 key)
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }
    this.cache.set(key, value);
  }
}

46. 手写版本号排序

思路:对版本号数组进行排序 。

javascript
function versionSort(versions) {
  return versions.sort((a, b) => {
    const aParts = a.split('.').map(Number);
    const bParts = b.split('.').map(Number);
    const maxLen = Math.max(aParts.length, bParts.length);
    
    for (let i = 0; i < maxLen; i++) {
      const aNum = aParts[i] || 0;
      const bNum = bParts[i] || 0;
      if (aNum !== bNum) return aNum - bNum;
    }
    return 0;
  });
}

// 使用示例
const versions = ['1.0.2', '1.0.1', '2.0', '1.1', '1.0.1-beta'];
console.log(versionSort(versions));

47. 手写数组乱序(shuffle)

思路:实现 Fisher-Yates 洗牌算法 。

javascript
function shuffle(arr) {
  const result = [...arr];
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}

// 使用示例
console.log(shuffle([1, 2, 3, 4, 5]));

48. 手写获取字符串中出现最多的字符及次数

思路:统计字符频率,找出最大值 。

javascript
function getMaxChar(str) {
  const map = {};
  let maxChar = '';
  let maxCount = 0;
  
  for (const char of str) {
    map[char] = (map[char] || 0) + 1;
    if (map[char] > maxCount) {
      maxCount = map[char];
      maxChar = char;
    }
  }
  
  return { char: maxChar, count: maxCount };
}

// 正则版本
function getMaxCharRegex(str) {
  const sorted = str.split('').sort().join('');
  const matches = sorted.match(/(\w)\1*/g);
  const max = matches.reduce((max, cur) => cur.length > max.length ? cur : max, '');
  return { char: max[0], count: max.length };
}

49. 手写二分查找

思路:在有序数组中查找目标值 。

javascript
function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;
  
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) return mid;
    if (arr[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }
  return -1;
}

// 查找第一个等于 target 的位置
function binarySearchFirst(arr, target) {
  let left = 0;
  let right = arr.length - 1;
  let result = -1;
  
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (arr[mid] >= target) {
      right = mid - 1;
    } else {
      left = mid + 1;
    }
    if (arr[mid] === target) result = mid;
  }
  return result;
}

50. 手写快速排序

思路:分治思想,原地排序 。

javascript
function quickSort(arr, left = 0, right = arr.length - 1) {
  if (left >= right) return;
  
  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[left];
  let i = left;
  let j = right;
  
  while (i < j) {
    while (i < j && arr[j] >= pivot) j--;
    arr[i] = arr[j];
    while (i < j && arr[i] <= pivot) i++;
    arr[j] = arr[i];
  }
  
  arr[i] = pivot;
  return i;
}

刷题建议

  1. 基础必会:防抖节流、call/apply/bind、深拷贝、Promise 系列
  2. 重点掌握:数组方法实现、发布订阅、LRU、JSONP
  3. 进阶挑战:Promise A+ 完整实现、MVVM、并发控制
  4. 练习技巧:先理解原理,再手写实现,注意边界条件

Released under the MIT License.