Câu 1Với const addFee = (price, fee = 500) => price + fee;, addFee(1000, undefined) trả về gì?
Tham số mặc định và rest parameter — giá trị mặc định và cách nhận phần còn lại
Tìm hiểu tham số mặc định, rest parameter gom các đối số vào một mảng, và options pattern truyền cấu hình bằng object.
Có những đối số mà gần như lần gọi nào bạn cũng truyền cùng một giá trị, như phí vận chuyển hay thuế suất. Nếu bỏ qua một đối số như vậy thì cũng không có lỗi gì; bạn chỉ phát hiện mình quên truyền khi thấy undefined lẫn vào chuỗi hiển thị.
Bài này nói về tham số mặc định để quy định giá trị dùng khi bỏ qua đối số, rest parameter để nhận số lượng đối số tùy ý, và options pattern để truyền cấu hình bằng một object duy nhất.
Cho phép bỏ qua đối số khi gọi — tham số mặc định
Giả sử bạn có một hàm tạo lịch đặt phòng họp, trong đó thời lượng hầu như luôn là 1 giờ và cách bố trí hầu như luôn là kiểu lớp học. Nếu lần gọi nào cũng phải viết đủ cả 3 đối số, giá trị bạn thật sự muốn đổi sẽ bị lẫn giữa các giá trị khác ở bên gọi.
Tham số mặc định (default parameter — cách quy định trước giá trị sẽ dùng khi đối số không được truyền vào) được viết bằng cách thêm = vào danh sách tham số, như hours = 1. Tham số có giá trị mặc định có thể được bỏ qua ở bên gọi.
// Viết giá trị mặc định bằng cách thêm "= giá trị" vào danh sách tham số
const reserveRoom = (roomName, hours = 1, layout = "lớp học") => {
return `${roomName} / ${hours} giờ / ${layout}`;
};
// Bỏ qua đối số thứ 3 thì giá trị mặc định "lớp học" được dùng
console.log(reserveRoom("Phòng A", 2)); // Phòng A / 2 giờ / lớp học
// Ở vị trí có truyền giá trị, giá trị mặc định không được dùng
console.log(reserveRoom("Phòng A", 2, "bàn họp")); // Phòng A / 2 giờ / bàn họp
// Bỏ qua cả đối số thứ 2 thì cả hai giá trị mặc định đều được dùng
console.log(reserveRoom("Phòng A")); // Phòng A / 1 giờ / lớp học
reserveRoom("Phòng A", 2) qua từng vị trí tham số. Dù có giá trị mặc định, nó không được dùng khi đã có giá trị truyền vào.Bạn chỉ bỏ qua được đối số tính từ bên phải, nên hãy đặt các tham số có giá trị mặc định ở cuối. Ngay cả khi chỉ muốn đổi đối số thứ 3, bạn vẫn phải viết một giá trị ở vị trí thứ 2.
Đối số không được truyền nhận giá trị gì — undefined và giá trị mặc định
Nếu bạn bỏ qua đối số của một tham số không có giá trị mặc định, mọi phép tính hay chuỗi dùng tới nó vẫn cứ chạy tiếp. Vì không có gì dừng lại, về sau bạn sẽ phải đi tìm xem lời gọi nào đã quên truyền đối số.
Tham số không được truyền đối số sẽ nhận undefined. Giá trị mặc định chỉ được dùng khi đối số là undefined — nếu bạn truyền null, 0 hay chuỗi rỗng, chính giá trị bạn truyền sẽ được dùng.
const notifyStatus = (orderId, status = "Đã tiếp nhận") => `${orderId}: ${status}`;
// Bỏ qua đối số hay truyền undefined thì đều nhận giá trị mặc định
console.log(notifyStatus("A-1021")); // A-1021: Đã tiếp nhận
console.log(notifyStatus("A-1021", undefined)); // A-1021: Đã tiếp nhận
// null được xem là có truyền giá trị, nên giá trị mặc định không được dùng
console.log(notifyStatus("A-1021", null)); // A-1021: null
// Bỏ qua tham số không có giá trị mặc định thì nó nhận undefined
const plainStatus = (orderId, status) => `${orderId}: ${status}`;
console.log(plainStatus("A-1021")); // A-1021: undefined
status bị bỏ qua sẽ nhận undefined, nên biểu thức giá trị mặc định được chạy. null được xem là giá trị đã truyền vào, nên giá trị mặc định không được chạy.Bảng dưới đây cho thấy giá trị mà status nhận được và nội dung hiển thị, ứng với từng giá trị truyền vào vị trí thứ 2 của notifyStatus. Truyền undefined cho kết quả giống hệt bỏ qua đối số, còn null thì giống như truyền một chuỗi: giá trị truyền vào được hiển thị nguyên vẹn.
| Lời gọi | Giá trị của status | Hiển thị |
|---|---|---|
| notifyStatus("A-1021") | Giá trị mặc định, Đã tiếp nhận | A-1021: Đã tiếp nhận |
| notifyStatus("A-1021", "Đã gửi hàng") | Đã gửi hàng | A-1021: Đã gửi hàng |
| notifyStatus("A-1021", undefined) | Giá trị mặc định, Đã tiếp nhận | A-1021: Đã tiếp nhận |
| notifyStatus("A-1021", null) | null | A-1021: null |
Truyền null thì giá trị mặc định không được dùng
null là giá trị mang nghĩa "cố ý để trống", nên giá trị mặc định viết trong danh sách tham số sẽ không thay thế nó. Nếu bạn muốn dùng giá trị dự phòng cả khi phản hồi API trả về null, đừng đặt giá trị mặc định ở tham số mà hãy dùng ?? bên trong hàm, như const shown = status ?? "Đã tiếp nhận";.
Gom số lượng đối số tùy ý — rest parameter
Có những hàm nhận số lượng giá trị khác nhau ở mỗi lần gọi, như các tag gắn cho sản phẩm hay các khoản mục trong bảng kê chi phí. Dù bạn khai báo sẵn 5 tham số, sớm muộn cũng sẽ có lời gọi cần tới tham số thứ 6, và những tham số không dùng tới lần nào cũng là undefined.
Rest parameter (cách gom các đối số còn lại vào một mảng, dùng cho hàm nhận số lượng đối số thay đổi) được viết là ...labels ở cuối danh sách tham số. Đây chính là dấu ... đã dùng để gom các phần tử còn lại khi destructuring, chỉ khác là ở đây nó gom các đối số. Nó chỉ được đặt ở vị trí tham số cuối cùng; nếu viết thêm tham số phía sau sẽ gây SyntaxError.
// Viết ...labels thì các đối số nhận được sẽ được gom thành một mảng
const buildTags = (...labels) => `Tag (${labels.length}): ${labels.join(" / ")}`;
console.log(buildTags("Miễn phí vận chuyển", "Còn hàng")); // Tag (2): Miễn phí vận chuyển / Còn hàng
console.log(buildTags("Hàng mới")); // Tag (1): Hàng mới
// Ngược lại, viết ... trong ngoặc tròn của lời gọi thì mảng được tách lại thành các đối số
console.log(buildTags(...["Hàng mới", "Số lượng có hạn"])); // Tag (2): Hàng mới / Số lượng có hạn
// Khi còn có tham số được đặt tên, hãy đặt rest parameter ở cuối
const buildNotice = (orderId, ...notes) => `${orderId} (${notes.join(", ")})`;
console.log(buildNotice("A-1021", "Giữ lạnh", "Hẹn giờ giao")); // A-1021 (Giữ lạnh, Hẹn giờ giao)
console.log(buildNotice("A-1022")); // A-1022 ()
orderId, các giá trị còn lại lần lượt trở thành phần tử của notes. Rest parameter nhận tất cả những gì còn lại sau các tham số được đặt tên.Rest parameter không thể có giá trị mặc định, nhưng khi không có đối số nào được truyền cho nó, notes là một mảng rỗng chứ không phải undefined. Theo chiều ngược lại, nếu bạn đang có một mảng và muốn truyền từng phần tử của nó thành từng đối số riêng, hãy viết ... bên trong ngoặc tròn của lời gọi.
Truyền cấu hình theo tên — options object và destructuring
Khi các mục cấu hình ngày càng nhiều, như lúc gửi email, bạn sẽ có những lời gọi như sendMail(to, subject, false, true). Nếu không mở định nghĩa của hàm ra xem, bạn không thể biết false và true ở cuối nghĩa là gì.
Với options pattern (cách gom nhiều mục cấu hình vào một object duy nhất để truyền), phía nhận sẽ destructuring object đó và đặt giá trị mặc định cho từng key. Để xử lý những lời gọi hoàn toàn không truyền cấu hình, hãy thêm = {} sau mẫu destructuring, để khi không có object nào được truyền thì một object rỗng được dùng thay.
// Càng nhiều tham số, bên gọi càng khó biết mình đang truyền gì
const sendMailOld = (to, subject, isUrgent, withPdf) =>
`${to} / ${subject} / ${isUrgent} / ${withPdf}`;
console.log(sendMailOld("minh@example.com", "Thông báo giao hàng", false, true));
// minh@example.com / Thông báo giao hàng / false / true
// Options pattern: gom cấu hình vào một object và destructuring ở phía nhận
const sendMail = (to, { subject = "Thông báo", isUrgent = false } = {}) =>
`${to} / ${subject} / ${isUrgent}`;
console.log(sendMail("minh@example.com", { isUrgent: true })); // minh@example.com / Thông báo / true
console.log(sendMail("minh@example.com")); // minh@example.com / Thông báo / false
Thay vì phụ thuộc vào thứ tự tham số, mỗi giá trị được gắn nhãn bằng key của nó, nên lời gọi vẫn dễ đọc dù cấu hình có tăng lên. Key được khớp theo tên, nên dù bạn viết theo thứ tự khác, như { isUrgent: true, subject: "Gửi lại" }, các giá trị nhận được vẫn như nhau.
Bỏ = {} sẽ gây ra TypeError
Nếu chỉ viết mẫu destructuring, như { subject = "Thông báo" }, thì lời gọi bỏ qua đối số thứ 2 sẽ ném ra TypeError: Cannot read properties of undefined (reading 'subject'). Đó là vì JavaScript cố đọc key từ undefined — giá trị mà tham số bị bỏ qua đang giữ.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Khi gọi const countNotes = (id, ...notes) => notes.length; theo dạng countNotes("A-1021"), hàm trả về gì?
Câu 3Khi gọi const send = (to, { urgent = false } = {}) => urgent; theo dạng send("minh@example.com"), hàm trả về gì?