کپی عمیق در جاوا اسکریپت با استفاده از structuredClone

پلتفرم اکنون با structuredClone()، یک تابع داخلی برای کپی عمیق ارسال می شود.

برای طولانی‌ترین زمان، برای ایجاد یک کپی عمیق از یک مقدار جاوا اسکریپت، مجبور بودید به راه‌حل‌ها و کتابخانه‌ها متوسل شوید. پلتفرم اکنون با structuredClone() عرضه می شود که یک تابع داخلی برای کپی عمیق است.

پشتیبانی مرورگر

  • کروم: 98.
  • لبه: 98.
  • فایرفاکس: 94.
  • سافاری: 15.4.

منبع

کپی کردن یک مقدار در جاوا اسکریپت تقریبا همیشه کم عمق است، برخلاف عمق . این بدان معنی است که تغییرات در مقادیر عمیق تو در تو و همچنین نسخه اصلی قابل مشاهده خواهد بود.

یکی از راه های ایجاد یک کپی کم عمق در جاوا اسکریپت با استفاده از عملگر شی گسترش ... :

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

const myShallowCopy = {...myOriginal};

افزودن یا تغییر یک ویژگی مستقیماً روی کپی کم عمق فقط بر روی کپی تأثیر می گذارد، نه بر روی نسخه اصلی:

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

با این حال، افزودن یا تغییر یک ویژگی عمیق تو در تو، هم روی کپی و هم بر روی نسخه اصلی تأثیر می گذارد:

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

عبارت {...myOriginal} با استفاده از عملگر Spread روی ویژگی های (شماری) myOriginal تکرار می شود. از نام و مقدار ویژگی استفاده می کند و آنها را یکی یکی به یک شیء خالی و تازه ایجاد شده اختصاص می دهد. به این ترتیب، شیء به‌دست‌آمده از نظر شکل یکسان است، اما با کپی خاص خود از فهرست ویژگی‌ها و مقادیر. مقادیر نیز کپی می‌شوند، اما مقادیر به اصطلاح اولیه با مقدار جاوا اسکریپت متفاوت از مقادیر غیر ابتدایی مدیریت می‌شوند. برای نقل قول از MDN :

در جاوا اسکریپت، یک primitive (مقدار اولیه، نوع داده اولیه) داده‌ای است که یک شی نیست و هیچ روشی ندارد. هفت نوع داده اولیه وجود دارد: رشته ای، عددی، بیگینت، بولی، تعریف نشده، نماد و تهی.

MDN - ابتدایی

مقادیر غیر ابتدایی به عنوان مرجع استفاده می شوند، به این معنی که عمل کپی کردن مقدار در واقع فقط کپی کردن یک مرجع به همان شی زیربنایی است که منجر به رفتار کپی کم عمق می شود.

کپی های عمیق

نقطه مقابل یک کپی کم عمق، یک کپی عمیق است. یک الگوریتم کپی عمیق نیز ویژگی های یک شی را یکی یکی کپی می کند، اما زمانی که مرجعی به شی دیگری پیدا می کند، خود را به صورت بازگشتی فراخوانی می کند و یک کپی از آن شی نیز ایجاد می کند. این می تواند بسیار مهم باشد تا مطمئن شوید که دو قطعه کد به طور تصادفی یک شی را به اشتراک نمی گذارند و ناخودآگاه وضعیت یکدیگر را دستکاری نمی کنند.

هیچ راه آسان یا خوبی برای ایجاد یک کپی عمیق از یک مقدار در جاوا اسکریپت وجود نداشت. بسیاری از مردم به کتابخانه های شخص ثالث مانند تابع cloneDeep() Lodash اعتماد می کردند. مسلما رایج ترین راه حل برای این مشکل هک مبتنی بر JSON بود:

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

در واقع، این یک راه حل محبوب بود، که V8 به طور تهاجمی JSON.parse() و به طور خاص الگوی بالا را بهینه کرد تا آن را تا حد امکان سریع کند. و در حالی که سریع است، با چند کاستی و سهولت همراه است:

  • ساختارهای داده بازگشتی : زمانی که به آن یک ساختار داده بازگشتی بدهید JSON.stringify() پرتاب می‌شود. این می تواند به راحتی هنگام کار با لیست ها یا درختان پیوندی اتفاق بیفتد.
  • انواع داخلی : اگر مقدار دارای JS داخلی دیگری مانند Map ، Set ، Date ، RegExp یا ArrayBuffer باشد، JSON.stringify() پرتاب می‌شود.
  • توابع : JSON.stringify() بی سر و صدا توابع را کنار می گذارد.

شبیه سازی ساختاریافته

پلتفرم قبلاً به توانایی ایجاد کپی های عمیق از مقادیر جاوا اسکریپت در چند مکان نیاز داشت: ذخیره یک مقدار JS در IndexedDB به نوعی سریال سازی نیاز دارد تا بتوان آن را روی دیسک ذخیره کرد و بعداً برای بازیابی مقدار JS از سریال خارج کرد. به طور مشابه، ارسال پیام به WebWorker از طریق postMessage() مستلزم انتقال یک مقدار JS از یک قلمرو JS به قلمرو دیگر است. الگوریتمی که برای این کار استفاده می شود "کلون ساختاریافته" نام دارد و تا همین اواخر برای توسعه دهندگان به راحتی قابل دسترسی نبود.

الان تغییر کرده! مشخصات HTML اصلاح شد تا تابعی به نام structuredClone() را نشان دهد که دقیقاً همان الگوریتم را به عنوان وسیله ای برای توسعه دهندگان اجرا می کند تا به راحتی کپی های عمیقی از مقادیر جاوا اسکریپت ایجاد کنند.

const myDeepCopy = structuredClone(myOriginal);

همین! این کل API است. اگر می‌خواهید عمیق‌تر در جزئیات غوطه‌ور شوید، نگاهی به مقاله MDN بیندازید.

ویژگی ها و محدودیت ها

شبیه سازی ساختاریافته بسیاری از (اگرچه نه همه) کاستی های تکنیک JSON.stringify() را برطرف می کند. شبیه‌سازی ساختاریافته می‌تواند ساختارهای داده چرخه‌ای را مدیریت کند، بسیاری از انواع داده‌های داخلی را پشتیبانی کند و عموماً قوی‌تر و اغلب سریع‌تر است.

با این حال، هنوز محدودیت هایی دارد که ممکن است شما را غافلگیر کند:

  • نمونه های اولیه : اگر از structuredClone() با یک نمونه کلاس استفاده کنید، یک شی ساده به عنوان مقدار بازگشتی دریافت خواهید کرد، زیرا شبیه سازی ساختاریافته زنجیره نمونه اولیه شی را دور می زند.
  • توابع : اگر شیء شما حاوی توابع باشد، structuredClone() یک استثنای DataCloneError ایجاد می کند.
  • غیر قابل شبیه سازی : برخی از مقادیر قابل شبیه سازی ساختاری نیستند، مهم ترین آنها گره های Error و DOM. باعث پرتاب structuredClone() می شود.

اگر هر یک از این محدودیت‌ها مانعی برای استفاده شما باشد، کتابخانه‌هایی مانند Lodash همچنان پیاده‌سازی‌های سفارشی دیگر الگوریتم‌های شبیه‌سازی عمیق را ارائه می‌کنند که ممکن است با مورد استفاده شما مطابقت داشته باشد یا نباشد.

عملکرد

در حالی که من مقایسه میکرو بنچمارک جدیدی انجام نداده ام، اما در اوایل سال 2018، قبل از افشای structuredClone() مقایسه ای انجام دادم . در آن زمان، JSON.parse() سریعترین گزینه برای اشیاء بسیار کوچک بود. من انتظار دارم که همینطور باقی بماند. تکنیک‌هایی که بر شبیه‌سازی ساختاری تکیه می‌کردند (به طور قابل توجهی) برای اشیاء بزرگ‌تر سریع‌تر بودند. با توجه به اینکه structuredClone() جدید بدون سربار سوء استفاده از سایر API ها ارائه می شود و از JSON.parse() قوی تر است، توصیه می کنم آن را رویکرد پیش فرض خود برای ایجاد کپی های عمیق قرار دهید.

نتیجه گیری

اگر می‌خواهید یک کپی عمیق از یک مقدار در JS ایجاد کنید - شاید به این دلیل باشد که از ساختارهای داده غیرقابل تغییر استفاده می‌کنید یا می‌خواهید مطمئن شوید که یک تابع می‌تواند یک شی را بدون تأثیرگذاری بر روی اصلی دستکاری کند - دیگر نیازی به یافتن راه‌حل یا راه‌حل نیست. کتابخانه ها اکوسیستم JS اکنون structuredClone() دارد. حوزه