JavaScript 中的地图与对象

IT技术 javascript dictionary ecmascript-6
2021-01-27 05:08:46

我刚刚发现了这个功能

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

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

该文档也没有帮助:

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

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

......对我来说仍然听起来像一个对象,很明显我错过了一些东西。

为什么 JavaScript 会获得(得到良好支持的)Map对象?它有什么作用?

6个回答

根据 Mozilla 的说法:

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

对象与 Maps 的相似之处在于,它们都允许您将键设置为值、检索这些值、删除键以及检测某个键是否存储了某些内容。正因为如此,对象在历史上被用作地图;然而,对象和地图之间有重要的区别,这使得使用地图更好。

一个对象有一个原型,所以地图中有默认的键。但是,这可以使用 map = Object.create(null) 绕过。对象的键是字符串,它们可以是 Map 的任何值。您可以轻松获得地图的大小,而您必须手动跟踪对象的大小。

地图

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

myMap.has(key)方法将特别方便,还有myMap.size财产。

@luxon 你在那里创建一个对象。ES6 规范要求new运算符与Map符号一起使用,new Map用于创建地图对象。var a = {}是(意思相当于)的简写var a = Object.create(Object.prototype)
2021-03-16 05:08:46
据推测,一个缺点是 Map 需要更多内存(但是在同一数量级内)以保持插入顺序。
2021-03-18 05:08:46
我在 Chrome 中的测试表明,地图不会使用更多内存来维持秩序。我认为一百万个密钥多出 0.1KB,我认为这不是为了维持秩序。然而,~0.1KB 似乎是一个恒定的开销。如果你用一个键创建一百万个地图并比较它比对象大得多。
2021-03-21 05:08:46
我没明白意思,你说的时候,一个对象有一个原型,所以地图中有默认的键。但是,这可以使用 绕过map = Object.create(null)什么是默认键?键是如何关联的Object.prototype
2021-04-01 05:08:46
Maps 除了这里提到的有序性之外还有其他特性(使用任何对象作为键,键和道具的分离等),但是 FWIW 在某些情况下,纯对象属性的迭代顺序是由 ES2015 定义的。请参阅stackoverflow.com/a/32149345
2021-04-05 05:08:46

主要区别在于对象仅支持字符串和符号键,而地图或多或少支持任何键类型。

如果我这样做obj[123] = true,那么Object.keys(obj)我会得到["123"]而不是[123]. Map 将保留键的类型并返回[123],这很好。地图还允许您使用对象作为键。传统上,要做到这一点,您必须为对象提供某种唯一标识符来对它们进行哈希处理(我认为我从未getObjectId在 JavaScript 中看到任何类似的东西作为标准的一部分)。地图还可以保证顺序的保存,因此在保存方面更全面,有时可以节省您需要做的一些事情。

在实践中,地图和对象之间有几个优点和缺点。对象的优点和缺点都非常紧密地集成到 JavaScript 的核心中,这使它们与 Map 的显着区别超出了关键支持的差异。

一个直接的优势是您对对象具有语法支持,从而可以轻松访问元素。您还可以通过 JSON 直接支持它。当用作散列时,获得一个完全没有任何属性的对象是很烦人的。默认情况下,如果您想将对象用作哈希表,它们将被污染,并且您hasOwnProperty在访问属性时通常必须调用它们。您可以在此处查看默认情况下对象如何被污染以及如何创建希望未受污染的对象以用作散列:

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

还有一些其他选项、​​方法、方法等,它们有不同的起伏(性能、简洁、便携、可扩展等)。对象作为语言的核心有点奇怪,因此您有很多静态方法可以使用它们。

除了 Maps 保留键类型的优点以及能够支持对象作为键之类的东西之外,它们还与对象具有的副作用隔离开来。Map 是一个纯粹的散列,尝试同时成为一个对象没有任何混淆。地图也可以通过代理功能轻松扩展。Object 目前有一个 Proxy 类,但是性能和内存使用情况很糟糕,事实上,创建自己的代理,看起来像 Map for Objects 目前比 Proxy 性能更好。

Maps 的一个重大缺点是它们不直接支持 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())
    });
};

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

其他脚本语言通常没有这样的问题,因为它们具有显式的 Map、Object 和 Array 非标量类型。对于非标量类型,Web 开发通常很痛苦,您必须处理诸如 PHP 将 Array/Map 与 Object 合并使用 A/M 作为属性以及 JavaScript 将 Map/Object 与 Array 扩展 M/O 合并之类的事情。合并复杂类型是高级脚本语言的恶魔。

