link:Use Maps More and Objects Less

如果在 JavaScript 中使用对象来存储任意键值对,并且频繁的添加和删除键值时,应该考虑使用 Map 而不是 Object

首先是性能问题,对于 Object的删除操作性能很差,而Map对则进行了优化,在某些情况下可以更快,同时 MDN 本身也指出,与对象相比,Map 专门针对频繁添加和删除进行了优化:JavaScript 标准内置对象 > Map,MDN 上关于 ObjectMap 的对比

MapObject
意外的键Map 默认情况不包含任何键,只包含显示插入的键Object 自身带有一个原型,原型链上的键名可能和你自己在对象上设置的键名冲突
键的类型一个 Map 的键可以是任意值,包括函数、对象或任意基本类型Object 的键必须是一个 String 或者 Symbol
键的顺序Map 中的键是有序的,当迭代的时候,一个 Map 对象以插入的顺序返回键值虽然 Object 的键目前是有序的,但并不总是这样,而且这个顺序是复杂的,最好不要依赖属性的顺序
SizeMap 的键值对个数可以轻易的通过 size 属性获取Object 的键值对个数只能手动计算
迭代Map 是可迭代的,所以可以直接被迭代Object 没有实现迭代协议,所以使用 JS 的 for…of 表达式并不能直接迭代对象,可以使用 Object.keys 或者 Obkect.entries/或者使用 for..in 表达式迭代一个对象的可枚举属性
性能在频繁增删键值对的场景下表现很好在频繁添加和删除键值对的场景下未做优化
序列化和解析没有元素的序列化和解析的支持JSON.stringify() 和 JSON.parse()

除了性能问题之外,Map 还解决了对象存在的其他几个问题:

内置键问题

当你创建一个空Object的时候,虽然它是空的,但是它其实已经有值了,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的时候,很有可能你稍微不注意,就会造成一些严重的错误。

迭代问题

JavaScript中遍历一个对象,充满了陷阱,例如使用 for..in

for (const key in myObj) {
	// 可能会发现一些无意中继承的属性
}

安全一点的做法

for (const key in muObj) {
	if (myObj.hasOwnProperty(key)) {
		// ..
	}
}

如果担心 hasOwnProperty被不小心覆盖,还可以更安全

for (const key in myObj) {
	if(Object.prototype.hasOwnProperty.call(myObject, key)) {
		//...
	}
}

如果觉得写起来繁琐,还可以使用新添加的 Object.hasOwn

for (const key in Object) {
	if(Object.hasOwn(myObject, key)) {
		//...
	}
}

你甚至可以放弃for循环,只使用Object.keys()forEach

Object.keys(myObject).forEach(key => {
	// ...
})

但是对于Map来说,根本不会出现这样的问题,你可以直接迭代

for (const [key, value] of myMap) {
	//...
}

或者直接迭代Map的键和值

for (const value of myMap.values()) {
	//...
}
 
for (const key of myMap.kleys()) {
	//...
}

甚至可以通过Object.entries方法将Object转换为一个Map后再迭代

for (const [key, value] of Object.entries(myObject)) {
	//...
}

键的顺序

Map中建的顺序是按照设置时的顺序排列的,你可以很安全的从Map中解构键

const [[firstKey, firstValye]] = myMap;

复制

Object很容易复制,使用对象扩展符或者Object.assign({}, myObject)
Map同样容易复制

const copied = new Map(myMap)

这是因为Map的构造函数接受了[key, value]元祖的可迭代对象,且Map是可迭代的,类似的,也可以做Map的深度拷贝:

const deepCopy = structuredClone(myMap)

structuredClone()

全局的方法,使用结构化克隆算法将给定的值进行深拷贝,该方法还支持把原始值中的可转移对象转移到新对象,而不是把属性引用拷贝过去。可转移动向与原始对象分别并附加到新对象,他们不可以在原始对象中访问被访问到

Map和 Object 的相互转换

MapObject

const obj = Object.fromEntries(map);

ObjectMap

const map = Object.entries(obj)

使用元祖来构造 Map

const map = new Map([[key, value], [[key, value]]])

或者像创建对象一样创建 Map

const map = new Map(Object.entries({
	key: 'value',
	key2: 'value2'
}))

键可以是任意值

Map中,键的类型不一定必须是String 或者 Symbol,它可以是任意值

myMap.set({}, value)
myMap.set([], value)
myMap.set(document.body, value)
myMap.set(function() {}, value)
myMap.set(myDog, value)

这样又很多好处,但是同时也带来了新的问题

如果 Map的键值是一个引用类型,例如一个对象,那么它可能永远不会被垃圾回收器回收,从而导致内存泄漏

为了避免这种问题,就出现了WeekMap类型,它是一个弱引用类型的数据结构,很好的解决了上面说的内存泄漏问题

WeekMap/WeekSet

WeekMap 持有对象的弱引用 ,当其他对象被删除时,对象将自动被垃圾收回器收集并从这个弱引用中删除。

const metadata = new WeekMap();
 
// 当没有其他引用时,myTodo 将自动从映射中删除
metadata.set(myTodo, {
	focused: true
})

除了Map之外,还有它的好兄弟Set,它提供了一种更好的方式来创建一个唯一的元素列表,在某些情况下,集合可以产生必使用数组的同等操作更好的性能。

同样,Set也有它所对应的弱引用类型WeekSet, 可以帮助我们避免Set内存泄漏.

const checkedTodos = new WeekSet([todo1, todo2, todo3]);

序列化和解析

SetMap本身不支持序列化和反序列化,但是我们可以使用JSON.stringify及它的第二个参数将mapset转换为对象和数组进行序列化

JSON.stringify(obj, (key, value) => {
	// 将 Map 转换为数组
	if (value instanceof Map) {
		return Object.formEntries(value);
	}
 
	// 将 sets 转换为数组
	if (value instanceof Set) {
		return Array.from(value)
	}
	return value;
})

相反我们同样可以使用JSON.parse 及它的第二个参数进行反序列化

JSON.parse(string, (key, value)) {
	if (Array.isArray(value)) {
		return new Set(value);
	}
	if (value && typeof value === 'object') {
		return new Map(Object.entries(value))
	}
	return value;
}

什么时候应该使用 Map/Set

对于具有良好的键集的结构化对象,例如每个 event都应该有一个标题和一个日期,这通常需要一个对象

const event =  {
	title: '',
	date: new Date()
}

当你需要拥有任意数量的键,并且可能需要频繁的添加和删除时,这时候需要考虑使用Map以获得更好的性能和使用体验

const eventMap = new Map()
eventMap.set(event.id, event);
eventMap.delete(event.id)

当需要一个不包含重复元素的数组时,并且数组元素的顺序无关紧要时,考虑使用Set