手写 reduce
问题
实现数组的 reduce 方法。
答案
reduce 是数组最强大的方法之一,可以实现几乎所有数组操作。
基础实现
interface Array<T> {
myReduce<U>(
callback: (accumulator: U, currentValue: T, index: number, array: T[]) => U,
initialValue: U
): U;
myReduce(
callback: (accumulator: T, currentValue: T, index: number, array: T[]) => T
): T;
}
Array.prototype.myReduce = function <T, U>(
this: T[],
callback: (acc: U, cur: T, index: number, array: T[]) => U,
initialValue?: U
): U {
if (this.length === 0 && initialValue === undefined) {
throw new TypeError('Reduce of empty array with no initial value');
}
let accumulator: U;
let startIndex: number;
if (initialValue !== undefined) {
accumulator = initialValue;
startIndex = 0;
} else {
accumulator = this[0] as unknown as U;
startIndex = 1;
}
for (let i = startIndex; i < this.length; i++) {
// 跳过稀疏数组的空位
if (i in this) {
accumulator = callback(accumulator, this[i], i, this);
}
}
return accumulator;
};
// 测试
console.log([1, 2, 3, 4].myReduce((acc, cur) => acc + cur)); // 10
console.log([1, 2, 3, 4].myReduce((acc, cur) => acc + cur, 10)); // 20
独立函数版本
function reduce<T, U>(
array: T[],
callback: (acc: U, cur: T, index: number, array: T[]) => U,
initialValue: U
): U;
function reduce<T>(
array: T[],
callback: (acc: T, cur: T, index: number, array: T[]) => T
): T;
function reduce<T, U>(
array: T[],
callback: (acc: T | U, cur: T, index: number, array: T[]) => T | U,
initialValue?: U
): T | U {
if (array.length === 0 && initialValue === undefined) {
throw new TypeError('Reduce of empty array with no initial value');
}
let accumulator: T | U;
let startIndex: number;
if (initialValue !== undefined) {
accumulator = initialValue;
startIndex = 0;
} else {
accumulator = array[0];
startIndex = 1;
}
for (let i = startIndex; i < array.length; i++) {
if (i in array) {
accumulator = callback(accumulator, array[i], i, array);
}
}
return accumulator;
}
reduceRight 实现
Array.prototype.myReduceRight = function <T, U>(
this: T[],
callback: (acc: U, cur: T, index: number, array: T[]) => U,
initialValue?: U
): U {
if (this.length === 0 && initialValue === undefined) {
throw new TypeError('Reduce of empty array with no initial value');
}
let accumulator: U;
let startIndex: number;
if (initialValue !== undefined) {
accumulator = initialValue;
startIndex = this.length - 1;
} else {
accumulator = this[this.length - 1] as unknown as U;
startIndex = this.length - 2;
}
for (let i = startIndex; i >= 0; i--) {
if (i in this) {
accumulator = callback(accumulator, this[i], i, this);
}
}
return accumulator;
};
// 测试
console.log([[1, 2], [3, 4]].myReduceRight((acc, cur) => acc.concat(cur), [] as number[]));
// [3, 4, 1, 2]
用 reduce 实现其他方法
实现 map
function mapWithReduce<T, U>(
array: T[],
callback: (value: T, index: number, array: T[]) => U
): U[] {
return array.reduce<U[]>((acc, cur, index, arr) => {
acc.push(callback(cur, index, arr));
return acc;
}, []);
}
实现 filter
function filterWithReduce<T>(
array: T[],
predicate: (value: T, index: number, array: T[]) => boolean
): T[] {
return array.reduce<T[]>((acc, cur, index, arr) => {
if (predicate(cur, index, arr)) {
acc.push(cur);
}
return acc;
}, []);
}
实现 flat
function flatWithReduce<T>(array: (T | T[])[], depth = 1): T[] {
return depth > 0
? array.reduce<T[]>((acc, cur) => {
if (Array.isArray(cur)) {
acc.push(...flatWithReduce(cur, depth - 1));
} else {
acc.push(cur);
}
return acc;
}, [])
: (array as T[]);
}
实现 flatMap
function flatMapWithReduce<T, U>(
array: T[],
callback: (value: T, index: number, array: T[]) => U | U[]
): U[] {
return array.reduce<U[]>((acc, cur, index, arr) => {
const result = callback(cur, index, arr);
if (Array.isArray(result)) {
acc.push(...result);
} else {
acc.push(result);
}
return acc;
}, []);
}
实现 some / every
function someWithReduce<T>(
array: T[],
predicate: (value: T, index: number, array: T[]) => boolean
): boolean {
return array.reduce<boolean>(
(acc, cur, index, arr) => acc || predicate(cur, index, arr),
false
);
}
function everyWithReduce<T>(
array: T[],
predicate: (value: T, index: number, array: T[]) => boolean
): boolean {
return array.reduce<boolean>(
(acc, cur, index, arr) => acc && predicate(cur, index, arr),
true
);
}
实现 find
function findWithReduce<T>(
array: T[],
predicate: (value: T, index: number, array: T[]) => boolean
): T | undefined {
return array.reduce<T | undefined>(
(acc, cur, index, arr) => (acc !== undefined || !predicate(cur, index, arr) ? acc : cur),
undefined
);
}
实现 groupBy
function groupBy<T, K extends string | number | symbol>(
array: T[],
keyFn: (item: T) => K
): Record<K, T[]> {
return array.reduce((acc, item) => {
const key = keyFn(item);
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(item);
return acc;
}, {} as Record<K, T[]>);
}
// 使用
const users = [
{ name: 'Alice', age: 20 },
{ name: 'Bob', age: 20 },
{ name: 'Charlie', age: 30 },
];
console.log(groupBy(users, (u) => u.age));
// { 20: [{ name: 'Alice', age: 20 }, { name: 'Bob', age: 20 }], 30: [{ name: 'Charlie', age: 30 }] }
常见用法示例
const numbers = [1, 2, 3, 4, 5];
// 求和
const sum = numbers.reduce((acc, cur) => acc + cur, 0);
// 求最大值
const max = numbers.reduce((acc, cur) => Math.max(acc, cur));
// 数组去重
const unique = numbers.reduce<number[]>((acc, cur) => {
if (!acc.includes(cur)) acc.push(cur);
return acc;
}, []);
// 对象计数
const count = ['a', 'b', 'a', 'c', 'b', 'a'].reduce<Record<string, number>>(
(acc, cur) => {
acc[cur] = (acc[cur] || 0) + 1;
return acc;
},
{}
);
// { a: 3, b: 2, c: 1 }
// 管道执行
const pipe =
<T>(...fns: Array<(arg: T) => T>) =>
(value: T): T =>
fns.reduce((acc, fn) => fn(acc), value);
const process = pipe(
(x: number) => x + 1,
(x: number) => x * 2,
(x: number) => x - 3
);
console.log(process(5)); // (5 + 1) * 2 - 3 = 9
常见面试问题
Q1: 为什么 reduce 需要初始值?
答案:
// 无初始值:空数组会报错
[].reduce((a, b) => a + b); // TypeError
// 有初始值:空数组返回初始值
[].reduce((a, b) => a + b, 0); // 0
// 无初始值:第一个元素作为初始值
[1].reduce((a, b) => a + b); // 1(没有执行回调)
建议总是提供初始值,避免边界问题。
Q2: reduce 和 forEach 的区别?
答案:
| 特性 | reduce | forEach |
|---|---|---|
| 返回值 | 累积结果 | undefined |
| 终止循环 | 不能 | 不能 |
| 用途 | 聚合、转换 | 副作用 |
Q3: 稀疏数组如何处理?
答案:
const sparse = [1, , 3]; // 稀疏数组,索引 1 为空
// 原生 reduce 会跳过空位
sparse.reduce((acc, cur, idx) => {
console.log(idx); // 0, 2(跳过了 1)
return acc + cur;
}, 0);
// 手写实现中使用 `i in array` 判断
if (i in this) {
// 只处理存在的元素
}