Array.prototype.reduce()
Baseline
Widely available
This feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.
Phương thức reduce() của các đối tượng Array thực thi một hàm callback "reducer" do người dùng cung cấp trên mỗi phần tử của mảng, theo thứ tự, truyền giá trị trả về từ phép tính trên phần tử trước đó vào. Kết quả cuối cùng của việc chạy reducer trên tất cả các phần tử của mảng là một giá trị duy nhất.
Lần đầu tiên callback được chạy, không có "giá trị trả về của phép tính trước". Nếu được cung cấp, giá trị khởi đầu có thể được dùng thay thế. Nếu không, phần tử mảng ở chỉ mục 0 được dùng làm giá trị khởi đầu và việc lặp bắt đầu từ phần tử tiếp theo (chỉ mục 1 thay vì chỉ mục 0).
Try it
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
Cú pháp
reduce(callbackFn)
reduce(callbackFn, initialValue)
Tham số
callbackFn-
Một hàm để thực thi cho mỗi phần tử trong mảng. Giá trị trả về của nó trở thành giá trị của tham số
accumulatorở lần gọi tiếp theo củacallbackFn. Ở lần gọi cuối cùng, giá trị trả về trở thành giá trị trả về củareduce(). Hàm được gọi với các đối số sau:accumulator-
Giá trị kết quả từ lần gọi trước của
callbackFn. Ở lần gọi đầu tiên, giá trị của nó làinitialValuenếu tham số đó được chỉ định; nếu không thì giá trị của nó làarray[0]. currentValue-
Giá trị của phần tử hiện tại. Ở lần gọi đầu tiên, giá trị của nó là
array[0]nếuinitialValueđược chỉ định; nếu không thì giá trị của nó làarray[1]. currentIndex-
Vị trí chỉ mục của
currentValuetrong mảng. Ở lần gọi đầu tiên, giá trị của nó là0nếuinitialValueđược chỉ định, nếu không thì là1. array-
Mảng mà
reduce()được gọi.
initialValueOptional-
Giá trị mà
accumulatorđược khởi tạo lần đầu tiên callback được gọi. NếuinitialValueđược chỉ định,callbackFnbắt đầu thực thi với giá trị đầu tiên trong mảng làcurrentValue. NếuinitialValuekhông được chỉ định,accumulatorđược khởi tạo bằng giá trị đầu tiên trong mảng, vàcallbackFnbắt đầu thực thi với giá trị thứ hai trong mảng làcurrentValue. Trong trường hợp này, nếu mảng rỗng (không có giá trị đầu tiên nào để trả về làmaccumulator), sẽ có lỗi được ném ra.
Giá trị trả về
Giá trị kết quả từ việc chạy hàm callback "reducer" đến hoàn thành trên toàn bộ mảng.
Ngoại lệ
TypeError-
Được ném ra nếu mảng không chứa phần tử nào và
initialValuekhông được cung cấp.
Mô tả
Phương thức reduce() là một phương thức lặp. Nó chạy hàm callback "reducer" trên tất cả các phần tử trong mảng, theo thứ tự chỉ mục tăng dần, và tích lũy chúng thành một giá trị duy nhất. Mỗi lần, giá trị trả về của callbackFn được truyền vào callbackFn lần tiếp theo dưới dạng accumulator. Giá trị cuối cùng của accumulator (là giá trị trả về từ callbackFn ở lần lặp cuối cùng của mảng) trở thành giá trị trả về của reduce(). Đọc phần iterative methods để biết thêm thông tin về cách các phương thức này hoạt động nói chung.
callbackFn chỉ được gọi cho các chỉ mục mảng có giá trị được gán. Nó không được gọi cho các ô trống trong mảng thưa.
Không giống các phương thức lặp khác, reduce() không nhận tham số thisArg. callbackFn luôn được gọi với undefined làm this, được thay bằng globalThis nếu callbackFn không nghiêm ngặt.
reduce() là một khái niệm trung tâm trong lập trình hàm, trong đó không thể thay đổi bất kỳ giá trị nào, vì vậy để tích lũy tất cả các giá trị trong mảng, người ta phải trả về một giá trị accumulator mới ở mỗi lần lặp. Quy ước này lan truyền sang reduce() của JavaScript: bạn nên dùng spread hoặc các phương thức sao chép khác khi có thể để tạo mảng và đối tượng mới làm accumulator, thay vì thay đổi đối tượng hiện có. Nếu bạn quyết định thay đổi accumulator thay vì sao chép nó, hãy nhớ vẫn trả về đối tượng đã thay đổi trong callback, nếu không lần lặp tiếp theo sẽ nhận được undefined. Tuy nhiên, lưu ý rằng việc sao chép accumulator có thể dẫn đến tăng sử dụng bộ nhớ và giảm hiệu suất — xem Khi nào không nên dùng reduce() để biết thêm chi tiết. Trong những trường hợp như vậy, để tránh hiệu suất kém và mã khó đọc, tốt hơn nên dùng vòng lặp for thay thế.
Phương thức reduce() là phương thức tổng quát. Nó chỉ yêu cầu giá trị this có thuộc tính length và các thuộc tính có khóa là số nguyên.
Các trường hợp biên
Nếu mảng chỉ có một phần tử (bất kể vị trí) và không có initialValue được cung cấp, hoặc nếu initialValue được cung cấp nhưng mảng rỗng, giá trị duy nhất sẽ được trả về mà không gọi callbackFn.
Nếu initialValue được cung cấp và mảng không rỗng, thì phương thức reduce sẽ luôn gọi hàm callback bắt đầu từ chỉ mục 0.
Nếu initialValue không được cung cấp thì phương thức reduce sẽ hoạt động khác nhau cho các mảng có độ dài lớn hơn 1, bằng 1 và 0, như trong ví dụ sau:
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
Ví dụ
>Cách reduce() hoạt động không có giá trị khởi đầu
Đoạn mã dưới đây cho thấy điều gì xảy ra nếu chúng ta gọi reduce() với một mảng và không có giá trị khởi đầu.
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);
Callback sẽ được gọi bốn lần, với các đối số và giá trị trả về trong mỗi lần gọi như sau:
accumulator |
currentValue |
index |
Giá trị trả về | |
|---|---|---|---|---|
| Lần gọi đầu | 15 |
16 |
1 |
31 |
| Lần gọi hai | 31 |
17 |
2 |
48 |
| Lần gọi ba | 48 |
18 |
3 |
66 |
| Lần gọi bốn | 66 |
19 |
4 |
85 |
Tham số array không bao giờ thay đổi trong quá trình — nó luôn là [15, 16, 17, 18, 19]. Giá trị trả về bởi reduce() sẽ là giá trị của lần gọi callback cuối cùng (85).
Cách reduce() hoạt động với giá trị khởi đầu
Ở đây chúng ta rút gọn cùng mảng bằng cùng thuật toán, nhưng với initialValue là 10 được truyền làm đối số thứ hai cho reduce():
[15, 16, 17, 18, 19].reduce(
(accumulator, currentValue) => accumulator + currentValue,
10,
);
Callback sẽ được gọi năm lần, với các đối số và giá trị trả về trong mỗi lần gọi như sau:
accumulator |
currentValue |
index |
Giá trị trả về | |
|---|---|---|---|---|
| Lần gọi đầu | 10 |
15 |
0 |
25 |
| Lần gọi hai | 25 |
16 |
1 |
41 |
| Lần gọi ba | 41 |
17 |
2 |
58 |
| Lần gọi bốn | 58 |
18 |
3 |
76 |
| Lần gọi năm | 76 |
19 |
4 |
95 |
Giá trị trả về bởi reduce() trong trường hợp này sẽ là 95.
Tổng các giá trị trong mảng đối tượng
Để tổng các giá trị trong một mảng đối tượng, bạn phải cung cấp initialValue, để mỗi phần tử đi qua hàm của bạn.
const objects = [{ x: 1 }, { x: 2 }, { x: 3 }];
const sum = objects.reduce(
(accumulator, currentValue) => accumulator + currentValue.x,
0,
);
console.log(sum); // 6
Nối tiếp hàm
Hàm pipe nhận một chuỗi các hàm và trả về một hàm mới. Khi hàm mới được gọi với một đối số, chuỗi các hàm được gọi theo thứ tự, mỗi hàm nhận giá trị trả về của hàm trước.
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
Chạy promise theo trình tự
Promise sequencing về cơ bản là nối tiếp hàm được minh họa trong phần trước, ngoại trừ được thực hiện bất đồng bộ.
// 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 cũng có thể được triển khai bằng async/await, điều này minh họa rõ hơn sự tương đồng của nó với pipe:
const asyncPipe =
(...functions) =>
(initialValue) =>
functions.reduce(async (acc, fn) => fn(await acc), initialValue);
Sử dụng reduce() với mảng thưa
reduce() bỏ qua các phần tử bị thiếu trong mảng thưa, nhưng không bỏ qua các giá trị undefined.
console.log([1, 2, , 4].reduce((a, b) => a + b)); // 7
console.log([1, 2, undefined, 4].reduce((a, b) => a + b)); // NaN
Gọi reduce() trên đối tượng không phải mảng
Phương thức reduce() đọc thuộc tính length của this và sau đó truy cập từng thuộc tính có khóa là số nguyên không âm nhỏ hơn 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
Khi nào không nên dùng reduce()
Các hàm bậc cao đa mục đích như reduce() có thể mạnh nhưng đôi khi khó hiểu, đặc biệt đối với các nhà phát triển JavaScript ít kinh nghiệm hơn. Nếu mã trở nên rõ ràng hơn khi sử dụng các phương thức mảng khác, các nhà phát triển phải cân nhắc sự đánh đổi về khả năng đọc so với các lợi ích khác của việc dùng reduce().
Lưu ý rằng reduce() luôn tương đương với vòng lặp for...of, ngoại trừ thay vì thay đổi một biến trong phạm vi trên, chúng ta trả về giá trị mới cho mỗi lần lặp:
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);
}
Như đã đề cập trước đó, lý do tại sao mọi người muốn dùng reduce() là để mô phỏng các thực hành lập trình hàm về dữ liệu bất biến. Do đó, các nhà phát triển giữ tính bất biến của accumulator thường sao chép toàn bộ accumulator cho mỗi lần lặp, như sau:
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,
};
}, {});
Mã này hoạt động kém vì mỗi lần lặp phải sao chép toàn bộ đối tượng allNames, có thể rất lớn tùy thuộc vào số lượng tên duy nhất. Mã này có hiệu suất xấu nhất là O(N^2), trong đó N là độ dài của names.
Một thay thế tốt hơn là thay đổi đối tượng allNames trong mỗi lần lặp. Tuy nhiên, nếu allNames bị thay đổi dù sao, bạn có thể muốn chuyển reduce() thành vòng lặp for thay thế, vì nó rõ ràng hơn nhiều:
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;
}
Do đó, nếu accumulator của bạn là mảng hoặc đối tượng và bạn đang sao chép mảng hoặc đối tượng trong mỗi lần lặp, bạn có thể vô tình đưa vào độ phức tạp bậc hai trong mã của mình, khiến hiệu suất giảm nhanh chóng trên dữ liệu lớn. Điều này đã xảy ra trong code thực tế — xem ví dụ Making Tanstack Table 1000x faster with a 1 line change.
Một số trường hợp sử dụng reduce() được chấp nhận được đưa ra ở trên (đáng chú ý nhất là tổng mảng, nối tiếp promise và nối tiếp hàm). Có những trường hợp khác tồn tại các lựa chọn tốt hơn reduce().
-
Làm phẳng mảng của các mảng. Dùng
flat()thay thế.jsconst flattened = array.reduce((acc, cur) => acc.concat(cur), []);jsconst flattened = array.flat(); -
Nhóm đối tượng theo thuộc tính. Dùng
Object.groupBy()thay thế.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); -
Nối mảng chứa trong mảng của đối tượng. Dùng
flatMap()thay thế.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); -
Xóa các phần tử trùng lặp trong mảng. Dùng
SetvàArray.from()thay thế.jsconst uniqArray = array.reduce( (acc, cur) => (acc.includes(cur) ? acc : [...acc, cur]), [], );jsconst uniqArray = Array.from(new Set(array)); -
Loại bỏ hoặc thêm phần tử trong mảng. Dùng
flatMap()thay thế.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]; });Nếu bạn chỉ loại bỏ phần tử khỏi mảng, bạn cũng có thể dùng
filter(). -
Tìm kiếm phần tử hoặc kiểm tra xem các phần tử có thỏa mãn điều kiện không. Dùng
find()vàfindIndex(), hoặcsome()vàevery()thay thế. Các phương thức này có lợi ích bổ sung là trả về ngay khi kết quả chắc chắn, không cần lặp qua toàn bộ mảng.jsconst allEven = array.reduce((acc, cur) => acc && cur % 2 === 0, true);jsconst allEven = array.every((val) => val % 2 === 0);
Trong các trường hợp reduce() là lựa chọn tốt nhất, tài liệu và đặt tên biến có nghĩa ngữ nghĩa có thể giúp giảm thiểu nhược điểm về khả năng đọc.
Thông số kỹ thuật
| Thông số kỹ thuật |
|---|
| ECMAScript® 2027 Language Specification> # sec-array.prototype.reduce> |