跳到主要内容

手写 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 的区别?

答案

特性reduceforEach
返回值累积结果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) {
// 只处理存在的元素
}

相关链接