这两个达到的效果是差不多的,但他们在语法上却不一样,Map
的语法看上去更符合函数式开发的风格,而 Object
的语法看上去更像是命令式的代码。我们再来看看他们在相同操作下执行 100000
次的情况下的性能差异:
除了这种测试方式之外,我还找到了另一种测试方式(example benchmark
)。当然了,这种基准测试的结果是不可靠的,因为它们依赖于 JavaScript 引擎的实现,也同时和浏览器的版本有关系,客户端的机器配置也会影响到测试结果,所以我们只能用它们来做一个参考。也就是说,你完全可以不相信任何人的测试结果,因为在 MDN
上的 Map 一节中也有提到过 Map
在频繁增删键值对的场景下是有特殊优化的,而 Object
则没有。
Map | Object | |
---|---|---|
省略… | 省略… | 省略… |
Performance | Performs better in scenarios involving frequent additions and removals of key-value pairs.(在频繁增删键值对的场景下表现更好。) | Not optimized for frequent additions and removals of key-value pairs.(在频繁添加和删除键值对的场景下未作出优化。) |
当然如果是只有这一点微乎其微的性能,那可能很难说服你使用 Map
,但接下来的内容可能可以改变你的想法。
这里的 obj
和 map
都有一个 toString
属性,但是它们的值是不一样的,obj
的 toString
属性是一个字符串,而 map
的 toString
属性是一个函数,对于 obj
来讲,自行设置的 toString
会覆盖原型链上的属性,而 map
中设置的 toString
键却不会。我认为仅此一点就足以说明 Map
的优势了,因为 Map
中的键值对是不会被覆盖的,而 Object
中的键值对则会被覆盖。
在 JS 中迭代对象的时候,我们通常会使用 for...in
循环,但是这种方式有一个陷阱,就是它会遍历原型链上的属性,而 Map
中的 for...of
循环则不会遍历原型链上的属性,所以 Map
更加安全。
但这么做依然是有问题的,因为 hasOwnProperty
方法也是从原型链上继承来的,没人能保证 hasOwnProperty
方法不会被覆盖。所以你最终可能需要这么做:
当然如果你不想这么麻烦,则可以完全放弃 for...in
循环,进而采用 Object.keys
和 forEach
:
但 Map
就不存在这些问题,因为它的 for...of
循环不会遍历原型链上的属性,所以你可以放心的使用 for...of
循环来遍历 Map
。
在 JS 中,对象的键值对是无序的,但 Map
中的键值对是有序的,这意味着你可以通过 Map
来实现一个有序的对象。
在 JS 中,对象的拷贝是浅拷贝,而 Map
的拷贝是深拷贝,这意味着你可以通过 Map
来实现一个深拷贝的对象。
简简单单对吧,但其实 Map 也很容易实现拷贝功能:
而之所以可以这么做,主要是因为 Map
的构造函数接收了一个可迭代的 [[key, value]]
元组,所以 Map
也可以接收一个 Map
实例,这样就可以实现深拷贝了。当然,Map
也是支持使用浏览器内置原生支持的 structuredClone
方法来实现深拷贝的,但因为兼容性的问题,所以这里就不展开了。语法如下:
在你理解了这种转换欢喜以后,你就可以用对象的形式来构造一个 Map
了:
如果你需要使用 TypeScript
,那你可以这么做:
在 JS 中,对象的键名只能是字符串或者 Symbol,而 Map
的键名可以是任意值,这意味着你可以使用 Map
来实现一个类似于 WeakMap
的功能,但是 Map
可以使用任意值作为键名,而 WeakMap
只能使用对象作为键名。
这适用于想将数据扁平化从而建立一种数据联结的场景,你可以以整个对象作为键名,同时将对象的某个属性作为键值,这样你就既可以使用对象来获取键值,也可以使用键值来获取对象了。
当然,Map 上还有很多有用的属性:
size
:返回 Map
中的键值对的数量。clear()
:移除 Map
对象中的所有键值对。keys()
:返回一个新的 Iterator
对象,它按插入 Map
对象中的顺序包含 Map
对象中每个元素的键。values()
:返回一个新的 Iterator
对象,它按插入 Map
对象中的顺序包含 Map
对象中每个元素的值。entries()
:返回一个新的 Iterator
对象,它按插入 Map
对象中的顺序包含 Map
对象中每个元素的 [key, value]
数组。forEach()
:对 Map
对象中的每个键值对执行指定的操作。当我们讨论 Map
时,我们也可以讨论一下 Set
,因为它们是一对好基友,它们的功能也是相似的,但是 Set
只有键名,没有键值,所以 Set
中的键名和键值是相同的。
在某些情况下,Set
可以完全替代 Array
做一些等效操作,且拥有 更好的性能。当然这种结果可能并不准确,所以你可以自己测试一下。同样的,我们可以使用 WeakSet
来帮我们解决内存泄漏的问题。
Map
和 Set
都是可序列化的,这意味着你可以使用 JSON.stringify
来序列化它们,但是你需要注意的是,Map
和 Set
中的键名和键值都必须是可序列化的,否则就会抛出错误。但是你有没有注意到,如果你想要打印出带有缩进格式或者其他任意风格的 JSON 时,你总是需要添加一个 null
作为参数,而这个 null
被称之为替换器。如果你想要使用替换器,你可以使用 JSON.stringify
的第二个参数来指定替换器,或者使用第三个参数来指定缩进空格的数量。这两个参数都是可选的,但是如果你想要使用第三个参数,你就必须使用第二个参数来指定替换器,即使你不需要使用替换器。
我们可以写一个转换器来解析 Map 和 Set,这样我们就可以使用 JSON.stringify
来序列化它们了,同时增加一个特殊属性来标识它们的类型。
最后,我们再来讨论一下,在什么时候、什么场景该用哪一个。
Object
。Map
。Set
。WeakSet
。WeakMap
。Array
。Array
,因为 Set
是无序的。以上。