Giải thích method reduce trong JS (tìm hiểu Array.reduce())

Cá nhân mình thấy method reduce() trong JS được ứng dụng rất nhiều, và nói thật cá nhân mình lúc mới tìm hiểu reduce thấy khá vất vả, search trên mạng khá nhiều rồi mới thấm được ít. Chính vì lẽ đó hôm này mình sẽ viết một bài dựa trên hiểu biết (đã tìm hiểu cộng với áp dụng) cộng thêm với dịch thuật từ một số trang web uy tín về JS để giải thích rõ hơn về method reduce() cũng như các (cách) ứng dụng của nó nhé. Oke, let’s go.

Khi hiểu rõ bản chất một thứ gì rồi, thì các vấn đề liên quan tới nó sẽ tự nhiên biến mất. Người ta sợ hãi những thứ họ không hiểu và lãng tránh nó để rồi mất một cơ hội để … “làm quen”. Lang man là vậy đó. Nhập đề ăn rơ lại nhé. Cú pháp của method reduce này như sau:

arr.reduce(callback, initialValue);

Thuật ngữ cơ bản

Tìm hiểu reduce() thì các bạn sẽ thấy có 2 thuật ngữ:

  • reducer (thực ra là một callback, tham số đầu tiên của hàm reduce)
  • accumulator (biến cộng dồn, tham số đầu tiên của callback reducer).

Bạn tưởng tượng nhé reduce nó giống như một cái vòng lặp, ví dụ mảng [1, 2, 3, 4, ..., 10] nó sẽ lặp (từ trái lần lần sang phải) đến hết mảng (1 -> 10). Với mỗi lần lặp, reduce sẽ gọi callback reducer.

Reducer và các tham số

Callback reducer nhận hai tham số (thực tế 4 nhưng phạm vi bài viết chúng ta tìm hiểu 2 tham số quan trọng nhất):

  • accumulator - biến cộng dồn nói phía trên
  • currentItem - giá trị hiện tại đang lặp

Và chỉ trả về duy nhất một giá trị. Giá trị này sẽ được truyền vào tham số accumulator của callback reducer ở lần gọi tiếp theo, nếu hết vòng lặp (không gọi reducer nữa) thì trả về giá trị này.

Ví dụ 1:

1
2
3
4
5
6
7
8
9
var array = [1, 2, 3, 4, 5];

var newArray = array.reduce((accumulator, currentItem) => {
return 10;
});

console.log(newArray);

// kết quả trả về 10;

Ví dụ 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var array = [9, 2, 3, 4, 5];

// Giá trị return trong callback sẽ được truyền vào tham số `accumulator` của callback reducer ở lần gọi tiếp theo
// đầu tiên là một vì reduce mới chạy chưa có giá trị nào nên nó lấy giá trị đầu tiên của array và currentItem sẽ là giá trị thứ hai của mảng
// callback được gọi 4 lần
var newArray = array.reduce((accumulator, currentItem) => {
console.log('in reduce', accumulator);
return 10;
});

console.log(newArray);

// in reduce 9
// in reduce 10
// in reduce 10
// in reduce 10
// 10

Ví dụ 3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var array = [9, 2, 3, 4, 5];

// số 7 kia chính là tham số khởi tạo cho callback
// currentItem sẽ là giá trị đầu tiên của mảng
// lúc này callback chạy 5 lần -> giống kiểu chạy mảng [7, 9, 2, 3, 4, 5]
var newArray = array.reduce((accumulator, currentItem) => {
console.log('in reduce', accumulator);
return 10;
}, 7);

console.log(newArray);
// in reduce 7
// in reduce 10
// in reduce 10
// in reduce 10
// in reduce 10
// 10

initialValue - giá trị khởi tạo

Giá trị khởi tạo cho callback reducer, giá trị này được truyền lúc gọi callback.

Khi gọi callback reducer mà bản thân cái mảng không có phần tử nào cộng với việc không có initialValue thì sẽ bị lỗi

1
2
3
4
5
6
7
8
9
var array = [];

var newArray = array.reduce((accumulator, currentItem) => {
console.log('in reduce', accumulator);
return accumulator;
});

console.log(newArray);
// Uncaught TypeError: Reduce of empty array with no initial value

Ứng dụng của reduce

1
2
3
4
5
6
7
8
9
var value = 0; 

var numbers = [5, 10, 15];

for (let i = 0; i < numbers.length; i++) {
value = value + numbers[i];
}

console.log(value); // 30

Kết quả in ra là 30.

Hàm này với mỗi lần lặp sẽ thay đổi giá trị biến value. Nhìn chung cách cũng ổn cơ mà xét về mặt pure function thì nó đã tác động đến biến (value) bên ngoài phạm vi vòng lặp. Nên để tránh ảnh hưởng đến biến value ta sẽ sử dụng reduce(). Kết quả in ra sẽ tương tự nhau.

