Thứ năm, 12/12/2019 | 00:00 GMT+7

Cách sử dụng map (), filter () và Reduce () trong JavaScript

Lập trình chức năng trong JavaScript mang lại lợi ích cho khả năng đọc mã, khả năng bảo trì và khả năng kiểm tra.

Một trong những công cụ từ tư duy lập trình chức năng là lập trình theo kiểu xử lý mảng. Điều này đòi hỏi phải sử dụng mảng làm cấu trúc dữ liệu cơ bản của bạn. Sau đó, chương trình của bạn trở thành một chuỗi các phép toán trên các phần tử trong mảng.

Có nhiều ngữ cảnh hữu ích, chẳng hạn như ánh xạ kết quả AJAX với các thành phần React với map , xóa dữ liệu không liên quan bằng filter và sử dụng reduce . Các hàm này, được gọi là “Array Extras”, là các hàm trừu tượng qua các vòng lặp for . Không có gì bạn có thể làm với với các chức năng mà bạn không thể đạt được với là for , và ngược lại.

Trong hướng dẫn này, ta sẽ hiểu sâu hơn về lập trình hàm trong JavaScript bằng cách xem xét filter , map và thu reduce .

Lặp lại với for

Ta sử dụng vòng lặp for để lặp lại mọi mục trong một mảng. Thông thường, ta làm một cái gì đó với từng mục trên đường đi.

Một ví dụ sẽ viết hoa mọi chuỗi trong một mảng.

const strings = ['arielle', 'are', 'you', 'there']; const capitalizedStrings = [];  for (let i = 0; i < strings.length; i += 1) {     const string = strings[i];     capitalizedStrings.push(string.toUpperCase()); }  console.log(capitalizedStrings); 

Trong đoạn mã này, ta bắt đầu với một loạt các tên viết thường. Sau đó, ta khởi tạo một mảng trống, trong đó ta sẽ lưu trữ các chuỗi được viết hoa của ta .

Bên trong vòng lặp for , ta lấy chuỗi tiếp theo trên mỗi lần lặp; viết hoa nó; và đẩy nó vào capitalizedStrings . Ở cuối vòng lặp, capitalizedStrings chứa mọi từ trong strings , nhưng… Bạn biết đấy, viết hoa.

Điều này đưa ta đến chức năng đầu tiên của ta : forEach . Đây là một phương thức trên mảng “tự động” lặp qua danh sách cho ta . Nói cách khác, nó xử lý các chi tiết của việc khởi tạo và tăng bộ đếm cho ta .

Thay vì ở trên, nơi ta lập index theo cách thủ công thành strings , ta có thể gọi forEach và nhận chuỗi tiếp theo sau mỗi lần lặp. Phiên bản cập nhật sẽ trông giống như sau:

const strings = ['arielle', 'are', 'you', 'there']; const capitalizedStrings = [];  strings.forEach(function (string) {     capitalizedStrings.push(string.toUpperCase()); }) 

Điều này gần giống như những gì ta đã bắt đầu. Nhưng loại bỏ điều đó, i làm cho mã của ta dễ đọc hơn.

Điều này cũng giới thiệu một mô hình chính mà ta sẽ thấy hết lần này đến lần khác. Cụ thể là: Ta thích sử dụng các phương thức trên Array.prototype để trừu tượng hóa các chi tiết như khởi tạo và tăng dần bộ đếm. Bằng cách đó, ta có thể tập trung vào logic quan trọng, thay vì bảng soạn sẵn.

Tìm hiểu về Mật mã Caesar và Lực lượng vũ phu

Trong các đoạn mã bên dưới, ta sẽ sử dụng mã hóa và giải mã các chuỗi trong các ví dụ về map , thu reducefilter của ta .

Ví dụ:

// Thanks to EvanHahn for this: https://gist.github.com/EvanHahn/2587465 const caesar = require('./caesar');  // We can encrypt the string: 'this is my secret message' with `caesar` // Here, we scramble the message by shifting each letter forward by 2 letters: 'a' becomes 'c'; 's' becomes  'u'; etc. const encryptedString = caesar('this is my super secret message.', 2); console.log(encryptedString); // 'vjku ku oa uwrgt ugetgv uvtkpi.' 

