Javascript中,可以在任何地方使用Object,但是仅仅可以使用并不意味着就一定要使用。
// 🚩
const mapOfThings = {}
mapOfThings[myThing.id] = myThing
delete mapOfThings[myThing.id]
例如,如果您在JavaScript中使用对象来存储任意键值对,并经常在对象中添加或删除key,那么您应该真正考虑使用Map而不是plain Object。
// ✅
const mapOfThings = new Map()
mapOfThings.set(myThing.id, myThing)
mapOfThings.delete(myThing.id)
使用objects的性能问题
对于object,delete运算符因性能差而臭名昭著, 上面的例子中,使用maps可以提升性能, 并且在其他一些情况下可以获得非常大的性能提升。
上面的对比图是一个 基准案例 (运行在 Chrome v109版本, Core i7 MBP). 你也可以对比这个案例 另一个基准 来自 Zhenghao He. 考虑一下 — 类似上面的基准是 众所周知的不完美的 所以还应该保持一个怀疑态度。
你不需要相信我或者任何人的基准, 在 MDN Maps的优化案例中 有如下描述:
(Map在在频繁添加和删除键值对的情况下,性能更好。Object未针对频繁添加和删除键值对进行优化。)
如果你好奇为什么,这与JavaScriptVM如何通过假设JS Object的形状来优化JS对象有关,而Map是专门为哈希映射的用例构建的,其中键是动态和不断变化的。
除了性能,map还可以解决Object存在的几个问题。
Built-in keys problem(内置属性问题)
一个主要的问题就是:在object刚创建好时,就已经被很多内置属性和方法污染了。
const myMap = {}
myMap.valueOf // => [Function: valueOf]
myMap.toString // => [Function: toString]
myMap.hasOwnProperty // => [Function: hasOwnProperty]
myMap.isPrototypeOf // => [Function: isPrototypeOf]
myMap.propertyIsEnumerable // => [Function: propertyIsEnumerable]
myMap.toLocaleString // => [Function: toLocaleString]
myMap.constructor // => [Function: Object]
所以即使你创建一个空的对象,这些方法也已经被创建了。所以当使用Object实现一个任意key值的hashMap会导致很多问题,后面会有相关介绍。
笨拙的迭代
JavaScript对象处理键的方式非常奇怪,遍历Object充满了陷阱。
例如,您可能已经知道不要这样做:
for (const key in myObject) {
// 🚩 你可能会发现一些你无意中继承的key值
}
你可能被告知要这样做:
for (const key in myObject) {
if (myObject.hasOwnProperty(key)) {
// 🚩
}
}
但是这也是有问题的, 例如 myObject.hasOwnProperty 函数也是可以被重载的,比如 myObject.hasOwnProperty = () => explode().
因此,这才是你应该做的:
for (const key in myObject) {
if (Object.prototype.hasOwnProperty.call(myObject, key) {
// 😕
}
}
或者,如果您希望代码更加简洁,可以使用最近添加的Object.hasOwn:
for (const key in myObject) {
if (Object.hasOwn(myObject, key) {
// 😐
}
}
或者你可以放弃使用 for 循环,转而使用 Object.keys 和 forEach.
Object.keys(myObject).forEach(key => {
// 😬
})
但是,如果使用Maps,以上问题都不存在. 你可以使用标准的 for 循环,使用标准迭代器和一个非常好的析构函数模式,可以同时获得key和value:
for (const [key, value] of myMap) {
// 😍
}
事实上,这非常好,我们现在有一个Object.entries方法来处理对象。
for (const [key, value] of Object.entries(myObject)) {
// 🙂
}
使用Map,也可以做到
for (const value of myMap.values()) {
// 🙂
}
for (const key of myMap.keys()) {
// 🙂
}
key值顺序
Map中的键以简单、直接的方式排序:Map对象按条目插入顺序排列entry、key和value。
这给了我们另一个非常酷的特性,那就是我们可以直接从地图中按键的精确顺序销毁键:
// 这个方法就可以获取到myMap的第一个插入的key和value值
const [[firstKey, firstValue]] = myMap
// 获取多个
const [[firstKey, firstValue],[secondKey, secondValue], [thirdKey, thirdValue]] = myMap
复制
你可能会说,Object的复制非常简单
const copied = {...myObject}
const copied = Object.assign({}, myObject)
但是Maps的复制也很简单
const copied = new Map(myMap)
这个能生效的原因是 Map 通过 [key, value] 元组(tuples)构造. 相反,Map也可以通过[key, value]的元组结构进行遍历.
而且,也可以像Object一样,通过 structuredClone进行深拷贝:
const deepCopy = structuredClone(myMap)
Maps和Object的相互转化
Maps转Object可以通过 Object.fromEntries实现(这个方法还挺好用的):
const myObj = Object.fromEntries(myMap)
Object转Maps, 使用 Object.entries:
const myMap = new Map(Object.entries(myObj))
Key 类型
可以使用各种数据结构当作key值
myMap.set({}, value)
myMap.set([], value)
myMap.set(document.body, value)
myMap.set(function() {}, value)
myMap.set(myDog, value)
WeakMaps
我们可以使用 WeakMap 类型. weakMap通过使用弱引用(weak reference),解决内存泄露的问题.
当其他引用被释放后, weakMap中的对象将会被垃圾回收机制回收
const metadata = new WeakMap()
// ✅ 没有内存泄露, myTodo 将自动释放当不存在其他的引用时
metadata.set(myTodo, {
focused: true
})