简体   繁体   中英

Dictionary: Map vs Object in JavaScript

In JavaScript, both Object and Map can be used for "Dictionary" (key-value pairs store) as they let you perform all the basic read and write operations you would expect when working with such a data structure: "set", "get", "delete" and "detect" keys, value & entries. Historically, and before Map even exists in JavaScript, we have been using Object for dictionaries. That's perfectly fine, however, there are important differences that make Map preferable in some cases and that could be found here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#objects_vs._maps

Personally, in the case that I am currently working on, I would like to use Map over Object because I feel that Map would be more suited for what I am trying to achieve at the moment: huge dictionary with hundred millions entries, lots of read/write operations, and based on custom benchmark, Map seems much faster that Object (again, in my case).

Unfortunately, when iterating on my Map dictionary (a Map being iterable, so it can be directly iterated with for...of ), I am facing the "Iterate-and-Mutate" problem: when iterating on a Map , you are not supposed to mutate that same Map within iterations. That's an anti-pattern!

With Object (which does not implement an iteration protocol, and so objects are not directly iterable using for...of by default), I am using Object.keys() , Object.values() or Object.entries() which returns an array of the given dictionary's own enumerable property names (keys), values or string-keyed property [key, value] pairs. Therefore, because I am not iterating on the dictionary itself, but actually on a separate Array in memory, I can write/mutate my dictionary safely, without facing any issue linked to the "Iterate-and-Mutate" problem.

When using Map as a dictionary and in order to iterate and mutate, one solution is to clone (either shallow or deep copy) the dictionary, then iterate on the original dictionary, but only mutate the clone. When the iteration cycle is finished, assign the clone to the original dictionary and delete the clone which is now useless. This requires having a clone of the original Map dictionary in memory which take unnecessary space.

Is there a better way to circumvent the "Iterate-and-Mutate" anti-pattern/problem associated with Map dictionary rather than cloning?

Thank you!

Examples:

  1. Object dictionary -> WORKS!
// Object dictionary -> WORKS!
const numbers = {
  '0': 0,
  '1': 1,
  '2': 2,
  '3': 3,
  '4': 4,
  '5': 5,
};

function compute(numbers) {
  /*
   * The Object.values() method returns an array of a given object's own
   * enumerable property values, in the same order as that provided by a
   * for...in loop.
   */
  for (number of Object.values(numbers)) {
    console.log('number:', number);

    const square = number * number;

    if (!numbers[`${square}`]) {
      numbers[`${square}`] = square;
    }
  }

  return numbers;
}

console.time('timer');
const result = compute(numbers);
console.timeEnd('timer');

console.log('result:', result);
  1. Map dictionary -> DOESN'T WORK AS IS!
// Map dictionary -> DOESN'T WORK AS IS!
const numbers = new Map([
  ['0', 0],
  ['1', 1],
  ['2', 2],
  ['3', 3],
  ['4', 4],
  ['5', 5],
]);

function compute(numbers) {
  /*
   * The values() method returns a new iterator object that contains the values
   * for each element in the Map object in insertion order.
   */
  for (number of numbers.values()) {
    console.log('number:', number);

    const squared = number * number;

    if (!numbers.has(`${squared}`)) {
      // "Iterate-and-Mutate" anti-pattern/problem:
      numbers.set(`${squared}`, squared);
    }
  }

  return numbers;
}

console.time('timer');
const result = compute(numbers);
console.timeEnd('timer');

console.log('result:', result);
  1. Map dictionary + clone (Deep Copy) -> WORKS! (slow and take a lot of memory)
// Map dictionary + clone (Deep Copy) -> WORKS!
let numbers = new Map([
  ['0', 0],
  ['1', 1],
  ['2', 2],
  ['3', 3],
  ['4', 4],
  ['5', 5],
]);

