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

null và undefined — Đọc An Toàn Với ?. và ??

Phân biệt null và undefined, rồi học cách đọc qua một bước lồng nhau bị thiếu và đặt mặc định an toàn.

Hiển thị dữ liệu đơn hàng từ một server, bạn sẽ gặp một địa chỉ chưa từng được điền, hay một phương thức thanh toán chưa được chọn. JavaScript có hai giá trị cho trạng thái "không có gì ở đây" này — undefinednull — và một lần đọc thất bại làm dừng chương trình với TypeError.

Bài này nói về khác biệt giữa hai giá trị đó, đọc qua một bước thiếu mà không dừng lại, và áp dụng giá trị mặc định khi có gì đó bị thiếu.

2 Giá Trị Nghĩa Là Không Có Gì Ở Đó — undefined và null

undefined (giá trị JavaScript tự động điền vào khi chưa có gì được gán) xuất hiện tự nó — bạn không cần tự gán nó. Bạn đã thấy nó trả về từ việc đọc quá cuối một mảng hay một key object chưa đăng ký, và một biến chỉ mới được khai báo cũng thuộc nhóm này.

  • Một biến chỉ mới khai báo, chưa từng gán giá trị, như let couponCode;
  • Một key chưa đăng ký trên một object, như order.customer (đã thấy trước đó)
  • Một index ngoài phạm vi trên mảng 2 phần tử, như tags[5] (đã thấy trước đó)

null (giá trị người viết cố tình gán để báo hiệu trống rỗng), ngược lại, không bao giờ tự xuất hiện. Khi bạn muốn chính sự thiếu vắng của một giá trị trở thành dữ liệu — "chưa chọn địa chỉ vận chuyển nào," "chưa áp dụng mã giảm giá nào" — bạn tự gán null.

JSON trả về từ một API cũng có thể mang null để báo hiệu một trường đang trống.

Vì chúng là các giá trị khác nhau, === trả về false khi so sánh chúng. ==, tuy nhiên, coi hai cái là giống nhau và trả về true.

Khi viết một kiểm tra, hoặc cố ý biết rõ bạn đang mong đợi cái nào và chỉ kiểm tra bên đó, như === null, hoặc giao hẳn cho ?.??, sẽ nói từ đây trở đi.

Ai Đặt undefined và null Vào Đó
Đọc biếnchỉ mới khai báoJavaScripttự điền vàoundefinedĐọc key thiếuhoặc index saiJavaScripttự điền vàoundefinedMuốn báo hiệutrống rỗngNgười viếttự gánnullTự động điền vàoNgười viết tự gán
Ở 2 hàng trên, undefined là giá trị JavaScript tự điền vào; ở hàng dưới, null là giá trị người viết tự gán.
let couponCode;                 // chỉ mới khai báo, chưa gán giá trị
console.log(couponCode);        // undefined

const order = { orderId: "ORD-1105", total: 12800 };
console.log(order.customer);    // undefined (một key chưa đăng ký)

const tags = ["Limited", "Preorder"];
console.log(tags[5]);           // undefined (một index ngoài phạm vi)

// null được người viết tự gán
let shippingAddress = null;     // chưa chọn địa chỉ vận chuyển nào
console.log(shippingAddress);   // null

console.log(shippingAddress === undefined);  // false (chúng là các giá trị khác nhau)
console.log(shippingAddress == undefined);   // true (== coi chúng giống nhau)

Không Dừng Lại Khi Một Bước Lồng Nhau Bị Thiếu — Optional Chaining ?.

Dữ liệu trả về từ một API không phải lúc nào cũng đầy đủ mọi trường. Code đọc sâu, như order.customer.address.city, dừng lại ngay khi dù chỉ một đơn hàng thiếu address ở giữa chuỗi.

Đây là lúc bạn muốn chính việc đọc không thất bại — mà tiếp tục như "không có gì ở đó" thay vào đó.

Khi order.customer.addressundefined, cố đọc .city sau nó sẽ ném ra TypeError: Cannot read properties of undefined (reading 'city'). Cấp đọc thứ 1 trót lọt, nhưng nó dừng ở cấp thứ 2.

Đó là lý do có optional chaining (toán tử ?., trả về undefined mà không đọc tiếp nếu bên trái là null hoặc undefined). Viết address?.city, và .city chỉ được đọc khi address có giá trị — nếu không, việc đọc dừng ở đó và trả về undefined.

Khi truyền một key qua [], viết nó là ?.[field].

