Học bằng cách đọc theo thứ tự

map và filter — biến đổi hay lọc mảng

Tìm hiểu map để biến đổi từng phần tử và filter để lọc theo điều kiện: mảng gốc không đổi, chaining và tham số index.

Dù là lấy riêng giá đã gồm thuế từ thực đơn hay chỉ chọn ra những sản phẩm còn hàng, lần nào bạn cũng phải khai báo một mảng rỗng rồi gọi push bên trong forEach.

Bài này nói về map để biến đổi phần tử và filter để chỉ giữ lại những phần tử thỏa điều kiện.

Biến đổi từng phần tử thành mảng mới — map

Giả sử bạn cần danh sách giá đã bao gồm thuế cho toàn bộ thực đơn của một quán cà phê. Kết quả cần có là một mảng giữ nguyên thứ tự 3 món ban đầu, chỉ thay giá. Bạn có thể tính từng giá bằng forEach, nhưng muốn gom các giá trị đã tính thì phải tự tạo một mảng để chứa kết quả và tự quản lý nó cho đến cuối.

Phương thức mảng map (phương thức đưa từng phần tử qua callback rồi trả về một mảng mới gồm các giá trị trả về, theo đúng thứ tự) lo cả việc tạo mảng kết quả lẫn vòng lặp. Trong hàm bạn truyền vào, chỉ cần viết cách biến đổi một phần tử và trả về giá trị sau khi biến đổi.

const menuItems = [
  { name: "Cà phê đen", price: 480 },
  { name: "Latte", price: 540 },
  { name: "Bánh phô mai", price: 620 },
];

// Hàm chỉ mô tả cách biến đổi một món
const toTaxIncluded = (item) => Math.floor(item.price * 1.1);

// toTaxIncluded được gọi một lần cho mỗi phần tử, giá trị trả về lần lượt được đưa vào mảng mới
const taxIncluded = menuItems.map(toTaxIncluded);
console.log(taxIncluded.join(", "));   // 528, 594, 682
console.log(taxIncluded.length);       // 3
console.log(menuItems.length);         // 3
Giá trị trả về nằm đúng vị trí tương ứng
Một hàm duy nhấttoTaxIncludedmenuItemsindex 0: 480menuItemsindex 1: 540menuItemsindex 2: 620480 × 1.1 →trả về 528540 × 1.1 →trả về 594620 × 1.1 →trả về 682taxIncludedindex 0taxIncludedindex 1taxIncludedindex 2
3 giá trị trả về được đưa vào taxIncluded theo đúng thứ tự của các phần tử gốc. map không làm thay đổi số phần tử, chỉ thay giá trị.

map chỉ lần lượt đưa từng phần tử cho callback rồi thêm giá trị nhận được vào mảng mới, nên số phần tử không thể thay đổi. Muốn lọc bớt — tức là thay đổi số phần tử — bạn cần một phương thức khác.

forEach không trả về kết quả

forEach chỉ gọi hàm bạn truyền vào, không gom các giá trị trả về. Nó luôn trả về undefined, nên dù bạn viết const list = menuItems.forEach(toTaxIncluded); thì list vẫn là undefined. Muốn nhận kết quả dưới dạng mảng, hãy dùng map.

Chuyển danh sách sản phẩm của một tiệm bánh thành danh sách giá khuyến mãi và danh sách tên. bakeryItems đã được khai báo sẵn.

① Hãy tạo mảng salePrices gồm các giá khuyến mãi (price nhân 0.8, làm tròn xuống).

② Hãy hiển thị salePrices, nối bằng ", ".

③ Hãy tạo một mảng chỉ gồm tên sản phẩm và hiển thị, nối bằng " / ".

④ Hãy hiển thị price của phần tử đầu tiên trong bakeryItems.

(Nếu chạy đúng, phần giải thích sẽ xuất hiện.)

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Chỉ giữ lại phần tử thỏa điều kiện — filter và số phần tử còn lại

Giả sử bạn chỉ muốn liệt kê những cuốn sách còn hàng. map không thay đổi số phần tử nên không thể bỏ đi những cuốn đã hết, và bạn lại quay về cách viết if trong forEach rồi gọi push.

filter (phương thức trả về một mảng mới chỉ gồm những phần tử mà callback trả về true) đảm nhận việc lọc này. Hàm bạn truyền vào nhận một phần tử và trả về giá trị boolean cho biết có giữ phần tử đó hay không.