Cách dùng reduce sẽ như vầy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* giá trị mặc định đầu tiên cho nó, giống value phía trên, giá trị mặc định đầu tiên là 0 */
var initialValue = 0;

var numbers = [5, 10, 15];

/*
đây là callback reducer, với 2 tham số là accumulator và currentItem
return trong reducer sẽ cộng dồn vào accumulator
currentItem là giá trị mỗi lần lặp
*/
var reducer = (accumulator, currentItem) => {
console.log(`giá trị accumulator: ${accumulator} và currentItem: ${currentItem}`);

return accumulator + currentItem;
};

/* gọi theo cú pháp hàm reduce */
var total = numbers.reduce(reducer, initialValue)

console.log(total); // 30
/*
// Viết ngắn gọn:
var numbers = [5, 10, 15];

var total = numbers.reduce((accumulator, currentItem) => {
console.log(`giá trị accumulator: ${accumulator} và currentItem: ${currentItem}`);
return accumulator + currentItem;
}, 0);
*/

Code ở trên rất đơn giản không có phức tạp cho lắm, lúc này bạn mở developer tool ra check xem console.log trong reducer in ra cái gì nhé.

Nó sẽ in ra như thế này.

image

Chúng ta sẽ đi phân tích chút xíu về cơ chế chạy chỗ này, mình sẽ mô phỏng 4 bước của quá trình chạy nhé:

  1. Vì mảng có 3 phần tử, nó sẽ chạy 3 lần. Ở lần chạy đầu tiên accumulator sẽ có giá trị là 0, giá trị này là từ initialValue truyền vào thông qua method reduce. Và currentItem lúc này là 5 sẽ cộng với accumulator (accumulator lúc này là 0), return ra 5 kết quả return của reducer sẽ “tự động cộng dồn” vào accumulator (thực ra cơ chế là nó lấy giá trị return này pass vào tham số reducer lần nữa).
  2. Lần 2 accumulator đang có giá trị là 5 (kết quả từ bước 1), cộng với currentItem lúc này là 10, return về 15.
  3. Lần chạy cuối cùng tương tự bước 2, currentItem lúc này là 15 cộng với accumulator giá trị là 15 return về 30.
  4. Vì đã hết mảng nên return giá trị accumulator về.

Đó là ví dụ đơn giản về reduce. Và sau đây mình sẽ nâng cao lên một chút.

Nâng cao chút - Ứng dụng reduce với array (flatten array)

Ví dụ này từ bài viết của mình

Ứng dụng reduce vào flatten mảng

Mình có mảng như sau:

1
var arr = [ 1, 2, 3, [ 1, 2, 3, 4, [ 2, 3, 4 ] ] ];

Và nếu JS trao cho mình cơ hội được viết lại hàm .flat thì mình sẽ dùng viết lại như ri:

1
2
3
4
5
6
7
8
9
10
11
12
13
function flatArray(arr) {
// giá trị mặc định của mảng đã làm phẳng là mảng rỗng
var initialValue = [];

return arr.reduce((accumulator, currentItem) => {
// nếu currentItem là mảng thì tiếp tục đệ quy để tìm đến khi nào không phải mảng nữa thì thôi
// nếu giá trị không phải là mảng thì nối vào accumulator
// sau khi đệ quy xong sẽ trả về mảng đã flatten chính là accumulator
return accumulator.concat(
Array.isArray(currentItem) ? flatArray(currentItem) : currentItem
);
}, initialValue);
}

Dùng như sau:

1
2
3
var flatten = flatArray(arr);

console.log(flatten); // kết quả trả về [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

Oke, xong ví dụ đơn gian thứ hai rồi nha.

Ứng dụng reduce với re-structure object

Giả sử mình có object như thế này:

1
2
3
4
5
var usersInfo = [
{ name: 'IronMan', age: 26 },
{ name: 'Captain', age: 24 },
{ name: 'Thor', age: 25 }
];

Và mình muốn nó re-structure lại như vầy:

1
2
3
4
5
var usersInfo = {
IronMan: { age: 26 },
Captain: { age: 24 },
Thor: { age: 25 }
};

Vậy ứng dụng reduce mình sẽ làm như thế nào nhỉ? Các bạn suy nghĩ chứ khoan kéo xuống phần mình làm nha. 3 phút nghĩ suy bắt đầu.

Đây là phần mình giải quyết:

1
2
3
4
5
var objectMapFromArray = (arr) => arr.reduce((accumulator, currentItem) => {
// chỉnh sửa lại currentItem và đẩy nó vào accumulator
accumulator[currentItem.name] = { age: currentItem.age };
return accumulator;
}, {});

Gọi sử dụng:

1
objectMapFromArray(usersInfo);

Và đây là kết quả:

image

Summary

Qua bài ni, mình hi vọng bạn đã hiểu cách thức hàm này hoạt động. Oh yeah.

Updated date: 21.03.2020

Tham khảo

 Comments
Comment plugin failed to load
Loading comment plugin