Ý tưởng ở đây là, nếu tôi gửi cho bạn một tin nhắn bình thường, như 'this is my super secret message' , và người khác nhúng tay vào, họ có thể đọc được bí mật ngay lập tức. Điều này rõ ràng là tệ hại nếu bạn đang gửi thông tin nhạy cảm, chẳng hạn như password , mà ai đó có thể đang nghe.

Mã hóa một chuỗi nghĩa là : “xáo trộn nó để làm cho nó khó đọc mà không cần giải mã”. Bằng cách này, thậm chí nếu ai đó đang lắng nghe, và thậm chí nếu họ làm chặn thông điệp của bạn, nó sẽ vẫn không thể đọc được cho đến khi họ xắp xếp lại nó. Để trích dẫn ví dụ trên, không rõ ràng là chuỗi 'vjku ku oa uwrgt ugetgv uvtkpi.' được cho là có nghĩa.

Mật mã Caesar là một cách để xáo trộn một chuỗi như thế này. Để mã hóa bằng mật mã Caesar, ta chọn một khóa , n , từ 1 đến 24, và thay thế mọi ký tự trong chuỗi root bằng n ký tự nằm sâu hơn trong bảng chữ cái.

Vì vậy, nếu ta chọn khóa 2, a trở thành c ; b trở thành d ; c trở thành e ; Vân vân.

Việc thay thế các chữ cái như thế này làm cho chuỗi root khá khó đọc. Nhưng, ta đã xáo trộn bằng cách dịch chuyển các chữ cái. Vì vậy, tất cả những gì bạn cần làm để sắp xếp là chuyển chúng trở lại. Nếu bạn nhận được một tin nhắn mà bạn biết đã được mã hóa bằng phím 2, tất cả những gì bạn cần làm để giải mã là chuyển các chữ cái trở lại hai dấu cách. Vì vậy, c trở thành a ; d trở thành b ; Vân vân.

Thật không may, đây không phải là một hình thức mã hóa mạnh mẽ ngày nay vì nó cực kỳ dễ bị phá vỡ. Một cách để giải mã bất kỳ chuỗi nào được mã hóa bằng mật mã Caesar là cố gắng giải mã bằng mọi khóa có thể. Một trong những kết quả sẽ chính xác. Tất cả những gì bạn cần làm là quét dãy 24 kết quả cho kết quả là tiếng Anh. Các chuỗi khác sẽ không thể đọc được như chuỗi ban đầu.

Dưới đây, tôi sẽ sử dụng một hàm được gọi là tryAll , thực hiện điều đó. Cụ thể: Nó lấy một chuỗi được mã hóa và trả về một mảng của mọi giải mã có thể. Một trong những kết quả đó sẽ là chuỗi mà ta muốn. Vì vậy, điều này luôn luôn phá vỡ mật mã.

Thật khó khăn để quét một loạt 24 giải mã có thể. Ta có thể loại bỏ những cái chắc chắn không chính xác.

Trong phần trên filter , bạn sẽ thấy một hàm, gọi là isEnglish , thực hiện điều này. Đặc biệt, nó đọc một chuỗi; đếm có bao nhiêu trong số 1.000 từ thông dụng nhất trong tiếng Anh xuất hiện trong chuỗi đó; và, nếu nó tìm thấy nhiều hơn 3 từ đó trong câu, nó sẽ phân loại chuỗi là tiếng Anh. Nếu chuỗi chứa ít hơn 3 từ từ mảng đó, nó sẽ ném nó ra ngoài.

Tất nhiên, có nhiều cách phức tạp hơn để kiểm tra xem một chuỗi có phải là tiếng Anh hay không, nhưng hiện tại thì cách này vẫn ổn.

Việc triển khai tryAllisEnglish không quan trọng, nhưng nếu bạn tò mò, bạn có thể tìm thấy chúng tại ý chính sau: tryAll / isEnglish .

