Sao chép sâu trong JavaScript bằng cấu trúcClone

Nền tảng này hiện đi kèm với cấu trúcClone(), một hàm tích hợp để sao chép sâu.

Trong thời gian dài nhất, bạn phải sử dụng các giải pháp và thư viện để tạo bản sao sâu của giá trị JavaScript. Nền tảng này hiện đi kèm với structuredClone(), một chức năng tích hợp sẵn để sao chép sâu.

Hỗ trợ trình duyệt

  • 98
  • 98
  • 94
  • 15,4

Nguồn

Bản sao nông

Việc sao chép một giá trị trong JavaScript hầu như luôn là shallow, trái ngược với deep. Điều đó có nghĩa là những thay đổi đối với các giá trị lồng nhau sâu sẽ hiển thị trong bản sao cũng như bản gốc.

Một cách để tạo bản sao nông trong JavaScript bằng toán tử dàn trải đối tượng ...:

const myOriginal = {
  someProp: "with a string value",
  anotherProp: {
    withAnotherProp: 1,
    andAnotherProp: true
  }
};

const myShallowCopy = {...myOriginal};

Việc thêm hoặc thay đổi thuộc tính ngay trên bản sao nông sẽ chỉ ảnh hưởng đến bản sao chứ không ảnh hưởng đến bản gốc:

myShallowCopy.aNewProp = "a new value";
console.log(myOriginal.aNewProp)
// ^ logs `undefined`

Tuy nhiên, việc thêm hoặc thay đổi một thuộc tính lồng ghép sâu ảnh hưởng đến cả bản sao và bản gốc:

myShallowCopy.anotherProp.aNewProp = "a new value";
console.log(myOriginal.anotherProp.aNewProp) 
// ^ logs `a new value`

Biểu thức {...myOriginal} lặp lại các thuộc tính (có thể liệt kê) của myOriginal bằng cách sử dụng Toán tử trải rộng. Phương thức này sử dụng tên và giá trị thuộc tính, đồng thời gán từng thuộc tính một cho một đối tượng trống, mới tạo. Do đó, đối tượng thu được có hình dạng giống hệt nhau, nhưng có bản sao riêng của danh sách các thuộc tính và giá trị. Các giá trị cũng được sao chép, nhưng các giá trị được gọi là giá trị gốc được JavaScript xử lý khác với các giá trị không phải là giá trị gốc. Cách trích dẫn MDN:

Trong JavaScript, dữ liệu gốc (giá trị gốc, kiểu dữ liệu gốc) là dữ liệu không phải là đối tượng và không có phương thức nào. Có 7 kiểu dữ liệu gốc: chuỗi, số, Bigint, boolean, không xác định, ký hiệu và rỗng.

MDN – Nguyên gốc

Các giá trị không gốc được xử lý dưới dạng references, nghĩa là hành động sao chép giá trị thực sự chỉ là sao chép tham chiếu vào cùng một đối tượng cơ bản, dẫn đến hành vi sao chép nông.

Bản sao sâu

Đối diện với bản sao nông là bản sao sâu. Thuật toán sao chép sâu cũng sao chép từng thuộc tính của một đối tượng nhưng sẽ gọi chính nó theo cách đệ quy khi tìm thấy tham chiếu đến một đối tượng khác, tạo ra bản sao của đối tượng đó. Việc này có thể rất quan trọng để đảm bảo hai đoạn mã không vô tình chia sẻ một đối tượng và vô tình thao túng trạng thái của nhau.

Trước đây, không có cách nào dễ dàng hay hiệu quả để tạo bản sao sâu của một giá trị trong JavaScript. Nhiều người sử dụng thư viện của bên thứ ba như hàm cloneDeep() của Lodash. Có thể cho rằng giải pháp phổ biến nhất cho vấn đề này là tấn công dựa trên JSON:

const myDeepCopy = JSON.parse(JSON.stringify(myOriginal));

Trên thực tế, đây là một giải pháp phổ biến đến mức V8 đã tối ưu hoá mạnh mẽ JSON.parse() và đặc biệt là mẫu ở trên để giúp thử nghiệm nhanh nhất có thể. Và mặc dù tốc độ nhanh, nhưng nó cũng đi kèm với một số thiếu sót và dây nối:

  • Cấu trúc dữ liệu đệ quy: JSON.stringify() sẽ gửi khi bạn cung cấp cấu trúc dữ liệu đệ quy. Điều này có thể xảy ra khá dễ dàng khi làm việc với các danh sách hoặc cây được liên kết.
  • Các loại tích hợp sẵn: JSON.stringify() sẽ gửi nếu giá trị chứa các JS tích hợp khác như Map, Set, Date, RegExp hoặc ArrayBuffer.
  • Hàm: JSON.stringify() sẽ tự động loại bỏ các hàm.

