Optional chaining (?.) trong JavaScript: đọc an toàn

Optional chaining trong JavaScript giúp đọc thuộc tính lồng nhau mà không lỗi. Xem cú pháp ?., kết hợp ?? và những lưu ý khi dùng.

5 phút

Optional chaining trong JavaScript, viết là ?., cho phép bạn đọc thuộc tính sâu trong một đối tượng. Bạn không sợ lỗi khi một mắt xích ở giữa bị thiếu. Trước đây bạn phải kiểm tra từng tầng một, code vừa dài vừa khó đọc. Toán tử này ra mắt trong ES2020 và hiện được các trình duyệt hiện đại hỗ trợ.

Vấn đề mà optional chaining giải quyết

Vấn đề mà optional chaining giải quyết
Vấn đề mà optional chaining giải quyết

Giả sử bạn nhận dữ liệu người dùng từ một API. Không phải ai cũng có địa chỉ, và không phải địa chỉ nào cũng có thành phố.

const user = { ten: "An" };
console.log(user.diaChi.thanhPho);
// TypeError: Cannot read properties of undefined (reading 'thanhPho')

Để tránh lỗi, trước đây ta phải viết user && user.diaChi && user.diaChi.thanhPho. Chuỗi này đọc rất mệt khi đối tượng lồng ba, bốn tầng.

Cú pháp ?.

Cú pháp ?.
Cú pháp ?.
console.log(user?.diaChi?.thanhPho);   // undefined, không lỗi
console.log(user?.["ten"]);             // "An"
console.log(user.chao?.());             // gọi hàm nếu tồn tại
const ds = null;
console.log(ds?.[0]);                   // undefined

Có ba dạng. Dạng a?.b đọc thuộc tính, dạng a?.[k] đọc theo khóa hoặc chỉ số, và dạng a?.() gọi hàm khi hàm có tồn tại. Nếu vế trái là null hoặc undefined, cả biểu thức dừng lại và cho kết quả undefined.

Đặc điểm quan trọng là cơ chế đoản mạch (short-circuit). Khi gặp mắt xích rỗng, phần còn lại của chuỗi không được tính nữa. Vì vậy trong a?.b.c.d, nếu a rỗng thì .c và .d cũng không bao giờ bị đọc.

Kết hợp với toán tử ?? để đặt giá trị mặc định

Kết hợp với toán tử ?? để đặt giá trị mặc định
Kết hợp với toán tử ?? để đặt giá trị mặc định

Optional chaining thường đi cùng toán tử hợp nhất null ??. Toán tử này trả về vế phải khi vế trái là null hoặc undefined.

const thanhPho = user?.diaChi?.thanhPho ?? "Chưa cập nhật";
console.log(thanhPho);   // "Chưa cập nhật"

const soLuong = 0;
console.log(soLuong ?? 10);   // 0
console.log(soLuong || 10);   // 10 (sai ý!)

Ví dụ cuối cho thấy vì sao nên ưu tiên ?? thay vì ||. Toán tử || coi số 0, chuỗi rỗng và false là “giả”, nên nó ghi đè cả những giá trị hợp lệ. ?? chỉ quan tâm tới null và undefined.

Dùng với mảng, Map và phản hồi API

Dùng với mảng, Map và phản hồi API
Dùng với mảng, Map và phản hồi API

Khi làm việc với danh sách trả về từ máy chủ, bạn thường cần phần tử đầu tiên mà danh sách có thể rỗng. Viết ketQua?.[0]?.ten an toàn cho cả hai trường hợp. Nếu chưa quen với truy cập phần tử, hãy xem lại bài về mảng trong JavaScript. Với Map, bạn gọi map.get(khoa)?.thuocTinh, vì get trả về undefined khi khóa không có.

Những lưu ý khi dùng

  • Không lạm dụng. Nếu một thuộc tính bắt buộc phải có, hãy để lỗi xảy ra. Bạn sẽ phát hiện dữ liệu sai sớm hơn.
  • Không dùng được ở vế trái của phép gán. Viết a?.b = 1 gây lỗi cú pháp.
  • Nó chỉ xử lý null và undefined. Nếu a.b là số hay chuỗi và bạn gọi a.b?.(), vẫn nhận TypeError vì giá trị đó không phải hàm.
  • Khi cần hỗ trợ trình duyệt cũ, hãy dùng công cụ biên dịch như Babel để chuyển cú pháp.

Chúng tôi có một thói quen tốt: đặt kiểm tra dữ liệu ở biên của chương trình. Ví dụ ngay khi nhận phản hồi từ API. Bên trong, ta dùng dữ liệu đã chuẩn hóa. Như vậy bạn chỉ cần ?. ở vài chỗ chứ không rải khắp nơi.

Ví dụ: đọc cấu hình lồng nhau

Giả sử ứng dụng có một đối tượng cấu hình, trong đó người dùng có thể chỉ khai báo vài mục. Optional chaining giúp bạn đọc giá trị mà không phải viết nhiều lớp kiểm tra.

const cauHinh = { giaoDien: { mau: "toi" } };

const mau = cauHinh.giaoDien?.mau ?? "sang";
const coChu = cauHinh.giaoDien?.chu?.coChu ?? 16;
const dong = cauHinh.nguoiDung?.ten?.toUpperCase();
console.log(mau, coChu, dong);   // toi 16 undefined

Ở dòng cuối, toUpperCase không bị gọi vì vế trước đã undefined. Đây là điểm khiến optional chaining an toàn hơn nhiều so với truy cập thông thường.

Gọi phương thức có thể không tồn tại

Dạng obj.phuongThuc?.() rất tiện khi làm việc với callback tùy chọn. Ví dụ hàm nhận một đối tượng tùy chọn có thể có onXong hoặc không; bạn gọi tuyChon.onXong?.(ketQua) và không cần câu lệnh if. Nhưng hãy nhớ rằng nếu onXong tồn tại mà không phải hàm, bạn vẫn nhận lỗi, vì ?. chỉ chặn null và undefined.

Hiệu năng và khả năng đọc

Về hiệu năng, chi phí của ?. gần như không đáng kể trong ứng dụng thông thường. Điều đáng quan tâm hơn là khả năng đọc. Một chuỗi dài năm sáu dấu ?. thường là dấu hiệu dữ liệu của bạn chưa được chuẩn hóa.

Khi thấy mình viết như vậy, hãy gom bước đọc vào một hàm nhỏ có tên rõ nghĩa, như layThanhPho(user). Để hàm đó lo mọi trường hợp thiếu dữ liệu.

Hỏi đáp nhanh

Optional chaining có thay thế được try-catch không? Không. Nó chỉ tránh lỗi đọc thuộc tính trên null hoặc undefined.

Các lỗi khác, như hàm ném ngoại lệ hay dữ liệu JSON hỏng, vẫn cần try-catch. Hai công cụ này bổ sung cho nhau chứ không thay thế nhau.

Còn kết quả của a?.b khi a là chuỗi rỗng hay số 0? Vì chuỗi rỗng và số 0 không phải null hay undefined nên biểu thức vẫn đọc thuộc tính bình thường. Chỉ hai giá trị null và undefined mới làm chuỗi dừng lại. Khi muốn duyệt các phần tử trong mảng kết quả, hãy xem lại bài về mảng.

Bạn có thể xem đặc tả đầy đủ trong trang Optional chaining trên MDN. Hãy mở console và thử đọc một thuộc tính ở bốn tầng. Cho dữ liệu thiếu ở từng tầng khác nhau để thấy rõ cơ chế đoản mạch.

Chuyên mục