我刚刚发现了这个特点:

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

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

文档也没有帮助:

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

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

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

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


当前回答

这两个技巧可以帮助你决定是使用Map还是Object:

当键在运行时之前未知时,以及当 所有键都是相同的类型,所有值都是相同的类型。 如果需要将原始值存储为键,则使用映射 因为object将每个键都视为字符串,要么是数字值, 布尔值或任何其他基本值。 当存在操作单个元素的逻辑时使用对象。

来源:键集合

其他回答

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

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还是Object:

当键在运行时之前未知时,以及当 所有键都是相同的类型,所有值都是相同的类型。 如果需要将原始值存储为键,则使用映射 因为object将每个键都视为字符串,要么是数字值, 布尔值或任何其他基本值。 当存在操作单个元素的逻辑时使用对象。

来源:键集合

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

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

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

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

什么时候使用映射而不是简单的JavaScript对象

纯JavaScript对象{key: 'value'}保存结构化数据。但是普通的JavaScript对象有其局限性:

Only strings and symbols can be used as keys of Objects. If we use any other things, say, numbers as keys of an object then during accessing those keys we will see those keys will be converted into strings implicitly causing us to lose consistency of types. const names= {1: 'one', 2: 'two'}; Object.keys(names); // ['1', '2'] There are chances of accidentally overwriting inherited properties from prototypes by writing JavaScript identifiers as key names of an object (e.g., toString, constructor, etc.) Another object cannot be used as key of an object, so no extra information can be written for an object by writing that object as key of another object and value of that another object will contain the extra information Objects are not iterators The size of an object cannot be determined directly

对象的这些局限性可以通过映射来解决,但我们必须将映射视为对象的补充而不是替代。基本上Map只是数组的数组,但我们必须将该数组作为参数传递给Map对象,并使用new关键字,否则仅对于数组数组,Map的有用属性和方法不可用。记住数组中的数组或Map中的键值对必须用逗号分隔,不能像普通对象那样用冒号分隔。

决定使用Map还是Object的三个技巧

当键在运行时之前是未知的时,使用map而不是对象,因为由用户输入形成的键或在不知情的情况下,如果这些键覆盖了对象的继承属性,则会破坏使用对象的代码,因此map在这种情况下更安全。当所有键都是相同类型且所有映射都是相同类型时,也要使用映射。 如果需要将基本值存储为键,则使用映射。 如果需要对单个元素进行操作,则使用对象。

使用地图的好处

1. Map接受任何键类型,并保留键类型:

我们知道,如果对象的键不是字符串或符号,那么JavaScript会隐式地将其转换为字符串。相反,Map接受任何类型的键:字符串、数字、布尔值、符号。等,Map保留原来的键类型。在这里,我们将在Map中使用number作为键,它将保持为数字:

    const numbersMap= new Map();
    numbersMap.set(1, 'one');
    numbersMap.set(2, 'two');
    const keysOfMap= [...numbersMap.keys()];

    console.log(keysOfMap);                        // [1, 2]

在Map中,我们甚至可以使用整个对象作为键。有时,我们希望存储一些与对象相关的数据,而不将这些数据附加到对象本身中,以便我们可以使用精简对象,但希望存储关于对象的一些信息。在这种情况下,我们需要使用Map,这样我们就可以将Object作为键,并将对象的相关数据作为值。

    const foo= {name: foo};
    const bar= {name: bar};
    const kindOfMap= [[foo, 'Foo related data'], [bar, 'Bar related data']];

但是这种方法的缺点是通过键访问值的复杂性,因为我们必须遍历整个数组才能获得所需的值。

    function getBy Key(kindOfMap, key) {
        for (const [k, v]  of kindOfMap) {
            if(key === k) {
                return v;
            }
        }
        return undefined;
    }
    getByKey(kindOfMap, foo);            // 'Foo related data'

