Lập trình · 19/10/2026

JavaScript structuredClone: sao chép sâu và chuyển quyền sở hữu ArrayBuffer

Một tác vụ cần bản sao dữ liệu độc lập; tác vụ khác cần nhận hẳn một vùng nhớ để xử lý. Hai yêu cầu này dẫn tới hai hành vi khác nhau của structuredClone: sao chép giá trị hoặc chuyển quyền sở hữu tài nguyên hỗ trợ transfer. Nhầm chúng có thể khiến code tiếp tục đọc một buffer đã bị detach.

JavaScript structuredClone: sao chép sâu và chuyển quyền sở hữu ArrayBuffer

Một tác vụ cần bản sao dữ liệu độc lập; tác vụ khác cần nhận hẳn một vùng nhớ để xử lý. Hai yêu cầu này dẫn tới hai hành vi khác nhau của structuredClone: sao chép giá trị hoặc chuyển quyền sở hữu tài nguyên hỗ trợ transfer. Nhầm chúng có thể khiến code tiếp tục đọc một buffer đã bị detach.

Bài viết dùng các ví dụ JavaScript độc lập, chạy trong môi trường có global structuredClone. Nếu vấn đề là spread vẫn dùng chung object lồng nhau, xem thêm bài về sao chép nông; ở đây ta đi sâu vào kiểu dữ liệu và vòng đời tài nguyên.

1. Giữ kiểu dữ liệu khi tạo bản sao

const source = {
  createdAt: new Date('2026-01-01T00:00:00Z'),
  counters: new Map([['views', 12]]),
  labels: new Set(['draft'])
};
const copy = structuredClone(source);
copy.counters.set('views', 20);

console.log(copy.createdAt instanceof Date); // true
console.log(copy.counters instanceof Map);   // true
console.log(copy.labels instanceof Set);    // true
console.log(source.counters.get('views'));   // 12

Ví dụ cho thấy bản sao Map có thể đổi mà không đổi nguồn. Khi chọn cách copy, hãy viết rõ kiểu nào phải được giữ: một Date dùng được getTime khác với chuỗi ngày tháng. JSON là định dạng trao đổi dữ liệu; JSON.parse(JSON.stringify(value)) không phải phép thay thế tương đương cho mọi giá trị JavaScript.

2. Tham chiếu vòng và quan hệ dùng chung

const shared = { enabled: true };
const graph = { left: shared, right: shared };
graph.self = graph;

const cloned = structuredClone(graph);
console.log(cloned !== graph);             // true
console.log(cloned.self === cloned);       // true
console.log(cloned.left === cloned.right); // true
console.log(cloned.left === shared);       // false

Sao chép sâu không có nghĩa mỗi đường dẫn sẽ tạo một object riêng. Trong bản sao, left và right vẫn cùng trỏ tới một object đã được clone. Vì vậy, sửa cloned.left.enabled cũng được nhìn thấy qua cloned.right.enabled, nhưng không sửa shared của nguồn.

Đây là điểm cần kiểm tra khi snapshot một đồ thị dữ liệu: quan hệ giữa các node cũng là một phần của ý nghĩa, bên cạnh giá trị từng trường.

3. Clone buffer giữ nguồn dùng được

const original = new Uint8Array([10, 20, 30]);
const copied = structuredClone(original);
copied[0] = 99;

console.log(original[0]);         // 10
console.log(original.byteLength); // 3
console.log(copied[0]);           // 99

Ở đây không truyền tùy chọn transfer. Bộ nhớ của bản sao độc lập với nguồn trong ví dụ. Cách này phù hợp nếu cả hai phía vẫn cần dữ liệu, nhưng cần tính đến kích thước dữ liệu và chi phí sao chép khi thiết kế luồng xử lý.

4. Transfer khiến buffer nguồn bị detach

const bytes = new Uint8Array([10, 20, 30]);
const moved = structuredClone(bytes, {
  transfer: [bytes.buffer]
});

console.log(bytes.byteLength); // 0
console.log(moved.byteLength); // 3
console.log(moved[0]);         // 10

Danh sách transfer chứa ArrayBuffer bên dưới typed array. Sau thao tác, dữ liệu nằm ở phía kết quả và buffer nguồn đã bị detach. Không tiếp tục coi bytes là nơi lưu dữ liệu hợp lệ; các view khác cùng dùng buffer nguồn cũng chịu ảnh hưởng.

Transfer list không phải nơi đính kèm dữ liệu: tài nguyên muốn nhận vẫn phải nằm trong giá trị được truyền vào. Ví dụ dùng typed array làm giá trị nên buffer của nó có mặt trong đồ thị kết quả.

Trong ứng dụng xử lý file, hãy xác định nơi sở hữu buffer sau mỗi bước. Khi giao việc cho Worker qua postMessage, đọc contract transfer của API đó; ví dụ này chỉ kiểm tra structuredClone trong cùng tiến trình, không đo tốc độ truyền giữa Worker.

5. Hàm và instance của lớp cần contract riêng

try {
  structuredClone({ run: () => 42 });
} catch (error) {
  console.log(error.name); // DataCloneError
}

Đừng bọc thao tác bằng catch rồi âm thầm trả lại object gốc: người gọi tưởng có bản sao độc lập nhưng thực tế lại chia sẻ dữ liệu. Có thể tách callback khỏi payload và định nghĩa object dữ liệu thuần để giao tiếp giữa các thành phần.

Không dùng clone như cơ chế khôi phục hành vi của class hay kiểm tra dữ liệu đầu vào. Nếu một đối tượng nghiệp vụ có phương thức và quy tắc bất biến, thiết kế cách xuất dữ liệu và dựng lại đối tượng một cách tường minh.

6. Checklist trước khi đưa vào luồng thật

  • Kiểm tra structuredClone có trong runtime mục tiêu.
  • Liệt kê kiểu dữ liệu cần bảo toàn và giá trị không được hỗ trợ.
  • Kiểm tra cả giá trị, danh tính object và quan hệ tham chiếu.
  • Chọn clone khi nguồn còn phải dùng; chọn transfer khi vòng đời cho phép bàn giao.
  • Kiểm tra tất cả view dùng chung một buffer trước khi transfer.
  • Đo thời gian và bộ nhớ với payload thực tế, tránh clone toàn bộ trạng thái ở mỗi lần gõ.

Các đoạn code đã được chạy bằng Node.js local, gồm Date/Map/Set, tham chiếu vòng, clone typed array, detach khi transfer và DataCloneError. Chưa kiểm thử Worker hay đo hiệu năng trình duyệt.

Tài liệu đối chiếu: structuredClone() và structured clone algorithm. Khi chọn API, xác định rõ cần giữ dữ liệu ở cả hai phía hay bàn giao tài nguyên cho bước tiếp theo.

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.