Menu

Optional chaining (?.) trong JavaScript: truy cập an toàn

Cách toán tử ?. giúp bạn truy cập vào đối tượng lồng nhau, mảng và phương thức mà không bị lỗi khi gặp null hoặc undefined.

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.

Vấn đề: truy cập lồng nhau rất dễ hỏng

Truy cập vào một đối tượng lồng nhau không có vấn đề gì, cho tới khi một trong các cấp không tồn tại:

user.address là undefined, và đọc .city từ undefined sẽ ném ra TypeError: Cannot read properties of undefined. Dữ liệu thực tế (response API, JSON đã phân tích, truy vấn DOM) đầy những trường có thể có hoặc không, và viết các bước kiểm tra phòng thủ cho từng cấp sẽ nhanh chóng trở nên rối rắm:

const city = user && user.address && user.address.city;

Với hai cấp thì còn tạm đọc được. Với bốn cấp thì rất mệt. Toán tử ?. là cách khắc phục gọn gàng hơn.

?. dừng lại khi gặp null và undefined

Optional chaining chỉ đọc một thuộc tính khi giá trị đứng trước nó không phải null hoặc undefined. Nếu đúng là vậy, cả biểu thức cho kết quả undefined và dừng lại.

Dòng đầu đọc name như bình thường. Dòng thứ hai gặp user.address, thấy undefined và dừng lại: phần còn lại của chuỗi không bao giờ chạy. Dòng thứ ba sẽ ném lỗi nếu không có ?., vì .toUpperCase() sẽ được gọi trên undefined; với ?., cả biểu thức lặng lẽ cho kết quả undefined.

Cách nghĩ dễ nhớ: ?. nghĩa là "nếu thứ đứng trước tôi là null hoặc undefined, dừng lại và trả về undefined, còn không thì đi tiếp".

Cũng dùng được với mảng và lời gọi hàm

Ba cú pháp, cùng một ý tưởng:

  • ?.[...] để truy cập phần tử mảng hoặc thuộc tính động.
  • ?.() để gọi một thứ có thể là hàm hoặc không.
  • ?.name để truy cập thuộc tính thông thường.

Cả ba đều dừng lại theo cùng một cách. user?.notAMethod?.() không ném lỗi dù notAMethod không tồn tại: ?. thứ hai thấy undefined và dừng lại.

Điều này đặc biệt tiện với các callback tùy chọn:

Không cần thêm những câu kiểm tra if (typeof onDone === "function") trước mỗi lần gọi nữa.

Chỉ null và undefined mới khiến chuỗi dừng lại

Đây là chi tiết khiến nhiều người vấp. ?. chỉ quan tâm tới các giá trị nullish. Những giá trị falsy khác như 0, "", false, NaN vẫn hoàn toàn có thể truy cập tiếp (trong phạm vi mà cơ chế autoboxing cho phép):

data.count là 0, falsy nhưng không phải nullish, nên ?.toFixed(2) vẫn chạy trên nó và trả về "0.00". So sánh với &&:

Phiên bản && trả về 0 vì data.count là falsy và khiến biểu thức dừng lại. Phiên bản ?. trả về "0.00" vì 0 không phải nullish. Nếu bạn thực sự muốn dừng lại ở 0, && là đúng. Nếu bạn chỉ muốn dừng khi "giá trị bị thiếu", ?. mới là thứ bạn cần.

Vị trí của ? rất quan trọng

?. bảo vệ giá trị đứng trước nó, không phải đứng sau. Vì vậy hãy đặt nó ở cấp có thể bị thiếu:

Cả ba đều chạy được ở đây vì không có gì thực sự bị thiếu. Nhưng nếu config.server có thể là undefined, bạn cần config.server?.host: một ?. đặt trước server sẽ không giúp được gì, vì vấn đề nằm ở việc đọc .host từ một server không tồn tại.

Một quy tắc tốt: đặt ?. ở mỗi cấp mà giá trị bên trái của nó thực sự có thể là nullish. Rải ?. lên mọi dấu chấm "cho chắc" sẽ che giấu bug và khiến code rối hơn.