Chuyển đổi Mảng với map

Cấu trúc lại một vòng lặp for để sử dụng forEach gợi ý về ưu điểm của kiểu này. Nhưng vẫn còn chỗ để cải thiện trong version cập nhật.

Trong ví dụ trên, ta đang cập nhật capitalizedStrings trong lệnh gọi lại thành forEach . Không có gì sai với điều này. Nhưng , nó tiết kiệm rất nhiều đau đầu để tránh tác dụng phụ như vậy khi nào có thể.

Nếu ta không phải cập nhật các cấu trúc dữ liệu nằm trong một phạm vi khác… Có lẽ ta không nên.

Trong trường hợp cụ thể này, ta muốn biến mọi chuỗi trong strings thành version viết hoa của nó. Đây là một trường hợp sử dụng rất phổ biến for vòng lặp for : Lấy mọi thứ trong một mảng; biến nó thành một thứ khác; và thu thập kết quả trong một mảng mới.

Việc chuyển đổi mọi phần tử trong một mảng thành một phần tử mới và thu thập kết quả được gọi là ánh xạ . JavaScript có một hàm tích hợp chỉ cho trường hợp sử dụng này, được gọi là bản đồ , đủ thích hợp.

Ta đã sử dụng forEach vì nó loại bỏ sự cần thiết phải quản lý biến lặp, i . Ta không phải lập index thủ công vào mảng, v.v. và vì vậy ta có thể tập trung vào logic thực sự quan trọng.

Tương tự như vậy, ta sử dụng map vì nó trừu tượng hóa việc khởi tạo một mảng trống và đẩy đến nó. Cũng giống như forEach chấp nhận một lệnh gọi lại thực hiện một điều gì đó với mỗi giá trị chuỗi, map chấp nhận một lệnh gọi lại thực hiện một điều gì đó với mỗi giá trị chuỗi.

Hãy xem bản demo nhanh trước khi có lời giải thích cuối cùng. Trong ví dụ này, tôi đang sử dụng một hàm để "mã hóa" một mảng tin nhắn. Ta có thể sử dụng vòng lặp for , và ta có thể sử dụng forEach … Nhưng, nó tốt hơn với map .

const caesar = require('../caesar');  const key = 12; const messages = [     'arielle, are you there?',     'the ghost has killed the shell',     'the liziorati attack at dawn' ]  const encryptedMessages = messages.map(function (string) {     return caesar(string, key); })  console.log(encryptedMessages); 

Lưu ý những gì đã xảy ra ở đây.

  • Ta bắt đầu với một loạt các messages , bằng tiếng Anh đơn giản.
  • Ta sử dụng phương pháp map trên messages để mã hóa từng chuỗi bằng hàm caesar và tự động lưu trữ kết quả trong một mảng mới.

Sau khi chạy mã trên, các Tin nhắn encryptedMessages trông giống như: ['mduqxxq, mdq kag ftqdq?', 'ftq staef tme wuxxqp ftq etqxx', 'ftq xuluadmfu mffmow mf pmiz'] .

Đây là mức trừu tượng cao hơn nhiều so với việc đẩy vào một mảng theo cách thủ công.

Bây giờ là thời điểm tốt để cho biết ta có thể sử dụng các hàm mũi tên để diễn đạt loại điều này một cách ngắn gọn hơn:

const caesar = require('../caesar');  const key = 12; const messages = [     'arielle, are you there?',     'the ghost has killed the shell',     'the liziorati attack at dawn' ];  const encryptedMessages = messages.map(string => string.toUpperCase()):  console.log(encryptedMessages); 

Quăng mọi thứ đi với filter

Một mẫu phổ biến khác là sử dụng vòng lặp for để xử lý các mục trong một mảng, nhưng chỉ đẩy / bảo toàn một số trong số chúng.

Ta thường quyết định những món nào nên giữ lại và những món nào nên vứt bỏ, bằng cách kiểm tra if .

Trong JS thô, điều này có thể giống như sau:

