[JS Basics] Phân biệt Shallow Copy và Deep Copy trong JavaScript

Khi làm việc với JavaScript, đặc biệt là với các đối tượng (objects) và mảng (arrays), bạn sẽ sớm gặp phải một khái niệm cực kỳ quan trọng nhưng cũng dễ gây nhầm lẫn: sao chép dữ liệu. Việc bạn vô tình thay đổi một object mà không hề hay biết có thể dẫn đến những bug khó tìm và tốn hàng giờ để sửa.

Thủ phạm chính đằng sau những vấn đề này chính là sự khác biệt giữa Shallow Copy (Sao chép nông) và Deep Copy (Sao chép sâu).

Phân biệt Shallow Copy và Deep Copy trong JavaScript

Bài viết này sẽ giúp bạn làm chủ hai khái niệm này, từ đó viết code sạch hơn, dễ đoán hơn và không còn những lỗi "từ trên trời rơi xuống". Hãy cùng bắt đầu nhé! 🚀

Nền tảng cốt lõi: Kiểu dữ liệu Tham trị (Primitive) và Tham chiếu (Reference)

Trước khi đi vào shallow và deep copy, chúng ta cần nắm vững cách JavaScript xử lý các kiểu dữ liệu.

1. Kiểu dữ liệu Tham trị (Primitives)

Các kiểu dữ liệu nguyên thủy như string, number, boolean, null, undefined, symbol, bigint được lưu trữ trực tiếp giá trị của chúng.

Khi bạn gán một biến tham trị cho một biến khác, bạn đang sao chép giá trị. Chúng hoàn toàn độc lập với nhau.

let a = 100
let b = a // Sao chép giá trị của a cho b

console.log(b) // 100

// Thay đổi b không ảnh hưởng đến a
b = 200
console.log(a) // 100 (Không thay đổi)
console.log(b) // 200

2. Kiểu dữ liệu Tham chiếu (References)

Các kiểu dữ liệu như Object, Array, Function không lưu trữ giá trị trực tiếp. Thay vào đó, biến sẽ lưu một "tham chiếu" hoặc một "địa chỉ" trỏ đến vị trí của đối tượng đó trong bộ nhớ.

💡 Ví von dễ hiểu: Hãy tưởng tượng biến là một tờ giấy.

  • Với tham trị, tờ giấy ghi thẳng giá trị ("100").
  • Với tham chiếu, tờ giấy ghi địa chỉ của một ngôi nhà (0x123...), và dữ liệu thực sự nằm trong ngôi nhà đó.

Khi bạn gán một biến tham chiếu cho biến khác, bạn chỉ đang sao chép địa chỉ của ngôi nhà, chứ không phải xây một ngôi nhà mới. Cả hai biến giờ đây đều trỏ đến cùng một ngôi nhà.

let user1 = { name: 'Alice', age: 25 }
let user2 = user1 // Sao chép địa chỉ bộ nhớ của user1 cho user2

// Thay đổi dữ liệu thông qua user2
user2.name = 'Bob'

// user1 cũng bị thay đổi!
console.log(user1.name) // "Bob" 😱

Đây chính là nguồn gốc của mọi vấn đề và là lý do tại sao chúng ta cần các kỹ thuật sao chép.

Shallow Copy (Sao chép nông): Chỉ sao chép bề mặt 🌊

Shallow copy là tạo ra một object hoặc array mới, sau đó sao chép các thuộc tính từ object gốc sang object mới.

  • Nếu thuộc tính là kiểu tham trị, giá trị của nó sẽ được sao chép.
  • Nếu thuộc tính là kiểu tham chiếu (một object hoặc array lồng bên trong), thì chỉ có tham chiếu (địa chỉ) của nó được sao chép, chứ không phải chính đối tượng đó.

Nói cách khác, object mới và object gốc sẽ chia sẻ chung các object/array lồng nhau.

Các cách thực hiện Shallow Copy

1. Spread Syntax (...)

Đây là cách phổ biến và hiện đại nhất cho cả object và array.

const original = {
  name: 'Laptop',
  price: 1000,
  details: {
    // Đây là một object lồng nhau
    brand: 'Apple',
    year: 2023,
  },
}

const copy = { ...original }

// Thay đổi thuộc tính cấp 1 (tham trị)
copy.price = 1200
console.log(original.price) // 1000 (Không bị ảnh hưởng ✅)

