手写题
实现 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
函数还支持更加丰富的配置,例如 leading
和 trailing
参数,分别表示是否在等待时间开始时立即执行和结束时执行。感兴趣请查阅 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
作为哈希表,记录已经拷贝过的对象,避免循环引用导致的栈溢出。 - 对于特殊的数据类型,例如
Date
、RegExp
,直接创建新的实例。
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