const books = [
  { title: "SQL thực hành", stock: 0 },
  { title: "Mạng máy tính cơ bản", stock: 5 },
  { title: "Linux qua hình vẽ", stock: 2 },
];

// Viết hàm trả về boolean: giữ hay không
const isInStock = (book) => book.stock > 0;

// Chỉ những phần tử trả về true mới vào mảng mới
const available = books.filter(isInStock);
console.log(available.length);      // 2
console.log(available[0].title);    // Mạng máy tính cơ bản
console.log(books.length);          // 3
Các phần tử còn lại được dồn lên trước
books index 0stock 0stock > 0 làfalseKhông vàoavailablebooks index 1stock 5stock > 0 làtrueavailableindex 0books index 2stock 2stock > 0 làtrueavailableindex 1
Chỉ 2 phần tử trả về true được đưa vào available. Bị loại bao nhiêu phần tử thì số phần tử giảm bấy nhiêu, các phần tử còn lại dồn lên phía trước.

Trong available, bạn chỉ biết vị trí mới sau khi đã dồn lên; thông tin phần tử đó từng ở index nào trong books gốc không còn nữa. Cách dùng vị trí gốc sẽ được giới thiệu ở cuối bài. Bảng dưới đây tóm tắt cách 3 phương thức dùng giá trị trả về của hàm bạn truyền vào.

Phương thứcGiá trị trả về của callbackSố phần tử của mảng kết quả
mapĐược đưa vào mảng mớiBằng mảng gốc
filterNếu là true thì giữ phần tửBằng hoặc ít hơn mảng gốc
forEachBị bỏ qua, không được dùngKhông tạo mảng nào (trả về undefined)

Có dấu ngoặc nhọn thì phải có return

Nếu viết thân hàm trong dấu ngoặc nhọn như (book) => { book.stock > 0 } mà không có return thì giá trị trả về là undefined. undefined là falsy nên không phần tử nào được giữ lại và bạn nhận về một mảng rỗng. Với phép kiểm tra chỉ một dòng, hãy bỏ dấu ngoặc nhọn.

Trên trang của một cửa hàng cây cảnh, hãy tách danh sách theo cỡ chậu. plants đã được khai báo sẵn.

① Hãy tạo mảng largePots chỉ gồm các sản phẩm chậu lớn và hiển thị số phần tử của nó.

② Hãy hiển thị tên các sản phẩm có trong largePots, nối bằng ", ".

③ Hãy tạo mảng smallPots chỉ gồm các sản phẩm chậu nhỏ và hiển thị tên sản phẩm, nối bằng ", ".

④ Hãy hiển thị số phần tử của plants.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Lọc trước, biến đổi sau — nối filter với map

Giả sử bạn chỉ muốn chuyển những khóa học đang mở thành chuỗi để hiển thị. Bạn có thể lưu kết quả của filter vào một biến rồi gọi .map(...) trên biến đó ở dòng thứ hai, nhưng như vậy là phải đặt tên cho một mảng trung gian mà về sau không dùng đến.

filter cũng trả về một mảng, nên bạn có thể viết .map(...) ngay sau giá trị trả về đó. Cách viết nối các lần gọi phương thức liên tiếp như thế này gọi là chaining (gọi phương thức tiếp theo trên giá trị trả về của phương thức trước).

const courses = [
  { title: "Git cho người mới", minutes: 45, published: true },
  { title: "Luyện tập SQL", minutes: 90, published: false },
  { title: "Linux cơ bản", minutes: 60, published: true },
];

// Chỉ giữ các khóa đang mở, rồi chuyển kết quả thành chuỗi để hiển thị
// Dấu . ở đầu dòng gọi phương thức trên giá trị trả về của dòng trước (xuống dòng vẫn là cùng một biểu thức)
const labels = courses
  .filter((course) => course.published)   // published vốn đã là boolean nên trả về luôn, không cần so sánh
  .map((course) => `${course.title} (${course.minutes} phút)`);

console.log(labels.join(" / "));
// Git cho người mới (45 phút) / Linux cơ bản (60 phút)
console.log(labels.length);    // 2
console.log(courses.length);   // 3
3 mảng mà chaining đi qua
courses3 objectMảng gốcvẫn có 3.filter(...)2 objectKhông có tên.map(...)2 chuỗiĐược lưuvào labels
Mảng do filter trả về được .map đọc mà không cần đặt tên. Mỗi lần nối thêm một phương thức là có thêm một mảng mới.

