Câu 1invoices.filter((invoice) => invoice.id === "B-302") trả về gì?
Tổng kết cú pháp — Bảng tra nhanh phương thức mảng, bất đồng bộ và ngoại lệ
Các bảng tra nhanh: chọn phương thức mảng theo giá trị trả về, so sánh callback, then và await, và nơi mỗi loại lỗi được bắt.
Cộng tổng một danh sách hóa đơn, chờ upload xong, bắt lỗi rồi hiện thông báo — mỗi việc này đều có vài cách viết. Nếu chọn sai, bạn có thể nhận về undefined ở chỗ đang chờ một mảng, hoặc gặp một lỗi không đến được catch nào cả.
Bài này tổng hợp khi nào dùng phương thức mảng nào, chờ xử lý bất đồng bộ theo cách nào, và bắt ngoại lệ ra sao.
Chọn theo kết quả bạn muốn — Phương thức mảng trả về gì
Giả sử từ một danh sách hóa đơn, bạn muốn biết ba điều: có bao nhiêu hóa đơn đã thanh toán, hóa đơn chưa thanh toán đầu tiên, và tổng số tiền. Các phương thức mảng đều được gọi theo cùng một kiểu, kèm một callback, nên chỉ nhìn lời gọi thì bạn không biết sẽ nhận về một mảng hay một giá trị đơn lẻ.
Ở đây, các phương thức được chia nhóm theo dạng giá trị trả về: một mảng mới, một trong các phần tử vốn có trong mảng, hoặc một giá trị tính từ mảng, như boolean hay một tổng. Biết được dạng đó, bạn cũng biết mình có thể gọi join trên kết quả hay đọc property như .id từ nó.
const invoices = [
{ id: "B-301", amount: 4800, paid: true },
{ id: "B-302", amount: 1200, paid: false },
{ id: "B-303", amount: 3600, paid: true },
{ id: "B-304", amount: 2500, paid: false },
];
// Trả về mảng: map giữ nguyên số phần tử, filter chỉ giữ các phần tử thỏa điều kiện
console.log(invoices.map((invoice) => invoice.id).join(", ")); // B-301, B-302, B-303, B-304
console.log(invoices.filter((invoice) => invoice.paid).length); // 2
// Trả về chính phần tử: phần tử đầu tiên thỏa điều kiện
console.log(invoices.find((invoice) => !invoice.paid).id); // B-302
// Trả về giá trị tính từ mảng: some cho boolean, reduce cho giá trị đã gộp
console.log(invoices.some((invoice) => invoice.amount >= 4000)); // true
console.log(invoices.reduce((total, invoice) => total + invoice.amount, 0)); // 12100
Nếu dùng filter khi chỉ cần một phần tử, bạn nhận về một mảng chứa một phần tử, nên phải thêm [0] rồi mới đọc được .id. Bảng dưới đây liệt kê các phương thức mảng nhận callback đã học trong khóa này, chia nhóm theo giá trị trả về.
| Phương thức | Trả về | Cần lưu ý |
|---|---|---|
| map | Mảng mới (cùng số phần tử) | Mảng gốc không thay đổi |
| filter | Mảng mới (chỉ phần tử thỏa) | Mảng rỗng nếu không phần tử nào thỏa |
| find / findIndex | Một phần tử / một index | undefined / -1 nếu không tìm thấy |
| some / every | Boolean | false / true với mảng rỗng |
| reduce | Giá trị lần gọi cuối trả về | Mảng rỗng thì cần giá trị khởi tạo |
| sort / toSorted | Mảng đã sắp xếp | sort thay đổi cả mảng gốc |
Khi nào nối tiếp được — Method chaining và forEach
Giả sử bạn chỉ muốn tính tổng các hóa đơn chưa thanh toán. Nếu lưu kết quả của filter vào một biến, gọi map trên biến đó rồi mới gọi reduce, bạn phải đặt tên cho những mảng trung gian không bao giờ dùng lại. Còn nếu cố viết tất cả trong một biểu thức mà không biết phương thức nào nối sau được phương thức nào, bạn sẽ gặp TypeError ở chỗ hai phương thức nối vào nhau.
Trong method chaining (gọi phương thức tiếp theo trên giá trị trả về của phương thức trước), mỗi lời gọi như .filter(...) được tính là một bước, và mỗi bước được gọi trên thứ mà bước trước nó trả về. Bạn chỉ gọi tiếp được phương thức mảng khi bước trước trả về một mảng.
// invoices là 4 hóa đơn giống phần trước
// Chừng nào còn nhận về mảng, bạn còn nối thêm được phương thức mảng
const unpaidTotal = invoices
.filter((invoice) => !invoice.paid) // Mảng: 2 hóa đơn B-302 và B-304
.map((invoice) => invoice.amount) // Mảng: [1200, 2500]
.reduce((total, amount) => total + amount, 0); // Số: 3700
console.log(unpaidTotal); // 3700
// toSorted cũng trả về mảng, nên nối tiếp được sang map và join
const idsByAmount = invoices.toSorted((a, b) => b.amount - a.amount).map((invoice) => invoice.id);
console.log(idsByAmount.join(", ")); // B-301, B-303, B-304, B-302
// forEach trả về undefined, nên nối gì phía sau cũng bị lỗi
invoices.forEach((invoice) => console.log(invoice.id)).map((invoice) => invoice.id);
// Sau khi in ra B-301 đến B-304
// TypeError: Cannot read properties of undefined (reading 'map')
Hãy đặt những bước không trả về mảng ở cuối chuỗi. Sau find, bạn đọc được property như .id nhưng không gọi được map, còn số mà reduce trả về thì cứ lưu vào biến để dùng.
Gọi map sau find sẽ ra thông báo khác
Nếu nối .map(...) vào phần tử mà find trả về hay số mà reduce trả về, chương trình sẽ dừng lại và báo TypeError, chẳng hạn invoices.find(...).map is not a function. Thông báo này khác với thông báo sau forEach, nhưng trong cả hai trường hợp, bước ngay trước .map đều không trả về mảng.
So sánh các cách chờ — Callback / then / await
Giả sử bạn upload một bức ảnh rồi tạo thumbnail cho nó. Cả hai việc đều mất thời gian mới có kết quả, nên hàm không thể trả kết quả ngay qua giá trị trả về, và bước thứ hai phải chờ đến khi có kết quả của bước thứ nhất.
Trong code dưới đây, ① dùng kiểu callback (bạn truyền cho hàm bắt đầu công việc một hàm khác, hàm này sẽ được gọi khi công việc xong; không có Promise nào được trả về), cũng là cách setTimeout hoạt động. ② truyền một hàm cho then của Promise nhận được, còn ③ dùng await bên trong hàm async.
// ① Kiểu callback: truyền hàm sẽ được gọi khi xong, và viết bước tiếp theo bên trong nó
// later gọi callback với value sau 100ms (không trả về Promise)
function later(value, callback) {
setTimeout(() => callback(value), 100);
}
later("photo-17.jpg", (fileName) => {
later(`thumbnail của ${fileName}`, (thumbnail) => {
console.log(`① Đã tạo ${thumbnail}`); // ① Đã tạo thumbnail của photo-17.jpg
});
});
// ② then: nối bước tiếp theo bằng .then (delay giống bài về Promise)
delay(100, "photo-17.jpg")
.then((fileName) => delay(100, `thumbnail của ${fileName}`))
.then((thumbnail) => console.log(`② Đã tạo ${thumbnail}`)); // ② Đã tạo thumbnail của photo-17.jpg
// ③ await: viết bước tiếp theo ở dòng dưới và nhận kết quả vào biến (giả định chạy bên trong hàm async, như bài trước)
const fileName = await delay(100, "photo-17.jpg");
const thumbnail = await delay(100, `thumbnail của ${fileName}`);
console.log(`③ Đã tạo ${thumbnail}`); // ③ Đã tạo thumbnail của photo-17.jpg
- Gọi
later("photo-17.jpg", (fileName) => { … })
- Kết quả thứ nhất được đưa vào
fileName - Gọi
laterthứ hai ở đây
- Kết quả thứ hai được đưa vào
thumbnail - Phần in ra nằm trong hàm trong cùng
- Bắt đầu từ
delay(100, "photo-17.jpg")
- Nhận
fileNamevà trả vềdelaytiếp theo
- Nhận
thumbnailvà in ra
const fileName = await delay(…)const thumbnail = await delay(…)- Viết
console.log(…)ở dòng tiếp theo
Hàm trong then thứ hai chỉ nhận thumbnail, nên muốn dùng cả fileName ở đó, bạn phải chép nó ra một biến bên ngoài. Với await, fileName nằm trong một biến mà mọi dòng bên dưới đều đọc được. Bảng dưới đây đặt ba cách viết cạnh nhau.
Kiểm tra lỗi rơi vào đâu — try / catch và .catch
Giả sử hàm giữ hàng trong kho sẽ thất bại khi số lượng đặt vượt quá số hàng còn trong kho. Với hàm bình thường không trả về Promise, bạn bắt được lỗi bằng cách bọc dòng gọi hàm trong try / catch, nhưng lỗi từ hàm trả về Promise thì có đến được khối catch hay không còn tùy vào cách bạn viết lời gọi.
Khối catch đi sau try nhận các ngoại lệ được ném ra từ những dòng nằm trong try. .catch của Promise thì gọi hàm bạn truyền vào khi Promise đó bị rejected.
function checkStock(count) {
if (count > 3) {
throw new Error(`Không đủ hàng trong kho: ${count} sản phẩm`);
}
}
async function reserveStock(count) {
await delay(100); // delay giống bài về Promise
checkStock(count); // throw ở đây thì Promise mà hàm này trả về bị rejected
}
// ① Hàm không trả về Promise: bọc dòng gọi hàm trong try
try { checkStock(5); } catch (error) { console.log(`① ${error.message}`); }
// ② Cách viết then: truyền cho .catch một hàm để nhận lỗi
reserveStock(5).catch((error) => console.log(`② ${error.message}`));
// ③ Cách viết await: bọc dòng await trong try
try { await reserveStock(5); } catch (error) { console.log(`③ ${error.message}`); }
// ① Không đủ hàng trong kho: 5 sản phẩm
// ② Không đủ hàng trong kho: 5 sản phẩm (sau 100ms)
// ③ Không đủ hàng trong kho: 5 sản phẩm (sau 100ms)
Dù nhận bằng .catch hay khối catch, bạn đều nhận được cùng một Error, nên error.message và phép kiểm tra instanceof đều dùng được như nhau. Bảng dưới đây tổng hợp các cách xử lý ngoại lệ đã học trong khóa này.
| Cách viết | Tác dụng | Cần lưu ý |
|---|---|---|
| try / catch | Nhận ngoại lệ trong try | catch để trống sẽ che mất lỗi |
| finally | Luôn chạy sau cùng | Đặt phần dọn dẹp ở đây |
| throw new Error | Ném ngoại lệ ngay tại chỗ đó | Không throw thì không dừng |
| class extends Error | Loại lỗi phân biệt bằng instanceof | Kiểm tra lớp tự tạo trước |
| cause của Error | Đính kèm lỗi gốc | Đọc bằng error.cause |
| .catch của Promise | Được gọi khi Promise bị rejected | Thêm vào cuối chuỗi then |
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Trong invoices.filter(...).map(...).reduce(...), reduce gộp mảng nào?
Câu 3Khi nối ba bước bất đồng bộ theo kiểu callback, bạn viết bước thứ ba ở đâu?