// Secret message! This was a string encrypted with a key between 1 and 24. const encryptedMessage = 'mduqxxq, mdq kag ftqdq?'];  // We can break this code by just decrypting with _every_ possible key. const possibilities = tryAll(encryptedMessage);  // Most of these decryption attempts aren't readable. Sad. // We can speed up finding the ones that are probably junk with an if check const likelyPossibilities = []; possibilities.forEach(function (decryptionAttempt) {     // Keep the string if it looks like an English sentence     if (isEnglish(decryptionAttempt)) {         likelyPossibilities.push(decryptionAttempt);     } }) 

Đầu tiên, ta cố gắng giải mã tin nhắn đã mã hóa bằng mọi khóa có thể. Điều đó nghĩa là ta kết thúc với một loạt 24 khả năng.

Trong vòng lặp này, ta kiểm tra từng cái để xem nó có giống chuỗi tiếng Anh không. Nếu vậy, ta giữ nó. Nếu không, ta vứt nó đi.

Đây rõ ràng là một trường hợp sử dụng phổ biến. Vì vậy, có một bộ lọc được tích hợp sẵn cho nó, bộ lọc được đặt tên phù hợp.

Giống như với map , ta chuyển filter một lệnh gọi lại, filter cũng lấy từng chuỗi. Sự khác biệt là, filter sẽ chỉ lưu các mục trong một mảng nếu lệnh gọi lại trả về true. Vì vậy, ta có thể diễn đạt ở trên như sau:

// Secret message! This was a string encrypted with a key between 1 and 24. const encryptedMessage = 'mduqxxq, mdq kag ftqdq?'];  // We can break this code by just decrypting with _every_ possible key. const possibilities = tryAll(encryptedMessage);  // Most of these decryption attempts aren't readable. Sad. // We can speed up finding the ones that are probably junk with an if check const likelyPossibilities = possibilities.filter(function (string) {     // If isEnglish(string) returns true, filter saves in the output array     return isEnglish(string); }) 

Vì lệnh gọi lại này chỉ gọi isEnglish , ta có thể viết nó ngắn gọn hơn, như thế này:

// Secret message! This was a string encrypted with a key between 1 and 24. const encryptedMessage = 'mduqxxq, mdq kag ftqdq?'];  // We can break this code by just decrypting with _every_ possible key. const possibilities = tryAll(encryptedMessage);  // Most of these decryption attempts aren't readable. Sad. // We can speed up finding the ones that are probably junk with an if check const likelyPossibilities = possibilities.filter(isEnglish); 

Kết hợp mọi thứ với nhau và reduce

Lúc này, ta đã thấy các bản tóm tắt cho ba trường hợp sử dụng phổ biến xung quanh mảng:

  • forEach , cho phép ta lặp qua một mảng như với for , nhưng loại bỏ sự cần thiết phải quản lý thủ công biến lặp i ; index vào mảng; Vân vân.
  • map , cho phép ta chuyển đổi từng phần tử của một mảng và thu thập các kết quả trong một mảng
  • filter , cho phép ta chọn các phần tử của một mảng để giữ lại

Một trường hợp sử dụng phổ biến khác là: Lặp lại một mảng để thu thập các phần tử của nó thành một kết quả duy nhất.

Ví dụ nguyên mẫu là cộng một loạt các số.

// prices of: (big) box of oreos, girl scout cookies, fidget spinner, gold-plated Arch linux magnet const prices = [12, 19, 7, 209];  // variable to store our total prices in let totalPrice = 0; for (let i = 0; i < prices.length; i += 1) {     totalPrice += prices[i]; }  // Report our total prices: $247 console.log(`Your total is ${totalPrice}.`); 

Như tôi chắc bạn đoán, cũng có một điều trừu tượng cho điều này: giảm .

Cấu trúc lại vòng lặp trên với reduce trông giống như:

const prices = [12, 19, 7, 209];  prices.reduce(function (totalPrice, nextPrice) {     // totalPrice is the price so far     console.log(`Total price so far: ${totalPrice}`)     console.log(`Next price to add: ${nextPrice}`)      // update totalPrice by adding the next price     totalPrice += nextPrice      // return the total price, and start over again     return totalPrice }, 0) // ^ the second argument to `reduce` is the totalPrice to start with on the first iteration 

Giống như mapfilter , reduce chấp nhận một lệnh gọi lại, nó chạy trên mỗi phần tử của mảng.

Không giống như mapfilter , lệnh gọi lại mà ta truyền để reduce chấp nhận hai đối số: totalPricenextPrice .

totalPrice giống như total trong ví dụ đầu tiên: Đó là giá mà ta nhận được bằng cách cộng tất cả các giá mà ta đã thấy cho đến nay.

nextPrice là con số ta nhận được khi tính prices[i] trong ví dụ đầu tiên. Nhớ lại map đó và tự động reduce index vào mảng cho ta và tự động chuyển giá trị này cho các lệnh gọi lại của chúng. reduce cũng làm điều tương tự, nhưng chuyển giá trị đó làm đối số thứ hai cho lệnh gọi lại của nó.

Cũng giống như map và rút reduce , trên mỗi lần lặp, ta phải trả về giá trị mà ta muốn lấy lại ở cuối vòng lặp. Cụ thể là totalPrice .

Cuối cùng, lưu ý ta truyền một đối số khác sau lệnh gọi lại để reduce . Trong trường hợp này, ta vượt qua 0 . Điều này tương tự như dòng trong ví dụ đầu tiên, nơi ta đặt const total = 0 . Đối số thứ hai đó là giá trị ban đầu của totalPrice .


reduce khó tìm kiếm hơn so với map hoặc filter , vì vậy hãy xem xét một ví dụ khác.

Trong ví dụ trước, ta đã sử dụng reduce để thu thập một mảng số thành một tổng. Nhưng reduce là đa năng. Ta có thể sử dụng nó để biến một mảng thành một kết quả bất kỳ .

Ví dụ, ta có thể sử dụng nó để xây dựng một chuỗi.

const courses = ['Introduction to Programming', 'Algorithms & Data Structures', 'Discrete Math'];  const curriculum = courses.reduce(function (courseList, course) {     // add the name to the class, with a preceding newline and tab for indentation     return courseList += `\n\t${course}`; }, 'The Computer Science curriculum consists of:');  console.log(curriculum); 

Điều này tạo ra kết quả :

The Computer Science curriculum consists of:     Introduction to Programming     Algorithms & Data Structures     Discrete Math 

Tôi không thấy rất nhiều ví dụ về việc sử dụng reduce để xây dựng các chuỗi như thế này, vì một số lý do, nhưng đó là cách thực hành phổ biến theo kiểu hàm.


, reduce rất linh hoạt: Ta có thể sử dụng nó để biến một mảng thành bất kỳ loại kết quả "duy nhất" nào.

Ngay cả khi kết quả duy nhất đó là một mảng khác.

Kiểm tra nó:

const names = ['arielle', 'jung', 'scheherazade'];  const titleCase = function (name) {     // uppercase first letter of name     const first = name[0];     const capitalizedFirst = first.toUpperCase();      // get rest of name     const rest = name.slice(1);      // create list with all letters of the name, including capitalized first letter     const letters = [capitalizedFirst].concat(rest);      // join letters, return result     return letters.join(''); }  const titleCased = names.reduce(function (titleCasedNames, name) {     // title-case the next name     const titleCasedName = titleCase(name);      // add the title-cased name to a list     titleCasedNames.push(titleCasedName);      // return list of capitalizedNames     return titleCasedNames }, []) // ^ start with an empty list  console.log(titleCased); // ["Arielle", "Jung", "Scheherazade"] 

Vì vậy… Điều này thật gọn gàng. Ta đã sử dụng reduce để biến một danh sách các tên viết thường thành danh sách các tên viết tắt theo tiêu đề.

Trước đây, ta đã biến một danh sách các số thành một tổng duy nhất và một danh sách các chuỗi thành một chuỗi duy nhất. Ở đây, ta đã chuyển một danh sách các tên thành một danh sách các tên theo tiêu đề.

Điều này vẫn được cho phép, vì danh sách các tên được đặt theo tiêu đề vẫn là một kết quả duy nhất . Nó chỉ là một tập hợp, chứ không phải là một kiểu nguyên thủy.


Bạn có thể nhận thấy điều gì đó về ví dụ cuối cùng đó: Ta đã sử dụng reduce cho cùng một tác vụ mà ta đã sử dụng map trước đó.

Nó có thể chưa rõ ràng, nhưng— đây là một vấn đề lớn .

Bạn có thể đoán rằng ta có thể viết map theo cách reduce . Bạn sẽ đúng.

const map = (list, mapFunction) => {     const output = list.reduce((transformedList, nextElement) => {         // use the mapFunction to transform the nextElement in the list          const transformedElement = mapFunction(nextElement);          // add transformedElement to our list of transformed elements         transformedList.push(transformedElement);          // return list of transformed elements         return transformedList;     }, [])     // ^ start with an empty list      return output; } 

Ta có thể nhận được kết quả tương tự như trên với:

// congratulations...you just implemented `map` with `reduce`. badass. const titleCased = map(names, titleCase);  console.log(titleCased); // ["Arielle", "Jung", "Scheherazade"] 

Tuyên bố từ chối trách nhiệm bắt buộc: Nếu bạn thực sự đang triển khai map sẵn sàng production , bạn cần thêm một số tính năng vào việc triển khai này. Ta sẽ để lại tất cả những điều đó vì sự rõ ràng.


Nếu ta có thể sử dụng reduce để triển khai map , còn filter thì sao?

const filter = (list, predicate) => {     const output = list.reduce(function (filteredElements, nextElement) {         // only add `nextElement` if it passes our test         if (predicate(nextElement)) {             filteredElements.push(nextElement);         }          // return the list of filtered elements on each iteration         return filteredElements;         }, [])     }) } 

Ta sẽ sử dụng điều này như:

 

Không sao cả nếu cái này cảm thấy khó hiểu. Đó là một ý tưởng mới ngay cả đối với các nhà phát triển có kinh nghiệm hơn. Tuy nhiên, rất đáng để bạn phải suy nghĩ kỹ về nó: Ta cần một chút hiểu biết sâu sắc này để hiểu rõ về quá trình chuyển đổi một chút sau đó.

… Và, tin tôi đi, chỉ riêng việc chuyển đổi đã đủ tuyệt vời để có giá trị công việc.

Kết luận

Trong hướng dẫn này, bạn đã học cách sử dụng map , filter và thu reduce để viết mã dễ đọc hơn.

Tất nhiên, không có gì sai khi sử dụng for . Nhưng nâng cao mức độ trừu tượng với các chức năng này mang lại lợi ích tức thì cho tính dễ đọc và khả năng bảo trì.

Từ đây, bạn có thể bắt đầu khám phá các phương thức mảng khác như flatten , concatflatMap .


Tags:

Các tin liên quan

Cách gói một gói JavaScript Vanilla để sử dụng trong React
2019-12-12
Cách phát triển một trình tải lên tệp tương tác với JavaScript và Canvas
2019-12-12
Giải thích về lập trình chức năng JavaScript: Ứng dụng một phần và làm xoăn
2019-12-12
Giới thiệu về Closures và Currying trong JavaScript
2019-12-12
Bắt đầu với các hàm mũi tên ES6 trong JavaScript
2019-12-12
Cách đếm số nguyên âm trong một chuỗi văn bản bằng thuật toán JavaScript
2019-12-12
Cách sử dụng phép gán cấu trúc hủy trong JavaScript
2019-12-12
Giải thích về lập trình chức năng JavaScript: Kết hợp & truyền tải
2019-12-12
Cách sử dụng .every () và .some () để điều khiển mảng JavaScript
2019-12-12
Chuyển đổi Mảng sang Chuỗi trong JavaScript
2019-12-05