到目前为止,这些主要是围绕实现的问题,但基本操作的性能也很重要。性能也很复杂,因为它取决于引擎和使用情况。对我的测试持保留态度,因为我不能排除任何错误(我必须赶这个)。您还应该运行自己的测试来确认,因为我只检查了非常具体的简单场景,以便仅给出粗略的指示。根据 Chrome 中对非常大的对象/地图的测试,对象的性能更差,因为删除显然与键的数量而不是 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 显然在获取和更新方面具有很强的优势,但删除性能却很糟糕。在这种情况下,地图使用了少量更多的内存(开销),但是只有一个对象/地图使用数百万个键进行测试,因此无法很好地表达地图开销的影响。如果我正确读取配置文件,内存管理对象似乎也会更早地释放,这可能是有利于对象的一个​​好处。

在 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 使用了五倍的内存。这个测试使用了四个键,而在上一个测试中我只设置了一个键,所以这可以解释内存开销的减少。我运行了几次这个测试,就整体速度而言,地图/对象或多或少与 Chrome 并驾齐驱。在 Firefox for small Objects 中,整体上比地图有明显的性能优势。

这当然不包括可能变化很大的个别选项。我不建议对这些数字进行微观优化。您可以从中得到的是,根据经验,对于非常大的键值存储和对象对小键值存储更强烈地考虑映射。

除此之外,这两个的最佳策略是实施它并使其首先起作用。在分析时,重要的是要记住,有时在查看它们时您认为不会很慢的事情可能会非常慢,因为在对象键删除案例中看到的引擎怪癖。

花了那么毫秒(something take 是说使用过的东西的缩写,所以在这种情况下它会用完时间)。虽然这是一个旧的测试,我没有基准代码了。现在可能大不相同了。例如,我认为删除问题已解决。
2021-03-17 05:08:46
尽管 aMap可以将任何值作为键,但键查找的语义使用对象引用相等,而不是使用值语义,这会导致问题
2021-03-27 05:08:46
对于许多开发人员来说,缺乏可序列化性一直是一个真正的痛苦。看看我如何在本地存储(或其他地方)中持久化 ES6 地图?以及你如何 JSON.stringify 一个 ES6 Map?.
2021-03-28 05:08:46
是毫秒数、字节数还是对象总数?
2021-04-02 05:08:46

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

Map()要好得多,因为它:

  • 提供getsethasdelete方法。
  • 接受任何类型的键,而不仅仅是字符串。
  • 提供迭代器以方便for-of使用并维护结果的顺序。
  • 在迭代或复制期间没有出现原型和其他属性的边缘情况。
  • 支持数百万项。
  • 非常快。

如果您需要字典,请使用Map().

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

这些类也被缓存,因此创建具有完全相同属性的新对象意味着引擎将重用现有的背景类。添加或删除属性会导致类的形状发生变化并重新编译后备类,这就是为什么将对象用作具有大量添加和删除的字典非常慢,但读取现有键而不更改对象非常快。

因此,如果您有使用字符串键的一次性写入读取繁重的工作负载,那么您可以将 anobject作为高性能字典使用,但对于其他所有内容,请使用Map().

@Andrew 我正在研究这些方法,根据您使用的内容和结果,功能也有所不同。迭代更容易,因为原型和本机属性不会出现在循环中,而是使用保持相同顺序的普通 JS 迭代器。
2021-03-13 05:08:46
Object 也提供了get set has delete等功能,只是不太优雅(但也不错)。哪种方式Map更容易用于迭代?不确定我能同意。
2021-03-25 05:08:46
这现在已被直接报告给 DEV,称其为抄袭。让我们看看他们是否认真对待抄袭(Quora肯定不会)。
2021-03-27 05:08:46
@IdoBleicher 原因列在答案中。地图在功能上更容易使用,遵循预期的行为,而且通常更快。此外,它们不是对对象的实现,而是一个单独的数据结构,这就是它与引擎支持相关联的原因。有什么你想更清楚的吗?
2021-03-28 05:08:46
这个答案的一部分被DEV抄袭,在Why to use Maps over Objects in JS ? [原文如此](接近“仅使用基于字符串的键并且需要最大读取性能”)。
2021-04-10 05:08:46

