Map, filter, reduce trong JavaScript

Ngay cả khi bạn không phải một lập trình viên sử dụng ngôn ngữ JavaScript thì cũng có thể bạn đã sử dụng 3 hàm map, filter, reduce ở những ngôn ngữ khác. Lý do đơn giản là vì 3 hàm này cực kỳ hữu dụng và giúp code do bạn viết ra được clean hơn. Giờ ta hãy đi vào cụ thể trong JavaScript chúng được dùng như thế nào và mang lại lợi ích gì.

1. Map

1.1 Mô tả

Hàm map tạo ra 1 mảng mới. Mảng mới này là kết quả của việc xử lý từng phần tử của mảng cũ bằng cùng 1 phương thức xử lí mà ta quy định.

1.2 Cú pháp

var new_array = arr.map(function callback(currentValue[, index[, array]]) {
    // Return element for new_array
}[, thisArg])
1.2.1 Các tham số

callback là hàm tạo ra một phần tử của mảng mới, lấy ba đối số:

  • currentValue: phần tử hiện tại đang được xử lý trong mảng.
  • index (không bắt buộc): chỉ mục của phần tử hiện tại đang được xử lý trong mảng.
  • array (không bắt buộc): mảng được dùng để map.
    thisArg (không bắt buộc): Giá trị để sử dụng khi thực hiện callback.
1.2.2 Giá trị trả về

Giá trị trả về là một mảng mới với mỗi phần tử là kết quả của hàm callback.

1.3 Ví dụ

1.3.1 Ánh xạ một mảng các số tới một mảng các căn bậc hai

Đoạn mã sau nhận một mảng các số và tạo một mảng mới chứa các căn bậc hai của các số trong mảng đầu tiên.

var numbers = [1, 4, 9];
var roots = numbers.map(Math.sqrt);
// roots is now [1, 2, 3]
// numbers is still [1, 4, 9]
1.3.2 Sử dụng map để định dạng đối tượng trong một mảng

Đoạn mã sau nhận một mảng các đối tượng và tạo một mảng mới chứa các đối tượng mới được định dạng lại.

var kvArray = [{key: 1, value: 10}, 
               {key: 2, value: 20}, 
               {key: 3, value: 30}];

var reformattedArray = kvArray.map(obj =>{ 
   var rObj = {};
   rObj[obj.key] = obj.value;
   return rObj;
});
// reformattedArray is now [{1: 10}, {2: 20}, {3: 30}], 

// kvArray is still: 
// [{key: 1, value: 10}, 
//  {key: 2, value: 20}, 
//  {key: 3, value: 30}]
1.3.3 Ánh xạ một mảng các số bằng cách sử dụng hàm chứa đối số

Đoạn mã sau đây cho thấy map hoạt động như thế nào khi một hàm yêu cầu một đối số được sử dụng với nó.

var numbers = [1, 4, 9];
var doubles = numbers.map(function(num) {
  return num * 2;
});

// doubles is now [2, 8, 18]
// numbers is still [1, 4, 9]
1.3.4 Sử dụng map một cách tổng quát

Ví dụ này cho thấy cách lặp qua một tập hợp các đối tượng được thu thập bởi querySelectorAll. Trong trường hợp này, ta nhận được tất cả các tùy chọn đã chọn trên màn hình và được in ra console:

var elems = document.querySelectorAll('select option:checked');
var values = Array.prototype.map.call(elems, function(obj) {
  return obj.value;
});

2. Filter

2.1 Mô tả

Hàm filter là cũng tạo ra 1 mảng mới. Mảng mới này là kết quả của việc lọc tất cả các phần tử của mảng cũ để chọn ra những phần tử thỏa mảng 1 điều kiện gì đó. Điều kiện này được kiểm tra bằng 1 phương thức mà ta quy định.

2.2 Cú pháp

var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])
2.2.1 Các tham số

callback dùng để kiểm tra từng phần tử của mảng. Trả về true để giữ nguyên tố, sai nếu không. Nó chấp nhận ba đối số:

  • element: Phần tử hiện tại đang được xử lý trong mảng.
  • index (không bắt buộc): chỉ mục của phần tử hiện tại đang được xử lý trong mảng.
  • array (không bắt buộc): mảng được filter gọi.
    thisArg (không bắt buộc): giá trị được dùng khi thực hiện callback.
2.2.2 Giá trị trả về

Một mảng mới với các phần tử vượt qua bài kiểm tra.Nếu không có phần tử nào vượt qua bài kiểm tra, một mảng trống sẽ được trả về.

2.3 Ví dụ

2.3.1 Lọc ra tất cả các giá trị nhỏ

Ví dụ sau sử dụng filter để tạo một mảng đã lọc có tất cả các phần tử có giá trị nhỏ hơn 10 bị loại bỏ.