// Thay đổi thuộc tính của object lồng nhau
copy.details.brand = 'Dell'
console.log(original.details.brand) // "Dell" (Bị ảnh hưởng 😱)

Như bạn thấy, originalcopy chia sẻ chung object details. Thay đổi ở một nơi sẽ ảnh hưởng đến nơi còn lại.

2. Object.assign()

Cách này hoạt động tương tự như Spread Syntax cho object.

const original = { name: 'Alice', address: { city: 'Hanoi' } }
const copy = Object.assign({}, original)

copy.address.city = 'Da Nang'
console.log(original.address.city) // "Da Nang" (Bị ảnh hưởng 😱)

3. Array.prototype.slice()Array.from()

Đây là các phương thức phổ biến để tạo shallow copy cho mảng.

const originalArray = [1, 2, [3, 4]]
const copiedArray = originalArray.slice()

// Thay đổi mảng lồng nhau
copiedArray[2][0] = 99
console.log(originalArray[2][0]) // 99 (Bị ảnh hưởng 😱)

Khi nào dùng Shallow Copy? Khi object/array của bạn không có các phần tử lồng nhau (flat object/array) hoặc khi bạn cố ý muốn chia sẻ các object lồng nhau để tiết kiệm bộ nhớ hoặc đồng bộ hóa dữ liệu.

Deep Copy (Sao chép sâu): Tạo ra một bản sao hoàn toàn độc lập 🧱

Deep copy là tạo ra một object/array mới và sao chép toàn bộ dữ liệu một cách đệ quy. Mọi object và array lồng nhau bên trong cũng sẽ được sao chép, tạo ra các bản sao mới hoàn toàn.

Kết quả là hai object/array hoàn toàn độc lập, không còn bất kỳ liên kết tham chiếu nào với nhau. Thay đổi ở bản sao sẽ không bao giờ ảnh hưởng đến bản gốc.

Các cách thực hiện Deep Copy

1. "Mánh" JSON: JSON.stringify()JSON.parse()

Đây là cách đơn giản và nhanh nhất để thực hiện deep copy cho các object chỉ chứa dữ liệu JSON hợp lệ (string, number, boolean, array, object).

const original = {
  name: 'Laptop',
  price: 1000,
  details: {
    brand: 'Apple',
    year: 2023,
  },
  buy: function () {
    console.log('Bought!')
  },
}

const deepCopy = JSON.parse(JSON.stringify(original))

// Thay đổi thuộc tính của object lồng nhau
deepCopy.details.brand = 'Dell'

console.log(original.details.brand) // "Apple" (Không bị ảnh hưởng ✅ Tuyệt vời!)

⚠️ Cảnh báo quan trọng về "mánh" JSON: Phương pháp này rất tiện lợi nhưng có những hạn chế nghiêm trọng:

  • Mất dữ liệu: Nó sẽ bỏ qua các thuộc tính có giá trị là function, undefined, Symbol.
  • Sai dữ liệu: Các đối tượng Date sẽ được chuyển thành chuỗi (string). NaN, Infinity sẽ trở thành null.
  • Không xử lý được các tham chiếu vòng tròn (circular references).

Chỉ nên dùng cách này khi bạn chắc chắn object của mình đơn giản và chỉ chứa dữ liệu tương thích với JSON.

2. API structuredClone() (Phương pháp hiện đại)

Đây là một API mới được tích hợp sẵn trong trình duyệt và Node.js, được thiết kế chuyên cho việc deep copy. Nó mạnh mẽ hơn "mánh" JSON rất nhiều.

const original = {
  name: 'Complex Object',
  date: new Date(),
  nested: { set: new Set([1, 2, 3]) },
}

const deepCopy = structuredClone(original)

deepCopy.nested.set.add(4)

console.log(original.nested.set) // Set(3) { 1, 2, 3 } (Không bị ảnh hưởng ✅)
console.log(deepCopy.nested.set) // Set(4) { 1, 2, 3, 4 }

structuredClone() có thể xử lý nhiều kiểu dữ liệu phức tạp hơn như Date, RegExp, Set, Map, ArrayBuffer,... Tuy nhiên, nó vẫn không thể sao chép function và sẽ báo lỗi nếu gặp tham chiếu vòng tròn.

3. Sử dụng thư viện ngoài (Cách đáng tin cậy nhất)

