[TOC]
Set(集合)
ES6提供了新的数据结构Set,它类似于数组,但是成员的值都是唯一的,没有重复的值。 Set与数组的区别:
- Set里面的数据是唯一的,每个元素只能被添加一次,添加重复元素无效。
- Set不能通过索引值来获取数据。
基本用法
Set本身是一个构造函数,用来生成Set数据结构。
new Set([iterable])
Set实例的属性和方法
Set结构的实例有以下属性:
- Set.prototype.constructor:构造函数,默认就是Set函数。
- Set.prototype.size:返回Set实例的成员总数。
Set实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。
操作方法
- add(value):添加某个值,返回Set结构本身(因此可以进行链式调用)。
- delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
- has(value):返回一个布尔值,表示该值是否为Set的成员。
- clear():清除所有成员,没有返回值。
const set = new Set();
// 链式调用
set.add(1).add(2).add(3);
console.log(set.has(1)); // true
console.log(set.has(5)); // false
console.log(set.delete(1)); // true
console.log(set.has(1)); // false
2
3
4
5
6
7
8
遍历方法
- for of
- forEach
const colors = new Set(['red', 'blue', 'green']);
console.log(colors.values()); // SetIterator { 'red', 'blue', 'green' }
// 返回一个迭代器对象
const setIterator = colors.values();
console.log(setIterator.next()); // { value: 'red', done: false }
console.log(setIterator.next()); // { value: 'blue', done: false }
console.log(setIterator.next()); // { value: 'green', done: false }
console.log(setIterator.next()); // { value: undefined, done: true }v
2
3
4
5
6
7
8
9
for (let item of colors) {
console.log(item);
}
colors.forEach((item, key, ownSet) => {
console.log(item, key, ownSet);
});
/*
red red Set { 'red', 'blue', 'green' }
blue blue Set { 'red', 'blue', 'green' }
green green Set { 'red', 'blue', 'green' }
*/
2
3
4
5
6
7
8
9
10
11
12
Set应用
Array.from结合Set实现数组去重
// 可以用来实现数组去重
function dedupe(arr) {
return Array.from(new Set(arr));
}
dedupe([1, 1, 2, 3]) // [1, 2, 3]
2
3
4
5
6
扩展运算符结合Set现数组去重
let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)];
console.log(unique); //[3, 5, 2]
console.log(Array.isArray(unique)); //true
2
3
4
数组的map和filter方法也可以间接用于Set
let set = new Set(['red', 'green', 'blue']);
set = new Set([...set].map(item => item + '-test'));
console.log(set);//Set { 'red-test', 'green-test', 'blue-test' }
let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x % 2) == 0));
// 返回Set结构:{2, 4}
2
3
4
5
6
7
Set实现并集(Union)、交集(Intersect)和差集(Difference)
let s1 = new Set([1, 2, 3]);
let s2 = new Set([2, 3, 4]);
// 并集
let union = new Set([...s1, ...s2]);
console.log(union); //Set { 1, 2, 3, 4 }
// 实现数组并集
let union2 = new Set([...s1, ...s2]);
console.log([...union2], Array.isArray([...union2])); //[ 1, 2, 3, 4 ] true
// 实现交集
let intersect = new Set([...s1].filter(item => s2.has(item)));
console.log(intersect);//Set { 2, 3 }
// 实现数组交集
console.log([...intersect]);//[2, 3]
//实现差集
let diff = new Set([...s1].filter(item => !s2.has(item)));
console.log(diff); //Set { 1 }
//实现数组差集
console.log([...diff]);// [1]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
WeakSet
WeakSet结构与Set类似,也是不重复的值的集合。
Set与WeakSet区别
WeakSet与Set有以下几个区别:
- WeakSet的成员只能是对象,而不能是其他类型的值。
- WeakSet不可以通过for...of(WeakSet没有迭代器对象属性)和forEach(没有forEach方法)来循环。
- 没有clear方法,但是可以自己清除元素,防止内存泄露。
person.add(111); // TypeError: Invalid value used in weak set
// TypeError: person is not iterable
for (let item of person) {
console.log(item);
}
// Uncaught TypeError: person.forEach is not a function
person.forEach(item => {
console.log(item);
});
2
3
4
5
6
7
8
9
10
11
WeakSet解决数组内存泄露问题
let obj = {name: 'lisi', age: 22};
let obj2 = {name: 'wangwu', age: 23};
const personArr = [obj, obj2];
console.log(personArr);
// 虽然这里将obj删除掉了,但是数据personArr中依然保存了对obj的引用,这就导致了内存泄露
obj = null;
console.log(obj);
console.log(personArr);
2
3
4
5
6
7
8
9
数组导致内存泄露:
let obj = {name: 'lisi', age: 22};
let obj2 = {name: 'wangwu', age: 23};
const personWeakSet = new WeakSet([obj, obj2]);
console.log(personWeakSet);
obj = null;
console.log(obj);
console.log(personWeakSet);
2
3
4
5
6
7
8
Map(字典)
Set可以类比于数组,那么Map就可以类比于对象。
JS对象本质上是键值对的集合(Hash结构),但是对象只能用字符串当作键。
const map = new Map([['a', 1], ['a', 20], ['b', 2]]); // 二维数组
// 自动去重
console.log(map); // Map { 'a' => 20, 'b' => 2 }
2
3
const data = {};
const element = document.getElementById('myDiv');
data[element] = 'metadata';
data['[object HTMLDivElement]'] // "metadata"
2
3
4
5
上面代码原意是将一个DOM节点作为对象data的键,但是由于对象只接受字符串作为键名,所以element被自动转为字符串[object HTMLDivElement]
。
const obj = {name: 'lisi'};
const person = {name: 'wangwu'};
const p = {};
// 对象只接受字符串作为键名
// 因此这里obj和person都被自动转换为`[object Object]`,下面的赋值会覆盖上面的,所有对象p其实只有一个属性`[object Object]`
p[obj] = 10;
p[person] = 20;
console.log(p[obj]); // 20
console.log(p); // {[object Object]: 20}
2
3
4
5
6
7
8
9
10
11
12
为了解决这个问题,ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是键的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object结构提供了字符串—值的对应,Map结构提供了值—值的对应,是一种更完善的 Hash 结构实现。
如果你需要键值对的数据结构,Map比Object更合适。
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
2
3
4
5
6
7
8
9
上面代码使用 Map 结构的set方法,将对象o当作m的一个键,然后又使用get方法读取这个键,接着使用delete方法删除了这个键。
对同一个键多次赋值,后面的值将覆盖前面的值
const map = new Map();
map.set(1, 'aaa').set(1, 'bbb');
map.get(1) // "bbb"
2
3
只有对同一个对象的引用,Map结构才将其视为同一个键
const map = new Map();
//这里的set和get方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的,因此get方法无法读取该键,返回undefined
map.set(['a'], 22);
console.log(map.get(['a'])); //undefined
const arr = ['a'];
map.set(arr, 22);
console.log(map.get(arr)); //22
2
3
4
5
6
7
8
9
同理,同样的值的两个实例,在Map结构中被视为两个键:
const map = new Map();
const k1 = ['a'];
const k2 = ['a'];
map.set(k1, 111).set(k2, 222);
map.get(k1) // 111
map.get(k2) // 222
2
3
4
5
6
7
8
9
上面代码中,变量k1和k2的值是一样的,但是它们在 Map 结构中被视为两个键。
由上述例子可知:Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。
这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。
Map结构转为数组结构(使用扩展运算符(...))
// 初始化的时候给Map赋值
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Map循环
const person = new Map();
person.set('lisi', 20);
person.set('wangwu', 21);
person.set('xiaohua', 22);
person.forEach((value, key, ownMap) => {
console.log(value, key, ownMap);
});
for (let item of person) {
const [key, value] = item;
console.log(key, value);
}
2
3
4
5
6
7
8
9
10
11
12
Map应用
记录每个按钮点击的次数。
<body>
<button>btn1</button>
<button>btn2</button>
<button>btn3</button>
<button>btn4</button>
<button>btn5</button>
<script>
const clickCounts = new Map();
const btns = document.querySelectorAll('button');
btns.forEach(btn => {
clickCounts.set(btn, 0);
btn.addEventListener('click', function() {
const val = clickCounts.get(this);
clickCounts.set(this, val + 1);
console.log(clickCounts);
});
});
</script>
</body>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
WeakMap
WeakMap与Map的区别
- WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
- WeakMap不可以通过for...of和forEach来循环
- 没有clear方法,但是可以自己清除元素,防止内存泄露
- WeakMap的键名所指向的对象,不计入垃圾回收机制。
总结:WeakMap的专用场合就是:它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。
let obj = {name: 'lisi', age: 22};
let obj2 = {name: 'wangwu', age: 23};
const map = new Map();
const weakMap = new WeakMap();
map.set(obj2, '222');
weakMap.set(obj, '1111');
console.log(weakMap);
console.log(weakMap.size); // undefined
console.log(map.size); // 1
obj = null; // 这里将obj赋值为null,weakMap中对应的键值也会被清除
obj2 = null;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
WeakMap中的元素如果在其它地方没有被引用的话会被自动清除。
class Person{}
let p = new Person();
// map有引用计数
let map = new Map();
map.set(p, 1);
// 这里并不会将p从内存中清除,可以在控制台的memory面板中查看
p = null;
2
3
4
5
6
7
class Person{}
let p = new Person();
// WeakMap没有引用计数
let map = new WeakMap();
map.set(p, 1);
// 这里会将p从内存中清除,可以在控制台的memory面板中查看
p = null;
2
3
4
5
6
7
WeakMap使用场景
- Map结构的键值必须是对象;
- 在数据集合中的某些数据不可用后,希望可以自动清除相关数据。以达到自动回收,优化内存的目的。
总结
- Set
- 成员唯一、无序且不重复;
- [value, value],键值与键名是一致的(或者说只有键值,没有键名);
- 可以遍历,方法有:add、delete、has。
- WeakSet
- 成员都是对象;
- 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏;
- 不能遍历,方法有add、delete、has。
- Map
- 本质上是键值对的集合,类似集合;
- 可以遍历,方法很多可以跟各种数据格式转换。
- WeakMap
- 只接受对象作为键名(null除外),不接受其他类型的值作为键名;
- 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的;
- 不能遍历,方法有get、set、has、delete。