function isBigEnough(value) {
  return value >= 10;
}

var filtered = [12, 5, 8, 130, 44].filter(isBigEnough);
// filtered is [12, 130, 44]
2.3.2 Lọc các mục không hợp lệ từ JSON

Ví dụ sau sử dụng filter để tạo một json được lọc với bài kiểm tra là các id phải là số và có giá trị khác 0.

var arr = [
  { id: 15 },
  { id: -1 },
  { id: 0 },
  { id: 3 },
  { id: 12.2 },
  { },
  { id: null },
  { id: NaN },
  { id: 'undefined' }
];

var invalidEntries = 0;

function isNumber(obj) {
  return obj !== undefined && typeof(obj) === 'number' && !isNaN(obj);
}

function filterByID(item) {
  if (isNumber(item.id) && item.id !== 0) {
    return true;
  } 
  invalidEntries++;
  return false; 
}

var arrByID = arr.filter(filterByID);

console.log('Filtered Array\n', arrByID); 
// Filtered Array
// [{ id: 15 }, { id: -1 }, { id: 3 }, { id: 12.2 }]

console.log('Number of Invalid Entries = ', invalidEntries); 
// Number of Invalid Entries = 5
2.3.3 Tìm kiếm trong mảng

Ví dụ sau sử dụng filter để lọc nội dung mảng dựa trên tiêu chí tìm kiếm

var fruits = ['apple', 'banana', 'grapes', 'mango', 'orange'];

/**
 * Array filters items based on search criteria (query)
 */
function filterItems(query) {
  return fruits.filter(function(el) {
      return el.toLowerCase().indexOf(query.toLowerCase()) > -1;
  })
}

console.log(filterItems('ap')); // ['apple', 'grapes']
console.log(filterItems('an')); // ['banana', 'mango', 'orange']
2.3.4 Implementation của ES2015
const fruits = ['apple', 'banana', 'grapes', 'mango', 'orange'];

/**
 * Array filters items based on search criteria (query)
 */
const filterItems = (query) => {
  return fruits.filter((el) =>
    el.toLowerCase().indexOf(query.toLowerCase()) > -1
  );
}

console.log(filterItems('ap')); // ['apple', 'grapes']
console.log(filterItems('an')); // ['banana', 'mango', 'orange']

3. Reduce

Bây giờ chúng ta đến với phần thú vị nhất chính là reduce. Nếu bạn biết đôi chút về reduce và nghĩ nó được dùng trong kịch bản để tính tổng của các phần tử kiểu là number trong mảng thì bạn ... cũng đúng nhưng thật đáng tiếc! Thật ra reduce cực kì đa năng và linh hoạt, nó có thể được dùng trong nhiều kịch bản khác nữa.

3.1 Mô tả

Hàm reduce áp dụng một phương thức lên accumulator (vật chứa) và từng phần tử trong mảng (từ trái sang phải) để từ input là 1 mảng (thường có nhiều phần tử), ta có được output là 1 giá trị đơn.

3.2 Cú pháp

arr.reduce(callback[, initialValue])
3.2.1 Các tham số đầu vào

callback là hàm để thực hiện trên mỗi phần tử trong mảng, lấy bốn đối số:

  • accumulator: tích lũy giá trị trả về cuộc gọi lại trong lần gọi lại cuối cùng của cuộc gọi lại hoặc giá trị ban đầu, nếu được cung cấp
  • currentValue: phần tử hiện tại đang được xử lý trong mảng.
  • currentIndex (không bắt buộc): bắt đầu tại chỉ mục 0, nếu giá trị initialValue được cung cấp và nếu không được cung cấp thì bắt đầu ở chỉ mục 1.
  • array: mảng được phương thức reduce() gọi.
    initialValue: Giá trị để sử dụng làm đối số đầu tiên cho cuộc gọi đầu tiên của cuộc gọi lại. Nếu không có giá trị ban đầu nào được cung cấp, phần tử đầu tiên trong mảng sẽ được sử dụng. Việc gọi hàm reduce() trên một mảng trống không có giá trị ban đầu là một lỗi.
3.2.2 Giá trị trả về

Giá trị kết quả từ việc gọi hàm reduce().

3.2 Ví dụ

3.2.1 Tổng hợp tất cả các giá trị của một mảng
var sum = [0, 1, 2, 3].reduce(function (accumulator, currentValue) {
  return accumulator + currentValue;
}, 0);
// sum is 6

Hoặc ta cũng có thể viết kết hợp với việc sử dụng arrow function

var total = [ 0, 1, 2, 3 ].reduce(
  ( accumulator, currentValue ) => accumulator + currentValue,
  0
);
3.2.2 Tổng các giá trị trong một mảng đối tượng

Để tổng hợp các giá trị chứa trong một mảng các đối tượng, bạn phải cung cấp một giá trị ban đầu để mỗi mục đi qua hàm của bạn.

