木灵鱼儿

木灵鱼儿

阅读:686

最后更新:2021/04/04/ 2:40:08

Set和WeakSet、Map和WeakMap数据结构

Set 基本用法

es6提供了一种新的数据结构Set,他是一个构造函数,初始化时可以接收一个参数,这个参数可以是数组,或者是带有iterable接口的数据。

const s = new Set([1,2,3,4,1,2]);
[...s];  //[1,2,3,4]

从这里我们看的出来,set有一个非常棒的特性,就是去重,重复的值会被自动忽略。

而带iterable接口的数据有数组,dom类数组等等,一般常用的就是数组了。

set是如何判断是否相等,用的算法叫Same-value equality;他类似于全等运算符===;但对于NaN的判断略有不同。

在全等中两个NaN是不想等的,但是在set中,NaN会被去重:

const s = new Set([NaN,NaN]);
[...s];  //[NaN]

其他判断和全等相同;

另外注意下两个对象也是不想等的。

const s = new Set([{},{}]);
[...s];  //[{},{}]

Set 实例的属性和方法

Set结构下有一下属性:

  • Set.prototype.constructor: class的构造函数,默认就是Set函数
  • Set.prototype.size: 返回Set实例中成员数量(对应length)
  • add(value):添加某个值,返回Set结构本身
  • delete(value):删除某个值,返回一个布尔值,表示是否删除成功
  • has(value):返回一个布尔值,判断某个值是否存在于Set中
  • clear():清楚所有成员,没有返回值。

constructor就没什么好说的了,class中的构造函数,不用管。

size就和length一样的效果。

const s = new Set([1,2,3,4]);
s.size;  //4

s.add(5);  //Set [1,2,3,4,5]

s.delete(5);  //true
[...s];  //[1,2,3,4]

s.has(2);  //true

s.clear();  
[...s];  // []

感觉has还是挺有用的,不过这个也有点难用的地方,就是你无法像数组那样通过小标直接拿到对应的值。

const s = new Set([1,2,3,4]);
s[0];  //undefined

如果想拿得话,要么丢入数组中,要么通过Set的遍历方法遍历。

Set 遍历操作

Set结构有四个遍历方法:

  • keys():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每个成员

有意思的是,由于Set并没有key值,所以keys()和values()他们返回的结果都是键值数组,所以他俩行为完全一致。

而keys、values、entries他们返回的结果是一个Iterator遍历器,而Iterator是需要使用for...of进行遍历的。

const s = new Set([1,2,3]);

//keys
for(let key of s.keys()){
  console.log(key);
}
//1
//2
//3

//values
for(let val of s.values()){
  console.log(val);
}
//1
//2
//3

entries实际上就是上面两个方法的合并效果:

const s = new Set([1,2,3]);

//keys
for(let keyVal of s.entries()){
  console.log(keyVal);
}
//[1,1]
//[2,2]
//[3.3]

感觉用处也不大,值都一样,合并效果反倒重复了。

事实上Set结构的实例默认可遍历,其默认的遍历器和values是一样的。

Set.prototype[Symbol.iterator] === Set.prototype.values;
//true

所以我们可以省略上面的那种写法,直接for..of这个set实例

const s = new Set([1,2,3]);

//keys
for(let val of s){
  console.log(val);
}
//1
//2
//3

是一个简写了。

而forEach则和数组的forEach行为一致。但是Set没有下标,所以原来index参数变为了key值。

const s = new Set([1,2,3]);

s.forEach((val,key)=>{
  console.log(val,key);
});
//1,1
//2,2
//3,3

forEach还有第三个参数,表示绑定的this对象,老传统了。

遍历的应用

在Set的实例上,我们可以使用...扩展运算符,因为扩展运算符本身也是用的Iterator遍历器,为此我们可以利用这个效果对数组进行快速去重

let arr = [1,1,2,2,3,3];
let unique = [...new Set(arr)];
//[1,2,3]

因为set和数组可以很快的相互转换,所以,我们可以很容易的实现:并集、交集、差集

并集

合并两个数组并去重

let arr1 = [1,2,3];
let arr2 = [1,3,4];

let arr3 = [...new Set([...arr1,...arr2])]

交集

获取set1中于set2相同的部分

let set1 = new Set([1,2,3]);
let set2 = new Set([1,3,4]);

let arr3 = [...new Set([...set1].filter(item=>set2.has(item)))]
//[1,3]

因为要用到has方法,所以先将数组转为set并去重,然后再new Set的参数中,将set1转为数组通过filter方法进行筛选,筛选的结果必须是这个值在set2中存在,这里用了has

然后就行了。

差集

获取set1中于set2不相同的部分,简单就是一个交集的求反

let set1 = new Set([1,2,3]);
let set2 = new Set([1,3,4]);

let arr3 = [...new Set([...set1].filter(item=>!set2.has(item)))]
//[2]

但是目前我们无法在遍历的同时改变被遍历的值,只能通过先转数组,在转回Set的方式。

