Câu 1Sau khi chạy prices.map((price) => price * 2), mảng prices gốc sẽ thế nào?
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
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.
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
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ức | Giá trị trả về của callback | Số phần tử của mảng kết quả |
|---|---|---|
| map | Được đưa vào mảng mới | Bằng mảng gốc |
| filter | Nếu là true thì giữ phần tử | Bằng hoặc ít hơn mảng gốc |
| forEach | Bị bỏ qua, không được dùng | Khô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.
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
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.
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.
map và filter 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
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.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
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?