Câu 1Viết const backup = order;, rồi chạy backup.total = 0;. Chuyện gì xảy ra với order.total?
Tham Chiếu và Sao Chép — Cạm Bẫy Của Mảng và Object
Học vì sao gán mảng hay object chỉ truyền tham chiếu, cùng sao chép nông, sao chép sâu và Object.freeze.
Cố sao lưu một đơn hàng trước khi sửa bằng cách viết const backup = order;, và sửa order sau đó sẽ đổi backup theo luôn. Chuyện này không bao giờ xảy ra với số hay chuỗi.
Bài này nói về cái thực sự được truyền khi bạn gán một mảng hay object, và cách xây một bản sao an toàn.
Gán Truyền Gì — Giá Trị Nguyên Thủy vs Tham Chiếu
Điều khiến sự khác biệt này khó lần ra là cái được truyền khi gán khác nhau tùy vào loại giá trị. Ở đây sẽ điểm qua cái gì được sao y nguyên khi gán, và cái gì không.
5 loại — số, chuỗi, boolean, null, và undefined (gọi là giá trị nguyên thủy (primitives) — các giá trị không phải object hay mảng) — có chính giá trị được sao chép khi bạn gán chúng.
2 biến đó không liên quan gì đến nhau sau lần sao chép đó, nên đổi cái này không bao giờ đổi cái kia.
Mảng và object là kiểu tham chiếu (reference types) (kiểu giữ thông tin trỏ đến nơi một giá trị nằm, thay vì chính giá trị đó). Gán chỉ truyền thông tin vị trí đó, nên 2 biến kết thúc trỏ vào đúng cùng một mảng hoặc object.
Ghi đè nó qua bất kỳ tên nào, và bạn đang ghi đè cùng một thứ duy nhất đó.
// Một giá trị nguyên thủy có chính nó được sao chép
let price = 3980;
let salePrice = price;
salePrice = 2980;
console.log(price); // 3980 (không đổi)
console.log(salePrice); // 2980
// Một mảng trỏ vào cùng một thứ duy nhất
const cart = ["A4 Notebook"];
const savedCart = cart;
savedCart.push("Permanent Marker");
console.log(cart.length); // 2 (cart cũng tăng lên)
console.log(cart === savedCart); // true (chúng trỏ vào cùng một thứ)
Ngay Cả Với const, Nội Dung Vẫn Có Thể Bị Ghi Đè
Cái const cố định chỉ là nơi biến trỏ đến. Viết const cart = [];, rồi cart.push("Permanent Marker"), và nội dung tăng lên; const order = {}, rồi order.total = 0, cũng hoạt động.
Cái bị cấm chỉ là gán lại trỏ nó sang một nơi hoàn toàn khác, như cart = [...].
Xây Một Bản Sao — === Thực Sự Nhìn Vào Cái Gì
Như bạn vừa thấy, chỉ gán thôi thì không cho ra bản sao an toàn, tách biệt khỏi bản gốc. Muốn một đơn hàng riêng chỉ đổi số tiền, giữ bản gốc nguyên vẹn? Xây một object mới trải nội dung bằng spread syntax, như { ...order }. Điều đáng xác nhận là mối quan hệ giữa cái bạn xây và bản gốc.
const copiedOrder = { ...order }; xây một object riêng, nên order === copiedOrder là false. Với mảng và object, === kiểm tra không phải nội dung có khớp không, mà là chúng có phải chính xác cùng một thứ duy nhất hay không.
Để so sánh nội dung, hãy tự viết ra những gì cụ thể bạn muốn so sánh — số lượng key, từng giá trị, v.v.
true khi chúng trỏ vào cùng một thứ, và false cho những thứ xây riêng biệt, dù nội dung giống hệt nhau.const cart = ["A4 Notebook"];
const copiedCart = [...cart];
// Là 2 thứ riêng biệt, nên === là false
console.log(cart === copiedCart); // false
// Ngay cả nội dung giống hệt nhau, vẫn false
console.log(["A"] === ["A"]); // false
// So sánh chính nội dung, và nó là true
console.log(cart[0] === copiedCart[0]); // true
// Chỉ true khi chúng trỏ vào cùng một thứ
const savedCart = cart;
console.log(cart === savedCart); // true
Sao Chép Không Chạm Đến Bên Trong Giá Trị Lồng Nhau — Sao Chép Nông vs Sâu
Spread syntax cho đến giờ đã tạo ra bản sao an toàn cho các giá trị nguyên thủy như số tiền. Nhưng sao chép một đơn hàng có một object lồng bên trong, như thông tin khách hàng, thì trong khi object bên ngoài trở thành một object riêng, cái bên trong vẫn trỏ vào chính xác cùng một thứ như bản gốc. Sửa một địa chỉ trên cái bạn tưởng là bản sao, và địa chỉ của đơn hàng gốc cũng thay đổi theo — một sự không khớp bạn không lường trước.
Cái spread syntax xây là một bản sao nông (shallow copy) (bản sao chỉ nhân bản các property ở cấp đầu tiên, để bất kỳ object lồng nào trỏ vào cùng một thứ). Các giá trị ở cấp đầu tiên trở nên độc lập nếu chúng là giá trị nguyên thủy, nhưng khi bên trong là một object hay mảng, thông tin vị trí — tham chiếu — chỉ đơn giản được sao chép nguyên trạng.
Khi bạn cần bên trong cũng được xây lại, dùng một bản sao sâu (deep copy) (bản sao xây lại mọi thứ, xuống tận các giá trị lồng bên trong). Viết structuredClone(order), và mọi cấp lồng nhau đều được xây lại thành một object riêng.
Đó là một hàm trình duyệt cung cấp, nên nó hoạt động trực tiếp trong console của trang này luôn. Nó chỉ không hoạt động trên một object chứa một hàm.
const order = {
orderId: "ORD-1342",
customer: { name: "Kenji Ross", city: "Denver" },
};
// Sao chép nông: cấp đầu tiên riêng biệt, bên trong vẫn trỏ vào cùng 1 thứ
const shallow = { ...order };
shallow.orderId = "ORD-9999";
console.log(order.orderId); // ORD-1342 (độc lập)
shallow.customer.city = "Boulder";
console.log(order.customer.city); // Boulder (bản gốc cũng đổi)
console.log(order.customer === shallow.customer); // true
// Sao chép sâu: cả bên trong cũng trở nên riêng biệt
const deep = structuredClone(order);
deep.customer.city = "Aurora";
console.log(order.customer.city); // Boulder (bản gốc an toàn)
console.log(order.customer === deep.customer); // false
Ngăn Ghi Đè — Object.freeze
Như bạn đã thấy, mảng và object có thể bị thay đổi ngoài ý muốn chỉ vì chia sẻ chung một tham chiếu. Một số giá trị, như cài đặt mặc định hay một thuế suất, cần một cách bảo vệ khác với const — bạn không muốn chúng bị ghi đè về sau. Chỉ const không ngăn nội dung bị ghi đè, nên bạn cần một cách đánh dấu chính object đó là bất khả xâm phạm với thay đổi.
Viết Object.freeze(settings), và object đó ngừng chấp nhận thay đổi, thêm, hay xóa property của nó. Cố gán, thêm, hay xóa gì đó sau khi đóng băng nó, và nó ném ra TypeError ngay tại đó, dừng chương trình.
Kiểm tra một thứ có bị đóng băng không bằng Object.isFrozen(settings). Đóng băng chỉ chạm đến cấp đầu tiên, nên một object lồng bên trong cần được đóng băng riêng.
const settings = { theme: "dark" };
// Có thể ghi đè trước khi đóng băng
settings.theme = "light";
console.log(settings.theme); // light
Object.freeze(settings);
console.log(Object.isFrozen(settings)); // true
// Gán sau khi đóng băng sẽ dừng ngay tại dòng đó
// settings.theme = "dark";
// TypeError: Cannot assign to read only property 'theme' of object '#<Object>'
// Đóng băng chỉ chạm đến cấp đầu tiên — bên trong không dừng
const nested = Object.freeze({ display: { theme: "dark" } });
nested.display.theme = "light";
console.log(nested.display.theme); // light
// Để đổi nó, xây một object mới thay vào đó
const updated = { ...settings, theme: "dark" };
console.log(updated.theme); // dark
console.log(settings.theme); // light (bản gốc an toàn)
Để Đổi Nó, Hãy Xây Một Object Mới
Gán, thêm, hay xóa trên một object đã đóng băng ném ra TypeError, và không gì sau dòng đó chạy. Để đổi một object đã đóng băng, đừng ghi đè nó — hãy xây một cái mới thay vào đó, như { ...settings, theme: "light" }.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Với const a = ["A"]; const b = ["A"];, a === b trả về gì?
Câu 3Sao chép dữ liệu có một object lồng nhau bằng { ...order }. Chuyện gì xảy ra với object lồng nhau đó?