structuredClone بدل حيلة JSON: نسخة عميقة صح في JavaScript

ليه JSON.parse(JSON.stringify()) بيبوّظ Date و Map والـ circular references، وإزاي structuredClone() بيعمل deep copy صح، وإيه اللي مش بينسخه.

لو لسه بتعمل نسخة عميقة (deep copy) من object بـ JSON.parse(JSON.stringify(obj))، فالحيلة دي شغالة لحد ما الداتا يبقى فيها Date أو Map أو reference دايري، وساعتها بتبوّظ من غير ما تحس. الحل موجود built-in من سنين: structuredClone().

المشكلة في حيلة الـ JSON

الـ JSON مش مصمم يشيل كل أنواع JavaScript، فلما بتعدّي الداتا من خلاله بيحصل الآتي:

  • Date بيرجع string، مش Date تاني.
  • Map و Set بيرجعوا {} فاضيين.
  • القيم اللي بـ undefined والـ functions بتختفي من الـ object.
  • لو فيه circular reference بيرمي TypeError على طول.
const order = { createdAt: new Date(), items: new Map([["sku-1", 2]]) };
const bad = JSON.parse(JSON.stringify(order));
typeof bad.createdAt; // "string"
bad.items;            // {}

الحل: structuredClone

structuredClone(value) بيعمل deep copy باستخدام الـ structured clone algorithm، نفس الخوارزمية اللي المتصفح بيستخدمها في postMessage() مع الـ Workers وفي IndexedDB.

const copy = structuredClone(order);
copy.createdAt instanceof Date; // true
copy.items.get("sku-1");        // 2

حسب MDN هو Baseline Widely available في المتصفحات من مارس 2022، وفي Node.js موجود كـ global من v17.0.0. يعني مش محتاج lodash عشان النقطة دي.

بيدعم إيه؟

  • الأنواع الأساسية (ماعدا symbol)، و Array و plain objects.
  • Date و Map و Set و RegExp و ArrayBuffer و TypedArrays.
  • أنواع الـ Error الأساسية، وكمان Blob و File و ImageData وغيرهم من الـ Web APIs.
  • Circular references: الـ reference الدايري بيتنسخ صح.
const a = { name: "MDN" };
a.itself = a;
const clone = structuredClone(a);
clone.itself === clone; // true

الحاجات اللي مش بينسخها

  • Functions بترمي DataCloneError.
  • DOM nodes بترمي DataCloneError برضه.
  • الـ prototype chain مش بيتنسخ، فالـ class instance بيرجع plain object من غير methods.
  • الـ getters/setters والـ property descriptors مش بتتنسخ، و lastIndex بتاع RegExp مش بيتحفظ.
  • الـ class private fields مش بتتنسخ.

Bonus: الـ transfer

تقدر تنقل (transfer) ملكية ArrayBuffer بدل ما تنسخه، ودي مفيدة مع الداتا الكبيرة. بعد النقل الـ buffer الأصلي بيبقى فاضي:

const u8 = new Uint8Array(1024);
const moved = structuredClone(u8, { transfer: [u8.buffer] });
u8.byteLength; // 0

رأيي

structuredClone هو الاختيار الافتراضي لأي deep copy لداتا عادية: state، config، أو response جاي من API. بس لو الـ object بتاعك class instance ومحتاج الـ methods، أو فيه functions، يبقى محتاج copy constructor أو factory بإيدك. وما تستخدمش حيلة الـ JSON غير لو فعلًا عايز تحوّل لـ JSON.

المصادر

  • javascript
  • structuredclone
  • deep-copy
  • nodejs
  • web-apis