Menu

Map ו-Set ב-JavaScript: מתי להעדיף אותם על אובייקטים ומערכים

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

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

שני אוספים מעבר לאובייקט ולמערך

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

הם קיימים בשפה מאז ES2015. שניהם איטרביליים, לשניהם יש מאפיין .size, ושניהם עובדים טוב עם אופרטור ה-spread. מודל החשיבה פשוט:

  • Map: כמו אובייקט, אבל המפתחות יכולים להיות כל דבר והסדר נשמר.
  • Set: כמו מערך, אבל הערכים ייחודיים והחיפוש מהיר.

יצירה ושימוש ב-Map

Map מחזיק זוגות של מפתח וערך. יוצרים אותו עם new Map() ומשתמשים ב-.set(), .get(), .has() ו-.delete():

אפשר גם להעביר לבנאי מערך של זוגות [key, value] כדי לאכלס אותו מראש:

הצורה הזאת של מערך בן שני איברים מופיעה בכל מקום שיש בו Maps: כך מיוצגות רשומות כשעוברים עליהן.

Map מול אובייקט: למה לטרוח?

אובייקטים רגילים נראים כאילו הם עושים את אותה עבודה. ברוב המקרים הם באמת עושים אותה. אבל Maps מתקנים כמה קצוות גסים מוגדרים:

אובייקטים יורשים מ-Object.prototype, כך שמפתחות כמו toString, constructor ו-hasOwnProperty כבר קיימים בכל אובייקט. ל-Maps אין מטען כזה: המפתחות שאתם מגדירים הם המפתחות היחידים שקיימים.

ההבדלים הנוספים ששווה להכיר:

  • כל סוג מפתח. Maps מקבלים אובייקטים, פונקציות, מספרים ובוליאניים כמפתחות. אובייקטים ממירים בשקט מפתחות שאינם מחרוזות למחרוזות: obj[1] ו-obj["1"] הם אותו תא.
  • סדר הכנסה מובטח. Maps עוברים על הרשומות לפי הסדר שבו נוספו. גם אובייקטים בדרך כלל, אבל מפתחות מחרוזת שנראים כמו מספרים ממוינים קודם, וזו מלכודת עדינה.
  • גודל מובנה. map.size הוא O(1). עבור אובייקט הייתם כותבים Object.keys(obj).length, שבונה מערך מחדש.
  • מותאם לשינויים תכופים. מנועים מכווננים את Maps להוספה והסרה תכופות. אובייקטים מכווננים לרשומות עם מבנה יציב.

השתמשו באובייקט כשאתם ממדלים רשומה עם מפתחות מחרוזת ידועים ({ name, email, age }). השתמשו ב-Map כשהמפתחות דינמיים, אינם מחרוזות, או כשתוסיפו ותסירו רשומות הרבה.

מעבר על Map

Maps הם איטרביליים, כלומר for...of עובדת ישירות ופירוק של כל רשומה טבעי:

אם אתם רוצים רק מפתחות או רק ערכים, קראו ל-.keys() או ל-.values(). ו-.forEach() קיימת אם אתם מעדיפים אותה:

כדי להפוך Map בחזרה לאובייקט או למערך רגיל, פרשו אותו:

יצירה ושימוש ב-Set

Set מחזיק ערכים ייחודיים. הוספה של ערך שכבר קיים לא עושה כלום:

הייחודיות נקבעת לפי אותו כלל שוויון כמו ===, עם מוזרות אחת: NaN נחשב שווה לעצמו בתוך Set, למרות ש-NaN === NaN הוא false בכל מקום אחר.

העבירו iterable לבנאי כדי לאכלס Set מראש. מכאן מגיע הטריק להסרת כפילויות:

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

Set מול מערך: מתי לעבור

גם מערכים וגם Sets מחזיקים אוסף של ערכים, אז מתי לבחור במה?

בחרו ב-Set כש:

  • הערכים חייבים להיות ייחודיים ואתם רוצים שסביבת הריצה תאכוף את זה.
  • אתם עושים הרבה בדיקות שייכות. set.has(x) הוא O(1), ו-array.includes(x) הוא O(n). בתוך לולאה, ההבדל הזה מצטבר מהר.
  • סדר ההכנסה הוא כל מה שצריך. Sets עוברים לפי סדר ההכנסה אבל לא תומכים בגישה לפי אינדקס.

