JavaScript中的Set和Map数据结构

在ES6中,JavaScript提供了两个新的数据结构Set和Map,使用它们可以在一些场景下方便很多。

本笔记摘自《ECMAScript6标准入门》第11章。

Set

Set代表集合,类似于数组,但数组中元素的值都是唯一的。Set本身是一个构造函数,用于生成一个Set数据结构,可以传入一个数组或者其他具有iterable接口的其他变量作为参数用于初始化,当然,仍然会保证Set不重复。

1
2
let s = new Set([1,1,2,2,3,3,4,4]);
// Set(4) {1, 2, 3, 4}

一行快速去重:[...new Set(arr)]

另一种写法:Array.from(new Set(arr))

可以使用.add方法向其中添加一个元素。使用.size方法返回元素个数。向Set中加入值时不会发生类型转换,所以不用担心数字变成字符串。在Set内部判断两个值相同的方式叫做Same-value equality,类似于精确相等运算符===,但和它也有一些区别,主要的区别在于在Same-value equality会认为NaN等于它本身,而===不会认为NaN === NaN。此外,在Set中,两个对象总是认为不相等的。

目前还暂时不能在遍历操作中直接改变Set的值,但可以使用一些变通的方式,比如在遍历时使用原Set造出一个新Set并迭代赋值。

Set的常用属性和方法

  • Set.prototype.constructor,就是Set本身(构造函数)。
  • Set.prototype.size,返回元素个数。
  • add(val)加入某个值,返回Set结构本身。
  • delete(val),删除某个值,返回bool表示删除是否成功。
  • has(val)判断某个值是否存在。
  • clear(),清空Set。
  • keys()返回键名的iterator,values()方法和它几乎完全一致,它返回键值的iterator。
  • entries()返回键值对的iterator。
  • forEach()使用回调函数对每个kv值进行操作,回调函数里需要两个参数分别代表k和v。
  • .map().filter()这两种数组的操作对set也适用。
1
2
3
let s = new Set('mon', 'tue', 'wed');
for (let item of s.entries()) {}
s.forEach((k, v) => {})

Set本身就是集合,使用Set可以实现并集,交集,差集。

1
2
3
4
5
6
7
8
let a = new Set([1,2,3]);
let b = new Set([4,3,2]);

let union = new Set([...a, ...b]);

let intersect = new Set([...a].filter(x => b.has(x)));

let difference = new Set([...a].filter(x => !b.has(x)));

WeakSet

与Set类似,也是值不重复的集合,和Set的区别是:

  • WeakSet的成员只能是对象,不能是其它类型的值。
  • WeakSet中的对象都是弱引用,也就是垃圾回收机制不考虑WeakSet对此对象的引用。换种方法解释就是,如果WeakSet引用的对象已经不被其他任何对象引用了,它就会被回收,WeakSet也访问不到了。

它的第二个区别很重要,这直接导致了它是不可遍历的。因为在它里面存放的对象有可能随时消失,所以在里面的元素可能遍历前一半时还在,后一遍就没了。所以它只适合临时存放一些对象,或者存放DOM结点。

它的构造函数和Set类似,可以传入一个数组,但这个数组里面的成员必须全都是对象。

常用方法:add,delete,has,和Set基本相同,WeakSet没有size属性。

Map

众所周知,JavaScript中的对象是一种键值对的集合,类似于Hash的结构,但是它的键只能是字符串,这有时候会带来一些限制。

Map对象类似于对象,但键的范围被扩大到多种类型,也就是将”串-值“结构升级成了”值-值“结构。

它的构造函数接受一个数组作为参,任何具有iterator接口并且每个成员都是一个双元素数组的数据结构都可以作为它的参。也就是说,Set和Map都可以用来生成新的Map。

1
2
3
4
5
6
7
8
let s = new Set({
['foo', 1],
['bar', 2]
});

let m = new Map(s);

let m1 = new Map([['baz', 3]]); // 是的,得套两层,里面那层才是内容

对同一个键多次赋值,后面的将覆盖前面的。什么是同一个键?对同一个对象的引用才是同一个键,必须内存地址一样才可以,内存地址不一样的,就算值一样也不认为是同一个键。

1
2
3
4
5
let m = new Map();

map.set(['a'], 123);

map.get(['a']); // 输出undefined,因为'a'和'a'不是同一个'a'

Map的键和内存地址绑定,只要内存地址不一样,就是两个键。这个特性彻底解决了同名属性碰撞的问题。

那如果Map的键是简单类型呢?只要严格相等就行,0-0也视为同样的。特殊的,Map也将多个NaN视为同一个键。

吐槽:所以NaN为什么不等于NaN?鬼知道当时他们怎么想的..

Map的常用属性和方法

  • Map.prototype.size属性返回成员数量。
  • Map.set(k,v),插入一个键值对,然后返回整个Map,如果已经有这个k值,则更新v。
  • Map.get(k)获得k对应的v,如果找不到k,返回undefined。
  • Map.has(k)判断这个k是否在这个Map里,返回bool。
  • Map.delete(k)删除k对应的kv对,删除成功返回true,否则返回false。
  • Map.clear()清除所有成员。
  • keys()返回键名的iterator,values()方法和它几乎完全一致,它返回键值的iterator。
  • entries()返回键值对的iterator。
  • forEach()使用回调函数对每个kv值进行操作,回调函数里需要两个参数分别代表k和v。

Map可以使用展开运算符快速转为数组,和Set差不多。将数组传入Map构造函数也可以构造Map,二者可以互相转化。

如果Map所有的键都是字符串,就可以转化成对象,但是这个得手写。

将对象转化为Map就是逆操作,但也得手写。

Map转JSON可以直接使用JSON.stringify,这种情况适用于k都是字符串。如果k里面有非字符串,需要转化为数组JSON,like this:JSON.stringify([...m]);

JSON转Map比较好办,毕竟正常情况下所有键名都是字符串。在特殊情况下,JSON满足数组Map结构时,可以一个一个转化为Map。

WeakMap

和Map类似,生成键值对集合。

使用set方法添加成员,接受一个数组作为构造函数的参数。但WeakMap只接受对象和null作为键名,这和Map不同。

重点来了,WeakMap的键名指向的对象不计入垃圾回收机制。它的键名引用的对象都是弱引用,也就是垃圾回收机制不考虑这个引用。这句话怎么解释呢?

比如有一个需求是,我想对两个对象通过某个结构添加一些其他信息,然后这个结构就必然会对这两个对象形成一个引用,在使用结束之后,垃圾回收机制不会自动回收这两个对象,因为它们现在还有一个引用在联系,所以此时需要先手动删除这个引用的结构才可以。但如果使用WeakMap进行弱引用,垃圾回收机制就不会考虑这个引用占用的联系,一旦我不需要这两个对象了,垃圾回收会自动把这俩对象收回,即便WeakMap还留着对它的引用。

使用场景:在网页DOM元素上添加数据时使用WeakMap,当该DOM被清除,对应的WeakMap记录就会被自动清除。注册监听事件的listener对象可以用WeakMap实现,它和绑定的DOM建立联系,一旦DOM消失了,事件也就跟着没了。

WeakMap依旧不支持遍历和获得大小。只能使用get,set,has,delete。

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2018-2023 Shawn Zhou
  • Hexo 框架强力驱动 | 主题 - Ayer
  • 访问人数: | 浏览次数:

感谢打赏~

支付宝
微信