function compute(numbers) {
  /*
   * We create a clone (Deep Copy).
   * The data itself is cloned (slow and take a lot of memory).
   */
  const clone = new Map([ ...numbers.entries() ]);

  // Then, we iterate on the original dictionary.
  for (number of numbers.values()) {
    console.log('number:', number);

    const square = number * number;

    if (!clone.has(`${square}`)) {
      // But we mutate the clone, not the original dictionary.
      clone.set(`${square}`, square);
    }
  }

  // Finally, we assign 'clone' to original dictionary 'numbers'.
  numbers = clone;
  // And delete 'clone'.
  delete clone;

  return numbers;
}

console.time('timer');
const result = compute(numbers);
console.timeEnd('timer');

console.log('result:', result);
  1. Map dictionary + clone (Shallow Copy) -> WORKS! (faster and take less memory than Deep Copy, but still...)
// Map dictionary + clone (Shallow Copy) -> WORKS!
let numbers = new Map([
  ['0', 0],
  ['1', 1],
  ['2', 2],
  ['3', 3],
  ['4', 4],
  ['5', 5],
]);

function compute(numbers) {
  /*
   * We create a clone (Shallow Copy).
   * The data itself is not cloned (faster and take less memory than Deep Copy).
   */
  const clone = new Map(numbers);

  // Then, we iterate on the original dictionary.
  for (number of numbers.values()) {
    console.log('number:', number);

    const square = number * number;

    if (!clone.has(`${square}`)) {
      // But we mutate the clone, not the original dictionary.
      clone.set(`${square}`, square);
    }
  }

  // Finally, we assign 'clone' to original dictionary 'numbers'.
  numbers = clone;
  // And delete 'clone'.
  delete clone;

  return numbers;
}

console.time('timer');
const result = compute(numbers);
console.timeEnd('timer');

console.log('result:', result);
  1. Map dictionary + fake clone -> DOESN'T WORK BECAUSE INSUFFICIENT!
// Map dictionary + fake clone -> DOESN'T WORK BECAUSE INSUFFICIENT!
let numbers = new Map([
  ['0', 0],
  ['1', 1],
  ['2', 2],
  ['3', 3],
  ['4', 4],
  ['5', 5],
]);

function compute(numbers) {
  /*
   * We create a "clone" (we believe we do, but in reality we don't!).
   * The 'clone' variable is not really a clone/copy of 'numbers'.
   * It's just a variable that points to the same data in memory.
   * Therefore, any mutation on 'numbers' will be reflected on 'clone',
   * which breaks the solution!
   */
  const clone = numbers;

  // Then, we iterate on the original dictionary.
  for (number of numbers.values()) {
    console.log('number:', number);

    const square = number * number;

    if (!clone.has(`${square}`)) {
      // But we mutate the clone, not the original dictionary.
      clone.set(`${square}`, square);
    }
  }

  // Finally, we assign 'clone' to original dictionary 'numbers'.
  numbers = clone;
  // And delete 'clone'.
  delete clone;

  return numbers;
}

console.time('timer');
const result = compute(numbers);
console.timeEnd('timer');

console.log('result:', result);

If you want to mimic the pattern that you use with a plain object (ie iterating over Object.values ), then iterate [...numbers.values()] , which really is the Map equivalent for that. The space cost is similar -- in both cases an array is created that has the values.

So:

 const numbers = new Map([['0', 0],['1', 1],['2', 2],['3', 3],['4', 4],['5', 5]]); function compute(numbers) { for (const number of [...numbers.values()]) { console.log('number:', number); const squared = number * number; if (.numbers.has(`${squared}`)) { numbers,set(`${squared}`; squared); } } return numbers. } console:log('result,'. ..;compute(numbers));

I have a possible solution but only in the case that you usually update only a small amount of items in the map. I think that maybe the case: you have a big amount of data and you update only some data each time.

In that scenary, you can work with 2 extra maps. You iterate your map: if a new key appears, you save in the updatesMap. If the content of an existing key has changes, you save that item in updatesMap. And if some key must be removed, you add this item in a removeMap map. So you only do changes in updatesMap and removeMap.

After iterate your map, you can iterate over updatesMap and add/update all their items in your original map. And finally, you can iterate over removeMap and delete all items in your original map.

removeMap can be a simple array with the keys to remove if you don't need the deleted values.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM