Map vs Object in JavaScript
I just discovered this feature:
Map: Map objects are simple key/value maps.
That confused me. Regular JavaScript objects are dictionaries, so how is a Map
different from a dictionary? Conceptually, they're identical (according to another question on Stack Overflow)
The documentation doesn't help either:
Map objects are collections of key/value pairs where both the keys and values may be arbitrary ECMAScript language values. A distinct key value may only occur in one key/value pair within the Map’s collection. Distinct key values as discriminated using the a comparision algorithm that is selected when the Map is created.
A Map object can iterate its elements in insertion order. Map object must be implemented using either hash tables or other mechanisms that, on average, provide access times that are sublinear on the number of elements in the collection. The data structures used in this Map objects specification is only intended to describe the required observable semantics of Map objects. It is not intended to be a viable implementation model.
…still sounds like an object to me, so clearly I've missed something.
Why is JavaScript gaining a (well-supported) Map
object? What does it do?
According to MDN:
A Map object can iterate its elements in insertion order - a
for..of
loop will return an array of [key, value] for each iteration.
and
Objects are similar to Maps in that both let you set keys to values, retrieve those values, delete keys, and detect whether something is stored at a key. Because of this, Objects have been used as Maps historically; however, there are important differences between Objects and Maps that make using a Map better.
An Object has a prototype, so there are default keys in the map. However, this can be bypassed using map = Object.create(null). The keys of an Object are Strings, where they can be any value for a Map. You can get the size of a Map easily while you have to manually keep track of size for an Object.
Map
The iterability-in-order is a feature that has long been wanted by developers, in part because it ensures the same performance in all browsers. So to me that's a big one.
The myMap.has(key)
method will be especially handy, and also the myMap.size
property.
The key difference is that Objects only support string and Symbol keys where as Maps support more or less any key type.
If I do obj[123] = true
and then Object.keys(obj)
then I will get ["123"]
rather than [123]
. A Map would preserve the type of the key and return [123]
which is great. Maps also allow you to use Objects as keys. Traditionally to do this you would have to give objects some kind of unique identifier to hash them (I don't think I've ever seen anything like getObjectId
in JavaScript as part of the standard). Maps also guarantee preservation of order so are all round better for preservation and can sometimes save you needing to do a few sorts.
Between maps and objects in practice there are several pros and cons. Objects gain both advantages and disadvantages being very tightly integrated into the core of JavaScript which sets them apart from significantly Map beyond the difference in key support.
An immediate advantage is that you have syntactical support for Objects making it easy to access elements. You also have direct support for it with JSON. When used as a hash it's annoying to get an object without any properties at all. By default if you want to use Objects as a hash table they will be polluted and you will often have to call hasOwnProperty
on them when accessing properties. You can see here how by default Objects are polluted and how to create hopefully unpolluted objects for use as hashes:
({}).toString
toString() { [native code] }
JSON.parse('{}').toString
toString() { [native code] }
(Object.create(null)).toString
undefined
JSON.parse('{}', (k,v) => (typeof v === 'object' && Object.setPrototypeOf(v, null) ,v)).toString
undefined
Pollution on objects is not only something that makes code more annoying, slower, etc., but can also have potential consequences for security.
Objects are not pure hash tables, but they are trying to do more. You have headaches like hasOwnProperty
, not being able to get the length easily (Object.keys(obj).length
) and so on. Objects are not meant to purely be used as hash maps, but as dynamic extensible Objects as well and so when you use them as pure hash tables problems arise.
Comparison/List of various common operations:
Object:
var o = {};
var o = Object.create(null);
o.key = 1;
o.key += 10;
for(let k in o) o[k]++;
var sum = 0;
for(let v of Object.values(m)) sum += v;
if('key' in o);
if(o.hasOwnProperty('key'));
delete(o.key);
Object.keys(o).length
Map:
var m = new Map();
m.set('key', 1);
m.set('key', m.get('key') + 10);
m.foreach((k, v) => m.set(k, m.get(k) + 1));
for(let k of m.keys()) m.set(k, m.get(k) + 1);
var sum = 0;
for(let v of m.values()) sum += v;
if(m.has('key'));
m.delete('key');
m.size();
There are a few other options, approaches, methodologies, etc. with varying ups and downs (performance, terse, portable, extendable, etc.). Objects are a bit strange being core to the language so you have a lot of static methods for working with them.
Besides the advantage of Maps preserving key types as well as being able to support things like objects as keys they are isolated from the side effects that objects much have. A Map is a pure hash, there's no confusion about trying to be an object at the same time. Maps can also be easily extended with proxy functions. Object's currently have a Proxy class however performance and memory usage is grim, in fact creating your own proxy that looks like Map for Objects currently performs better than Proxy.
A substantial disadvantage for Maps is that they are not supported with JSON directly. Parsing is possible, but it has several hangups:
JSON.parse(str, (k,v) => {
if(typeof v !== 'object') return v;
let m = new Map();
for(k in v) m.set(k, v[k]);
return m;
});
The above will introduce a serious performance hit and will also not support any string keys. JSON encoding is even more difficult and problematic (this is one of many approaches):
// An alternative to this it to use a replacer in JSON.stringify.
Map.prototype.toJSON = function() {
return JSON.stringify({
keys: Array.from(this.keys()),
values: Array.from(this.values())
});
};
This is not so bad if you're purely using Maps, but it will have problems when you are mixing types or using non-scalar values as keys (not that JSON is perfect with that kind of issue as it is, IE circular object reference). I haven't tested it, but chances are that it will severely hurt performance compared to stringify.
Other scripting languages often don't have such problems as they have explicit non-scalar types for Map, Object and Array. Web development is often a pain with non-scalar types where you have to deal with things like PHP merges Array/Map with Object using A/M for properties and JavaScript merges Map/Object with Array extending M/O. Merging complex types is the devil's bane of high level scripting languages.
So far these are largely issues around implementation, but performance for basic operations is important as well. Performance is also complex because it depends on engine and usage. Take my tests with a grain of salt as I cannot rule out any mistake (I have to rush this). You should also run your own tests to confirm as mine examine only very specific simple scenarios to give a rough indication only. According to tests in Chrome for very large objects/maps the performance for objects is worse because of delete which is apparently somehow proportionate to the number of keys rather than O(1):
Object Set Took: 146
Object Update Took: 7
Object Get Took: 4
Object Delete Took: 8239
Map Set Took: 80
Map Update Took: 51
Map Get Took: 40
Map Delete Took: 2
Chrome clearly has a strong advantage with getting and updating, but the delete performance is horrific. Maps use a tiny amount more memory in this case (overhead), but with only one Object/Map being tested with millions of keys the impact of overhead for maps is not expressed well. With memory management objects also do seem to free earlier if I am reading the profile correctly which might be one benefit in favor of objects.
In Firefox for this particular benchmark it is a different story:
Object Set Took: 435
Object Update Took: 126
Object Get Took: 50
Object Delete Took: 2
Map Set Took: 63
Map Update Took: 59
Map Get Took: 33
Map Delete Took: 1
I should immediately point out that in this particular benchmark deleting from objects in Firefox is not causing any problems, however in other benchmarks it has caused problems especially when there are many keys just as in Chrome. Maps are clearly superior in Firefox for large collections.
However this is not the end of the story, what about many small objects or maps? I have done a quick benchmark of this, but not an exhaustive one (setting/getting) of which performs best with a small number of keys in the above operations. This test is more about memory and initialization.
Map Create: 69 // new Map
Object Create: 34 // {}
Again these figures vary, but basically Object has a good lead. In some cases the lead for Objects over maps is extreme (~10 times better), but on average it was around 2-3 times better. It seems extreme performance spikes can work both ways. I only tested this in Chrome and creation to profile memory usage and overhead. I was quite surprised to see that in Chrome it appears that Maps with one key use around 30 times more memory than Objects with one key.
For testing many small objects with all the above operations (4 keys):
Chrome Object Took: 61
Chrome Map Took: 67
Firefox Object Took: 54
Firefox Map Took: 139
In terms of memory allocation these behaved the same in terms of freeing/GC, but Map used five times more memory. This test used four keys where as in the last test I only set one key so this would explain the reduction in memory overhead. I ran this test a few times and Map/Object are more or less neck and neck overall for Chrome in terms of overall speed. In Firefox for small Objects there is a definite performance advantage over maps overall.
This of course doesn't include the individual options which could vary wildly. I would not advice micro-optimizing with these figures. What you can get out of this is that as a rule of thumb, consider Maps more strongly for very large key value stores and objects for small key value stores.
Beyond that the best strategy with these two it to implement it and just make it work first. When profiling it is important to keep in mind that sometimes things that you wouldn't think would be slow when looking at them can be incredibly slow because of engine quirks as seen with the object key deletion case.