Lập trình · 26/09/2026

JavaScript spread: Vì sao sửa bản sao lại đổi cả object gốc?

Bạn tạo bản nháp bằng {...profile}, sửa địa chỉ trong form rồi bấm Hủy. Nhưng hồ sơ gốc đã đổi trước khi lưu. Nguyên nhân thường nằm ở một hiểu nhầm: object mới ở tầng ngoài không có nghĩa mọi object bên trong đều được sao chép độc lập.

JavaScript spread: Vì sao sửa bản sao lại đổi cả object gốc?

Bạn tạo bản nháp bằng {...profile}, sửa địa chỉ trong form rồi bấm Hủy. Nhưng hồ sơ gốc đã đổi trước khi lưu. Nguyên nhân thường nằm ở một hiểu nhầm: object mới ở tầng ngoài không có nghĩa mọi object bên trong đều được sao chép độc lập.

1. Tái hiện lỗi dùng chung tham chiếu

const profile = {
  name: 'Lan',
  address: { city: 'Hue' }
};
const draft = { ...profile };

draft.address.city = 'Da Nang';
console.log(profile.address.city); // Da Nang
console.log(draft === profile); // false
console.log(draft.address === profile.address); // true

Theo tài liệu spread của MDN, spread tạo bản sao nông. Ví dụ có hai object ngoài khác nhau nhưng cùng trỏ tới một address. Việc sửa city vì thế được nhìn thấy từ cả hai phía.

Đây là lỗi trong bộ nhớ của ví dụ, không tự chứng minh database đã bị ghi. Khi điều tra giao diện thật, cần phân biệt trạng thái đang hiển thị, dữ liệu trong store và request lưu đã gửi tới server.

2. Sao chép đúng nhánh cần sửa

Nếu chỉ đổi city trong một cấu trúc đã biết, tạo object mới trên đường dẫn đó. Chạy ví dụ này độc lập với đoạn trên để dữ liệu ban đầu vẫn là Hue:

const original = {
  name: 'Lan',
  address: { city: 'Hue' }
};
const updated = {
  ...original,
  address: { ...original.address, city: 'Da Nang' }
};

console.log(original.address.city); // Hue
console.log(updated.address.city); // Da Nang

Cách này diễn đạt chính xác trường cần đổi. Tuy nhiên, nếu address còn chứa object lồng sâu hơn, chúng vẫn có thể dùng chung tham chiếu. Không nên gọi đây là sao chép sâu toàn bộ hồ sơ.

3. Khi cần bản nháp độc lập

Với dữ liệu thuộc các kiểu được hỗ trợ, structuredClone tạo bản sao sâu. Ví dụ sau dùng object dữ liệu đơn giản và môi trường có hỗ trợ API này:

const source = {
  name: 'Lan',
  address: { city: 'Hue' }
};
const independentDraft = structuredClone(source);
independentDraft.address.city = 'Da Nang';

console.log(source.address.city); // Hue

Không phải mọi giá trị JavaScript đều sao chép được bằng cách này; dữ liệu chứa hàm có thể gây DataCloneError. Kiểm tra cấu trúc thật và môi trường chạy trước khi dùng. Đừng mặc định sao chép một instance của lớp sẽ bảo toàn đầy đủ hành vi của lớp.

4. Thiết kế luồng Lưu và Hủy rõ ràng

Một biểu mẫu chỉnh sửa nên xác định đâu là dữ liệu đã xác nhận và đâu là bản nháp. Hủy có thể bỏ bản nháp; Lưu mới thực hiện validation và gửi thay đổi. Nếu thao tác trên form sửa thẳng object đang dùng chung, nút Hủy chỉ đóng giao diện sẽ không hoàn tác được những thay đổi đó.

Cũng cần xử lý việc dữ liệu gốc được cập nhật từ nơi khác trong lúc người dùng đang sửa. Sao chép sâu không giải quyết xung đột phiên bản hoặc ghi đè thay đổi mới trên server. Đây là bài toán đồng bộ riêng cần có quy tắc sản phẩm.

5. Kiểm thử giá trị lẫn tham chiếu

  • Sửa trường lồng nhau trong bản nháp không làm đổi nguồn khi yêu cầu là độc lập.
  • Hủy rồi mở lại form hiển thị dữ liệu đã xác nhận.
  • Sửa phần tử object trong mảng được kiểm tra riêng; [...items] không tự sao chép sâu từng phần tử.
  • Trường không liên quan được giữ đúng sau cập nhật.
  • Dữ liệu không sao chép được có cách xử lý lỗi rõ ràng.
  • Đo trên dữ liệu thực tế trước khi sao chép toàn bộ cây trạng thái ở mỗi lần gõ.

Các output trong bài mô tả kết quả kỳ vọng của ví dụ, không phải báo cáo kiểm thử trình duyệt của sản phẩm. Chọn sao chép theo nhánh hoặc sao chép sâu dựa trên phạm vi chỉnh sửa sẽ giúp trạng thái ứng dụng dễ kiểm soát hơn.

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.