Array.prototype.reduce()
reduce() 方法對陣列中的每個元素按序執行一個使用者提供的“reducer”回撥函式,並將前一個元素計算的返回值作為輸入。執行 reducer 遍歷陣列所有元素的最終結果是一個單一值。
第一次執行回撥時,沒有“前一個計算的返回值”。如果提供了一個初始值,則可以使用它來代替。否則,陣列索引 0 處的元素用作初始值,迭代從下一個元素(索引 1 而不是索引 0)開始。
試一試
const array = [1, 2, 3, 4];
// 0 + 1 + 2 + 3 + 4
const initialValue = 0;
const sumWithInitial = array.reduce(
(accumulator, currentValue) => accumulator + currentValue,
initialValue,
);
console.log(sumWithInitial);
// Expected output: 10
語法
reduce(callbackFn)
reduce(callbackFn, initialValue)
引數
callbackFn-
一個用於陣列中每個元素執行的函式。其返回值成為
callbackFn下一次呼叫時accumulator引數的值。對於最後一次呼叫,返回值成為reduce()的返回值。該函式使用以下引數呼叫:accumulator-
上次呼叫
callbackFn的結果值。在第一次呼叫時,如果指定了initialValue,則其值為initialValue;否則其值為array[0]。 currentValue-
當前元素的值。在第一次呼叫時,如果指定了
initialValue,則其值為array[0];否則其值為array[1]。 currentIndex-
currentValue在陣列中的索引位置。在第一次呼叫時,如果指定了initialValue,則其值為0,否則為1。 array-
呼叫
reduce()的陣列。
initialValue可選-
回撥函式第一次呼叫時,
accumulator的初始化值。如果指定了initialValue,callbackFn將從陣列中的第一個值作為currentValue開始執行。如果未指定initialValue,則accumulator將初始化為陣列中的第一個值,並且callbackFn將從陣列中的第二個值作為currentValue開始執行。在這種情況下,如果陣列為空(因此沒有第一個值可以作為accumulator返回),則會丟擲錯誤。
返回值
透過對整個陣列執行“reducer”回撥函式完成後的結果值。
異常
TypeError-
如果陣列不包含任何元素且未提供
initialValue,則丟擲此錯誤。
描述
reduce() 方法是一個迭代方法。它按升序索引順序遍歷陣列中的所有元素,並將其累積為一個單一值。每次 callbackFn 的返回值在下一次呼叫時再次作為 accumulator 傳遞給 callbackFn。accumulator 的最終值(即陣列最後一次迭代中 callbackFn 返回的值)成為 reduce() 的返回值。有關這些方法如何工作的更多資訊,請閱讀迭代方法部分。
callbackFn 僅對具有已賦值的陣列索引呼叫。對於稀疏陣列中的空槽,它不會被呼叫。
與其他迭代方法不同,reduce() 不接受 thisArg 引數。callbackFn 總是以 undefined 作為 this 呼叫,如果 callbackFn 不是嚴格模式,則 undefined 會被替換為 globalThis。
reduce() 是函數語言程式設計中的一個核心概念,在這種程式設計正規化中,不能修改任何值,因此為了累積陣列中的所有值,每次迭代都必須返回一個新的累積值。這個約定傳播到 JavaScript 的 reduce():在可能的情況下,您應該使用展開語法或其他複製方法來建立新的陣列和物件作為累加器,而不是修改現有的。如果您決定修改累加器而不是複製它,請記住仍要在回撥中返回修改後的物件,否則下一次迭代將收到 undefined。但是,請注意複製累加器可能會導致記憶體使用量增加和效能下降——有關詳細資訊,請參閱何時不使用 reduce()。在這種情況下,為了避免糟糕的效能和難以閱讀的程式碼,最好改用 for 迴圈。
reduce() 方法是通用的。它只期望 this 值具有 length 屬性和以整數為鍵的屬性。
邊緣情況
如果陣列只有一個元素(無論位置如何)並且沒有提供 initialValue,或者如果提供了 initialValue 但陣列為空,則將返回該單獨的值,而不呼叫 callbackFn。
如果提供了 initialValue 並且陣列不為空,則 reduce 方法將始終從索引 0 開始呼叫回撥函式。
如果未提供 initialValue,則 reduce 方法對於長度大於 1、等於 1 和 0 的陣列將表現不同,如以下示例所示:
const getMax = (a, b) => Math.max(a, b);
// callback is invoked for each element in the array starting at index 0
[1, 100].reduce(getMax, 50); // 100
[50].reduce(getMax, 10); // 50
// callback is invoked once for element at index 1
[1, 100].reduce(getMax); // 100
// callback is not invoked
[50].reduce(getMax); // 50
[].reduce(getMax, 1); // 1
[].reduce(getMax); // TypeError
示例
reduce() 在沒有初始值的情況下如何工作
下面的程式碼展示了當我們使用一個數組並且沒有初始值呼叫 reduce() 時會發生什麼。
const array = [15, 16, 17, 18, 19];
function reducer(accumulator, currentValue, index) {
const returns = accumulator + currentValue;
console.log(
`accumulator: ${accumulator}, currentValue: ${currentValue}, index: ${index}, returns: ${returns}`,
);
return returns;
}
array.reduce(reducer);
回撥函式將被呼叫四次,每次呼叫的引數和返回值如下:
accumulator |
currentValue |
index |
返回值 | |
|---|---|---|---|---|
| 第一次呼叫 | 15 |
16 |
1 |
31 |
| 第二次呼叫 | 31 |
17 |
2 |
48 |
| 第三次呼叫 | 48 |
18 |
3 |
66 |
| 第四次呼叫 | 66 |
19 |
4 |
85 |
array 引數在整個過程中從未改變——它始終是 [15, 16, 17, 18, 19]。reduce() 返回的值將是最後一次回撥呼叫的值 (85)。
reduce() 在有初始值的情況下如何工作
在這裡,我們使用相同的演算法來減少相同的陣列,但將 initialValue 設為 10,並作為第二個引數傳遞給 reduce():
[15, 16, 17, 18, 19].reduce(
(accumulator, currentValue) => accumulator + currentValue,
10,
);
回撥函式將被呼叫五次,每次呼叫的引數和返回值如下:
accumulator |
currentValue |
index |
返回值 | |
|---|---|---|---|---|
| 第一次呼叫 | 10 |
15 |
0 |
25 |
| 第二次呼叫 | 25 |
16 |
1 |
41 |
| 第三次呼叫 | 41 |
17 |
2 |
58 |
| 第四次呼叫 | 58 |
18 |
3 |
76 |
| 第五次呼叫 | 76 |
19 |
4 |
95 |
在這種情況下,reduce() 返回的值將是 95。
物件陣列中值的和
要將物件陣列中包含的值求和,您必須提供 initialValue,以便每個專案都透過您的函式。
const objects = [{ x: 1 }, { x: 2 }, { x: 3 }];
const sum = objects.reduce(
(accumulator, currentValue) => accumulator + currentValue.x,
0,
);
console.log(sum); // 6
函式順序管道
pipe 函式接受一系列函式並返回一個新函式。當使用一個引數呼叫新函式時,這一系列函式會按順序呼叫,每個函式接收前一個函式的返回值。
const pipe =
(...functions) =>
(initialValue) =>
functions.reduce((acc, fn) => fn(acc), initialValue);
// Building blocks to use for composition
const double = (x) => 2 * x;
const triple = (x) => 3 * x;
const quadruple = (x) => 4 * x;
// Composed functions for multiplication of specific values
const multiply6 = pipe(double, triple);
const multiply9 = pipe(triple, triple);
const multiply16 = pipe(quadruple, quadruple);
const multiply24 = pipe(double, triple, quadruple);
// Usage
multiply6(6); // 36
multiply9(9); // 81
multiply16(16); // 256
multiply24(10); // 240
按順序執行 Promise
Promise 序列化本質上是上一節中演示的函式管道,只是以非同步方式完成。
// Compare this with pipe: fn(acc) is changed to acc.then(fn),
// and initialValue is ensured to be a promise
const asyncPipe =
(...functions) =>
(initialValue) =>
functions.reduce((acc, fn) => acc.then(fn), Promise.resolve(initialValue));
// Building blocks to use for composition
const p1 = async (a) => a * 5;
const p2 = async (a) => a * 2;
// The composed functions can also return non-promises, because the values are
// all eventually wrapped in promises
const f3 = (a) => a * 3;
const p4 = async (a) => a * 4;
asyncPipe(p1, p2, f3, p4)(10).then(console.log); // 1200
asyncPipe 也可以使用 async/await 實現,這更好地展示了它與 pipe 的相似性。
const asyncPipe =
(...functions) =>
(initialValue) =>
functions.reduce(async (acc, fn) => fn(await acc), initialValue);
將 reduce() 與稀疏陣列一起使用
reduce() 會跳過稀疏陣列中缺失的元素,但不會跳過 undefined 值。
console.log([1, 2, , 4].reduce((a, b) => a + b)); // 7
console.log([1, 2, undefined, 4].reduce((a, b) => a + b)); // NaN
在非陣列物件上呼叫 reduce()
reduce() 方法讀取 this 的 length 屬性,然後訪問其鍵是非負整數且小於 length 的每個屬性。
const arrayLike = {
length: 3,
0: 2,
1: 3,
2: 4,
3: 99, // ignored by reduce() since length is 3
};
console.log(Array.prototype.reduce.call(arrayLike, (x, y) => x + y));
// 9
何時不使用 reduce()
像 reduce() 這樣的多用途高階函式功能強大,但有時難以理解,特別是對於經驗不足的 JavaScript 開發人員。如果使用其他陣列方法程式碼會更清晰,開發人員必須權衡可讀性與使用 reduce() 的其他優點。
請注意,reduce() 總是等同於 for...of 迴圈,只是我們現在每次迭代都返回新值,而不是在父作用域中修改變數。
const val = array.reduce((acc, cur) => update(acc, cur), initialValue);
// Is equivalent to:
let val = initialValue;
for (const cur of array) {
val = update(val, cur);
}
如前所述,人們可能希望使用 reduce() 的原因是模仿函數語言程式設計中不可變資料的實踐。因此,堅持累加器不可變性的開發人員通常會為每次迭代複製整個累加器,如下所示:
const names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];
const countedNames = names.reduce((allNames, name) => {
const currCount = Object.hasOwn(allNames, name) ? allNames[name] : 0;
return {
...allNames,
[name]: currCount + 1,
};
}, {});
此程式碼效能不佳,因為每次迭代都必須複製整個 allNames 物件,而該物件可能很大,具體取決於有多少個唯一的名稱。此程式碼在最壞情況下的效能為 O(N^2),其中 N 是 names 的長度。
一個更好的替代方法是在每次迭代中修改 allNames 物件。但是,如果 allNames 無論如何都會被修改,您可能希望將 reduce() 轉換為 for 迴圈,這樣會更清晰:
const names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];
const countedNames = names.reduce((allNames, name) => {
const currCount = allNames[name] ?? 0;
allNames[name] = currCount + 1;
// return allNames, otherwise the next iteration receives undefined
return allNames;
}, Object.create(null));
const names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];
const countedNames = Object.create(null);
for (const name of names) {
const currCount = countedNames[name] ?? 0;
countedNames[name] = currCount + 1;
}
因此,如果您的累加器是一個數組或物件,並且您在每次迭代中都在複製該陣列或物件,您可能會無意中將二次複雜度引入到您的程式碼中,導致在大資料上效能迅速下降。這在實際程式碼中已經發生——例如,請參閱透過一行程式碼使 Tanstack Table 提速 1000 倍。
上面給出了一些 reduce() 的可接受用例(最值得注意的是,陣列求和、Promise 序列化和函式管道)。在其他情況下,存在比 reduce() 更好的替代方案。
-
展平陣列的陣列。改用
flat()。jsconst flattened = array.reduce((acc, cur) => acc.concat(cur), []);jsconst flattened = array.flat(); -
按屬性對物件進行分組。改用
Object.groupBy()。jsconst groups = array.reduce((acc, obj) => { const key = obj.name; const curGroup = acc[key] ?? []; return { ...acc, [key]: [...curGroup, obj] }; }, {});jsconst groups = Object.groupBy(array, (obj) => obj.name); -
連線物件陣列中包含的陣列。改用
flatMap()。jsconst friends = [ { name: "Anna", books: ["Bible", "Harry Potter"] }, { name: "Bob", books: ["War and peace", "Romeo and Juliet"] }, { name: "Alice", books: ["The Lord of the Rings", "The Shining"] }, ]; const allBooks = friends.reduce((acc, cur) => [...acc, ...cur.books], []);jsconst allBooks = friends.flatMap((person) => person.books); -
刪除陣列中的重複項。改用
Set和Array.from()。jsconst uniqArray = array.reduce( (acc, cur) => (acc.includes(cur) ? acc : [...acc, cur]), [], );jsconst uniqArray = Array.from(new Set(array)); -
刪除或新增陣列中的元素。改用
flatMap()。js// Takes an array of numbers and splits perfect squares into its square roots const roots = array.reduce((acc, cur) => { if (cur < 0) return acc; const root = Math.sqrt(cur); if (Number.isInteger(root)) return [...acc, root, root]; return [...acc, cur]; }, []);jsconst roots = array.flatMap((val) => { if (val < 0) return []; const root = Math.sqrt(val); if (Number.isInteger(root)) return [root, root]; return [val]; });如果您只是從陣列中刪除元素,也可以使用
filter()。 -
搜尋元素或測試元素是否滿足條件。改用
find()和findIndex(),或some()和every()。這些方法還有一個額外的好處,即一旦結果確定,它們就會返回,而無需迭代整個陣列。jsconst allEven = array.reduce((acc, cur) => acc && cur % 2 === 0, true);jsconst allEven = array.every((val) => val % 2 === 0);
在 reduce() 是最佳選擇的情況下,文件和語義變數命名可以幫助減輕可讀性方面的缺點。
規範
| 規範 |
|---|
| ECMAScript® 2026 語言規範 # sec-array.prototype.reduce |
瀏覽器相容性
載入中…