我刚刚发现了这个特点:

Map: Map对象是简单的键/值映射。

这让我很困惑。常规JavaScript对象是字典,那么Map与字典有什么不同呢?从概念上讲,它们是相同的(根据Stack Overflow的另一个问题)

文档也没有帮助:

Map对象是键/值对的集合,其中键和值都可以是任意的ECMAScript语言值。不同的键值只能出现在Map集合中的一个键/值对中。使用创建Map时选择的比较算法进行区分的不同键值。

Map对象可以按插入顺序迭代其元素。Map对象必须使用哈希表或其他机制来实现,这些机制提供的访问时间平均与集合中元素的数量呈次线性关系。本Map对象规范中使用的数据结构仅用于描述Map对象所需的可观察语义。它并不是一个可行的实现模型。

听起来还是像个物件,显然我错过了什么。

为什么JavaScript获得一个(受良好支持的)Map对象?它能做什么?


当前回答

除了按定义良好的顺序可迭代,以及能够使用任意值作为键(除了-0)之外,map还很有用,原因如下:

The spec enforces map operations to be sublinear on average. Any non-stupid implementation of object will use a hash table or similar, so property lookups will probably be constant on average. Then objects could be even faster than maps. But that is not required by the spec. Objects can have nasty unexpected behaviors. For example, let's say you didn't set any foo property to a newly created object obj, so you expect obj.foo to return undefined. But foo could be built-in property inherited from Object.prototype. Or you attempt to create obj.foo by using an assignment, but some setter in Object.prototype runs instead of storing your value. Maps prevent these kind of things. Well, unless some script messes up with Map.prototype. And Object.create(null) would work too, but then you lose the simple object initializer syntax.

其他回答

除了其他答案之外,我还发现map比对象操作起来更笨拙和冗长。

obj[key] += x
// vs.
map.set(map.get(key) + x)

这一点很重要,因为较短的代码读起来更快,表达更直接,并且更好地记在程序员的脑海里。

另一方面:因为set()返回的是映射,而不是值,所以不可能链式赋值。

foo = obj[key] = x;  // Does what you expect
foo = map.set(key, x)  // foo !== x; foo === map

调试地图也更加痛苦。下图中,你实际上看不到地图中的键是什么。你必须编写代码才能做到这一点。

对象可以被任何IDE计算:

对象的行为类似于字典,因为JavaScript是动态类型的,允许您随时添加或删除属性。

但是Map()更好,因为它:

提供get、set、has和delete方法。 接受任何类型的键,而不仅仅是字符串。 提供一个易于for-of使用的迭代器,并维护结果的顺序。 在迭代或复制过程中不会出现原型和其他属性的边缘情况。 支持数百万项。 非常快。

如果需要字典,则使用Map()。

但是,如果您只使用基于字符串的键,并且需要最大的读取性能,那么对象可能是更好的选择。这是因为JavaScript引擎在后台将对象编译为c++类,并且属性的访问路径比Map().get()的函数调用要快得多。

这些类也被缓存,所以创建一个具有完全相同属性的新对象意味着引擎将重用一个现有的后台类。添加或删除属性会导致类的形状改变,并重新编译支持类,这就是为什么将一个对象用作添加和删除大量内容的字典会非常慢,但是在不更改对象的情况下读取现有键会非常快。

因此,如果您有一个写一次读的繁重的工作负载和字符串键,那么您可以使用对象作为高性能的字典,但对于其他所有事情使用Map()。

除了按定义良好的顺序可迭代,以及能够使用任意值作为键(除了-0)之外,map还很有用,原因如下:

The spec enforces map operations to be sublinear on average. Any non-stupid implementation of object will use a hash table or similar, so property lookups will probably be constant on average. Then objects could be even faster than maps. But that is not required by the spec. Objects can have nasty unexpected behaviors. For example, let's say you didn't set any foo property to a newly created object obj, so you expect obj.foo to return undefined. But foo could be built-in property inherited from Object.prototype. Or you attempt to create obj.foo by using an assignment, but some setter in Object.prototype runs instead of storing your value. Maps prevent these kind of things. Well, unless some script messes up with Map.prototype. And Object.create(null) would work too, but then you lose the simple object initializer syntax.

关键的区别是对象只支持字符串和符号键,而映射则支持或多或少的任何键类型。

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.

实际上,在Map和对象之间有一些优点和缺点。对象被紧密地集成到JavaScript的核心中,这使得它们与Map的区别大大超出了关键支持的差异。

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