?. Dừng Đọc Ở Đâu
customercó giá trị?.addresscó giá trị?.cityAustinshippingnull?.carrierdừng ở đâyundefinedpaymentkhông có key này?.methoddừng ở đâyundefined
Nơi có giá trị, nó tiếp tục. Ngay khi gặp null, undefined, hoặc chính một key thiếu, nó dừng và trả về undefined mà không đọc phần còn lại.
const order = {
  orderId: "ORD-1105",
  customer: { name: "Ryan Cole" },
};

// address bị thiếu, nên đọc city sau nó sẽ dừng lại
// console.log(order.customer.address.city);
// TypeError: Cannot read properties of undefined (reading 'city')

// Chèn ?. và việc đọc dừng lại, trả về undefined
console.log(order.customer.address?.city);   // undefined

// Nơi có giá trị, nó tiếp tục như bình thường
console.log(order.customer.name);            // Ryan Cole

// Truyền một key qua biến với ?.[]
const field = "city";
console.log(order.customer.address?.[field]);   // undefined

Chỉ Dùng Nó Nơi Một Trường Hợp Lý Có Thể Bị Thiếu

Đặt ?. ở mọi bước, và ngay cả một tên key gõ nhầm cũng trượt qua thành undefined mà không báo lỗi. Bỏ qua nó ở các trường chắc chắn sẽ có mặt, và chỉ dùng nó ngay trước một trường hợp lý có thể thiếu — như vậy các lỗi bạn thực sự muốn bắt vẫn dừng chương trình.

Đọc giá trị từ một object có hình dạng như phản hồi API đơn hàng, mà không dừng lại khi trường bị thiếu. response đã được khai báo sẵn, và customer luôn có mặt.

① Hiển thị thành phố vận chuyển, viết sao cho không dừng lại ngay cả khi có gì đó dọc đường bị thiếu.

② Hiển thị tên hãng vận chuyển, shipping.carrier, với cùng cú pháp.

③ Hiển thị phương thức thanh toán, payment.method, với cùng cú pháp.

④ Đọc tên trường từ một biến, và hiển thị mã zip vận chuyể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

Thiết Lập Giá Trị Mặc Định — ?? vs ||

Thay vì hiện một undefined trơ trọi khi một trường bị thiếu, thường thì bạn sẽ muốn hiển thị một giá trị mặc định — "Guest," "20 items."

Dùng || cho việc này, tuy nhiên, đôi khi một giá trị thực sự có ở đó lại bị thay bằng mặc định. Số lượng tồn kho 0, hay một trường ghi chú trống, là ví dụ.

nullundefined gộp lại gọi là nullish. Dùng Nullish coalescing operator (toán tử ??, chỉ trả về bên phải khi bên trái là nullish), và 0, một chuỗi rỗng, hay false giữ nguyên đúng như chúng vốn có.

|| trả về bên phải bất cứ khi nào bên trái falsy, nên nó không phân biệt được "không có giá trị" với "giá trị là 0". Khi mục tiêu của bạn là áp dụng mặc định, ?? tránh việc thay thế ngoài ý muốn đó.

?? và || Khác Nhau Ở Giá Trị Falsy
stock là 0|| kiểm trathay falsy20đã thaystock là 0?? kiểm trachỉ thay null0không đổistock là null?? kiểm trachỉ thay null20đã thay
Kết quả chỉ khác nhau khi stock là 0. ?? chỉ thay null và undefined, nên 0 giữ nguyên là giá trị.
Giá Trị Bên Trái|| Trả Về Gì?? Trả Về Gì
nullgiá trị mặc địnhgiá trị mặc định
undefinedgiá trị mặc địnhgiá trị mặc định
0giá trị mặc địnhvẫn là 0
chuỗi rỗnggiá trị mặc địnhvẫn là chuỗi rỗng
falsegiá trị mặc địnhvẫn là false
const settings = { displayName: null, itemsPerPage: 0 };

// ?? chỉ trả về bên phải khi bên trái là null hoặc undefined
console.log(settings.displayName ?? "Guest");   // Guest
console.log(settings.itemsPerPage ?? 20);        // 0 (giữ nguyên)

// || trả về bên phải bất cứ khi nào bên trái falsy, nên ngay cả 0 cũng bị thay
console.log(settings.itemsPerPage || 20);        // 20

// Một key chưa đăng ký quay về mặc định theo cả hai cách
console.log(settings.language ?? "en");          // en
console.log(settings.language || "en");          // en

Lưu ý viết ??|| cạnh nhau mà không có dấu ngoặc, như displayName ?? "Guest" || "Anonymous", là lỗi cú pháp — JavaScript không thể quyết định cái nào tính trước. Để dùng cả hai, tách chúng bằng dấu ngoặc, như (displayName ?? "Guest") || "Anonymous".