var initialValue = 0;
var sum = [{x: 1}, {x:2}, {x:3}].reduce(function (accumulator, currentValue) {
    return accumulator + currentValue.x;
},initialValue)

console.log(sum) // logs 6

Hoặc ta cũng có thể viết kết hợp với việc sử dụng arrow function

var initialValue = 0;
var sum = [{x: 1}, {x:2}, {x:3}].reduce(
    (accumulator, currentValue) => accumulator + currentValue.x
    ,initialValue
);

console.log(sum) // logs 6
3.2.3 Làm phẳng mảng
var flattened = [[0, 1], [2, 3], [4, 5]].reduce(
  function(accumulator, currentValue) {
    return accumulator.concat(currentValue);
  },
  []
);
// flattened is [0, 1, 2, 3, 4, 5]

Hoặc ta cũng có thể viết kết hợp với việc sử dụng arrow function

var flattened = [[0, 1], [2, 3], [4, 5]].reduce(
  ( accumulator, currentValue ) => accumulator.concat(currentValue),
  []
);
3.2.4 Đếm các instances của các giá trị trong một đối tượng
var names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];

var countedNames = names.reduce(function (allNames, name) { 
  if (name in allNames) {
    allNames[name]++;
  }
  else {
    allNames[name] = 1;
  }
  return allNames;
}, {});
// countedNames is:
// { 'Alice': 2, 'Bob': 1, 'Tiff': 1, 'Bruce': 1 }
3.2.4 Nhóm đối tượng theo thuộc tính
var people = [
  { name: 'Alice', age: 21 },
  { name: 'Max', age: 20 },
  { name: 'Jane', age: 20 }
];

function groupBy(objectArray, property) {
  return objectArray.reduce(function (acc, obj) {
    var key = obj[property];
    if (!acc[key]) {
      acc[key] = [];
    }
    acc[key].push(obj);
    return acc;
  }, {});
}

var groupedPeople = groupBy(people, 'age');
// groupedPeople is:
// { 
//   20: [
//     { name: 'Max', age: 20 }, 
//     { name: 'Jane', age: 20 }
//   ], 
//   21: [{ name: 'Alice', age: 21 }] 
// }
3.2.5 Các mảng liên kết chứa trong một mảng các đối tượng sử dụng toán tử spread và initialValue
// friends - an array of objects 
// where object field "books" - list of favorite books 
var friends = [{
  name: 'Anna',
  books: ['Bible', 'Harry Potter'],
  age: 21
}, {
  name: 'Bob',
  books: ['War and peace', 'Romeo and Juliet'],
  age: 26
}, {
  name: 'Alice',
  books: ['The Lord of the Rings', 'The Shining'],
  age: 18
}];

// allbooks - list which will contain all friends' books +  
// additional list contained in initialValue
var allbooks = friends.reduce(function(accumulator, currentValue) {
  return [...accumulator, ...currentValue.books];
}, ['Alphabet']);

// allbooks = [
//   'Alphabet', 'Bible', 'Harry Potter', 'War and peace', 
//   'Romeo and Juliet', 'The Lord of the Rings',
//   'The Shining'
// ]
3.2.5 Xóa các mục trùng lặp trong mảng
let arr = [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4];
let result = arr.sort().reduce((accumulator, current) => {
    const length = accumulator.length
    if (length === 0 || accumulator[length - 1] !== current) {
        accumulator.push(current);
    }
    return accumulator;
}, []);
console.log(result); //[1,2,3,4,5]
3.2.6 Thực thi promise theo tuần tự

Reduce truyền kết quả của việc gọi 1 callback function tới 1 callback function tiếp theo. Điều này cho phép ta đạt được những thứ hay ho như là Chaining Promises. Ta xét đoạn code dưới đây:

let itemIDs = [1, 2, 3, 4, 5]; 
itemIDs.reduce((promise, itemID) => {
  return promise.then(_ => api.deleteItem(itemID));
}, Promise.resolve());

Đoạn code trên có thể được dịch thành như sau:

Promise.resolve()
.then(_ => api.deleteItem(1))
.then(_ => api.deleteItem(2))
.then(_ => api.deleteItem(3))
.then(_ => api.deleteItem(4))
.then(_ => api.deleteItem(5));

Viết cách sử dụng reduce, code của bạn ở trên dễ nhìn và clean hơn rất nhiều. Hãy thử tưởng tượng trường hợp mảng itemIDs không phải chỉ có 5 phần tử mà có 40 phần tử thì bạn sẽ cảm nhận được viết theo cách sử dụng reduce thì code sẽ clean như thế nào.

Tài liệu tham khảo

https://medium.com/jsguru/javascript-functional-programming-map-filter-and-reduce-846ff9ba492d
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array