Không thể gán giá trị qua ?.

Optional chaining chỉ dùng để đọc. Bạn không thể dùng nó ở vế trái của phép gán:

user?.address?.city = "Paris";   // SyntaxError

Nghĩ kỹ thì điều này hợp lý: gán giá trị cho một thuộc tính của undefined thì có nghĩa gì? Nếu bạn chỉ muốn gán giá trị khi đối tượng cha tồn tại, hãy viết rõ ra:

Khi nào nên dùng, và khi nào không

?. phát huy tác dụng khi một giá trị thực sự là tùy chọn:

  • Response API mà các trường có thể có hoặc không.
  • Truy vấn DOM: document.querySelector(".banner")?.remove().
  • Callback có thể không được truyền vào: options.onError?.(err).
  • Chuỗi lời gọi thư viện mà kết quả trung gian có thể là null.

Nó là công cụ sai khi giá trị lẽ ra luôn phải tồn tại. Rải ?. để làm im lỗi trong trường hợp đó sẽ biến một bug rõ ràng, dễ tìm ("TypeError ở dòng 42") thành một bug âm thầm (một biến bỗng dưng là undefined ở cách đó ba hàm). Khi một thứ bắt buộc phải có, hãy để nó ném lỗi: stack trace đang giúp bạn đấy.

Một ví dụ thực tế

Lấy một giá trị từ một response API có thể thiếu dữ liệu:

Mỗi ?. xử lý một cấp không chắc chắn. avatarUrl cuối cùng là undefined một cách gọn gàng. onClick?.() gọi hàm xử lý nếu nó tình cờ tồn tại.

Bạn sẽ để ý thấy ?? ở dòng displayName: đó là nửa còn lại của cách viết này. ?. cho bạn undefined khi thiếu thứ gì đó; ?? cho phép bạn thay bằng giá trị mặc định mà không vấp phải các giá trị falsy hợp lệ như 0 hay "".

Tiếp theo: nullish coalescing

?. và ?? là cùng một ý tưởng áp dụng cho hai vấn đề khác nhau: cả hai đều coi null và undefined là "bị thiếu" và không đụng tới các giá trị falsy khác. Bài tiếp theo: cách ?? cho bạn những giá trị mặc định hợp lý, và vì sao || đã âm thầm làm sai điều đó suốt nhiều năm.

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

?. làm gì trong JavaScript?

Toán tử ?. chỉ truy cập một thuộc tính, chỉ số mảng hoặc phương thức khi giá trị đứng trước nó không phải null hoặc undefined. Nếu đúng là vậy, cả biểu thức dừng lại và cho kết quả undefined thay vì ném lỗi. user?.address?.city sẽ không bị crash khi thiếu user hoặc address.

Khi nào nên dùng optional chaining trong JavaScript?

Dùng ?. khi một giá trị thực sự được phép vắng mặt: response API có trường tùy chọn, truy vấn DOM có thể không tìm thấy phần tử, callback có thể được truyền vào hoặc không. Đừng dùng nó để che giấu bug khi một giá trị lẽ ra luôn phải có; trong những trường hợp đó, giá trị bị thiếu là thông tin thật mà bạn cần thấy.

?. và && khác nhau thế nào?

Trong phần lớn trường hợp chúng cho cùng kết quả, nhưng ?. chỉ dừng lại khi gặp null hoặc undefined, còn && dừng lại với mọi giá trị falsy, kể cả 0, '' và false. obj && obj.count trả về 0 khi count là 0; obj?.count cũng trả về 0, nhưng chuỗi truy cập chỉ dừng gọn gàng ở các giá trị nullish.

Có thể dùng optional chaining với mảng và lời gọi hàm không?

Có. arr?.[0] đọc chỉ số mảng một cách an toàn, và fn?.() chỉ gọi hàm khi hàm đó tồn tại. Cả hai đều theo cùng một quy tắc: nếu giá trị đứng trước ?. là null hoặc undefined, biểu thức cho kết quả undefined và không có gì khác được chạy.

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