本文作为 ES6 入门第十三章的学习整理笔记, 可能会包含少部分个人的理解推测, 若想阅读更详细的介绍, 还请阅读原文 ES6 入门 http://es6.ruanyifeng.com/#docs/set-map
一, set 数据结构
1.set 不接受重复值
ES6 新增了 Set 构造函数用于创建 set 数据结构, 这种结构类似于数组, 但有很大的一个区别就是, set 数据结构不接受重复值, 每个值都是唯一的.
我们可以通过 Set 构造函数快速创建一个 set 数据结构, 顺便打印看看究竟长什么样:
- let s = new Set();
- console.dir(s);
那么可以看到, set 实例具有一个 size 属性, 因为我们还未给此结构添加值, 所以是 0, 类似于数组的 length 属性.
set 实例还有很多方法, 例如 add 添加, clear 清除, 还有在数组拓展中已经介绍过的 keys,values 等比较熟悉的方法, 这些后面具体再说.
我们尝试在 new 命令时直接初始化值:
let s = new Set([1,2,1,3]);
可以看到, 尽管我添加了两个数字 1, 最终的 set 实例结构中只有一个不重复的 1, 这是因为 set 不接受重复的值, 自带去重效果.
你可能看过以下数组去重的快捷方法, 正式利用的 set 的这一特点:
- // 数组去重
- [...new Set([1, 1, 2, 3, 4, 4])];
- Array.from(new Set([1, 1, 2, 3, 4, 4]));
2.set 实例的增删改查方法
add 方法: 添加某个值, 返回添加值后的 set 解构, 类似数组的 push, 后添加的元素在 set 解构后面.
- let s = new Set();
- s.add(1).add(2);
has 方法: 查找 set 解构是否包含某值, 返回一个布尔值.
- s.has(1); //true
- s.has(3); //false
delete 方法: 删除某个值, 返回一个布尔值对应是否删除成功.
- s.delete(1);//true
- s.delete(1);//false
clear 方法: 清除整个 set 解构, 无返回值.
s.clear();
3.set 的遍历方法
keys 方法: 遍历元素的键名
values 方法: 遍历元素的键值
entries 方法: 遍历元素的键值对
forEach 方法: 用的贼多, 回调函数遍历每个元素
在数组拓展这一章节中也有介绍这三个方法, 这里就简单说下; 三个方法都是结合 for...of 循环使用, 分别遍历元素的 key,value 与 key/value 组合.
- let s = new Set([{a:1}, {b:2}, {c:3}]);
- for (let item of s.keys()) {
- console.log(item);// {a:1}, {b:2}, {c:3}
- };
- for (let item of s.values()) {
- console.log(item);// {a:1}, {b:2}, {c:3}
- };
- for (let item of s.entries()) {
- console.log(item);// [{a:1},{a:1}],[{b:2},{b:2}],[{c:3},{c:3}]
- };
通过上述代码中的输出可以了解到, keys 方法与 values 方法执行完全相同, 这是因为 set 解构没有 key 名导致, key 名与 value 相同; 而 entries 方法每次返回的是一个包含了 key 与 value 的数组.
当我们想遍历出 set 解构的每个元素理论上使用 values 方法, 有趣的是 set 解构的默认遍历器刚好与 values 相等, 所以我们甚至能省略掉 values 方法直接遍历解构中的每个元素.
- let s = new Set([1, 2, 3]);
- Set.prototype[Symbol.iterator] === Set.prototype.values; //true
- // 省略 values 方法
- for(let item of s){
- console.log(item);//1 2 3
- };
与数组中使用这三个方法的区别在于, 数组中的 keys 遍历的是元素的下标, values 相同, entries 是下标和元素组成键值对, 且不是数组.
当我们使用 forEach 遍历 set 结构数据时, 回调参数三个参数的前两个完全相同, 这也是因为 key 名与 key 值相同的缘故, 这点需要注意.
- let s = new Set([1, 2, 3]);
- s.forEach((val,key) => console.log(val,key))//1 1,2 2,3 3
4.set 解构的作用
a. 数组去重, 主要利用了 set 不接受重复值做参数的特点.
b.set 结构实现并集, 简单点说, 就是把两个 set 重复项去掉, 原理还是利用 set 不接受重复项
- let a = new Set([1, 2, 3]);
- let b = new Set([2, 3, 4]);
- let s1 = new Set([...a, ...b]); //set {
- 1,2,3,4
- }
c.set 结构实现交集, 原理是利用了 set 实例的 has 方法
let s2 = new Set([...a].filter(x => b.has(x)))//set {2,3}
d.set 结构实现差集, 同理利用了 has 方法
let s3 = new Set([...a].filter(x => !b.has(x)))//set {1}
你的直觉是不是这里应该是{1,4}, 这里的差集其实是 a 里面有且 b 里面没有的元素, 而不是 ab 互相没有.
二, WeakSet 结构
WeakSet 数据结构与 Set 类似, 也不接受重复的值, 但也有三点不同, 一是 WeakSet 解构的成员只能是对象, 二是 WeakSet 中的对象都是弱引用, 三是 WeakSet 无法遍历.
1.WeakSet 成员只能是对象
- let s = new WeakSet();
- s.add([{
- a:1
- },{
- b:2
- }]);
- console.dir(s);
- s.add(1);// 报错 Invalid value used in weak set
创建 WeakSet 结构可通过 new 命令完成, WeakSet 接受任何含有 Iterable 接口的对象作为参数. 可以看到当我们 add 非对象元素, 该操作报错, 但是 add 添加对象没问题.
那么我们看这段代码, 为什么报错了:
let s = new WeakSet([1,2,3]);
我在前面你说了, WeakSet 的每个成员必须是对象, 前面我们使用的是 add 方法, 每次添加都是一个成员, 这是直接使用 new 初始化, 虽然传递的参数是数组, 但本质上等同于:
- let s = new WeakSet();
- s.add(1).add(2).add(3);
所以我们需要保证数组中的每个元素也是对象, 这样就不会报错了:
let s = new WeakSet([{a:1},{b:2}]);
其次可以看到 WeakSet 方法并不多, add,has,delete 三个, 用法和 set 相同, 这里就不重复介绍了.
2.WeakSet 结构成员均为弱引用
我们都知道, 当一个对象不被任何地方引用, 垃圾回收机制就会释放掉这个对象所占用内存. 我们在前面说 WeakSet 的成员都是对象, 但是垃圾回收机制不考虑 WeakSet 的引用.
说直白点, 现在对象 a 被 A 和 WeakSet 同时引用, A 不再引用了垃圾回收机制就直接释放了, 完全不管 WeakSet 还在引用它.
也正是因为 WeakSet 成员是弱引用的原因, 我们无法保证什么时候成员就被释放了, 所以 WeakSet 没有 size 属性, 也不可遍历.
三, map 数据结构
1. 基本用法与增删改查方法
传统意义上的对象都是键值对组成的集合, 键为字符串, 值为一个对象, 我们是无法使用对象作为键的.
但 Map 打破了这个规则, 我们可以通过 Map 创建键值都是对象的数据结构, 这样键不再是作为保存值的存在, 在遍历时, 键值都可以是有效的对象.
- let m = new Map();
- console.dir(m);
从上图中, 可以看到百分之 80 的方法与 Set 数据结构完全相同, 只是多了一个 set 方法和 get 方法.
set(key,value)方法: 按照 key/value 添加成员, 返回 Map 结构, 支持链式写法; 如果 key 已存在, 则覆盖.
get(key)方法: 按照 key 查找返回对应的 value, 如果未找到, 返回 undefined.
has(key)方法: 查找是否包含某个 key, 返回一个布尔值.
delete(key)方法: 删除对应的 key, 返回一个布尔值, 表示是否成功删除.
clear()方法: 清空整个 Map 数据结构.
- let m = new Map();
- let o = {
- name:'echo'
- };
- m.set(o,{
- age:26
- });
- m.get(o);//{
- age:26
- }
- m.has(o);//true
- m.delete(o);//true
- m.has(o);//false
那么在上述代码中, 我们为 map 数据结构添加了一个 key 为 {name:'echo'} 值为 {age:26} 的成员.
同时我们可以通过 get 指定的 key 访问到对应的 value,delete 还是一样返回是否删除成功, has 依旧是判断该数据结构是否含有此成员.
添加成员当然不要求通过 set, 在 new 命令执行时, 我们可以以一个数组的形式传递需要添加的成员.
- let m = new Map([
- ['name', '听风是风'],
- ['age', 26]
- ]);
- m.has('name') //true
- m.get('name') // 听风是风
- m.has('age') //true
- m.get('age') //26
其实初次看到这我是有点懵逼的, 为什么我一个数组成员的两个元素, 成了 Map 数据结构中一个成员的 key 与 value. 其实这个不难理解, 它等同于以下的执行:
- let arr = [
- ['name', '听风是风'],
- ['age', 26]
- ];
- let m = new Map();
- arr.forEach(([key, value]) => m.set(key, value))
数组每个元素又是一个双元素数组, 前者作为 map 的 key, 后者作为 map 的 value
需要注意的是, map 数据结构同样不接受重复的值作为成员, 这里的重复是指 key 名相同, 如果相同, 后者会覆盖前者:
- const m = new Map([
- ['name', 1],
- ['name', 2]
- ]);
- console.log(m);//key:name value:2
除此之外, 当我们 map 的 key 是对象时, 需要注意对象引用的问题:
- let o = {
- name:1
- };
- let m = new Map();
- m.set(o,2)
- console.log(m.get(o));//2
- m.set({
- name:1
- },2)
- console.log(m.get({
- name:1
- }));//undefined
在上述代码中, 如果我们直接将 {name:1} 作为 key 用于存值, 在 set 执行时, 无法拿到对应的 value, 这是因为对象尽管写法相同, 但仍然是完全不同的两个东西;
所以在需要将对象做 key 时, 请将此对象赋予一个变量, 利用此变量作为 key 进行存储, 在读取时再次读取这个变量, 就可以避免这个问题了.
其实说到这里, 关于 map 的 key, 其实是跟内存地址相关. 如果 key 是一个简单数据类型, 那么只要两个 key 完全相等, 就视为一个 key, 且后者覆盖前者, 如果不相等, 则反之.
如果 key 是一个对象, 想正确的存取, 请将对象赋予给一个变量再做 set 操作. 否则会因为引用地址问题无法访问到你已经添加的 key.
2.Map 数据结构的遍历方法
keys()方法: 遍历并返回键名
- let m = new Map([
- ['name', '听风是风'],
- ['age', 26]
- ]);
- for(let key of m.keys()){
- console.log(key);//name age
- };
values()方法: 遍历并返回键值
- for(let value of m.values()){
- console.log(value);// 听风是风 26
- };
entries()方法: 遍历返回所有成员, 注意, 我没说这里是返回键值对
- for(let item of m.entries()){
- console.log(item);
- };
如上图, 返回两个数组, 每个数组分别包含了 key 和 value, 所以如果我们想直接访问 key,value, 应该这么写:
- for(let [key,value] of m.entries()){
- console.log(key,value);//name 听风是风, age 26
- };
还记得在介绍 Set 数据结构是, 我们说 Set 的默认遍历器接口等于 values 方法, 所以我们可以简写遍历, 比较好运的是, Map 数据结构的默认遍历器接口等于 entries 方法, 所以我们还可以继续简写:
- m[Symbol.iterator] === m.entries; //true
- for (let [key, value] of m) {
- console.log(key, value);//name 听风是风, age 26
- };
forEach 方法, 通过回调参数也可以方便的访问到 Map 结构的 key 与 value
m.forEach((value, key, m) => console.log(value, key));// 听风是风 name,26 "age"
四, WeakMap 数据结构
WeakMap 与 Map 结构类似, 但也有两点不同, 一是 WeakMap 成员的 key 只接受对象:
- let m = new WeakMap();
- m.set('name',1);// 报错
二是 WeakMap 的键名所引用对象为弱引用, 也就是不计入垃圾回收机制, 这点与 WeakSet 一致.
- let m = new WeakMap();
- let ele = document.querySelector("#div");
- m.set(ele, '这是一个 div 元素');
- m.get(ele); // 这是一个 div 元素
在上述代码中, 我们先是将获取的 dom 存在了 ele 变量中, 此时对于 div dom 的引用次数是 1 次.
然后我们又将 ele 作为 key, 为这个 ele 添加了一些说明, 照理说, div dom 此时又被 WeakMap 结构引用, 所以 div 引用次数是 2 次.
但由于 WeakMap 的 key 名对象是弱引用, 所以这里 div 一共的引用此事还是 1 次. 当我们让 ele 不再引用 div 元素时, 垃圾回收机制不会考虑 WeakMap 对于 div 的引用, 而是直接释放, 这点其实与 WeakSet 是保持一致的.
强调一点的是, WeakMap 弱引用的是 key, 而不是 value, 这里有个例子:
- let m = new WeakMap();
- let key = {
- };
- let value = {
- a: 1
- };
- m.set(key, value);
- value = null;
- m.get(key) //{
- a:1
- }
即便我们将 WeakMap 中 value 所引用的对象释放, 其实垃圾回收机制还是将 WeakMap 的引用计为 1 次, 所以还能正常读取到.
因为 key 是弱引用的缘故, 所以与 WeakSet 一样, 不存在遍历方法.
WeakMap 结构最大的一个用处就是用于保存 dom, 这样 dom 元素被删除也不会造成内存泄漏问题:
- let ele = document.getElementById('logo');
- let fn = function () {
- console.log(1)
- };
- let m = new WeakMap();
- // 将 dom 元素与需要执行的函数作为 WeakMap 结构的 key 与 value
- m.set(ele, fn);
- // 为 dom 元素增加监听
- ele.addEventListener('click', function () {
- // 执行监听函数
- m.get(ele)();
- }, false);
关于 WeakMap 这里就不多做介绍了, 至少我目前开发基本使用不到.....
不只是是 WeakMap,Set 与 Map 的使用概率基本很低, 这里就纯做一个整理了, 日后万一用到, 或者说使用逐渐普及, 也方便查找.
那么就写到这里了.
来源: https://www.cnblogs.com/echolun/p/11001138.html