העתקה עמוקה ב-JavaScript באמצעות structuredClone

הפלטפורמה זמינה עכשיו עם structuredClone(), פונקציה מובנית להעתקה עמוקה.

במשך הזמן הארוך ביותר נאלצתם להשתמש בספריות ובפתרונות עקיפים כדי ליצור עותק עמוק של ערך JavaScript. עכשיו אפשר למצוא את הפלטפורמה עם structuredClone(), פונקציה מובנית להעתקה עמוקה.

תמיכה בדפדפן

  • Chrome: 98.
  • קצה: 98.
  • Firefox: 94.
  • Safari: 15.4.

מקור

עותקים רדודים

העתקת ערך ב-JavaScript היא כמעט תמיד חלשה, בניגוד להעתקת ערך עמוקה. המשמעות היא ששינויים בערכים בתצוגת עומק יוצגו בעותק וגם במקור.

אחת מהדרכים ליצור עותק שטחי פרסום ב-JavaScript באמצעות אופרטור הפצת אובייקט ...:

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} חוזר על המאפיינים (המספרים) של myOriginal באמצעות אופרטור מרווח. היא משתמשת בשם ובערך של המאפיין, ומקצה אותם אחד אחרי השני לאובייקט ריק שנוצר מחדש. לכן האובייקט שמתקבל זהה בצורתו, אבל עם עותק משלו של רשימת המאפיינים והערכים. גם הערכים מועתקים, אבל המערכת מתייחסת לערכים פרימיטיביים בצורה שונה על ידי ערך JavaScript מאשר על ידי ערכים לא פרימיטיביים. כדי לצטט את MDN:

ב-JavaScript, רכיב ראשוני (ערך ראשוני, סוג נתונים פרימיטיבי) הוא נתונים שאינם אובייקט ושאין להם שיטות. יש שבעה סוגים של נתונים פרימיטיביים: מחרוזת, מספר, Bigint, בוליאני, לא מוגדר, סמל ו-null.

MDN – פרימיטיבי

ערכים לא פרימיטיביים מטופלים כהפניות. כלומר, פעולת העתקת הערך היא למעשה העתקה של הפניה לאותו אובייקט בסיסי, וכתוצאה מכך נוצרת התנהגות רטובה של העתקה.

עותקים עמוקים

ההפך מעותק רדוד הוא עותק עמוק. אלגוריתם של עותק עמוק מעתיק גם את המאפיינים של אובייקט בזה אחר זה, אבל מפעיל את עצמו באופן רקורסיבי כשהוא מוצא הפניה לאובייקט אחר, וכך יוצר עותק של האובייקט הזה גם כן. זה יכול להיות חשוב מאוד כדי להבטיח ששני קטעי קוד לא ישתפו אובייקט בטעות ומשפיעים ללא כוונה אחד על השני.

בעבר לא הייתה דרך קלה או נחמדה ליצור עותק עמוק של ערך ב-JavaScript. אנשים רבים הסתמכו על ספריות של צד שלישי כמו הפונקציה cloneDeep() של Lodash. נראה שהפתרון הנפוץ ביותר לבעיה הזו היה פריצת JSON מבוססת:

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

למעשה, זו הייתה שיטה פופולרית לעקיפת הבעיה, שV8 מבצעת אופטימיזציה אגרסיבית JSON.parse() ובמיוחד את הדפוס שלמעלה כדי להפוך אותה למהירה ככל האפשר. אומנם הוא מהיר, אבל יש בו כמה חסרונות ופונקציות:

  • מבני נתונים רקורסיביים: הפונקציה JSON.stringify() תחזיר המערכת אם תיתנו לה מבנה נתונים רקורסיבי. זה יכול לקרות בקלות כשעובדים עם רשימות או עצים מקושרים.
  • סוגים מובנים: JSON.stringify() יופעל אם הערך מכיל רכיבי JS מובנים אחרים כמו Map, Set, Date, RegExp או ArrayBuffer.
  • פונקציות: הפונקציה JSON.stringify() תמחק פונקציות באופן שקט.

שכפול מובנה

הפלטפורמה כבר הייתה זקוקה ליכולת ליצור עותקים עמוקים של ערכי JavaScript בכמה מקומות: כדי לאחסן ערך JS ב-IndexedDB צריך סוג כלשהו של סריאליזציה כדי שיהיה אפשר לאחסן אותו בדיסק ולבצע פעולת deserialing כדי לשחזר את ערך ה-JS בשלב מאוחר יותר. באופן דומה, שליחת הודעות ל-WebWorker דרך postMessage() מחייבת העברה של ערך JS מתחום JS אחד לאחר. האלגוריתם שבו משתמשים לשם כך נקרא 'שכפול מובנה', ועד לאחרונה לא היה פשוט נגיש למפתחים.

זה השתנה! מפרט ה-HTML תוקן כדי לחשוף פונקציה בשם structuredClone() המריצה בדיוק את אותו אלגוריתם כאמצעי למפתחים ליצור בקלות עותקים עמוקים של ערכי JavaScript.

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(). חוזה.