Sao chép có cấu trúc

Nền tảng này đã cần đến khả năng tạo các bản sao sâu của các giá trị JavaScript ở một vài nơi: Việc lưu trữ giá trị JS trong IndexedDB yêu cầu một số dạng chuyển đổi tuần tự để có thể lưu trữ trên ổ đĩa và sau đó giải tuần tự để khôi phục giá trị JS. Tương tự, việc gửi thông báo đến một WebWorker qua postMessage() yêu cầu chuyển giá trị JS từ vùng JS này sang vùng JS khác. Thuật toán được sử dụng cho việc này có tên là "Bản sao có cấu trúc" và cho đến gần đây, vẫn không thể dễ dàng truy cập được đối với các nhà phát triển.

Giờ đây, thông tin này đã thay đổi! Sửa đổi thông số kỹ thuật HTML để hiển thị một hàm có tên là structuredClone(). Hàm này chạy chính xác thuật toán đó để nhà phát triển dễ dàng tạo bản sao sâu của các giá trị JavaScript.

const myDeepCopy = structuredClone(myOriginal);

Vậy là xong! Đó là toàn bộ API. Nếu muốn tìm hiểu sâu hơn, bạn có thể xem bài viết này về MMDN.

Tính năng và giới hạn

Tính năng sao chép có cấu trúc giải quyết nhiều thiếu sót (mặc dù không phải tất cả) của kỹ thuật JSON.stringify(). Tính năng sao chép có cấu trúc có thể xử lý các cấu trúc dữ liệu theo chu kỳ, hỗ trợ nhiều loại dữ liệu tích hợp sẵn, đồng thời thường mạnh mẽ hơn và thường nhanh hơn.

Tuy nhiên, việc này vẫn còn một số hạn chế có thể khiến bạn không thấy rõ:

  • Nguyên mẫu: Nếu sử dụng structuredClone() với một thực thể lớp, bạn sẽ nhận được một đối tượng thuần tuý làm giá trị trả về, vì tính năng sao chép có cấu trúc sẽ loại bỏ chuỗi nguyên mẫu của đối tượng đó.
  • Hàm: Nếu đối tượng của bạn chứa các hàm, structuredClone() sẽ gửi một ngoại lệ DataCloneError.
  • Không thể sao chép: Một số giá trị không thể sao chép có cấu trúc, đáng chú ý nhất là Error và nút DOM. Việc này sẽ khiến structuredClone() gửi.

Nếu bất kỳ giới hạn nào trong số này có thể là yếu tố quyết định trường hợp sử dụng của bạn, thì các thư viện như Lodash vẫn cung cấp các phương pháp triển khai tuỳ chỉnh cho các thuật toán sao chép sâu khác có thể phù hợp hoặc không phù hợp với trường hợp sử dụng của bạn.

Hiệu suất

Mặc dù tôi chưa thực hiện phép so sánh mới về điểm chuẩn vi mô, nhưng tôi đã làm một phép so sánh vào đầu năm 2018, trước khi structuredClone() được công bố. Trước đó, JSON.parse() là lựa chọn nhanh nhất cho các đối tượng rất nhỏ. Tôi hy vọng chiến lược đó sẽ không thay đổi. Các kỹ thuật sử dụng tính năng sao chép có cấu trúc hoạt động nhanh hơn (đáng kể) đối với các đối tượng lớn hơn. Xét rằng structuredClone() mới không gây ra hao tổn tài nguyên trong việc lạm dụng các API khác và hoạt động hiệu quả hơn JSON.parse(), nên bạn nên đặt đây làm phương pháp mặc định để tạo bản sao sâu.

Kết luận

Nếu bạn cần tạo bản sao sâu của một giá trị trong JS – có thể là do bạn sử dụng cấu trúc dữ liệu không thể thay đổi hoặc bạn muốn đảm bảo rằng một hàm có thể thao tác với đối tượng mà không ảnh hưởng đến giá trị gốc – bạn không cần phải tiếp cận để tìm giải pháp hoặc thư viện nữa. Hệ sinh thái JS hiện có structuredClone(). Tuyệt vời.