Set
ES6提供了新的数据结构 —— Set 。它类似于数组,但是成员的值都是唯一的,没有重复。Set 本身是一个构造函数,用来生成Set数据结构
[Set结构的实例属性]
- Set.Prototype.constructor: 构造函数,默认就是Set函数
- Set.prototypr.size: 返回Set实例的成员总数
Set实例的方法分为两大类: 操作方法(用于操作数据)和遍历方法(用于遍历成员)。
[Set 实例的操作方法]
- add(value): 添加某个值,返回Set结果本身
- delete(value): 删除某个值,返回一个布尔值,表示删除是否成功
+ has(value): 返回一个布尔值,表示参数是否为Set成员
+ clear(value): 清除所有成员,没有返回值
[创建Set集合、add添加元素]
调用new Set()创建Set实例集合,调用add()方法向集合中添加元素。访问属性size,得到集合中元素数量
let set = new Set();
set.add(5);
set.add('5');
console.log(set.size);// 2
在Set集合中,不会对所存元素强制的类型转换,数字5和字符串‘5’可以作为两个独立元素存在.
Set 内部判断两个值是否相同使用的算法是“same-value equality”,它类似于精确相等运算符(===),主要的区别是NaN等于本身,而精确相等运算符认为NaN不等于自身。
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
console.log(set);// Set(1) {NaN}
另外两个对象总是不想等的,它们之间彼此保持独立
let set = new Set();
set.add({});
console.log(set.size);// 1
set.add({});
console.log(set.size);// 2
Set结构不会添加重复的值:
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(function (item) {
s.add(item);
})
for(let i of s) {
console.log(i);
}
// 2 3 5 4
上面的代码用add方法向Set结构加入成员,结构表明Set结构不会添加重复的值。
[Set初始化]
Set函数可以接受一个数组(或者具有iterable接口的其他数据结构)作为参数,用来初始化.
// 例1:
const set = new Set([1, 2, 3, 4, 4]);
console.log([...set]);// [1, 2, 3, 4]
console.log(set.size);// 4
// 例2:
function divs() {
return [...document.querySelectorAll('div')];
}
const set = new Set(divs());
console.log(set.size);// 3
上面两个例子中Set函数分别接受数组和一个类似数组的对象作为参数。
[注意]:
Set集合不能像访问数组那样直接通过索引访问集合中的元素。如果需要,最好先将Set集合装换成一个数组。
方法就是像上例一样([…new Set(array)]),将数组传入Set的构造函数中,在使用扩展运算符(…)将Set集合转为数组。
另外,这也展示了一种数组去重的方法:
扩展运算符与Set结构相结合就可以去除数组的重复元素
[has()检测元素]
通过has()方法可以检测Set集合中是否存在某个值。
let set = new Set();
set.add(5);
set.add('5');
console.log(set.has(5));// true
console.log(set.has(6));// false
[delete()和clear()移除元素]
调用delete()方法可以移除Set集合中的某一个元素,调用clear()方法会移除集合中的所有元素
let set = new Set();
set.add(5);
set.add('5');
console.log(set.has(5));// true
set.delete(5);
console.log(set.has(5));// false
console.log(set.size);// 1
set.clear();
console.log(set.size);// 0
[遍历方法]
Set实例有4个遍历方法:
- Keys(): 返回键名的遍历器
- values(): 返回键值的遍历器
- entries(): 返回键值对的遍历器
- forEach(): 使用回调函数遍历每个成员
[注意]:
Set 的遍历顺序就是插入顺序。
[keys()、values()、entries()]
keys方法、values方法、entries方法返回的都是遍历器对象。由于Set结果没有键名,只有键值(或者说键名和键值是同一值)所以keys方法和values方法的行为完全一致
let set = new Set(['red', 'green', 'blue']);
for(let item of set.keys()) {
console.log(item);
}
// red
// green
// blue
for(let item of set.values()) {
console.log(item);
}
// red
// green
// blue
for(let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
上面的代码中,entries方法返回的遍历器同时包括键名和键值,所以每次输出的数组,其成员完全相等。
Set结构的实例默认可遍历,其默认遍历器生成函数就是它的values方法
console.log( Set.prototype[Symbol.iterator] === Set.prototype.values ) // true
这意味着,可以省略values方法,直接用for…of循环遍历Set
let set = new Set(['red', 'green', 'blue']);
for(let x of set) {
console.log(x);
}
// red
// green
// blue
[forEach()]
Set 结构实例的forEach方法用于对每个成员执行某种操作,没有返回值。
let set = new Set(['a', 'b', 'c']);
set.forEach(function(value, key, set) {
console.log(value, key, set);
} )
//a a Set(3){'a','b','c'}
//b b Set(3){'a','b','c'}
//c c Set(3){'a','b','c'}
上面的代码说明,forEach方法的参数是一个处理函数。该函数的参数依次为键值、键名、集合本身.
在Set集合的forEach()方法中,也可以有第二个参数this,表示绑定的this对象。
let set = new Set([1, 2]);
let obj = {
output(value) {
console.log(value);
},
process(dataSet) {
dataSet.forEach(function(value){
this.output(value);
}, this);
}
}
obj.process(set);
// 1
// 2
上面的代码中,obj.process()方法调用了Set集合的forEach()方法并将this传入作为回调函数的this值,从而 this.output()方法可以正确的调用obj.output()方法。
这里可以使用箭头函数,这样就不需要将this作为第二个 参数传入回调函数中。
let set = new Set([1, 2]);
let obj = {
output(value) {
console.log(value);
},
process(dataSet) {
dataSet.forEach( value => this.output(value));
}
}
obj.process(set);
WeakSet
WeakSet结构与Set类似,也是不重复的值的集合。但它与Set有两个区别:
- WeakSet 的成员只能是对象,而不能是其他类型的值
let ws = new WeakSet();
ws.add(1); // TypeError: Invalid value used in weak set
ws.add(Symbol(1));// TypeError: Invalid value used in weak set
- WeakSet中的对象都是弱引用,即如果其他对象都不在引用该对象,那么垃圾回收机制会自动回收该对象所占用的空间。但在Set中,只要Set实例存在引用,垃圾回收机制就不能释放该对象的内存空间,即Set类型可以看作是一个强引用。
在Set中:
let set = new Set();
let key = {
value:1
};
set.add(key);
console.log(set.size);// 1
// 取消原始引用
key = null;
console.log(set.size);// 1
// 重新获得原始引用
key = [...set][0];
console.log(key);// {value: 1}
外部设置key = null,而Set内部并没有取消引用。
在WeakSet中:
let ws = new WeakSet();
let key = [1, 2];
ws.add(key);
console.log(ws.has(key));// true
key = null;
console.log(ws.has(key));// false
外部设置key = null,WeakSet内部的引用也取消
[创建WeakSet集合]
WeakSet是一个构造函数,可以使用new命令创建WeakSet集合。集合支持3个方法:add()、has()和delete()
- WeakSet.prototype.add(value): 向WeakSet实例添加一个新成员
- WeakSet.prototype.delete(value): 清除WeakSet实例的指定成员
- WeakSet.prototype.has(value): 返回一个布尔值,表示某个值是否在WeakSet实例中。
let set = new WeakSet()
let key = {};
set.add(key);
console.log(set.has(key));// true
set.delete(key);
console.log(set.has(key)); // false
作为构造函数,WeakSet集合使用方式与Set集合类似,可以接受一个数组或类似数组的对象作为参数。实际上, 任何具有iterable接口的对象都可以作为WeakSet的参数。该数组的所有成员都会自动成为WeakSet实例的成员。
const a = [[1, 2],[3, 4]];
let ws = new WeakSet(a);
console.log(ws);// WeakSet { Array(2), Array(2)}
上面的代码中,a是一个数组,它的两个元素都是数组。将a作为WeakSet构造函数的参数,a的成员会自动成为WeakSet的成员。
[注意]: 成为WeakSet实例成员的是a数组的成员,而不是a本身。因为weakSet的成员只能是对象。
[与Set的区别]
WeakSet是弱引用,在其内部有多少个成员取决于垃圾回收机制有没有运行, 运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此es6规定weakSet是不可遍历的。
- 在WeakSet的实例中,如果向add()、has()和delete()这三个方法传入非对象参数都会导致程序报错
- WeakSet集合是不可遍历的,所以不能被用于for-of循环
+ WeakSet集合不暴露任何迭代器
+ WeakSet没有size属性,也没有forEach方法
虽然WeakSet集合的功能受限,但它能够正确处理内存中的数据。如果只需要跟踪对象引用,更应该使用WeakSet集合而不是普通的Set集合.
Map
js的对象,本质上是键值对的集合(Hash结构),但是传统上只能用字符串当作键。这给它带来了很大的限制。
为了解决这个问题,ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object结构提供了“字符串 —— 值”的对应,Map结构提供了“值 —— 值”的对应,是一种更完善的Hash结构实现。如果需要“键值对”的数据结构,Map比
Object更合适。
[注意]:
Map集合中将 +0 和-0视为相等。
[Map实例的操作方法]
- Map实例支持size属性: 返回Map结构中的成员数目数量
- set(key, value)方法: 设置Key所对应的键值,然后返回整个Map结构
- get(key)方法: 读取key对应的键值,如果找不到key,返回undefined
+ has(key)方法: 返回一个布尔值,表示某个键是否在Map结构中
+ delete(key)方法: 删除某个键,返回true,删除失败返回false
+ clear()方法: 清除所有成员,没有返回值
[创建Map集合、使用操作方法]
let map = new Map();
map.set('title', 'Es6');
map.set('year', 2017);
console.log(map.get('title'))// Es6
console.log(map.get('year'))// 2017
在对象中,无法用对象作为属性的键名。但是在Map集合中,却可以这样做:
let map = new Map();
key1 = {};
key2 = {};
map.set(key1, 1);
map.set(key2, 42);
console.log(map.get(key1));// 1
console.log(map.get(key2));// 42
[使用操作方法]
let map = new Map();
map.set('name', 'xiaoqi');
map.set('age', 20);
console.log(map.size);// 2
console.log(map.has('name'));// true
console.log(map.get('name'));// xiaoqi
console.log(map.has('age'));// true
console.log(map.get('age'));// 20
map.delete('name');
console.log(map.has('name'));// false
console.log(map.get('name'));// undefined
console.log(map.size);// 1
map.clear();
console.log(map.has('name'));// false
console.log(map.has('age'));// false
console.log(map.size);// 0
[接受数组初始化Map集合]
作为构造函数,Map也接受数组作为参数初始化一个Map集合,这一点也与Set集合相似。数组中的每一个元素都是一个子数组, 子数组中包含一个键值对的键名与键值两个元素。
let map = new Map([['name', 'xiaoqi'], ['age', 20]]);
console.log(map.has('name'));// true
console.log(map.get('name'));// xiaoqi
console.log(map.has('age'));// true
console.log(map.get('age'));// 20
console.log(map.size);// 2
[同名键碰撞]
Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这样就解决了同名键碰撞的问题。
const map = new Map();
map.set(['a'], 555);
console.log( map.get(['a']));// undefined
上面的代码的set与get方法,表面是针对同一个键,但实际上这是两个值,内存地址不一样,因此get方法无法读取该键,返回undefined。
const map = new Map();
let k1 = ['a'];
let k2 = ['a'];
map.set(k1, 111).set(k2, 222);
console.log(map.get(k1));// 111
console.log(map.get(k2));// 222
上面代码中,变量k1和k2的值一样,但是它们在Map结构中被视为两个键
[Map实例的遍历方法]
Map 结构原生提供三个遍历器生成函数和一个遍历方法:
- keys(): 返回键名的遍历器
+ values(): 返回键值的遍历器
+ entries(): 返回所有成员的遍历器
+ forEach():遍历Map的所有成员
[注意]:
Map的遍历顺序就是插入顺序
const map = new Map([
['F', 'no'],
['T', 'yes']
]);
for(let key of map.keys()) {
console.log(key)
}
// "F"
// "T"
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"
for(let item of map.entries()) {
console.log(item[0],item[1]);
}
// "F" "no"
// "T" "yes"
// 等同于
for(let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
Map结构的默认遍历器接口(Symbol iterable属性)就是entries方法
console.log( map[Symbol iterable] === map.entries )// true
[转为数组]
Map结构转为数组,比较快速的方法是使用扩展运算符(…)
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three']
]);
console.log( [...map.keys()] );// [1, 2, 3]
console.log( [...map.values()]);// ['one', 'two', 'three']
console.log( [...map.entries()])// [[1, 'one'], [2, 'two'], [3, 'three']]
console.log([...map]);// [[1, 'one'], [2, 'two'], [3, 'three']]
结合数组的map方法、filter方法,可以实现Map的遍历和过滤
const map0 = new Map();
map0.set(1, 'a');
map0.set(2, 'b');
map0.set(3, 'c');
const map1 = new Map(
[...map0].filter(([k, v]) => k < 3)
);
console.log(map1);// Map(2) {1 => "a", 2 => "b"}
const map2 = new Map(
[...map0].map(([k,v]) => [k * 2, '_' + v])
);
console.log(map2);// Map(3) {2 => "_a", 4 => "_b", 6 => "_c"}
[forEach()]
Map还有个forEach方法,与数组的forEach方法类似,也可以实现遍历
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three']
]);
map.forEach((value, key, map) => {
//one 1 Map(3) {1 => "one", 2 => "two", 3 => "three"}
//two 2 Map(3) {1 => "one", 2 => "two", 3 => "three"}
//three 3 Map(3) {1 => "one", 2 => "two", 3 => "three"}
console.log(value, key, map);
})
[注意]:
遍历过程中,Map会按照键值对插入Map集合的顺序将相应信息传入forEach()方法的回调函数中。
forEach方法还可以接受第二个参数,用来绑定this
const reporter = {
report: function (key, value) {
console.log('key: %s, Value: %s', key,value);
}
};
map.forEach(function(value, key, map) {
this.report(key, value);
},reporter);
// key: 1, Value: one
// key: 2, Value: two
// key: 3, Value: three
上面的代码中,forEach方法的回调函数的this,就指向reporter。
WeakMap
WeakMap与Map结构类似,也用于生成键值对的集合。
[WeakMap与Map的区别]
-
WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
-
WeakMap的键名所指向的对象不计入垃圾回收机制。它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此所有引用的对象的其他引用都被清除,垃圾回收站就会释放该对象所占用的内存。
-
WeakMap没有遍历操作(即没有key()、values()和entries()方法),也没有size属性。
-
WeakMap无法清空,即不支持clear()方法。
[使用WeakMap集合]
Es6中的WeakMap类型是一种存储着许多键值对的无序列表,列表的键名必须是非null类型的对象,键名对应的值可以是任意类型。通过set()方法添加数据,通过get()方法获取数据。
let wm = new WeakMap();
let element = document.querySelector('.element');
wm.set(element, 'Original');
let value = wm.get(element);
// 移除元素
element.parentElement.removeChild(element);
element = null;
// 该WeakMap 在此处为空
这个示例中存储了一个键值对,键名element 是一个DOM元素,其对应的值是一个字符串,将DOM元素传入get()方法即可获取之前存取过的值,如果随后从document对象中移除并将引用这个元素的变量为null,那么WeakMap集合中的数据也会被同步清除。
[WeakMap实例的方法]
WeakMap只支持4个方法: get()、set() 、has()、delete()
let wm = new WeakMap();
let element = document.querySelector('.element');
wm.set(element, 'Original');
console.log(map.has(element));// true
console.log(map.get(element));// "Original"
map.delete(element);
console.log(map.has(element));// false
console.log(map.get(element))// undefined
[WeakMap集合的初始化]
WeakMap集合的初始化过程与Map集合类似,调用WeakMap构造函数并传入一个数组容器,容器包含其他数组,每个数组有两个元素构成;第一个元素是键名,传入的值必须是非null的对象;第二个元素是这个键对应的值。(可以是任意类型)
let key1 = {},
key2 = {},
wm = new WeakMap([[key1, 'hello'],[key2, 42]]);
console.log(wm.has(key1));// true
console.log(wm.get(key1)); // "hello"
console.log(wm.has(key2));// true
console.log(wm.get(key2)); // 42
[WeakMap用途]
- 存储DOM元素
WeakMap应用的典型场合就是DOM节点作为键名:
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);
上面的代码中,myElement是一个DOM节点,每当发生click事件,就更新一下状态。将这个状态作为键值放在WeakMap里,对应的键名就是myElement。一旦这个DOM节点删除,该状态就会自动消失,不存在内存泄露风险。
进步一说,注册监听事件的listener对象,就很适合用WeakMap实现:
const listener = new WeakMap();
listener.set(element1, handler1);
listener.set(element2, handler2);
element1.addEventListener('click', listener.get(element1), false);
element2.addEventListener('click', listener.get(element2), false);
上面代码中,监听函数放在WeakMap里面。一旦DOM对象消失,跟它绑定的监听函数也会自动消失
- 部署私有属性
WeakMap的另外一个用处是部署私有属性
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('down'))
c.dec();
c.dec();// down
上面的代码中,CountDown类的两个内部属性 —— _counter和_action是实例的弱引用,如果删除实例,它们也会随之消失,不会造成内存泄漏。