//map方法
let arr1 = [1,2,3];
let set1 = new Set(arr1.map(item=>item*2));
console.log(set1);
// Set [2,4,6]

//Array.from方法
let arr1 = [1,2,3];
let set2 = new Set(Array.from(arr1,(item)=>item*2));
console.log(set2);
// Set [2,4,6]

Array.from的第二个参数是个函数,用于对数据进行操作。

WeakSet

weakSet结构与set类似,也是不重复的值的集合,weakSet的成员只能是对象,而且不能是其他类型的值。但是,他与set有两个区别:

  1. weakSet不能被遍历
  2. 他的成员都是弱引用,随时可能被垃圾回收给回收,所以他不能被遍历

所以,weakSet常用来保存dom节点,不容易造成内存泄漏。

他的弱引用,既垃圾回收机制不会考虑weakSet的引用,如果其他对象都不引用该对象,几遍weakSet中存在,也会被回收掉。弱引用的原理就是引用次数不会被计算,所以只要对象在外部消失,他在weakSet中的引用也会消失。

语法

const ws = new WrakSet();

他接收一个数组或类数组对象作为参数,并且weakset的成员并不是参数本身,而是参数的成员,加上参数必须是对象,所以,代码如下:

const a = [[1,2],[3,4]];
let ws = new WeakSet(a);  //WeakSet [[1,2],[3,4]]

const b =  [1,2];
let  bws = new WeakSet(b);  //报错:ncaught TypeError: WeakSet value must be an object, got 1

weakSet有三个方法:

  • add 向weakset对象添加参数
  • delete 删除weakSet中指定的成员
  • has 返回布尔值,是否存在某个值

具体用法和set相同,new出后使用。

使用例子:

let foos = new WeakSet();

class Foo {
  constructor() {
    foos.add(this);
  },
  method() {
    if(!foos.has(this)) {
      throw new Error("Foo.prototype.method 只能在Foo的实例上调用");
    }
  }
}

Map

map实际上是对object对象的一个补充,因为{}只能使用string值作为key值,即便有Symbol,我们不能使用一个对象来作为key值。

const a = [1];
const b = {a:2};  //a最终还是被toString方法转为了字符,a不能是对象

而使用map则可以将对象作为key值

const a = [1];
let b = new Map();

b.set(a,2);
b.get(a);  //2
b.has(a);  //true
b.delete(a);  //删除
b.get(a); //undefined

map的方法和set相同,也有这么四个。

当然,map在创建的时候也支持传入参数,但是这个参数必须是一个双元素数组的数据结构

let a = new Map([
  ["key","value"]
])

a.get("key");  //value

这个参数本身也是先new出map,然后for循环数组,set赋值

let a = new Map();
[["key","value"]].forEach([key,value]=> a.set(key,value));

如果我们对同一个键赋值多次,后一个会覆盖前一个

let a = new Map([
  ["key","value"]
])

a.set("key",1)
 .set("key",2);


a.get("key");  //2

key值是一个对象时,只有是同一个对象的引用才能被识别为同一个键值。

let a = new Map();


const b = [1];
const c = [1];
a.set(b,"b").set(c,"c");

a.get([1]);  //undefined
a.get(b);  //b
a.get(c); //c

由上可知,map的键实际上和内存地址绑定的,只要内存中的地址不一样,就视为两个键,这就解决了同名属性碰撞的问题(clash);我们扩展别人的库时,如果使用对象作为键名,就不会发生冲突。

如果map的键是一个简单类型,数字、字符、布尔值、则只要两个值严格相等,map就视为同一个键,包括0和-0;另外NaN严格意义上是不想等的,但是在map中多个NaN对象会被视为同一个键。

let map  = new Map()

map.set(0,123);
map.get(-0);  //123

map.set(NaN,123);
map.set(NaN,456);
map.get(NaN);  //456

map.set(true,123);
map.set(true,456);
map.get(true);  //456


map.set(undefined,123);
map.set(null,456);
map.get(undefined);  //456

实例的属性和操作方法

size属性

返回map结构的成员总数

set(key,val)

set方法设置key对应的键值,然后返回整个map结构,如果已经存在key值,就进行更新键值,可以链式写法使用。

get(key)

读取对应的键值,没有则返回undefined

has(key)

判断是否存在某个键值,布尔值

delete(key)

删除某个键值,返回布尔值,删除成功返回true,失败返回false

clear()

用于清楚map中的所有成员,没有返回值。

遍历的方法

map原生提供了3个遍历器生成函数和一个遍历方法

  • keys() 返回键名数组的遍历器
  • values() 返回键值数组的遍历器
  • entries() 返回所有成员的遍历器
  • forEach 遍历map的所有成员

前三个效果和set一样,只是map的值都是有key的,所以效果更直观一些。其中entries是作为默认遍历器接口,所以我们可以直接通过for of遍历

for(let [key,val] of map) {
  console.log(key,val);
}

map结构有iterator接口,所以也支持...扩展运算符。

[...map]

配合数组的filter和map可以实现过滤和遍历。

[...map].filter([key,val]=> key<3);


