• בלוג
  • טיפ JavaScript: שמירת אוביקטים ב Map ו Set

טיפ JavaScript: שמירת אוביקטים ב Map ו Set

22/02/2020

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

const text = "one two three one two one";
const wordCount = {};

for (let word of text.split(/\W+/)) {
  if (wordCount[word] == null) {
    wordCount[word] = 0;
  }
  wordCount[word] += 1;
}

console.log(wordCount);

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

const text = "hello toString world";
const wordCount = {};

for (let word of text.split(/\W+/)) {
  if (wordCount[word] == null) {
    wordCount[word] = 0;
  }
  wordCount[word] += 1;
}

console.log(wordCount);

פלט התוכנית:

{ hello: 1,
  toString: 'function toString() { [native code] }1',
  world: 1 }

לא ממש משקף את העובדה שהמילה toString הופיעה בקלט. או אולי נכון להגיד שהוא כן משקף את העובדה הזאת אבל בצורה קצת מוזרה.

מתכנתים טובים ידעו לעקוף את הבעיה באמצעות שימוש ב Object.create באופן הבא:

const text = "hello toString world";
const wordCount = Object.create(null);

for (let word of text.split(/\W+/)) {
  if (wordCount[word] == null) {
    wordCount[word] = 0;
  }
  wordCount[word] += 1;
}

console.log(wordCount);

אבל ממילא אף פעם לא היה קל למצוא מתכנתים טובים.

תקן ES6 הוסיף תמיכה בסוג חדש של אוביקטים לשמירה וניהול של אוביקטים אחרים והם Set ו Map. בשני המקרים מדובר על אוסף של אוביקטים בהם "המפתח", כלומר האוביקט שנשמר הוא ייחודי וההבדל הוא ש Map מאפשר לכם לשמור כל ערך שתרצו (בדומה לאוביקט רגיל) וב Set הערך לא ממש קיים והשאלה היא רק אם מפתח מסוים חבר ב Set.

אני יכול לתקן את תוכנית הספירה שלי באמצעות Map די בקלות:

const text = "hello toString world";
const wordCount = new Map();

for (let word of text.split(/\W+/)) {
  const value = wordCount.get(word) || 0;
  wordCount.set(word, value + 1);
}

console.log(wordCount);

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

בחזרה לתוכנית הספירה - הקוד הבא לא יצליח לזהות שהמערך [2, 5] מופיע 3 פעמים בקלט:

const data = [[1, 2], [2, 3], [5, 2], [5, 2], [5, 2], [2, 3]];
const count = new Map();

for (let item of data) {
  const value = count.get(item) || 0;
  count.set(item, value + 1);
}

console.log(count);

וידפיס את זה:

Map {
  [ 1, 2 ] => 1,
  [ 2, 3 ] => 1,
  [ 5, 2 ] => 1,
  [ 5, 2 ] => 1,
  [ 5, 2 ] => 1,
  [ 2, 3 ] => 1 }

אבל אם נחליף את ה Map באוביקט רגיל הקוד יעבוד בלי בעיה:

const data = [[1, 2], [2, 3], [5, 2], [5, 2], [5, 2], [2, 3]];
const count = {};

for (let item of data) {
  if (!count[item]) {
    count[item] = 0;
  }
  count[item] += 1;
}

console.log(count);

וידפיס:

{ '1,2': 1, '2,3': 2, '5,2': 3 }

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

const data = [[1, 2], [2, 3], [5, 2], [5, 2], [5, 2], [2, 3]];
const count = new Map();

for (let iitem of data) {
  const item = String(iitem);
  const value = count.get(item) || 0;
  count.set(item, value + 1);
}

console.log(count);

אבל לא בטוח שזה שווה את המאמץ.