Shallow copy và Deep copy trong JavaScript

Lời mở đầu

Copy là một phần quan trọng trong quá trình làm việc với JavaScript, đặc biệt khi làm việc với đối tượng và mảng. Hiểu rõ cách thức copy đối tượng và mảng là một kỹ năng cần thiết để đảm bảo tính nhất quán và an toàn của dữ liệu trong ứng dụng. Trong bài viết này, chúng ta sẽ cùng tìm hiểu về sự khác biệt giữa Shallow copy và Deep copy trong JavaScript.

Khi làm việc với JavaScript, ta thường muốn tạo ra bản sao của một đối tượng hoặc mảng để làm việc với dữ liệu mà không ảnh hưởng tới đối tượng gốc. Điều này đặc biệt quan trọng khi làm việc với React hoặc các thư viện quản lý trạng thái, nơi việc sao chép dữ liệu là một yêu cầu phổ biến.

Trong bài viết này, chúng ta sẽ tìm hiểu về các phương pháp copy khác nhau và cách sử dụng chúng để thực hiện sao chép an toàn cho đối tượng và mảng trong JavaScript.

Các kiểu dữ liệu nguyên thủy

  • Boolean
  • Null
  • Undefined
  • String
  • Number
  • Symbol

Các kiểu dữ liệu nguyên thuỷ là bất biến (chúng không có phương thức hoặc thuộc tính có thể thay đổi chúng).

Các đối tượng là tất cả các phần tử JavaScript khác, chẳng hạn như đối tượng bằng chữ, mảng, ngày tháng, v.v. và chúng có thể thay đổi được . Điều đó có nghĩa là một số phương pháp có thể thay đổi chúng.

Ví dụ chứng minh giá trị nguyên thủy là không thể thay đổi:

 

Khi cố gắng thay đổi chữ cái đầu tiên của chuỗi từ “B” thành “C”, ta thấy biến name vẫn không thay đổi.

Và một ví dụ chứng minh các đối tượng có thể thay đổi:

Ở đây chúng ta có thể thấy rằng việc thay đổi phần tử đầu tiên của mảng dẫn đến thay đổi biến name.

Để tạo 1 bản sao của biến nguyên thuỷ, rất đơn giản:

Tạo 1 biến cloneName và cho nó “=” với biến name là đủ để tạo một bản sao của nó và sẽ không gây ra bất kỳ vấn đề nào vì các biến nguyên thủy là bất biến. Tuy nhiên, phương pháp này nên tránh để sao chép các đối tượng. Hãy tìm hiểu lý do tại sao.

Từ ví dụ trên, chúng ta có thể thấy rằng việc gán một biến đối tượng cho một biến đối tượng khác sẽ cho chúng ta một biến mới có cùng giá trị. Các bạn nghĩ cách này là đúng?. Nhưng thực tế không phải vậy. Và đây là lý do tại sao:

Ồ, các bạn thấy điều không đúng rồi chứ? Áp dụng các thay đổi cho biến cloneUserObj đã biến đổi luông cả biến userObj ban đầu, nhưng bằng cách nào? Để hiểu điều đó, trước tiên chúng ta cần tìm hiểu các nguyên tắc cơ bản của khoa học máy tính.

Trong các ngôn ngữ lập trình có hai nơi để lưu trữ dữ liệu trong bộ nhớ máy tính: stack và heap.

  • Stack là bộ nhớ lưu trữ tạm thời để lưu trữ các biến nguyên thủy cục bộ và các tham chiếu đến các đối tượng.
  • Heap lưu trữ các biến toàn cục. Các giá trị đối tượng được lưu trữ trên heapstack chỉ chứa các tham chiếu đến chúng (con trỏ).

Như chúng ta đã tìm hiểu ở phần đầu, các giá trị nguyên thủy được “truyền theo giá trị”. Theo hình minh hoạ bên trên, chúng ta có thể thấy sau khi tạo hai chuỗi name và cloneName, chuỗi cloneName được gán giá trị của chuỗi name cho nó, hai giá trị riêng biệt và không có mối liên hệ nào giữa chúng. Lúc này 2 biến đó được lưu trữ trên stack.

Việc tạo các đối tượng theo cùng một cách dẫn đến việc tạo hai con trỏ (tham chiếu) trong stack và chỉ một giá trị trong heap. Khi chúng ta tạo một biến đối tượng, bộ nhớ sẽ lưu “địa chỉ” vào vị trí thực của giá trị chứ không phải chính giá trị đó. Trong ví dụ này, chúng ta nhận được hai tham chiếu trỏ đến cùng một giá trị và đây là lý do tại sao việc thay đổi một trong các đối tượng sẽ luôn thay đổi cả hai đối tượng. Và đây cũng là lý do tại sao chúng ta cần các bản sao.

Chúng ta có hai loại bản sao đối tượng trong JavaScript, đó là shallow và deep. Tóm lại, các bản sao Shallow được sử dụng cho các đối tượng “phẳng” (chỉ chứa các giá trị nguyên thuỷ) và các bản sao Deep được sử dụng cho các đối tượng “lồng nhau” (nested object, nested array).

Để tạo một bản sao Shallow, chúng ta có thể sử dụng 1 trong các phương pháp sau:

  • Cú pháp Spread […] {…}
  • Object.assign()

Và để tạo một bản sao Deep , chúng ta có thể sử dụng:

  • JSON.parse(JSON.stringify())
  • Thư viện bên thứ ba như Lodash

Bản sao Shallow

1. Cú pháp Spread […] {…}

Ví dụ:

2. Object.assign()

Ví dụ:

Các bạn có thể thấy, khi chúng ta dùng bản sao Shallow, nếu thay đổi biến bản sao thì lúc đó biến gốc cũng sẽ thay đổi theo. Chắc hẳn không ai trong chúng ta muốn điều này phải không? Cùng tìm hiểu về bản sao Deep nhé!

Bản sao Deep

1. JSON.parse(JSON.stringify())

Ví dụ:

Ồ, có vẻ như đây là cách đơn giản và hiệu quả để tạo một bản sao Deep nhỉ? Sau khi thay đổi biến bản sao thì biến gốc không hề thay đổi. Nhưng hãy xem cách dùng này có nhược điểm gì nhé

Các bạn thấy đấy một số params đã mất đi không một lời từ biệt, chính vì vậy hãy cẩn thận những gì JavaScript mang lại cho chúng ta. Điều đó không đùa được với một JavaScript Developer chuyên nghiệp.

2. Sử dụng _.cloneDeep của thư viện Lodash

Sử dụng cloneDeep của thư viện lodash có thể xử lý được trường hợp khi tạo bản sao các nested object, nested array giống như JSON.parse(JSON.stringify), đồng thời nó cũng khắc phục được nhược điểm không bị mất các params như NaN, undefined, Infinity,…

Lời kết

Tóm lại, bản sao Shallow chia sẻ tham chiếu với các đối tượng nguồn, chúng phù hợp hơn với các đối tượng không lồng nhau và thường được sử dụng trong lập trình với JavaScript. Nhưng nếu chúng ta phải xử lý các đối tượng lồng nhau hoặc chúng ta không biết giá trị nào chúng ta nhận được (ví dụ: từ API), chúng ta nên sử dụng các bản sao Deep để thay thế, vì chúng không chia sẻ bất kỳ tham chiếu nào với các đối tượng gốc.

LongND1

Comments

Let’s make a great impact together

Be a part of BraveBits to unlock your full potential and be proud of the impact you make.