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

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

Trong thời gian dài, 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 một giá trị JavaScript. Nền tảng hiện đi kèm với structuredClone(), một hàm tích hợp để sao chép sâu.

Hỗ trợ trình duyệt

  • Chrome: 98.
  • Edge: 98.
  • Firefox: 94.
  • Safari: 15.4.

Nguồn

Việc sao chép một giá trị trong JavaScript hầu như luôn nhẹ, trái ngược với sâu. Điều đó có nghĩa là các thay đổi đối với các giá trị lồng 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 cách sử dụng toán tử truyền đố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 một thuộc tính trực tiếp 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 sâu sẽ ả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ử truyền dữ liệu. Hàm này sử dụng tên và giá trị thuộc tính, rồi gán từng thuộc tính 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 thuộc tính và giá trị. Các giá trị cũng được sao chép, nhưng giá trị JavaScript xử lý các giá trị nguyên gốc theo cách khác với các giá trị không nguyên gốc. 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. Có 7 loại dữ liệu gốc: chuỗi, số, bigint, boolean, không xác định, ký hiệu và rỗng.

MDN — Nguyên hàm

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

Sao chép sâu

Ngược lại 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 đối tượng, nhưng tự gọi lại khi tìm thấy tệp tham chiếu đến một đối tượng khác, đồng thời tạo một bản sao của đối tượng đó. Điều này có thể rất quan trọng để đảm bảo rằng 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 hoặc hiệu quả để tạo bản sao sâu của một giá trị trong JavaScript. Nhiều người đã dựa vào các thư viện của bên thứ ba như hàm cloneDeep() của Lodash. Có thể nói, giải pháp phổ biến nhất cho vấn đề này là một cuộc 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ụ thể là mẫu ở trên để thực hiện nhanh nhất có thể. Mặc dù nhanh nhưng phương thức này cũng có một số điểm hạn chế và rủi ro:

  • Cấu trúc dữ liệu đệ quy: JSON.stringify() sẽ gửi khi bạn cung cấp cho nó một 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 danh sách liên kết hoặc cây.
  • Các loại tích hợp sẵn: JSON.stringify() sẽ gửi nếu giá trị chứa các loại tích hợp sẵn khác của JS như Map, Set, Date, RegExp hoặc ArrayBuffer.
  • Hàm: JSON.stringify() sẽ âm thầm loại bỏ các hàm.

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

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

Điều đó đã thay đổi! Quy cách HTML đã được sửa đổi để hiển thị một hàm có tên là structuredClone() chạy chính xác thuật toán đó dưới dạng một phương tiệ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 bạn muốn tìm hiểu sâu hơn về thông tin chi tiết, hãy xem bài viết trên MDN.

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 (mặc dù không phải tất cả) điểm yếu 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 tuần hoàn, hỗ trợ nhiều loại dữ liệu tích hợp và thường mạnh mẽ hơn và thường nhanh hơn.

Tuy nhiên, tính năng này vẫn có một số hạn chế mà bạn có thể không lường trước được:

  • 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 có cấu trúc có thể sao chép, đáng chú ý nhất là Error và các nút DOM. Điều này sẽ khiến structuredClone() gửi.

Nếu bất kỳ giới hạn nào trong số này là vấn đề lớn đối với 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 thức triển khai tuỳ chỉnh của các thuật toán nhân bản 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 điểm chuẩn vi mô mới, nhưng tôi đã so sánh vào đầu năm 2018, trước khi structuredClone() được hiển thị. Trước đây, JSON.parse() là tuỳ chọn nhanh nhất cho các đối tượng rất nhỏ. Tôi cho rằng điều đó sẽ không thay đổi. Các kỹ thuật dựa vào tính năng sao chép có cấu trúc nhanh hơn (đáng kể) đối với các đối tượng lớn hơn. Xét thấy structuredClone() mới không có hao tổn do lạm dụng các API khác và mạnh mẽ hơn JSON.parse(), bạn nên sử dụng phương pháp này làm phương pháp mặc định để tạo bản sao sâu.

Kết luận

Nếu 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 một hàm có thể thao tác với một đối tượng mà không ảnh hưởng đến đối tượng ban đầu, thì bạn không cần phải tìm giải pháp thay thế hoặc thư viện nữa. Hệ sinh thái JS hiện có structuredClone(). Huzzah.