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

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

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

תמיכה בדפדפן

  • 98
  • 98
  • 94
  • 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, ערך בוליאני, לא מוגדר, סמל ואפס.

MDN – פרימיטיבי

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

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

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

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

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

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

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

שכפול מובנה

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