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

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 đó.

Gán Truyền Một Giá Trị Hay Một Vị Trí?
Gán một sốhoặc chuỗiChính giá trịđược sao chépGiữ như 2giá trị riêngMột mảnghoặc objectChỉ thông tin vịtrí được truyền2 biến trỏvào 1 thứGhi đè qua1 tênTrỏ vào cùng1 thứ duy nhấtCái kia cũngthay đổi
Một giá trị nguyên thủy có chính nó được sao chép và trở nên độc lập, nhưng một mảng hay object có 2 biến trỏ vào 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 = [...].

Ghi đè thứ bạn tưởng là bản sao lưu của một đơn hàng, và xem chuyện gì xảy ra. orderbackup đã được khai báo sẵn, với backup được gán trực tiếp order.

① Lấy tổng tiền ra một biến riêng (dùng let, vì bạn sẽ gán lại nó), gán lại chỉ biến đó thành 500, và hiển thị biến đó rồi đến tổng tiền của order.

② Ghi đè tổng tiền của backup thành 0, và hiển thị tổng tiền của backup.

③ Hiển thị tổng tiền của order gốc.

④ Hiển thị orderbackup có trỏ vào cùng một thứ không.

(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

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 === copiedOrderfalse. 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.

=== Không Nhìn Vào Nội Dung
backup = orderTrỏ vào cùng1 thứ duy nhất=== là true{ ...order }Xây 1 thứriêng biệt=== là falseNội dunggiống hệt nhau=== không nhìnvào nội dungVẫn false
Nó là 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

Xây một đơn hàng riêng chỉ đổi số tiền, giữ bản gốc nguyên vẹn. order đã được khai báo sẵn.

① Xây một object riêng sao chép nội dung của order, ghi đè tổng tiền của nó thành 0, và hiển thị.

② Hiển thị tổng tiền của order gốc.

③ Hiển thị order và object ở ① có phải cùng một thứ không.

④ Hiển thị cả hai có cùng số lượng key không.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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.

Một Bản Sao Thực Sự Đi Sâu Đến Đâu
{ ...order }Chỉ xây lạicấp đầu tiênorderId trở nênđộc lậpcustomerlồng bên trongTham chiếu chỉsao chép nguyênSửa nó, bản gốccũng thay đổistructuredCloneXây lại tậnbên trongSửa nó, bản gốcvẫn an toàn
Spread syntax chỉ xây lại cấp đầu tiên. Dùng structuredClone khi bạn cần bên trong cũng độc lập.
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

Sao chép một đơn hàng có thông tin khách hàng lồng bên trong, và xem một lần ghi đè đi xa đến đâu. order đã được khai báo sẵn.

① Xây một bản sao bằng spread syntax, ghi đè thành phố của bản sao thành "Tacoma," rồi hiển thị thành phố của order gốc.

② Hiển thị customer của bản gốc và bản sao có phải cùng một thứ không.

③ Xây một bản sao riêng xây lại tận bên trong, ghi đè thành phố của nó thành "Renton," rồi hiển thị thành phố của order gốc.

④ Hiển thị customer của bản gốc và bản sao ở ③ có phải cùng một thứ không.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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.

Ghi Đè Sau Khi Đóng Băng Dừng Với TypeError
Gán trướckhi đóng băngChấp nhậnbình thườngThành giátrị mớiGán saukhi đóng băngNém raTypeErrorDừng chươngtrình ở đóThêm/xóa saukhi đóng băngNém raTypeErrorDừng chươngtrình ở đó
Một khi đã đóng băng, gán, thêm, và xóa đều thất bại. TypeError kích hoạt ngay tại đó, và không gì sau dòng đó chạy.
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" }.

Code dưới đây cố ghi đè cài đặt thuế suất đã đóng băng sau đó. Chạy nguyên bản và xem nó chạy được đến đâu và thông báo gì làm nó dừng lại.

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 const backup = order;, rồi chạy backup.total = 0;. Chuyện gì xảy ra với order.total?

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 đó?