Đối với các ứng dụng phức tạp, cách tốt nhất và an toàn nhất là sử dụng các hàm đã được kiểm chứng từ các thư viện nổi tiếng như Lodash.

Hàm cloneDeep của Lodash có thể xử lý hầu hết mọi trường hợp, bao gồm cả function, các kiểu dữ liệu phức tạp và tham chiếu vòng tròn.

// Cần cài đặt thư viện lodash: npm install lodash
import _ from 'lodash'

const original = {
  name: 'Super Object',
  run: () => console.log('Running!'),
  details: { brand: 'BrandX' },
}

const deepCopy = _.cloneDeep(original)

deepCopy.details.brand = 'BrandY'

console.log(original.details.brand) // "BrandX" (Không bị ảnh hưởng ✅)
console.log(typeof original.run) // "function"
console.log(typeof deepCopy.run) // "function" (Hàm cũng được sao chép ✅)

Kết luận: Chọn đúng công cụ cho đúng việc

Việc hiểu rõ sự khác biệt giữa Shallow CopyDeep Copy không phải là một kiến thức lý thuyết suông, mà là một kỹ năng thiết yếu để viết code JavaScript vững chắc và dễ bảo trì.

Tiêu chíShallow CopyDeep Copy
Định nghĩaTạo object/array mới, sao chép các thuộc tính cấp 1.Tạo object/array mới, sao chép đệ quy tất cả các thuộc tính.
Object lồng nhauChia sẻ chung tham chiếu với bản gốc.Được sao chép thành các object mới, hoàn toàn độc lập.
Sự độc lậpPhụ thuộc một phần (ở các cấp sâu).Hoàn toàn độc lập.
Tốc độNhanh hơn.Chậm hơn vì phải duyệt qua toàn bộ cấu trúc.
Các phương pháp..., Object.assign, slice()structuredClone(), JSON.parse/stringify, _.cloneDeep() (Lodash)
Khi nào dùngDữ liệu phẳng, không có lồng nhau. Cần hiệu năng.Dữ liệu có cấu trúc phức tạp. Cần sự độc lập tuyệt đối.

Shallow Copy vs Deep Copy

  • Shallow Copy nhanh và hiệu quả cho các cấu trúc dữ liệu đơn giản. Hãy dùng Spread (...) làm lựa chọn mặc định.
  • Deep Copy là cứu cánh khi bạn cần đảm bảo sự độc lập hoàn toàn của dữ liệu, tránh các hiệu ứng phụ không mong muốn. structuredClone() là lựa chọn hiện đại và mạnh mẽ, trong khi Lodash _.cloneDeep() là giải pháp toàn diện nhất cho các trường hợp phức tạp.

Hy vọng qua bài viết này, bạn đã có một cái nhìn thật rõ ràng và sâu sắc về hai khái niệm quan trọng này. Giờ đây, bạn đã có thể tự tin hơn trong việc quản lý và thao tác với dữ liệu trong các dự án của mình!

Bài viết liên quan

[JS Basics] Cách thiết lập môi trường chạy mã JavaScript

Bạn là người mới học lập trình và muốn bắt đầu với JavaScript? Bài viết này sẽ hướng dẫn bạn từng bước cách thiết lập môi trường chạy JavaScript trên máy tính một cách dễ dàng và nhanh chóng nhất.

[JS Basics] ES6+ Features: Những điều cần biết để viết code JavaScript hiện đại

Bài viết này là cẩm nang chi tiết nhất về JavaScript Events. Tìm hiểu cách lắng nghe và phản ứng với các hành động của người dùng trên website, giúp bạn xây dựng ứng dụng web tương tác và mượt mà hơn.

[JS Basics] Lập trình bất đồng bộ trong JavaScript: Callback, Promise, Async/Await

Tìm hiểu lập trình bất đồng bộ trong JavaScript với các ví dụ thực tế về Async/Await, Promise, và Callback. Bài viết giúp bạn nắm vững kiến thức, tránh lỗi thường gặp và tối ưu hiệu suất code.

[JS Basics] Câu lệnh điều kiện trong JavaScript: Ví dụ & cách dùng hiệu quả

Nắm vững các câu lệnh điều kiện phổ biến trong JavaScript như if, if-else, switch. Hướng dẫn chi tiết từ cú pháp đến cách áp dụng trong các dự án thực tế.