我认为到目前为止答案中没有提到以下几点,我认为它们值得一提。


地图可以更大

在 Chrome 中,我可以获得1670万个键/值对,Map普通对象则为1110万个。带有Map. 它们在崩溃之前都占用了大约 2 GB 的内存,所以我认为这可能与 chrome 的内存限制有关(是的,尝试填充 2Maps并且在崩溃之前你只能得到 830 万对)。您可以使用此代码自行测试(显然,单独运行它们而不是同时运行它们):

var m = new Map();
var i = 0;
while(1) {
    m.set(((10**30)*Math.random()).toString(36), ((10**30)*Math.random()).toString(36));
    i++;
    if(i%1000 === 0) { console.log(i/1000,"thousand") }
}
// versus:
var m = {};
var i = 0;
while(1) {
    m[((10**30)*Math.random()).toString(36)] = ((10**30)*Math.random()).toString(36);
    i++;
    if(i%1000 === 0) { console.log(i/1000,"thousand") }
}

对象已经有一些属性/键

这个以前让我绊倒了。有规则物体的有toStringconstructorvalueOfhasOwnPropertyisPrototypeOf和一堆其他已存在的属性。对于大多数用例来说,这可能不是一个大问题,但它之前给我带来了问题。

地图可能会更慢:

由于.get函数调用开销和缺乏内部优化,对于某些任务,Map可能比普通的旧 JavaScript 对象得多。

关于现有属性,它们不只是对象原型的一部分吗?也就是说,它们不是不同的属性。
2021-03-12 05:08:46
我肯定会用普通的旧物件去,如果你是罚款1100万键/值对,并不在乎像预先存在的钥匙toStringconstructor等(即你的密钥是非常不可能的碰撞与他们)。它们更容易使用 - 例如 increment is obj[i] = (obj[i] || 0) + 1,而使用Mapit'smap.set(i, (map.get(i) || 0) + 1)仍然不是太糟糕,但它只是表明事情会变得不必要地混乱。地图肯定有它们的用例,但通常一个普通的对象就可以了。
2021-03-20 05:08:46
请注意,您可以摆脱违约toStringconstructor写,(等)对象的属性obj = Object.create(null),而不是obj = {}
2021-03-29 05:08:46
在您看来,语义是否超过了性能?如果您需要字典,地图听起来很完美,但很难接受较慢的查找。快速查找不是字典的全部内容吗?
2021-04-05 05:08:46

概括:

  • Object:一种数据结构,其中数据以键值对的形式存储。在对象中,键必须是数字、字符串或符号。值可以是任何其他对象、函数等。对象是无序的数据结构,即不会记住键值对的插入顺序
  • ES6 Map:一种数据结构,其中数据以键值对的形式存储。其中唯一键映射到值键和值都可以是任何数据类型地图是一种可迭代的数据结构。这意味着插入的顺序会被记住,我们可以在例如for..of循环中访问元素

主要区别:

  • AMap是有序且可迭代的,而对象是无序且不可迭代的

  • 我们可以将任何类型的数据作为Map键,而对象只能以数字、字符串或符号作为键。

  • AMap继承自Map.prototype. 这提供了各种实用功能和属性,使处理Map对象变得更加容易。

例子:

目的:

let obj = {};

// adding properties to a object
obj.prop1 = 1;
obj[2]    =  2;

// getting nr of properties of the object
console.log(Object.keys(obj).length)

// deleting a property
delete obj[2]

console.log(obj)

地图:

const myMap = new Map();

const keyString = 'a string',
    keyObj = {},
    keyFunc = function() {};

// setting the values
myMap.set(keyString, "value associated with 'a string'");
myMap.set(keyObj, 'value associated with keyObj');
myMap.set(keyFunc, 'value associated with keyFunc');

console.log(myMap.size); // 3

// getting the values
console.log(myMap.get(keyString));    // "value associated with 'a string'"
console.log(myMap.get(keyObj));       // "value associated with keyObj"
console.log(myMap.get(keyFunc));      // "value associated with keyFunc"

console.log(myMap.get('a string'));   // "value associated with 'a string'"
                         // because keyString === 'a string'
console.log(myMap.get({}));           // undefined, because keyObj !== {}
console.log(myMap.get(function() {})) // undefined, because keyFunc !== function () {}

来源:MDN