1. Map
通常我们是使用对象来表示键值对关系,如下:
let obj = {
name: '李四',
age: 18,
desc: '一个憨批'
};
对于大部分情况,使用对象已经足够了。但是对象的键名只能使用字符串,其余类型会被转为字符串。
let student1 = {
name: '李四',
age: 18
};
let student2 = {
name: '李四',
age:19
};
let score = {
[student1]: 79,
[student2]: 89
};
// {[object Object]: 89}
如上,对象被转为字符串,所以score
中只有一个元素,前面的student1
被后面的覆盖了。为了解决这种情况,我们可以使用Map
。
let student1 = {
name: '李四',
age: 18
};
let student2 = {
name: '李四',
age:19
};
let score = new Map([
[student1, 79],
[student2, 89]
]);
此时score
如下:
这时我们发现两个学生的成绩被分开了。这是因为Map
的键名可以是任意类型的值,JavaScript
内部使用SameValueZero比较操作
来区分不同的键。基本相当于使用严格对象相等的标准来检测。接下来介绍Map
的语法。
Map
和伪数组类似,存在一个属性用来表示其长度。
let map = new Map([
['元素1', 1],
['元素2', 2]
]);
map.size; // 2
Map
使用set
添加元素,使用get
获取元素,使用delete
删除元素,使用has
判断Map
中是否存在某个键值对,使用clear
清空Map
let map = new Map();
map.set('元素1', 70);
map.get('元素1'); // 70
map.delete('元素1'); // true
map.set('元素2', 80);
map.has('元素2'); // true
map.has('元素3'); // false
map.clear(); // 此时map为空
Map
原型上有keys()
、values()
和entries()
方法,他们返回一个迭代器。因此可以使用for...of...
方法进行遍历。
let map = new Map([
['元素1', 'one'],
['元素2', 'two'],
['元素3', 'three']
]);
for (const key of map.keys()) {
console.log(key);
}
// 元素1
// 元素2
// 元素3
for (const value of map.values()) {
console.log(value);
}
// one
// two
// three
for (const item of map.entries()) {
console.log(item);
}
// ['元素1','one']
// ['元素2','two']
// ['元素3','three']
由于Symbol.iterator
引用entries
,因此还可以这么使用:
for (const item of map[Symbol.iterator]()) {
console.log(item);
}
map.entries === map[Symbol.iterator]
// ['元素1','one']
// ['元素2','two']
// ['元素3','three']
// true
结合解构,可以这么用:
for (const [key, value] of map.entries()) {
console.log(`key: ${key} <-----> value: ${value}`);
}
// key: 元素1 <-----> value: one
// key: 元素2 <-----> value: two
// key: 元素3 <-----> value: three
因为entries
是默认迭代器,因此可以直接这么写:
for (const [key, value] of map) {
console.log(`key: ${key} <-----> value: ${value}`);
}
// key: 元素1 <-----> value: one
// key: 元素2 <-----> value: two
// key: 元素3 <-----> value: three
也可以解构Map
[...map]
// [["元素1", "one"]
// ["元素2", "two"]
// ["元素3", "three"]]
也可以使用forEach
遍历,用法和数组一样
map.forEach((value, key)=> {
console.log(value, key);
});
// one 元素1
// two 元素2
// three 元素3
可以使用二维数组初始化Map
。数组元素第一项为键,第二项为值。
new Map([
['one', 1],
['two', 2]
])
如果键名为对象,那么即使修改了对象属性,也不会影响其在Map
映射中的位置。
let map = new Map();
let obj = { a: 1 };
map.set(obj, '值');
obj.a = 2;
map.get(obj); // 值
需要注意的事,在Map
中,如果两个键名都为NaN
,那么Map
中只会存储一个,后一个会覆盖前一个的值。
let map = new Map([
[NaN, 1],
[NaN, 2]
]);
map.forEach((value, key) => {
console.log(value, key);
});
// 2 NaN
Map
和Object
的区别
Object
是无序的,Map
循环时会有序输出- 给定内存的情况下,使用
Map
可以比Object
多存储大约50%
的键值对 Map
和Object
的插入性能差不多,但是涉及大量插入操作时Map
性能更佳。Map
和Object
的查找性能差不多,但是如果数据量小则Object
更快,另外在将Object
键名为连续整数时,浏览器可以进行优化,此时Object
性能更好。Object
的删除操作不推荐使用delete
(性能不好),但是可以放心的使用delete
删除Map
中的元素,且在Map
中删除的性能比查找和插入更好。因此大量删除操作使用Map
更好。
2. WeakMap
WeakMap
和Map
最大的区别在于垃圾回收机制对其的态度。在Map
中,即使作为键名的对象被解除引用了,在Map
中仍然存在这个值:
let map = new Map();
let obj = {
o: { a: 1 }
};
map.set(obj.o, 'obj');
obj.o = null;
console.log(map);
而WeakMap
是弱映射
,它的键不会计入垃圾回收机制。也就是说,一旦键不再被引用了,则垃圾回收机制会将它回收掉。
let wm = new WeakMap();
let obj = {
o: {
a: '3'
}
};
wm.set(obj.o, '测试');
console.log(wm);
// 删除 obj.o 的引用
obj.o = null;
console.log(wm);
我们发现WeakMap
里面已经清空了。因此,WeakMap
的长度是不确定的,所以他没有size
属性,也不能遍历,没有clear()
方法。WeakMap
有set()
、get()
、has()
、delete()
方法。且WeakMap
里面的键只能为对象类型的,这是因为如果使用基本数据类型,则无法判断初始化时使用的键名和初始化后使用的是否是同一个键名。
WeakMap
的API
是Map
的子集。因此使用方法和Map
相同。
let wm = new WeakMap([])
obj = {};
wm.set(obj, 1);
wm.get(obj); // 1
wm.has(obj); // true
wm.delete(obj); // true
WeakMap
的一个应用是用于实现私有变量。
const User = (function () {
const wm = new WeakMap();
class User {
constructor(id) {
this.idProperty = Symbol(id);
this.setId(id);
}
setId(id) {
this.setPrivate(this.idProperty, id);
}
getId() {
return this.getPrivate(this.idProperty);
}
setPrivate(property, id) {
const privateMembers = wm.get(this) || {};
privateMembers[property] = id;
wm.set(this, privateMembers);
}
getPrivate(property) {
return wm[this][property];
}
}
return User;
})();
WeakMap
还可以用于存储DOM
节点。这样在删除DOM
节点后则WeakMap
里面存储的键值对也会被垃圾回收机制回收。
const li = document.getElementsByTagName[0],
wm = new WeakMap();
wm.set(li, li.innerHTML);
document.removeChild(li);
如上,如果li
节点被删除了,则wm
中的键值对也被清空了。这样不需要手动清空,避免了内存浪费。