Khi muốn kiểm tra giá trị ở giữa chừng, hãy lưu kết quả của filter vào một biến, in ra bằng console.log, rồi gọi .map(...) trên biến đó. Kiểm tra xong, bạn có thể viết lại thành dạng chaining.

Cộng thêm 5 điểm cho buổi học bù rồi so sánh từng điểm với mức đạt 75. records đã được khai báo sẵn.

① Hãy dùng chaining để tạo mảng passedScores: cộng 5 điểm cho tất cả mọi người, sau đó chỉ giữ các điểm từ 75 trở lên.

② Hãy hiển thị passedScores, nối bằng ", ".

③ Hãy hiển thị score của phần tử thứ 2 trong records.

④ Hãy dùng chaining để lấy tên những người đạt từ 75 điểm trở lên sau khi cộng điểm, rồi hiển thị, nối bằng " / ".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Lấy vị trí của phần tử — index trong callback

Giả sử bạn muốn gắn thứ hạng như "Hạng 1", "Hạng 2" vào một bảng xếp hạng. Bản thân các phần tử không chứa thứ hạng, nên nhiều người sẽ đặt một biến đếm bên ngoài rồi tăng nó thêm 1 trong callback.

mapfilter truyền phần tử vào tham số thứ nhất của callback, và vị trí của phần tử đó (index, đếm từ 0) vào tham số thứ hai. Các tham số được truyền theo thứ tự từ trái sang, nên kể cả khi chỉ cần vị trí, bạn vẫn phải viết tham số thứ nhất. Nếu nhận vị trí này với tên index, bạn có thể dùng nó để hiển thị thứ hạng, hoặc để chỉ giữ lại vài phần tử đầu tiên.

const ranking = ["Tai nghe không dây", "Sạc dự phòng", "Giá đỡ điện thoại", "Hub USB"];

// Tham số thứ hai là vị trí, đếm từ 0
const lines = ranking.map((item, index) => `Hạng ${index + 1}: ${item}`);
console.log(lines.join("\n"));
// Hạng 1: Tai nghe không dây
// Hạng 2: Sạc dự phòng
// Hạng 3: Giá đỡ điện thoại
// Hạng 4: Hub USB

// Callback của filter cũng nhận tham số theo cùng thứ tự
const topThree = ranking.filter((item, index) => index < 3);
console.log(topThree.length);   // 3
Hai cách dùng cùng một index
map nhận(item, index)4 phần tử lần lượtnhận 0 đến 3index + 1 đượcđưa vào chuỗiVẫn 4 phần tử,đã có thứ hạngfilter nhận(item, index)Cũng nhận 0 đến 3như mapindex < 3quyết định giữCòn 3 phần tử,giá trị giữ nguyên
Cả hai callback đều nhận vị trí, đếm từ 0, ở tham số thứ hai. map đưa vị trí vào giá trị, còn filter dùng vị trí để quyết định giữ hay bỏ.

index truyền vào filter là vị trí trong mảng trước khi lọc. Việc các phần tử được giữ lại về sau dồn lên phía trước không ảnh hưởng đến phép kiểm tra, nên index < 3 giữ lại đúng 3 phần tử đầu tiên của mảng gốc.

Một trang chuyên đề liệt kê các bài viết theo thứ tự đề xuất. articles và viewCounts đã được khai báo sẵn; viewCounts là số lượt xem, xếp theo cùng thứ tự.

① Hãy dùng viewCounts để tạo một mảng theo dạng "Tiêu đề (N lượt xem)" và hiển thị từng dòng.

② Hãy chỉ giữ các bài có từ 5000 lượt xem trở lên và hiển thị, nối bằng ", ".

③ Hãy chỉ giữ các bài ở hạng chẵn, như hạng 2 và hạng 4, rồi hiển thị, nối bằng ", ".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Sau khi chạy prices.map((price) => price * 2), mảng prices gốc sẽ thế nào?

Câu 2Trong courses.filter(isPublished).map(toLabel), map đọc mảng nào?

Câu 3Trong callback của ranking.map((item, index) => ...), giá trị nào được truyền vào index?