Câu 1Khi bạn viết orders.forEach(printOrder), ai là bên gọi printOrder?
Higher-order function và callback — Truyền hàm và trả về hàm
Tìm hiểu higher-order function nhận hàm làm đối số và callback. Kèm forEach, tự viết filter và hàm trả về hàm.
Đôi khi bạn muốn làm những việc hơi khác nhau với cùng một danh sách: chỉ hiển thị từng phần tử, lọc theo điều kiện, hoặc cộng dồn số tiền. Điểm chung của chúng là khung vòng lặp; khác biệt duy nhất là cách xử lý từng phần tử.
Bài này nói về higher-order function — hàm nhận hàm khác làm đối số — và callback — hàm được truyền vào và được gọi.
Truyền phần xử lý làm đối số — Callback và forEach
Giả sử bạn muốn hiển thị lần lượt từng đơn hàng trong danh sách. Dùng for...of thì làm được, nhưng mỗi lần hiển thị một danh sách bạn lại phải viết cùng một vòng lặp, trong khi thứ duy nhất thay đổi là cách hiển thị từng phần tử.
Higher-order function (hàm nhận một hàm làm đối số, hoặc trả về một hàm) giúp bạn tách khung vòng lặp khỏi phần xử lý từng phần tử. Còn hàm được truyền vào làm đối số để hàm nhận nó gọi tới được gọi là callback. Phương thức forEach của mảng gọi callback bạn truyền vào một lần cho mỗi phần tử.
const orders = [
{ id: "A-1101", item: "Vở", amount: 480 },
{ id: "A-1102", item: "Bút bi", amount: 150 },
{ id: "A-1103", item: "Giấy note", amount: 320 },
];
// Hàm chỉ lo hiển thị một phần tử
const printOrder = (order) => {
console.log(`${order.id} ${order.item} ${order.amount} yên`);
};
// Chỉ truyền tên hàm. Việc gọi hàm do forEach đảm nhận
orders.forEach(printOrder);
// A-1101 Vở 480 yên
// A-1102 Bút bi 150 yên
// A-1103 Giấy note 320 yên
printOrder, và forEach gọi nó. Mỗi lần gọi, chỉ có phần tử nằm trong order là thay đổi.Dòng orders.forEach(printOrder) không có điều kiện lặp, cũng không có index. Muốn đổi cách hiển thị, bạn chỉ cần sửa printOrder, và có thể truyền nguyên hàm đó cho các danh sách khác.
forEach không dừng giữa chừng được
Bạn không thể viết break bên trong forEach, còn return chỉ kết thúc callback của phần tử đó — các phần tử còn lại vẫn tiếp tục được xử lý. Với vòng lặp cần dừng ngay khi tìm thấy phần tử mình cần, hãy dùng for...of cùng break.
Nhận phép kiểm tra dưới dạng hàm — Tự viết filter và boolean
Giả sử bạn muốn lọc danh sách theo những điều kiện khác nhau: lúc thì chỉ sản phẩm còn hàng, lúc thì chỉ sản phẩm từ 3000 yên trở lên. Nếu viết riêng một vòng lặp cho mỗi điều kiện, bạn sẽ có những đoạn code gần như y hệt nhau, giống nhau đến cả mảng chứa kết quả lẫn dòng push.
Thứ duy nhất thay đổi là dòng điều kiện, nên hãy nhận điều kiện dưới dạng một hàm kiểm tra và chỉ gom những phần tử mà hàm đó trả về true. Truyền gì cho callback, và theo thứ tự nào, là do hàm nhận và gọi callback quyết định. filterItems dưới đây chỉ truyền một phần tử cho hàm kiểm tra.
const products = [
{ name: "Tấm lót bàn", stock: 4 },
{ name: "Kệ màn hình", stock: 0 },
{ name: "Hộp đựng dây cáp", stock: 9 },
];
// Nhận một hàm kiểm tra và chỉ gom những phần tử mà nó trả về true
function filterItems(items, judge) {
const kept = [];
items.forEach((item) => {
if (judge(item)) {
kept.push(item);
}
});
return kept;
}
const inStock = filterItems(products, (product) => product.stock > 0);
console.log(inStock.length); // 2
console.log(inStock[0].name); // Tấm lót bàn
true, sản phẩm còn 0 nhận false, và chỉ sản phẩm đầu được đưa vào kept. Phần tử có được giữ lại hay không phụ thuộc vào giá trị trả về của hàm bạn truyền vào.Bên trong filterItems không hề nhắc tới tồn kho. Nếu sau này bạn chỉ muốn lấy sản phẩm còn nhiều hàng, chỉ cần đổi phép kiểm tra ở dòng gọi thành product.stock >= 5, hoàn toàn không phải động vào filterItems.
Truyền nguyên phép kiểm tra đã tạo — Higher-order function trả về hàm
Có lúc bạn phải truyền cho filterItems nhiều phép kiểm tra chỉ khác nhau ở danh mục đem so sánh: "chỉ văn phòng phẩm", "chỉ chén bát", v.v. Nếu lần gọi nào cũng viết lại biểu thức kiểm tra, thì khi cần đổi tên property đem so sánh, bạn phải sửa tất cả những lời gọi đó.
Dạng còn lại của higher-order function là hàm trả về hàm. Nó có cùng hình dạng với function factory ở bài trước: bạn tạo byCategory, hàm nhận một danh mục và trả về một hàm kiểm tra. Hàm kiểm tra được trả về ghi nhớ category dưới dạng closure, nên bạn có thể truyền thẳng nó cho filterItems.
const items = [
{ name: "Vở", category: "Văn phòng phẩm" },
{ name: "Cốc", category: "Chén bát" },
{ name: "Bút bi", category: "Văn phòng phẩm" },
];
// Nhận một danh mục và trả về hàm kiểm tra xem phần tử có thuộc danh mục đó không
function byCategory(category) {
return (item) => item.category === category;
}
// Lúc này chưa so sánh gì cả; thứ được trả về là hàm kiểm tra
console.log(typeof byCategory("Văn phòng phẩm")); // function
// Truyền thẳng hàm kiểm tra được trả về cho filterItems ở phần trước
console.log(filterItems(items, byCategory("Văn phòng phẩm")).length); // 2
console.log(filterItems(items, byCategory("Chén bát")).length); // 1
filterItems(items, byCategory("Văn phòng phẩm"))— tạo phép kiểm tra và truyền nó làm đối số thứ 2
category— nhận "Văn phòng phẩm" ở dòng gọi nóreturn (item) => ...— trả về hàm kiểm tra mà chưa so sánh gì
item— nhận một phần tử dofilterItemstruyền vàoitem.category === category— so với category đã ghi nhớ
judge— chứa hàm kiểm tra dobyCategorytrả vềjudge(item)— truyền lần lượt từng phần tử và gọi hàm kiểm tra
category được xác định ở dòng gọi byCategory, còn item được xác định bên trong filterItems. Nơi tạo ra phép kiểm tra và nơi gọi nó là hai chỗ khác nhau.Dòng chạy byCategory("Văn phòng phẩm") chưa so sánh gì cả; phép so sánh chỉ diễn ra khi judge(item) được gọi bên trong filterItems. Với 3 sản phẩm, cùng một hàm kiểm tra được gọi 3 lần, category vẫn giữ nguyên là "Văn phòng phẩm" và chỉ có item thay đổi.
Thiếu cặp ngoặc thì mọi phần tử đều được giữ lại
Nếu bạn viết filterItems(items, byCategory), từng sản phẩm sẽ được truyền cho byCategory làm category, và thứ được trả về làm kết quả kiểm tra là một hàm. Hàm là giá trị truthy (bất kỳ giá trị nào không phải falsy), nên sản phẩm nào cũng được giữ lại. Thứ bạn cần truyền là hàm kiểm tra do byCategory("Văn phòng phẩm") trả về.
Một khung cho mọi trường hợp — Hàm dùng chung với phần xử lý thay được
Giả sử bạn đã viết hai hàm tính tổng phí vận chuyển, một cho giao thường và một cho giao nhanh. Vòng lặp và biến chứa tổng đều giống nhau; khác biệt duy nhất là biểu thức tính phí cho từng đơn. Viết như vậy thì rất dễ sửa bên này mà quên sửa bên kia.
Hãy biến phần khác nhau thành một tham số kiểu hàm và chỉ giữ một bản của khung. sumShipping nhận mảng đơn hàng và một hàm tính phí, truyền lần lượt từng đơn cho hàm đó và cộng dồn các giá trị trả về. Bên gọi chỉ viết biểu thức tính phí, còn bên nhận chỉ lo vòng lặp và phép cộng tổng.
const shippingOrders = [{ id: "A-1101", total: 6200 }, { id: "A-1102", total: 3400 }];
// Chỉ một khung. Riêng cách tính phí được nhận dưới dạng hàm
function sumShipping(orders, calcFee) {
let total = 0;
for (const order of orders) {
total = total + calcFee(order);
}
return total;
}
const normalFee = (order) => (order.total >= 5000 ? 0 : 500);
const expressFee = () => 800; // Không dùng order nên có thể bỏ tham số
const flatFee = () => 300;
console.log(sumShipping(shippingOrders, normalFee)); // 500
console.log(sumShipping(shippingOrders, expressFee)); // 1600
console.log(sumShipping(shippingOrders, flatFee)); // 600
calcFee, tổng của cùng hai đơn hàng sẽ là 500, 1600 hoặc 600. Code bên trong sumShipping giống hệt nhau ở cả ba lần.Ngay cả một thay đổi như đặt mức trần cho tổng cũng chỉ cần sửa một chỗ trong sumShipping, và nó áp dụng cho cả ba kiểu tính phí. Bảng dưới đây cho biết ba hàm nhận callback trong bài này gọi callback ra sao và dùng giá trị trả về thế nào.
| Hàm nhận callback | Cách gọi callback | Callback trả về |
|---|---|---|
| forEach | Truyền lần lượt từng phần tử | Không dùng |
| filterItems tự viết | Truyền từng phần tử qua judge(item) | Chỉ gom phần tử trả về true |
| sumShipping tự viết | Gọi calcFee(order) với từng đơn | Cộng làm phí của đơn đó |
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Trong filterItems tự viết, điều gì xảy ra với một phần tử khi hàm kiểm tra bạn truyền vào trả về false?
Câu 3Nếu truyền byCategory mà không có cặp ngoặc, như filterItems(items, byCategory), mảng được trả về chứa gì?