Deep Copy vs Shallow Copy: Hiểu Đúng Để Không Bao Giờ Gặp Bug Dữ Liệu
Bạn đã từng thay đổi một Object "mới" nhưng bất ngờ thấy Object gốc cũng bị thay đổi theo? Đây là một trong những bug khó chịu và phổ biến nhất trong JavaScript — xuất phát từ việc không phân biệt rõ giữa Shallow Copy (sao chép nông) và Deep Copy (sao chép sâu). Hiểu sai cách dữ liệu được lưu trong bộ nhớ là nguyên nhân dẫn đến vô số lỗi logic nghiêm trọng, đặc biệt trong các ứng dụng quản lý trạng thái phức tạp như React hay Redux. Bài viết này giúp bạn hiểu tận gốc và chọn đúng phương pháp.

Trung Vũ Hoàng
Tác giả
1. Giá Trị vs Tham Chiếu — Nguồn Gốc Mọi Rắc Rối
JavaScript xử lý hai nhóm kiểu dữ liệu theo cách hoàn toàn khác nhau:
Kiểu nguyên thủy (Primitive):
string,number,boolean... — khi gána = b, một bản sao giá trị độc lập được tạo ra. Thay đổiakhông ảnh hưởng đếnb.Kiểu tham chiếu (Reference):
Object,Array— khi gánobj2 = obj1, JavaScript không tạo ra Object mới. Nó chỉ tạo thêm một "đường dẫn" (reference) cùng trỏ về một vùng nhớ duy nhất trên Heap. Đây chính là gốc rễ của mọi sự nhầm lẫn.
2. Shallow Copy — Sao Chép Bề Mặt, Nguy Hiểm Bên Trong
Shallow Copy tạo ra một Object mới, nhưng chỉ sao chép các thuộc tính ở tầng bề mặt:
Thuộc tính là kiểu nguyên thủy → sao chép giá trị, an toàn.
Thuộc tính là Object/Array con → chỉ sao chép địa chỉ tham chiếu, vẫn dùng chung bộ nhớ với bản gốc.
Các cú pháp phổ biến để Shallow Copy: { ...obj } (Spread), Object.assign({}, obj).
const original = { name: "JS", meta: { version: 1 } };
const copy = { ...original };
copy.name = "Python"; // OK — original.name không đổi
copy.meta.version = 99; // NGUY HIỂM — original.meta.version cũng thành 99!Shallow copy rất hữu ích khi Object "phẳng" (không có lồng nhau). Nhưng với JSON đa tầng trả về từ API, nó là "quả bom nổ chậm".
3. Deep Copy — Độc Lập Hoàn Toàn, Không Dây Liên Kết
Deep Copy đi sâu vào từng tầng của Object, tạo ra bản sao hoàn toàn mới ở mọi cấp độ — không còn bất kỳ liên kết bộ nhớ nào với bản gốc. Thay đổi bản sao sẽ không bao giờ ảnh hưởng đến bản gốc, bất kể cấu trúc lồng sâu thế nào.
Deep Copy thường dùng trong: lưu trạng thái (state snapshot), tính năng Undo/Redo, xử lý dữ liệu yêu cầu tính bất biến cao (Immutability).
4. Các Phương Pháp Thực Hiện Deep Copy
JSON.parse(JSON.stringify(obj))
const deepCopy = JSON.parse(JSON.stringify(original));Nhanh và đơn giản — nhưng mất dữ liệu với các kiểu đặc biệt: Date, undefined, Function, RegExp. Chỉ dùng khi chắc chắn Object thuần JSON.
structuredClone() — API Hiện Đại Không Cần Thư Viện
const deepCopy = structuredClone(original);Có sẵn trên các trình duyệt hiện đại và Node.js 17+. Nhanh hơn JSON, xử lý được nhiều kiểu dữ liệu hơn (Date, Map, Set...). Đây là lựa chọn tốt nhất cho hầu hết trường hợp.
Lodash _.cloneDeep()
import cloneDeep from 'lodash/cloneDeep';
const deepCopy = cloneDeep(original);Giải pháp "vàng" trong môi trường dự án lớn — xử lý được cả trường hợp tham chiếu vòng (circular reference) và mọi kiểu dữ liệu phức tạp. Phù hợp khi dự án đã có sẵn Lodash.
5. Khi Nào Dùng Shallow, Khi Nào Dùng Deep?
Dùng Shallow Copy khi: Object "phẳng" (không có lồng nhau), hoặc bạn chủ động muốn các object con được chia sẻ để tiết kiệm bộ nhớ.
Dùng Deep Copy khi: Xử lý dữ liệu đa tầng, cần đảm bảo bất biến (Immutability) tuyệt đối, hoặc làm việc với tính năng Undo/Redo.
6. Lưu Ý Về Hiệu Năng
Deep copy toàn bộ một Object lớn hàng nghìn phần tử có thể khiến ứng dụng bị khựng — vì engine phải duyệt qua toàn bộ cây dữ liệu.
Mẹo tối ưu: Khi chỉ cần thay đổi một phần nhỏ của Object lồng nhau, hãy dùng Immutable Update Pattern (phổ biến trong Redux Toolkit) — chỉ tạo mới tầng đang thay đổi, các tầng khác giữ nguyên tham chiếu cũ:
const newState = {
...state,
user: {
...state.user,
name: "Updated" // Chỉ tầng user được tạo mới
}
};7. Tổng Kết
Phân biệt Deep copy vs Shallow copy không chỉ là vấn đề cú pháp — đây là tư duy về kiến trúc bộ nhớ. Một developer giỏi biết chính xác dòng code của mình đang tác động đến vùng nhớ nào:
Object phẳng + hiệu năng ưu tiên → Spread / Object.assign (Shallow).
Object đa tầng + cần độc lập → structuredClone (Deep, không cần thư viện).
Dự án lớn, dữ liệu phức tạp → Lodash _.cloneDeep (Deep, toàn diện nhất).
Câu hỏi thường gặp
Bài viết liên quan

Zustand Async: 5 Cách Xử Lý Bất Đồng Bộ Hiệu Quả Trong React
Mọi ứng dụng React thực tế đều phải giao tiếp với API bất đồng bộ. Nếu quản lý kém, ứng dụng dễ rơi vào các vấn đề như UI bị đơ, race condition, rò rỉ bộ nhớ, hay hiển thị dữ liệu cũ. Zustand giải quyết bài toán này với một cú pháp cực kỳ đơn giản — định nghĩa async actions trực tiếp trong store, không cần middleware phức tạp như Redux Thunk hay Saga.

Promise Nâng Cao: all, allSettled, race, any — Khi Nào Dùng Cái Nào?
Nếu bạn chỉ dùng async/await tuần tự, ứng dụng của bạn đang bỏ phí tiềm năng hiệu suất — mỗi request phải chờ request trước xong mới bắt đầu. Các phương thức tĩnh của Promise (all, allSettled, race, any) cho phép bạn điều phối nhiều tác vụ bất đồng bộ song song theo chiến lược phù hợp từng bài toán.

Zustand Async: 5 Cách Xử Lý Bất Đồng Bộ Hiệu Quả Trong React
Mọi ứng dụng React thực tế đều phải giao tiếp với API bất đồng bộ. Nếu quản lý kém, ứng dụng dễ rơi vào các vấn đề như UI bị đơ, race condition, rò rỉ bộ nhớ, hay hiển thị dữ liệu cũ. Zustand giải quyết bài toán này với một cú pháp cực kỳ đơn giản — định nghĩa async actions trực tiếp trong store, không cần middleware phức tạp như Redux Thunk hay Saga.