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有两个区别:
- weakSet不能被遍历
- 他的成员都是弱引用,随时可能被垃圾回收给回收,所以他不能被遍历
所以,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的区别有以下几点:
- WeakMap只支持对象作为key值(null除外)
- 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个方法可用:
- get
- set
- delete
- 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
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据