Skip to content

手写题

实现 Promise.all

🎯 题目描述

Promise.all() 静态方法接受一个 Promise 可迭代对象作为输入,并返回一个 Promise。

  • 当所有输入的 Promise 都被兑现时,返回的 Promise 也将被兑现(即使传入的是一个空的可迭代对象),并返回一个包含所有兑现值的数组。
  • 如果输入的任何 Promise 被拒绝,则返回的 Promise 将被拒绝,并带有第一个被拒绝的原因。

📚 示例代码

js
const promiseAll = (proms) => {
  return new Promise((resolve, reject) => {
    if (proms == null || typeof proms[Symbol.iterator] !== "function") {
      throw new TypeError("proms must be an iterable");
    }
    proms = [...proms];

    if (proms.length === 0) resolve([]);
    let count = 0;
    const result = [];
    proms.forEach((prom, index) => {
      Promise.resolve(prom)
        .then((res) => {
          result[index] = res;
          if (++count === proms.length) resolve(result);
        })
        .catch(reject);
    });
  });
};

防抖

🎯 题目描述

实现一个防抖函数:事件触发后等待一段时间再执行回调函数,如果在等待期间内再次触发了同一事件,则重新计时,以避免回调函数的多次执行。

📚 示例代码

需要注意的细节:

  • setTimeout 回调函数执行时, this 指向 window,需要记录原函数的 this
  • 为了支持回调函数的参数传递,使用 ...args 获取参数并利用 apply 传递参数。