[...map].map([key,val]=>{
  return [key*2,"_"+val];
})

forEach方法则和数组的用法相同

map.forEach([val,key,map]=>{
  conselo.log(val,key,map)
})

forEach除了第一个回调函数,第二个参数则是修改this的指向。

map与其他数据的互相转换

map转数组

直接是用扩展运算符

[...map]

数组转map

数组的值是双值数组

const map = new Map([
  ["key","val"]
])

map转对象

需要for循环手动转

function strMapToObj(map){
  let obj = {};
  for(let [key,val] of map) {
    obj[key] = val;
  }
  return obj;
}

strMapToObj(map);

对象转map

也是for循环手动转

function objToMap(obj){
  let map = new Map();
  for(let key of Object.keys(obj)) {
    map.set(key,obj[key]);
  }
  return map;
}

objToMap({key:"val"});

map转json

分两种情况:

一种是,如果key值都是string,那么就先转为obj再json格式化。

另一种是key值是对象,如果转为obj后key值就不对了,所以这种要转为数组,然后再格式化

//string 
let map = new Map().set("key",1);
JSON.stringify(strMapToObj(map));


//obj
let map2 = new Map().set([1],2);
JSON.stringify([...map]);

扩展运算符后,map的键值转成了双值数组。

json转map

也是看两种情况,键名都是string,或者是双值数组的情况

//string 
let map = objToMap(JSON.parse(jsonStringMap));  //先转成obj,在obj转map


//obj
let map2 = new Map(JSON.parse(jsonArrMap));   //双值数组可以直接作为参数

WeakMap

WeakMap的结构和map类似,也是用于生成键值对的集合。和WeakSet一样,也是一个弱引用。

let wm = new WeakMap();
const key = {foo:1};

wm.set(key,2);  // set赋值
wm.get(key); //2

WeakMap和Map的区别有以下几点:

  1. WeakMap只支持对象作为key值(null除外)
  2. WeakMap的key所指向的对象不计入垃圾回收机制

WeakMap的设计目的就在于,当我们想在某个对象上面存放一些数据,但是这会形成这个对象的引用,如:

const s1 = document.getElementById("foo");

const str = [
  [s1,"foo元素"]
]

当我们不需要了,就必须手动删除str数组中的s1对象,否则它将不会被垃圾回收机制回收。

str[0] = null; //手动回收

而WeakMap就是为了解决这个问题而诞生的,他的引用不会被计数,那么当s1没有人使用时,就会被回收,WeakMap并不会阻拦回收。

但是需要注意的是,WeakMap的弱引用只是键名,键值并不是弱引用,所以,即便我们在外部消除了键值的引用,WeakMap内部的引用还是存在的。

const wm = new WeakMap();
let key = {};
let value = {foo:1};

vm.set(key,value);
value = null;
wm.get(key);  //{foo:1}

WeakMap语法

WeakMap没有遍历操作,没有keys()、values()、entries()、也没有size属性,因此没有办法列出所有的键名,因为不确定什么时候键名对象就被回收了,而且无法使用clear()方法清空,因此WeakMap只有4个方法可用:

  1. get
  2. set
  3. delete
  4. has

因为垃圾回收机制不可控,所以无法复现weakMap的实际效果。

WeakMap用途

经典场景就是以dom节点为key值

let myElement = document.getElementById("logo");
let myWeakMap = new WeakMap();

myWeakMap.set(myElement,{timesClicked:0});

myElement.addEventListener("click",function(){
  let logoData = myWeakMap.get(myElement);
  logoData.timesClicked++;
},false)

每当logo的点击事件触发,都会更新WeakMap中dom对应的数据,一但这个dom被删除,相应的数据也会消失。

进一步说,注册事件监听的listener对象也很合适使用WeakMap

const listener = new WeakMap();
listener.set(element1,fn1);
listener.set(element2,fn2);

element1.addEventListener("click",listener.get(element1),false);
element2.addEventListener("click",listener.get(element2),false);

一旦对应的dom元素消失,那么对应的click的回调函数也就没了,那么事件就无法生效。

另一个用处就是部署私有属性

通过将this作为key值传入,然后存放一些方法或者数据属性啥的,当this指向的对象消失了,对应的方法也会消失,即便我们运行了方法,里面也不会发送内存泄漏。

const _counter = new WeakMap();
const _action = new WeakMap();


class Countdown() {
  constructor(counter,action) {
    _counter.set(this,counter);
    _action.set(this,action);
  },
  dec(){
    let counter = _counter.get(this);
    if(counter < 1 ) return;
    counter--;
    _counter.set(this,counter);
    if(counter===0) {
      _action.get(this)();
    }
  }
};

const c = new Countdown(2,()=>{
  console.log("DONE")
});

c.dec();
c.dec(); //DONE

版权申明

本文系作者 @木灵鱼儿 原创发布在木灵鱼儿 - 有梦就能远航站点。未经许可,禁止转载。

关于作者

站点职位 博主
获得点赞 0
文章被阅读 686

相关文章