הישארו עם מערך כש:

  • צריך גישה לפי מיקום: arr[0], חיתוך, מיון.
  • לכפילויות יש משמעות: עגלת קניות עם שני פריטים זהים.
  • תשתמשו הרבה במתודות מערך כמו .map, .filter, .reduce. ל-Sets אין אותן, והייתם צריכים לפרוש קודם למערך.

דוגמה קצרה שקשורה לביצועים:

אם banned היה מערך, כל callback של filter היה סורק את כל הרשימה. כ-Set, כל חיפוש לוקח זמן קבוע.

מעבר על Set

אותו סיפור כמו ב-Map: for...of פשוט עובדת, ופרישה נותנת מערך:

ל-Sets יש גם .keys(), .values() ו-.entries() לשם סימטריה עם Map, למרות שב-Set המפתחות והערכים הם אותו דבר. רוב הזמן פשוט תעברו עליהם ישירות.

דוגמה מלאה: ספירת מבקרים ייחודיים לכל עמוד

שילוב של שניהם: Map מנתיבי עמודים ל-Set של מזהי מבקרים:

ה-Map מטפל במיפוי מנתיב לדלי, וה-Set מטפל בהסרת הכפילויות בתוך כל דלי. לעשות את אותו דבר עם אובייקט רגיל ומערכים היה עובד, אבל הייתם כותבים בדיקות indexOf ושומרי hasOwnProperty נוספים לאורך כל הדרך.

WeakMap ו-WeakSet בקצרה

שני אוספים קשורים קיימים למקרה שימוש צר: WeakMap ו-WeakSet. הם מחזיקים הפניות חלשות, כלומר רשומה שהמפתח שלה (ב-WeakMap) או הערך שלה (ב-WeakSet) לא מופנה מאף מקום אחר נאספת אוטומטית על ידי ה-garbage collector.

הם מקבלים רק אובייקטים כמפתחות, אינם איטרביליים ואין להם .size. זה מכוון: אם הייתם יכולים לעבור עליהם, היה אפשר לצפות בפעולת ה-garbage collector. הם שימושיים לשמירת מטא-דאטה על אובייקטים שאינם שלכם, ונדירים בקוד יומיומי.

הבא בתור: JSON

Map ו-Set מצוינים בזיכרון, אבל אף אחד מהם לא שורד את JSON.stringify בשלמותו: Maps הופכים ל-{} וגם Sets הופכים ל-{}. העמוד הבא עוסק ב-JSON: איך ממירים ומפענחים נתונים, והתבניות לטיפול באוספים שהוצגו כאן כשהם צריכים לעבור ברשת או לקובץ.

שאלות נפוצות

מה ההבדל בין Map לאובייקט ב-JavaScript?

Map יכול להשתמש בכל ערך כמפתח: אובייקטים, פונקציות, מספרים, כל דבר, בעוד שאובייקט ממיר מפתחות למחרוזות (או ל-symbols). Map גם עוקב אחרי הגודל שלו עם .size, עובר על הפריטים לפי סדר ההכנסה, ולא יורש מפתחות מ-prototype, כך שאין סיכון להתנגשות עם toString או constructor. בחרו ב-Map כשהמפתחות אינם מחרוזות, או כשצריך להוסיף ולהסיר רשומות לעתים קרובות.

למה משמש Set ב-JavaScript?

Set שומר ערכים ייחודיים ומתעלם בשקט מכפילויות. הדרך המהירה ביותר להסיר כפילויות ממערך היא [...new Set(arr)]. Sets גם נותנים בדיקות .has() ב-O(1), וזה עדיף על array.includes() כשבודקים שייכות בתוך לולאה.

איך עוברים על Map?

for...of עובדת ישירות: for (const [key, value] of myMap) מפרקת כל רשומה. אפשר גם לעבור על myMap.keys(), myMap.values() או myMap.entries(). סדר המעבר מובטח להתאים לסדר ההכנסה, דבר שאובייקטים רגילים לא תמיד מבטיחים עבור מפתחות שנראים כמו מספרים.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל