我刚刚发现了这个特点:

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

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

文档也没有帮助:

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

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

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

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


当前回答

什么时候使用映射而不是简单的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);

其他回答

除了按定义良好的顺序可迭代,以及能够使用任意值作为键(除了-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.

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

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

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

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

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

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

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

根据MDN:

Map对象可以按插入顺序迭代其元素——for..of循环每次迭代将返回一个[key, value]数组。

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

按顺序迭代是开发人员一直想要的功能,部分原因是它可以确保在所有浏览器中都具有相同的性能。所以对我来说,这是一个大问题。

myMap.has(key)方法将特别方便,还有myMap.has(key)方法。大小属性。

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

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

来源:键集合

TL;DR:你不能在对象中使用键' length'来存储值,但在Map中可以。

对于我来说,不使用object而使用Map的实际考虑是,从实际的角度来看,object的键不是字符串或数字,而是字符串或数字的子集。具体来说,使用与原型的属性名或众所周知的属性冲突的键可能会导致问题。例如,存储具有键长度的值可能会使使用length键来确定给定对象是否为数组的代码混淆。