对象上的污染不仅会使代码变得更烦人、更慢等等,而且还会对安全性产生潜在的影响。

对象不是纯粹的哈希表,但它们正在尝试做更多的事情。你有像hasOwnProperty这样的头痛,不能轻易获得长度(Object.keys(obj).length)等等。对象不是纯粹用作哈希映射,而是用作动态可扩展对象,因此当您将它们用作纯哈希表时,就会出现问题。

各种常用操作比较/列表:

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();

还有一些其他的选项、方法、方法等等,它们有不同的起伏(性能、简洁、可移植、可扩展等)。对象作为语言的核心有点奇怪,所以你有很多静态方法来处理它们。

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.

map的一个重大缺点是JSON不直接支持它们。解析是可能的,但它有几个难题:

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;
});

上述操作将严重影响性能,也不支持任何字符串键。JSON编码甚至更加困难和有问题(这是许多方法之一):

// 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())
    });
};

如果你纯粹使用map,这还不是很糟糕,但是当你混合类型或使用非标量值作为键时就会出现问题(并不是说JSON是完美的,因为它是IE循环对象引用)。我还没有对它进行测试,但与stringify相比,它可能会严重损害性能。

其他脚本语言通常不会有这样的问题,因为它们为Map、Object和Array提供了显式的非标量类型。Web开发通常是非标量类型的痛苦,你必须处理一些事情,比如PHP使用a /M将数组/Map与对象合并为属性,JavaScript将Map/Object与数组合并为扩展M/O。合并复杂类型是高级脚本语言的魔鬼祸害。

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显然在获取和更新方面有很强的优势,但删除性能非常糟糕。在这种情况下,映射使用了少量的内存(开销),但是只有一个对象/Map要测试数百万个键,映射开销的影响没有很好地表达出来。如果我正确阅读配置文件,内存管理对象似乎也更早释放,这可能是有利于对象的一个好处。

在Firefox中,这是一个不同的故事:

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

我应该立即指出,在这个特定的基准测试中,从Firefox中的对象删除不会引起任何问题,但是在其他基准测试中,它会引起问题,特别是当有很多键时,就像在Chrome中一样。在Firefox中,地图对于大型集合来说显然更胜一筹。

然而,这并不是故事的结束,那么许多小物体或地图呢?我已经做了一个快速的基准测试,但不是一个详尽的(设置/获取),它在上面的操作中使用少量的键执行得最好。这个测试更多的是关于内存和初始化。

Map Create: 69    // new Map
Object Create: 34 // {}

这些数字也有所不同,但基本上Object有一个很好的领先。在某些情况下,物体比地图的优势是极端的(10倍),但平均而言,它大约是2-3倍。极端的性能峰值似乎可以双向发挥作用。我只在Chrome和创建中测试了这一点,以配置内存使用情况和开销。我很惊讶地发现,在Chrome中,带有一键的地图使用的内存是带有一键的对象的30倍。

使用以上所有操作(4个键)测试许多小对象:

Chrome Object Took: 61
Chrome Map Took: 67
Firefox Object Took: 54
Firefox Map Took: 139

在内存分配方面,它们在释放/GC方面表现相同,但Map使用了5倍多的内存。这个测试使用了四个键,而在上次测试中,我只设置了一个键,所以这可以解释内存开销的减少。我运行了几次这个测试,Map/Object在整体速度方面对Chrome来说或多或少是不分上下的。在用于小对象的Firefox中,总体上比地图有明显的性能优势。

当然,这还不包括个体选择,因为个体选择可能会有很大的差异。我不建议使用这些数据进行微优化。从中可以得到的经验是,对于非常大的键值存储,更强烈地考虑map,而对于小的键值存储,更强烈地考虑对象。

除此之外,这两者的最佳策略是先实现它,然后让它工作。在分析时,重要的是要记住,有时你认为不会慢的东西,当看到它们时,可能会非常慢,因为引擎的怪叫,如对象键删除情况所见。

Map的一个方面在这里没有给予太多关注,那就是查找。根据规格:

Map对象必须使用哈希表或其他方式实现 平均而言,提供次线性访问时间的机制 关于集合中元素的数量。使用的数据结构 在此Map对象规范中仅用于描述 需要Map对象的可观察语义。这并非有意为之 一个可行的实现模型。

对于具有大量项并需要项查找的集合,这将极大地提高性能。

TL;DR -没有指定对象查找,因此它可以按照对象中元素数量的顺序,即O(n)。映射查找必须使用哈希表或类似的方法,因此无论映射大小如何,即O(1),映射查找都是相同的。