Áp dụng mặc định cho mỗi trường của một object cài đặt người dùng, xây dựng các giá trị bạn sẽ hiển thị. Các trường đã có giá trị cần giữ nguyên. userSettings đã được khai báo sẵn.

① Áp dụng mặc định "Guest" cho tên hiển thị và hiển thị nó.

② Áp dụng mặc định "en" cho ngôn ngữ hiển thị và hiển thị nó.

③ Áp dụng mặc định 20 cho số lượng mỗi trang và hiển thị nó.

④ Áp dụng mặc định false cho cài đặt thông báo chưa đăng ký và hiển thị nó.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Đừng Để Mặc Định Xóa Mất 0 hoặc Chuỗi Rỗng

Số lượng tồn kho 0, tỷ lệ giảm giá 0, một ghi chú trống — tất cả đều tính là "có giá trị". Áp dụng mặc định bằng ||, và vì chúng falsy, chúng bị thay bằng mặc định, tạo ra sự không khớp như một mặt hàng hết hàng lại hiện thành còn hàng. Dùng ?? khi bạn chỉ quan tâm một giá trị có tồn tại hay không.

So sánh cách hiển thị thay đổi tùy vào toán tử mặc định bạn dùng, cho một sản phẩm với tồn kho 0 và ghi chú trống. product đã được khai báo sẵn.

① Áp dụng mặc định 10 cho tồn kho bằng || và hiển thị.

② Áp dụng cùng mặc định 10 cho tồn kho bằng ?? và hiển thị.

③ Áp dụng mặc định "No notes" cho ghi chú bằng ||, bọc trong dấu ngoặc vuông, và hiển thị.

④ Áp dụng cùng mặc định "No notes" cho ghi chú bằng ??, bọc trong dấu ngoặc vuông, và hiển thị.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Không Gọi Thứ Không Tồn Tại — Lệnh Gọi ?.()

Trong xử lý đơn hàng, đôi khi bạn sẽ cất "làm gì khi thành công" và "làm gì khi thất bại" bên trong một object, gọi chúng khi cần. Gọi một trường chưa được thiết lập gì cả, tuy nhiên, và nó dừng lại với TypeError: handlers.onError is not a function — nên bạn cần một cách để chỉ gọi nó khi nó thực sự có ở đó.

Hàm nói ở bài 3 cũng có thể sống bên trong một object như giá trị của một property. () => ... ở ví dụ tiếp theo là cú pháp để tạo một hàm ngay tại chỗ — cú pháp đó sẽ nói riêng ở nhóm cú pháp, nên ở đây chỉ tập trung vào kết quả thay đổi thế nào tùy vào việc property có chứa hàm hay không.

Gọi một property có thể chứa hàm dưới dạng handlers.onError?.(), và khi onErrornull hoặc undefined, lệnh gọi bị bỏ qua và undefined trả về thay vào đó.

?.() Bỏ Qua Lệnh Gọi Khi Không Có Gì Đăng Ký
onSuccess làmột hàmGọi bằng?.()Nó chạyonError lànullGọi bằng?.()Bỏ qua,undefinedonError lànullGọi màkhông ?.Dừng vớiTypeError
Gọi nó nếu có một hàm ở đó; trả về undefined mà không gọi gì nếu nó null. Gọi nó không có ?. thì dừng lại với TypeError.
const handlers = {
  onSuccess: () => console.log("Order confirmed"),
  onError: null,
};

// Có một hàm ở đó, nên nó được gọi
handlers.onSuccess?.();              // Order confirmed

// Nó là null, nên lệnh gọi bị bỏ qua và undefined trả về
console.log(handlers.onError?.());   // undefined

// Gọi mà không có ?. sẽ dừng chương trình
// handlers.onError();
// TypeError: handlers.onError is not a function

So sánh gọi với ?. và gọi mà không có nó, dùng một object gói các handler xử lý đơn hàng. handlers đã được khai báo sẵn.

① Gọi handler thành công bằng cú pháp bỏ qua lệnh gọi nếu nó chưa đăng ký.

② Gọi handler thất bại theo cùng cách, và hiển thị bất cứ thứ gì trả về.

③ Gọi cùng handler thất bại đó mà không có ?., và xem thông báo nào làm dừng chương trình.

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 1Viết let couponCode; và đọc nó mà không gán giá trị. Giá trị của biến là gì?

Câu 2Khi bên trái của ?.null hoặc undefined, chuyện gì xảy ra với phần còn lại của chuỗi?

Câu 3Với const discountRate = 0;, discountRate || 5discountRate ?? 5 mỗi cái trả về gì?