ES6 —— Set and Map

2023/8/7

我在用mapset的时候,一直有点迷,所以写一篇来专门记录一下吧~

# 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对象的键名必须是stringSymbol

# WeakMap

WeakMapkey 只能是 Object 类型

JS 里,MapAPI 是通过公用两个数组(一个存放键,一个存放值)来实现的,比如当使用Set的时候,会同时将键和值添加到这两个数组的末尾,从而使得键和值的索引在两个数组中相对应。当使用 get 时,需要遍历存放键的数组,然后使用索引从存储值的数组中检索出相应的值。

但这样的实现会有两个很大的缺点:

  1. 首先赋值和搜索操作都是 O(n) 的时间复杂度(n 是键值对的个数),因为这两个操作都需要遍历整个数组来进行匹配
  2. 另外一个缺点是可能会导致内存泄漏,因为数组会一直引用着每个键和值。这种引用使得垃圾回收算法不能回收处理他们,即使它们没用了

相比之下,原生的 WeakMap 持有的是每个键对象的“弱引用”,这表示如果没有其他引用存在了,那 WeakMap 保存的值就会被垃圾回收。原生 WeakMap 的结构是特殊且有效的,其用于映射的 key 只有在其没有被回收时才是有效的

因为是弱引用,所以 WeakMapkey不可枚举的,也就是无法通过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对象就好了。能有效的防止内存泄漏的问题

# 拓展

可以自己实现一个SetMap~

我的实现

How to love
Lil Wayne