我们可以通过使用适当的Map来解决不能直接访问值的问题。

    const foo= {name: 'foo'};
    const bar= {name: 'bar'};
    const myMap= new Map();
    myMap.set(foo, 'Foo related data');
    myMap.set(bar, 'Bar related data');

    console.log(myMap.get(foo));            // 'Foo related data'

我们本可以用WeakMap来实现,只需要写入const myMap= new WeakMap()。Map和WeakMap之间的区别在于,WeakMap允许键(这里是对象)的垃圾收集,因此它可以防止内存泄漏,WeakMap只接受对象作为键,并且WeakMap减少了方法集。

2. Map对键名没有限制:

对于普通的JavaScript对象,我们可能会意外地覆盖从原型继承的属性,这可能是危险的。在这里,我们将重写actor对象的toString()属性:

    const actor= {
        name: 'Harrison Ford',
        toString: 'Actor: Harrison Ford'
    };

现在让我们定义一个函数isPlainObject(),来确定所提供的参数是否是一个普通对象,这个函数使用toString()方法来检查它:

    function isPlainObject(value) {
        return value.toString() === '[object Object]';
    }

    isPlainObject(actor);        // TypeError : value.toString is not a function

    // this is because inside actor object `toString` property is a
    // string instead of inherited method from prototype

Map对键名没有任何限制。虽然actorMap对象有一个名为toString的属性,但我们可以使用toString()方法继承自actorMap对象的原型,工作完美。

    const actorMap= new Map();
    actorMap.set('name', 'Harrison Ford');
    actorMap.set('toString', 'Actor: Harrison Ford');
    function isMap(value) {
      return value.toString() === '[object Map]';
    }

    console.log(isMap(actorMap));     // true

如果我们遇到用户输入创建键的情况,那么我们必须在Map中而不是普通对象中获取这些键。这是因为用户可以选择自定义字段名,如toString、构造函数等,那么在普通对象中这样的键名可能会破坏以后使用该对象的代码。所以正确的解决方案是将用户界面状态绑定到map上,没有办法破坏map:

    const userCustomFieldsMap= new Map([['color', 'blue'], 
            ['size', 'medium'], ['toString', 'A blue box']]);

3.Map是可迭代的:

要迭代一个普通对象的属性,我们需要object .entries()或object .keys()。object .entries(plainObject)返回一个从对象中提取的键值对数组,然后我们可以解构这些键和值,并得到正常的键和值输出。

    const colorHex= {
      'white': '#FFFFFF',
      'black': '#000000'
    }

    for(const [color, hex] of Object.entries(colorHex)) {
      console.log(color, hex);
    }
    //
    'white' '#FFFFFF'
    'black' '#000000'

由于Map是可迭代的,这就是为什么我们不需要entries()方法来遍历Map和析构键,值数组可以直接在Map上完成,因为在Map中每个元素都是由逗号分隔的键值对数组。

    const colorHexMap = new Map();
    colorHexMap.set('white', '#FFFFFF');
    colorHexMap.set('black', '#000000');

    for(const [color, hex] of colorHexMap) {
      console.log(color, hex);
    }
    //'white' '#FFFFFF'   'black' '#000000'

map.keys()返回键的迭代器,map.values()返回值的迭代器。

4. 我们很容易知道地图的大小

我们不能直接确定一个普通对象中属性的数量。我们需要一个helper函数,比如object. keys(),它返回一个包含对象键的数组,然后使用length属性,我们可以获得键的数量或普通对象的大小。

    const exams= {'John Rambo': '80%', 'James Bond': '60%'};
    const sizeOfObj= Object.keys(exams).length;
    console.log(sizeOfObj);       // 2

但在地图的情况下,我们可以直接访问地图的大小使用地图。大小属性。

    const examsMap = new Map([['John Rambo', '80%'], ['James Bond', '60%']]);

    console.log(examsMap.size);

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

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

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

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

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

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

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