Menu

Tham số mặc định trong JavaScript: đối số tùy chọn

Tham số hoạt động thế nào trong JavaScript: giá trị mặc định, khi nào undefined kích hoạt chúng, thứ tự đánh giá và sự khác nhau giữa tham số với đối số.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Tham số và đối số

Hai từ liên tục bị nhầm lẫn. Tham số (parameter) là tên trong phần định nghĩa hàm. Đối số (argument) là giá trị bạn truyền vào khi gọi hàm. Ý tưởng này giống trong Python và phần lớn các ngôn ngữ khác:

a và b là tham số. 2 và 3 là đối số. JavaScript gán chúng theo vị trí: đối số thứ nhất cho tham số thứ nhất, và cứ thế tiếp tục. Khác biệt này nhỏ, nhưng đó là từ vựng mà thông báo lỗi và MDN sử dụng.

JavaScript dễ dãi về số lượng đối số

Không như nhiều ngôn ngữ khác, JavaScript không quan tâm bạn truyền thiếu hay thừa đối số. Tham số bị thiếu trở thành undefined. Đối số thừa bị âm thầm bỏ qua:

Lời gọi đầu tiên in ra Hello, Ada undefined: last không nhận được giá trị nào nên là undefined, và template literal vui vẻ chèn giá trị đó vào. Không lỗi, không cảnh báo. Sự dễ dãi này đôi khi có ích và đôi khi là nguồn gốc của bug, và đó chính là lý do tham số mặc định tồn tại.

Đặt giá trị mặc định

Đặt = và một giá trị sau tên tham số. Nếu nơi gọi không truyền đối số đó (hoặc truyền undefined), giá trị mặc định sẽ được dùng:

Cả ba lời gọi đều chạy được. Lời gọi thứ nhất và thứ ba dùng giá trị mặc định; lời gọi thứ hai dùng giá trị được truyền vào. Đây là cú pháp ES6: trước năm 2015, bạn phải viết name = name || "friend" trong thân hàm, và cách đó có bug riêng với các giá trị falsy như 0 và "".

Giá trị mặc định có thể là bất kỳ biểu thức nào, không chỉ là giá trị cố định:

Biểu thức được đánh giá ở mỗi lần gọi, không phải một lần duy nhất khi hàm được định nghĩa. Vì vậy không có cái bẫy giá trị mặc định có thể thay đổi như trong Python: mỗi lần gọi nhận một new Date() mới.

Chỉ undefined mới kích hoạt giá trị mặc định

Đây là quy tắc khiến nhiều người bất ngờ. Giá trị mặc định được dùng khi đối số là undefined, không phải khi đối số là falsy, và không phải khi đối số là null:

Chỉ lời gọi cuối cùng dùng "friend". Truyền null một cách rõ ràng nghĩa là bạn đang nói "giá trị là null", và JavaScript tin lời bạn. Chuỗi rỗng và số 0 cũng vậy: chúng là giá trị, không phải đối số bị thiếu.

Nếu bạn muốn coi null giống như bị thiếu, bạn phải tự xử lý:

Toán tử ?? (nullish coalescing) coi cả null lẫn undefined là "bị thiếu". Chi tiết sẽ có ở một chương sau.

Giá trị mặc định có thể tham chiếu tham số phía trước

Các tham số được đánh giá từ trái sang phải, nên giá trị mặc định phía sau có thể dùng bất kỳ tham số nào đứng trước nó:

Gọi với một đối số sẽ tạo ra hình lập phương. Hai đối số cho một lăng trụ đáy vuông. Chiều tham chiếu rất quan trọng: bạn không thể tham chiếu tới một tham số chưa được khai báo:

function bad(a = b, b = 1) {
  return a + b;
}

bad();   // ReferenceError: Cannot access 'b' before initialization

Quy tắc giống như với biến khai báo bằng let: dùng trước khi khai báo là lỗi.

Giá trị mặc định kết hợp với phân rã

