Menu

Phương thức mảng JavaScript: map, filter, reduce và hơn nữa

Những phương thức mảng thay thế gần hết vòng lặp for của bạn: map, filter, reduce, find, some, every, và phương thức nào sửa mảng gốc, phương thức nào trả về mảng mới.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Mảng đi kèm một hộp đồ nghề

Mảng trong JavaScript có sẵn rất nhiều phương thức. Phần lớn những việc bạn hay viết vòng lặp for để làm, như biến đổi giá trị, lọc ra một số phần tử hay cộng tổng, đều có một phương thức làm được trong một dòng, dễ đọc hơn và kết hợp gọn gàng với các phương thức khác.

Bộ khởi đầu gồm ba phương thức (method): map, filter và reduce. Nắm vững ba phương thức này cùng vài phương thức họ hàng, đoạn code chi chít vòng lặp của bạn sẽ gọn lại thành thứ bạn đọc lướt là hiểu.

Mỗi phương thức nhận một callback và trả về một thứ gì đó. Không phương thức nào sửa đổi nums. Đây là điều đáng ghi nhớ ngay từ đầu.

map: biến đổi từng phần tử

map nhận một hàm và gọi hàm đó trên từng phần tử, gom các giá trị trả về vào một mảng mới có cùng độ dài. Dùng nó khi bạn muốn "mỗi đầu vào cho ra một đầu ra".

Callback còn nhận chỉ số làm đối số thứ hai nếu bạn cần: arr.map((item, i) => ...). Không cần thì cứ bỏ qua.

Một lỗi thường gặp: dùng map khi bạn không cần mảng trả về. Nếu bạn chỉ muốn in từng phần tử hay ghi vào cơ sở dữ liệu, đó là việc của forEach hoặc một vòng lặp thông thường.

filter: giữ lại những phần tử khớp

filter gọi một vị từ (predicate), tức một hàm trả về true hoặc false, trên từng phần tử và giữ lại những phần tử cho kết quả truthy. Mảng mới có độ dài bằng hoặc ngắn hơn mảng ban đầu.

map và filter nối chuỗi với nhau rất tự nhiên. Hãy đọc chuỗi gọi từ trái sang phải như một pipeline:

Lọc trước, rồi mới map. Như vậy map chỉ chạy trên những phần tử còn lại.

reduce: gộp mảng thành một giá trị

reduce là phương thức tổng quát nhất trong ba phương thức. Bạn đưa cho nó một hàm reducer (accumulator, item) => newAccumulator và một giá trị khởi đầu. Nó duyệt qua mảng, đưa từng phần tử vào reducer cùng với giá trị tích lũy hiện có, rồi trả về giá trị tích lũy cuối cùng.

Kết quả không nhất thiết phải là số. Nó có thể là một đối tượng, một mảng khác, một chuỗi, bất cứ thứ gì bạn dựng lên:

Luôn truyền giá trị khởi đầu (đối số thứ hai). Thiếu nó, reduce lấy phần tử đầu tiên làm giá trị tích lũy ban đầu, cách này lỗi với mảng rỗng và thường cũng không cho ra điều bạn muốn.

reduce rất mạnh nhưng có thể khó đọc khi logic trở nên rối. Nếu reducer của bạn dài hơn vài dòng, một vòng lặp for...of thông thường thường rõ ràng hơn.

forEach: tác dụng phụ, không trả về gì

forEach giống map nhưng không trả về mảng. Nó dành cho lúc bạn muốn làm gì đó với từng phần tử, như in ra, gọi API hay cập nhật DOM, mà không cần một tập hợp mới.

Hai điều cần biết:

  • forEach trả về undefined. Bạn không thể nối .map() sau nó.
  • Bạn không thể dùng break để thoát sớm khỏi forEach. Nếu cần dừng sớm, dùng for...of hoặc some/every.

Nếu thấy mình đang viết arr.forEach(x => results.push(transform(x))), đó thực ra là một map.

find và findIndex: chỉ cần một phần tử

find trả về phần tử đầu tiên khớp với vị từ, hoặc undefined nếu không có phần tử nào khớp. findIndex trả về chỉ số (hoặc -1).

find dừng lại ở lần khớp đầu tiên. Đừng dùng filter(...)[0]: cách đó quét toàn bộ mảng chỉ để vứt đi phần còn lại.

some và every: những câu hỏi boolean

some trả về true nếu có ít nhất một phần tử vượt qua phép kiểm tra. every chỉ trả về true khi tất cả đều vượt qua.

Cả hai đều dừng sớm (short-circuit): some dừng ở true đầu tiên, every dừng ở false đầu tiên. Chúng là công cụ phù hợp cho các câu hỏi "có phần tử nào…" và "tất cả có…".

slice và splice: sao chép hay cắt

