我在用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
~