js
const debounce = (fn, wait) => {
  let timer;
  return function (...args) {
    const context = this;
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
};
ts
function debounce<T extends (...args: any[]) => void>(
  func: T,
  wait: number
): (...args: Parameters<T>) => void {
  let timeout: ReturnType<typeof setTimeout> | undefined;

  return function (...args: Parameters<T>): void {
    const context = this;
    clearTimeout(timeout!);
    timeout = setTimeout(() => {
      func.apply(this, args);
    }, wait);
  };
}
html
<body>
  <button onclick="handleClick()">Click</button>
</body>

<script>
  const debounce = (fn, wait) => {
    let timer;
    return function (...args) {
      const context = this;
      clearTimeout(timer);
      timer = setTimeout(() => {
        fn.apply(this, args);
      }, wait);
    };
  };
  const handleClick = debounce(() => {
    console.log("click");
  }, 1000);
</script>
Pro:支持立即执行的防抖函数 ⏰

有的时候我们希望在事件触发时立即执行一次回调函数,然后在等待时间内不再执行,例如点击按钮后立即发送请求。

js
// 此处笔者偷懒只给出了 js 版本的实现。
const debounce = (fn, wait = 0, immediate = false) => {
  let timer;
  return function (...args) {
    const context = this;
    clearTimeout(timer);

    if (immediate) {
      const callNow = !timer;
      timer = setTimeout(() => (timer = null), wait);
      if (callNow) fn.apply(context, args);
    } else {
      timer = setTimeout(() => {
        fn.apply(context, args);
      }, wait);
    }
  };
};

值得一提的是,在广泛使用的 lodash 库中,_.debounce 函数还支持更加丰富的配置,例如 leadingtrailing 参数,分别表示是否在等待时间开始时立即执行和结束时执行。感兴趣请查阅 lodash 文档。

🎨 应用场景

  • 输入框展示搜索建议:当用户在输入框中连续输入时,只在用户停止输入后发送请求。
  • 窗口大小变化(resize)事件:当用户调整窗口大小时,只在停止调整后执行布局计算,避免页面抖动。

节流

🎯 题目描述

实现一个节流函数:在一定时间内,事件多次触发只执行一次回调函数。不论事件触发多频繁,都会按照固定的时间间隔执行。

📚 示例代码

ts
const throttle = (fn: Function, wait = 0) => {
  let lastTime = 0;

  return function (...args: any[]) {
    const now = Date.now();
    if (now - lastTime >= wait) {
      fn.apply(this, args);
      lastTime = now;
    }
  };
};
ts
const throttle = (fn: Function, wait = 0) => {
  let isThrottle = false;

  return function (...args: any[]) {
    if (!isThrottle) {
      isThrottle = true;
      fn.apply(this, args);
      setTimeout(() => (isThrottle = false), wait);
    }
  };
};
html
<body>
  <div style="height: 200vh; background-color: lightblue"></div>
</body>

<script>
  const throttle = (fn, wait) => {
    // 实现略...
  };

  const handleScroll = (e) => {
    console.log(e.target.scrollingElement.scrollTop);
  };

  const throttleHandleScroll = throttle(handleScroll, 1000);
  document.addEventListener("scroll", throttleHandleScroll);
</script>

🎨 应用场景

  • 图片滚动加载:页面滚动时,不断执行图片加载函数。(不使用防抖是因为不可能等到用户停止滚动才加载图片吧!)
  • 拖拽(touchmove)事件:拖拽元素时,我们可能需要在拖拽过程中不断计算元素位置,但不希望计算过于频繁。

深拷贝

🎯 题目描述

实现一个深拷贝函数,支持拷贝常见的数据类型,例如对象、数组、函数、正则、日期等,并且能够正常处理循环引用。

📚 示例代码

  • 使用 WeakMap 作为哈希表,记录已经拷贝过的对象,避免循环引用导致的栈溢出。
  • 对于特殊的数据类型,例如 DateRegExp,直接创建新的实例。
ts
function deepCloneEasy<T>(obj: T): T {
  // 不能处理函数、正则、undefined、循环引用
  return JSON.parse(JSON.stringify(obj));
}

// 顺便复习一下浅拷贝吧!
// 1. Object.assign
// 2. 扩展运算符 ...
ts
function deepClone<T>(obj: T, hashMap = new WeakMap()): T {
  if (obj === null || typeof obj !== "object") return obj;
  if (obj instanceof Date) return new Date(obj) as any;
  if (obj instanceof RegExp) return new RegExp(obj) as any;

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

  if (Array.isArray(obj)) {
    const copy: any[] = [];
    hashMap.set(obj, copy);
    obj.forEach((item) => copy.push(deepClone(item, hashMap)));
    return copy as any;
  } else {
    const copy: Record<string, any> = {};
    hashMap.set(obj, copy);
    Object.entries(obj).forEach(
      ([key, value]) => (copy[key] = deepClone(value, hashMap))
    );
    return copy as any;
  }

  // 该代码并没有考虑传入参数为 Map、Set 等特殊对象的情况
  // 使用 obj instanceof Map 然后做类似处理即可
}

函数柯里化

🎯 题目描述

实现一个柯里化函数,支持多参数传递,例如 curry(fn)(a)(b)(c)

  • 柯里化将一个多参数函数转换为一系列函数,这些函数每次接收一个或多个参数,直到所有参数都被提供为止。
  • 柯里化的主要作用是参数复用延迟执行,之前传递的参数可以在后续调用中复用。

📚 示例代码

js
const curry = (fn, ...args) => {
  return args.length >= fn.length
    ? fn(...args)
    : (..._args) => curry(fn, ...args, ..._args);
};

// 【使用示例】
const saySomething = (name, str) => console.log(`${name} says: ${str}`);
const tomSay = curry(saySomething, "Tom");
tomSay("hello"); // Tom says: hello

// 实现add(1)(2)(3)输出6的函数
const add = (a, b, c) => a + b + c;
const addCurry = curry(add);
console.log(addCurry(1)(2)(3)); // 6

场景题

并发请求控制

实现一个 PromisePool 类,限制并发请求的数量。

js
class PromisePool {
  constructor(capacity) {
    this.capacity = capacity;
    this.tasks = [];
    this.running = 0;
  }

  add(fn) {
    return new Promise((resolve, reject) => {
      this.tasks.push({
        fn,
        resolve,
        reject,
      });
      this._run();
    });
  }

  _run() {
    while (this.tasks.length && this.running < this.capacity) {
      const { fn, resolve, reject } = this.tasks.shift();
      this.running++;
      fn()
        .then(resolve, reject)
        .finally(() => {
          this.running--;
          this._run();
        });
    }
  }
}

const sleep = (time) => new Promise((resolve) => setTimeout(resolve, time));
const addTask = (time, str) => {
  pool
    .add(() => sleep(time))
    .then(() => {
      console.log(str);
    });
};
const pool = new PromisePool(2);
addTask(10000, "1");
addTask(5000, "2");
addTask(3000, "3");
addTask(4000, "4");
addTask(5000, "5");
// 2 3 1 4 5