Hai phương thức này nghe giống nhau nhưng làm những việc rất khác nhau.

slice(start, end) trả về một bản sao nông của một phần mảng. Nó không sửa đổi gì cả. end không được tính vào; bỏ nó đi để lấy đến hết mảng.

splice(start, deleteCount, ...items) sửa trực tiếp mảng tại chỗ. Nó xóa deleteCount phần tử bắt đầu từ start, có thể chèn thêm phần tử mới, rồi trả về các phần tử đã xóa.

Mẹo nhớ: slice thì an toàn (sao chép), splice thì phẫu thuật mảng ngay tại chỗ.

Sửa mảng gốc và không sửa mảng gốc

Sự phân biệt này rất quan trọng. Code vô tình sửa một mảng đang được dùng chung là một trong những bug khó chịu nhất khi phải truy tìm.

Sửa mảng gốc (thay đổi mảng ban đầu, thường trả về thứ khác):

  • push, pop, shift, unshift
  • splice, sort, reverse
  • fill, copyWithin

Không sửa mảng gốc (trả về mảng hoặc giá trị mới, mảng ban đầu giữ nguyên):

  • map, filter, slice, concat
  • flat, flatMap
  • find, findIndex, some, every, includes, indexOf
  • reduce, reduceRight

Hai phương thức cần để ý là sort và reverse: trông vô hại nhưng lại âm thầm sửa mảng. Nếu cần một bản sao đã sắp xếp, hãy slice trước:

JS hiện đại còn có các phiên bản song sinh không sửa mảng gốc: toSorted, toReversed, toSpliced và with. Chúng trả về mảng mới và để nguyên mảng gốc. Mọi môi trường chạy hiện nay đều hỗ trợ, nên có thì cứ dùng.

flat và flatMap

flat làm phẳng mảng lồng nhau một cấp (hoặc nhiều hơn, với đối số độ sâu). flatMap là map rồi đến flat một cấp, hữu ích khi mỗi phần tử cho ra không hoặc nhiều đầu ra.

flatMap là cách gọn gàng để "bung" các phần tử, một đầu vào thành nhiều đầu ra, mà không cần thêm bước flat().

Kết hợp tất cả

Một ví dụ nhỏ sát với thực tế. Từ một danh sách đơn hàng, tính tổng doanh thu của các đơn đã hoàn tất có giá trị trên $50:

Ba phương thức, một pipeline, không cần lo chuyện quản lý vòng lặp. Mỗi bước tự mô tả việc nó làm. Bạn có thể gộp hai lời gọi filter làm một, nhưng tách riêng vẫn dễ đọc và đôi khi giúp ích khi bạn gỡ lỗi.

Tiếp theo: Map và Set

Mảng xử lý tốt các dãy có thứ tự, nhưng lại vụng về khi bạn cần tra cứu nhanh theo khóa hoặc một tập hợp các giá trị không trùng lặp. JavaScript có hai cấu trúc dữ liệu dựng sẵn cho đúng những việc đó, là Map và Set, và chúng là nội dung của trang tiếp theo.

Câu hỏi thường gặp

map, filter và reduce trong JavaScript khác nhau thế nào?

map biến đổi từng phần tử và trả về một mảng mới có cùng độ dài. filter chỉ giữ lại các phần tử vượt qua phép kiểm tra và trả về một mảng mới (thường ngắn hơn). reduce duyệt qua mảng và gộp nó lại thành một giá trị duy nhất: một tổng, một đối tượng, một mảng khác, hay bất cứ thứ gì bạn dựng lên.

forEach và map khác nhau thế nào?

forEach chạy một hàm cho mỗi phần tử và trả về undefined, nên nó dùng cho các tác dụng phụ. map chạy một hàm cho mỗi phần tử và trả về một mảng mới chứa các kết quả. Nếu bạn muốn một mảng đã biến đổi, dùng map. Nếu bạn chỉ muốn làm gì đó với từng phần tử và không cần kết quả, dùng forEach (hoặc vòng lặp for...of).

Những phương thức mảng nào sửa đổi mảng gốc?

Các phương thức sửa mảng gốc là push, pop, shift, unshift, splice, sort, reverse, fill và copyWithin. Tất cả những phương thức còn lại, như map, filter, slice, concat, flat, flatMap, find, some, every, reduce, đều để nguyên mảng gốc và trả về một giá trị mới.

Khi nào nên dùng slice, khi nào dùng splice?

slice(start, end) trả về một bản sao nông của một đoạn trong mảng và không đụng đến mảng gốc. splice(start, deleteCount, ...items) sửa trực tiếp mảng: nó xóa và/hoặc chèn phần tử ngay tại chỗ rồi trả về các phần tử đã xóa. Mẹo nhớ: slice thì an toàn, splice thì phẫu thuật mảng.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu