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

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
Đối số được điền vào từ trái sang phải
Truyền Phòng Avào roomNameKhông cógiá trị mặc địnhroomName làPhòng ATruyền 2vào hoursMặc định 1không được dùnghours là 2Không truyền gìcho layoutGiá trị mặc địnhđược dùng ở đâylayout làlớp học
Lần theo 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.

Ước tính phí vận chuyển. boxCount và đơn giá mỗi thùng theo từng khu vực đã được khai báo sẵn; phí vận chuyển bằng đơn giá mỗi thùng × số thùng.

① Hãy viết một hàm nhận số thùng và khu vực rồi trả về phí vận chuyển, với giá trị mặc định là 1 thùng và Honshu (đảo chính của Nhật Bản).

② Hãy bỏ qua khu vực và hiển thị phí vận chuyển cho boxCount thùng theo dạng "Honshu: ◯ yên".

③ Hãy chỉ định khu vực là Hokkaido và hiển thị "Hokkaido: ◯ yên".

④ Hãy gọi hàm mà không truyền đối số nào và hiển thị "Mặc định: ◯ yên".

(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

Đố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
Bỏ qua đối số và truyền null
Gọi mà khôngtruyền statusstatus làundefined= "Đã tiếp nhận"được áp dụngA-1021:Đã tiếp nhậnTruyền nullvào statusstatus lànullGiá trị mặc địnhkhông áp dụngA-1021:null
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ọiGiá trị của statusHiển thị
notifyStatus("A-1021")Giá trị mặc định, Đã tiếp nhậnA-1021: Đã tiếp nhận
notifyStatus("A-1021", "Đã gửi hàng")Đã gửi hàngA-1021: Đã gửi hàng
notifyStatus("A-1021", undefined)Giá trị mặc định, Đã tiếp nhậnA-1021: Đã tiếp nhận
notifyStatus("A-1021", null)nullA-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";.

Hiển thị số lượng tồn kho. products đã được khai báo sẵn, trong đó có một sản phẩm tồn kho bằng 0, một sản phẩm không có key stock và một sản phẩm có stock là null.

① Hãy viết stockLabel nhận số tồn kho và trả về chuỗi dạng "(tồn kho: ◯)", với giá trị mặc định là "đang kiểm tra".

② Hãy hiển thị mọi sản phẩm theo dạng "[tên sản phẩm] (tồn kho: ◯)".

③ Hãy viết stockLabelSafe, chuyển cả null thành "đang kiểm tra", mà không dùng giá trị mặc định.

④ Hãy hiển thị lại Cáp và Hub USB, lần này bằng stockLabelSafe.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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 ()
Mỗi giá trị đi vào tham số nào
Thứ 1"A-1021"Đi vàoorderIdThứ 2"Giữ lạnh"Đi vàonotes[0]Thứ 3"Hẹn giờ giao"Đi vàonotes[1]
Giá trị thứ nhất đi vào 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.

Cộng tổng chi phí đi lại của một chuyến công tác. trainFare, busFare và taxiFare đã được khai báo sẵn.

① Hãy viết sumExpenses bằng rest parameter, cộng tất cả các khoản tiền nhận được và trả về tổng.

② Hãy hiển thị tổng của 3 khoản theo dạng "Tổng: ◯ yên".

③ Hãy hiển thị kết quả khi gọi hàm mà không truyền đối số nào, theo cùng dạng đó.

④ Hãy viết summarize nhận tên loại chi phí rồi đến số lượng khoản tiền tùy ý, sau đó truyền vào "Chi phí đi lại" cùng 3 khoản và hiển thị "Chi phí đi lại: ◯ yên (◯ khoản)".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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 falsetrue ở 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
Chỉ những key bạn viết mới bị ghi đè
Truyền{ isUrgent: true }true đi vàoisUrgentisUrgent làtrueLời gọi nàykhông có subjectsubject dùnggiá trị mặc địnhsubject làThông báoLời gọi khác,không có đối số 2= {} đặt vàoobject rỗngCả hai dùnggiá trị mặc định
Chỉ những key bạn viết mới được truyền vào; những key bạn bỏ qua vẫn giữ giá trị mặc định. Phần = {} ở cuối xử lý những lời gọi bỏ qua cả object.

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ữ.

Chuyển đổi cách hiển thị giá cơm hộp bằng một object cấu hình. lunchPrice đã được khai báo sẵn.

① Hãy viết formatPrice nhận giá chưa thuế và một object cấu hình, rồi trả về chuỗi dạng "1058 yên". Nhận cấu hình bằng destructuring, với thuế suất mặc định là 0.08 và đơn vị mặc định là "yên".

② Hãy hiển thị kết quả khi gọi mà không truyền cấu hình.

③ Hãy hiển thị kết quả với thuế suất 0.1 và đơn vị "yên (ăn tại quán)".

④ Hãy hiển thị kết quả với thuế suất là 0.

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 1Với const addFee = (price, fee = 500) => price + fee;, addFee(1000, undefined) trả về gì?

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ì?