Bạn có thể vừa phân rã (destructuring) tham số vừa đặt giá trị mặc định cùng lúc. Đây là cách viết phổ biến cho đối số dạng "đối tượng tùy chọn":

Có hai lớp giá trị mặc định đang hoạt động. Lớp bên trong (role = "member", active = true) điền vào các thuộc tính bị thiếu. Lớp bên ngoài = {} xử lý trường hợp nơi gọi hoàn toàn không truyền đối số: nếu không có nó, createUser() sẽ cố phân rã undefined và ném lỗi.

Lúc đầu cách viết này trông khá dày đặc, nhưng nó xuất hiện ở khắp nơi trong các codebase JavaScript hiện đại. Khi mắt bạn đã quen nhận ra { ... } = {} là "tùy chọn có giá trị mặc định", bạn sẽ đọc nó rất nhanh.

Bỏ qua một đối số ở giữa

JavaScript không có đối số theo tên kiểu Python. Để bỏ qua một tham số ở giữa và dùng giá trị mặc định của nó, bạn phải truyền undefined một cách rõ ràng:

Truyền undefined cho prefix giữ lại giá trị mặc định. Cách này trông khá xấu. Nếu thấy mình thường xuyên viết undefined trong lời gọi hàm, đó là dấu hiệu nên chuyển sang dùng đối tượng tùy chọn:

Giờ nơi gọi chỉ cần gọi tên thứ muốn ghi đè, và thứ tự không còn quan trọng.

Giá trị mặc định không được tính vào length

Một chi tiết nhỏ hiếm khi quan trọng nhưng đôi khi gây bối rối. Thuộc tính length của một hàm cho biết số tham số đứng trước tham số đầu tiên có giá trị mặc định:

Các thư viện kiểm tra hàm (như một số công cụ test và DI) dùng length để đếm số tham số "bắt buộc". Biết quy tắc này tồn tại là đủ; không cần ghi nhớ, trừ khi bạn đang viết loại công cụ đó.

Tiếp theo: rest và spread

Giá trị mặc định xử lý trường hợp bạn biết trước các tham số. Đôi khi bạn không biết: bạn muốn một hàm nhận số lượng đối số bất kỳ, hoặc chuyển tiếp cả một nhóm đối số sang hàm khác. Đó là việc của ...rest và toán tử spread, nội dung của bài tiếp theo.

Câu hỏi thường gặp

Làm sao đặt giá trị mặc định cho tham số trong JavaScript?

Đặt = và giá trị mặc định sau tên tham số trong phần khai báo hàm: function greet(name = 'friend') { ... }. Nếu nơi gọi không truyền giá trị (hoặc truyền undefined), giá trị mặc định sẽ được dùng. Đây là cú pháp ES6 và chạy được trong mọi môi trường JavaScript hiện đại.

Tham số và đối số khác nhau thế nào?

Tham số là các tên trong phần định nghĩa hàm: function add(a, b) có hai tham số a và b. Đối số là các giá trị thực tế được truyền khi gọi hàm: add(2, 3) truyền hai đối số 2 và 3. Sự phân biệt này quan trọng khi đọc thông báo lỗi và tài liệu.

Tham số mặc định có được dùng khi truyền null không?

Không. Giá trị mặc định chỉ được dùng khi đối số là undefined (hoặc hoàn toàn không được truyền). Truyền null một cách rõ ràng nghĩa là 'tôi đưa cho bạn một giá trị, và giá trị đó là null', nên giá trị mặc định bị bỏ qua. Điều này khiến những người đến từ các ngôn ngữ coi null và undefined là một hay bị vấp.

Giá trị mặc định có thể tham chiếu tới tham số khác không?

Có. Các tham số được đánh giá từ trái sang phải, nên giá trị mặc định của tham số sau có thể dùng các tham số trước: function box(width, height = width). Bạn cũng có thể gọi hàm trong giá trị mặc định, như function log(msg, time = Date.now()), và biểu thức đó chạy lại từ đầu ở mỗi lần gọi.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu