我在用map和set的时候,一直有点迷,所以写一篇来专门记录一下吧~
# Set
类数组,但它没有重复的元素,所以可以把它称之为集合
关键词: 无重复,无序
Set 没有 value 只有 key,value 就是 key —— 所以keys()和values()方法的行为完全一致
# 使用
如果你想用set,就new一个它来使用,同时可以往构造函数里传入一个一维数组来进行初始化👇
var a = new Set([1, 2, 3, { 1: "2" }, ["3", "4"]]);
// a: Set(5) { 1, 2, 3, { '1': '2' }, [ '3', '4' ] }
console.log("a: ", a);
这样你传入的一维数组就变成了一个Set类型的对象
# API
只记录一些常用的方法👇
add(value):添加某个值,返回 Set 结构本身has(value):返回一个布尔值,表示该值是否为Set的成员delete(value):删除某个值,返回一个布尔值,表示删除是否成功
let set = new Set();
set.add(1);
set.add(2);
console.log("set: ", set);
// set: Set(2) { 1, 2 }
if (set.has(1)) {
console.log("set中有1");
}
// set中有1
if (!set.has(3)) {
console.log("set中没有3");
}
// set中没有3
set.delete(1);
if (!set.has(1)) {
console.log("set中没有1");
}
// set中没有1
虽然遍历键名和键值是两者方法,但是结果是一样的,因为set只有键值 —— 键名就是键值
var a = new Set([1, 2, 3, { 1: "2" }, ["3", "4"]]);
// a: Set(5) { 1, 2, 3, { '1': '2' }, [ '3', '4' ] }
for (let i of a.keys()) {
console.log(i);
}
for (let i of a.values()) {
console.log(i);
}
/*
1
2
3
{ '1': '2' }
[ '3', '4' ]
*/
如果你就想拿到键值对形式的 Set,那就调用它的entries方法 —— 得到一个Set Iterator对象
for (let i of a.entries()) {
console.log(typeof i);
}
/*
[ 1, 1 ]
[ 2, 2 ]
[ 3, 3 ]
[ { '1': '2' }, { '1': '2' } ]
[ [ '3', '4' ], [ '3', '4' ] ]
*/
# Map
类对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键,是一种更完善的 Hash 结构实现
如果你需要“键值对”的数据结构,Map 比 Object 更合适
集合和字典的比较
- 共同点:集合、字典都可以存储不重复的值
- 不同点:集合是以**[值,值]的形式存储元素,字典是以[键,值]**的形式存储
# 使用
也是直接new,通常不需要初始化(如果非要初始化需要传进去一个二维数组),用的时候往里边set就可以
const map = new Map();
map.set("0", 0);
map.set("1", 1);
map.set("2", 2);
console.log("map: ", map);
// map: Map(3) { '0' => 0, '1' => 1, '2' => 2 }
const m = new Map([
[0, "first"],
[1, "second"],
[2, "three"],
]);
console.log("m: ", m);
// m: Map(3) { 0 => 'first', 1 => 'second', 2 => 'three' }
# API
也是只记录最常用的API
set(key, val): 向字典中添加新元素get(key): 通过键值查找特定的数值并返回
const map = new Map();
map.set(0, "first");
map.set(1, "second");
map.set(2, "three");
console.log("map: ", map);
// map: Map(3) { '0' => 0, '1' => 1, '2' => 2 }
console.log(map.get(0));
// first
这里最需要注意的就是一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的给覆盖掉
const map = new Map();
map.set(0, "first");
map.set(0, "second");
console.log("map: ", map);
// map: Map(2) { 0 => 'second'}
# 与对象之间的转换
Map其实和对象非常像,而且有时候这两者使用起来并没有什么区别
但是在一些场景中,我们是要知道二者的区别的,具体的内容可以参考MDN (opens new window)~😄
这里我只简单总结几个常见的地方👇
- 默认情况下,
Map是不包括任何键的,当我们初始化时传入空时,Map就是空的,但是Object不简单,它是有原型的 ——Object.prototype,原型上默认是存在着一些键(属性)的 Map更安全,Object可能会被攻击原型Map的键名可以是任何类型的值,但是Object对象的键名必须是string或Symbol
# WeakMap
WeakMap 的 key 只能是 Object 类型
在 JS 里,Map的 API 是通过公用两个数组(一个存放键,一个存放值)来实现的,比如当使用Set的时候,会同时将键和值添加到这两个数组的末尾,从而使得键和值的索引在两个数组中相对应。当使用 get 时,需要遍历存放键的数组,然后使用索引从存储值的数组中检索出相应的值。
但这样的实现会有两个很大的缺点:
- 首先赋值和搜索操作都是
O(n)的时间复杂度(n是键值对的个数),因为这两个操作都需要遍历整个数组来进行匹配 - 另外一个缺点是可能会导致内存泄漏,因为数组会一直引用着每个键和值。这种引用使得垃圾回收算法不能回收处理他们,即使它们没用了
相比之下,原生的 WeakMap 持有的是每个键对象的“弱引用”,这表示如果没有其他引用存在了,那 WeakMap 保存的值就会被垃圾回收。原生 WeakMap 的结构是特殊且有效的,其用于映射的 key 只有在其没有被回收时才是有效的
因为是弱引用,所以 WeakMap 的 key 是不可枚举的,也就是无法通过for...in或其他方法来获得所有的 key,因为如果是可枚举的话,加上会受到 GC 的影响,就会得到不确定的结果
# Vue3使用WeakMap
Vue3 中的响应式,就是使用了 WeakMap 来缓存了 reactive 封装的 Proxy 对象
// packages/reactivity/src/reactive.ts
export const reactiveMap = new WeakMap<Target, any>()
export const shallowReactiveMap = new WeakMap<Target, any>()
export const readonlyMap = new WeakMap<Target, any>()
export const shallowReadonlyMap = new WeakMap<Target, any>()
使用reactive
// 此处的{ count: 1 }为WeakMap的键名,p为WeakMap的值
// new WeakMap({count: 1}, p)
var p = reactive({ count: 1 });
effect(function() {
console.log(p.count);
});
可以看到 ☝️ 我们的变量 p没有对原对象进行引用,而是对封装后的 Proxy 进行了引用
同时,原对象只被Proxy对象引用,且在模块的 reactiveMap变量中以弱引用的方式被缓存。如果Proxy对象不可用,被 GC 回收了,则{ count: 1 }这个对象也应该会被 GC 回收。这么做的好处就是,响应式系统不用维护原始对象的引用,只用维护Proxy对象就好了。能有效的防止内存泄漏的问题
# 拓展
可以自己实现一个Set和Map~
