一直以来, JS只能使用数组和对象来保存多个数据, 缺乏像其他语言那样拥有丰富的集合类型, 因此, ES6新增了两种新的集合类型( Set和Map ), 用于在不同的场景中发挥作用
目录:
- Set
- Set集合的创建
- 对Set集合的后续操作
- Set的最佳实践
- 【 扩展 】手写Set
- Map
- Map集合的创建
- 对Map集合进行后续操作
- 【 扩展 】手写Map
- 【 扩展 】WeakMap & WeakSet
- WeakSet
- WeakMap
Set
Set集合用于存放不能重复的数据
Set集合的创建
使用构造函数Set
创建
const set = new Set();
console.log(set);
同时Set
构造函数还可以接收一个可迭代对象( iterable )作为参数, iterable中的每一次迭代的结果会放进Set中作为初始值
const set = new Set([1, 2, 3, 4, 5, 5]);
console.log(set); // 上面的重复的5会被剔除掉
对Set集合后续操作
-
add: 添加一个数据到Set集合末尾, 如果数据已经存在则不进行任何操作
const set = new Set(); set.add('hello'); set.add(1); set.add({ a: 10 }); set.add(1); // 无效 console.log(set);
set使用Object.is的方式判断两个数据是否相等, 唯一的例外就是对+0和-0做了特殊处理, set认为+0和-0是相等的
-
has: 判断set集合中是否有对应的数据
const set = new Set([1, 2, 3]); set.add('hello'); console.log(set.has('hello')); // true
-
delete: 删除set集合中对应的数据
const set = new Set([1, 2, 3]); set.add('hello'); console.log(set.delete('hello')); // true , 表示删除成功
-
clear: 清空整个set集合
const set = new Set([1, 2, 3]); set.clear(); console.log(set); // set集合直接被清空啥玩意都没有了
-
遍历
- 使用for…of循环
const set = new Set([1, 2, 3]); for(const item of set) { console.log(item); // 依次输出1, 2, 3 }
- 使用set实例方法forEach
const set = new Set([1, 2, 3]); // set集合中不提供访问索引的方法, 所以forEach的第一个参数和第二个参数都是值 set.forEach((value1, value2, self) => { console.log(value1, value2, self); })
- 使用for…of循环
-
size
返回当前Set集合的元素个数( 只读不可更改 )
const set = new Set( [1, 2, 3] ); console.log(set.size); // 3
Set的最佳实践
-
和数组的相互转换 + 数组去重
const arr = [1, 2, 2, 3, 3, 4]; const set = new Set(arr); // 转化为set console.log(set); const newArr = Array.from(set); // 使用Array.from可以将set结构转化为数组 console.log(newArr);
我们可以发现, 在相互转换的过程中, 数组已经实现了去重操作
上面的方式还可以写成如下, 因为set集合本身就是一个可迭代对象, 所以我们可以使用扩展运算符直接实现去重
const arr = [1, 2, 2, 3, 3, 4]; const set = new Set(arr); const newArr = [...set]; console.log(newArr); // [1, 2, 3, 4]
-
字符串的去重
const str = 'hhhelloo'; const set = new Set(str); const newStr = [...set].join(''); console.log(newStr) // helo
-
求两个数组的交集, 并集, 差集( 不能出现重复项 )
const arr = [33, 22, 55, 33, 11, 5];
const arr2 = [22, 55, 77, 88, 99, 99];
// 并集
const andSet = [...new Set([...arr, ...arr2])];
console.log(andSet); // [33, 22, 55, 11, 5, 77, 88, 99]
// 交集
const set = new Set(arr);
const intersection = [...set].filter( it => arr2.includes( it ) );
console.log(intersection);
// 差集 直接拿andSet做文章, 拿到并集 然后从并集里找 arr有arr2没有, arr没有 arr2有
const diffrenteSet = andSet.filter( it => (arr.includes(it) && (!arr2.includes(it))) || ((!arr.includes(it)) && (arr2.includes(it))) )
【 扩展 】手写Set
OK, 我们直接来手写一个Set, 当然我们只能实现一样的功能, 官方的Set是通过底层写好的, 像一模一样的数据结构的样子我们没法实现, 我们可以实现他的所有Api和所有的系统功能帮助大家扩展视野
const MySet = (function () {
// 判断传入的参数是不是迭代对象
function validateIterable(iterator) {
return typeof iterator[Symbol.iterator] === 'function';
}
// 判断两个数据是否相等
function isEqual( fst, sec ) {
if( fst === 0 && sec === 0 ) {
return true;
}
return Object.is( fst, sec );
}
const _data = Symbol('_data');
return class MySet {
constructor(iterator = []) {
// 验证传入的参数是否为可迭代对象
const isIterable = validateIterable(iterator);
if ( !isIterable ) {
// 如果不是可迭代对象
throw new TypeError('The param you provided is not iterable')
}
this[_data] = [];
// 如果是可迭代对象, 直接for..of循环他找到他的每一项
for ( const item of iterator ) {
this.add(item); // 直接调用add方法
}
}
// size直接用get定义返回当前数组的长度
get size() {
return this[_data].length;
}
// 用生成器给MySet提供可迭代接口
*[Symbol.iterator]() {
for( const item of this[_data] ) {
yield item;
}
}
// forEach遍历函数
forEach( callback ) {
for( const item of this[_data] ) {
callback( item, item, this );
}
}
add(data) {
// 如果当前的集合里已经有data了, 我就不能再加了
if( this.has(data) ) {
return;
}
this[_data].push(data);
}
delete( data ) {
this[_data].forEach((it, index) => {
if( isEqual( data, it ) ) {
this[_data].splice( index, 1 );
}
})
return true;
}
has( data ) {
for( const item of this[_data] ) {
if( isEqual( data, item ) ) {
return true; // 只要找到一样的就返回true
}
}
return false;
}
// 清空的方法
clear() {
this[_data] = [];
}
}
}())
const set = new MySet([2, 2, 3, 4, 4]);
console.log(set.has( 4 ))
set.add(10);
console.log(set);
set.delete(2);
console.log(set);
for(const item of set) {
console.log(item);
}
你从constructor
处开始看, 一层一层缕下去肯定可以看懂的, 代码不难
Map
Map集合用于专门存储多个键值对数据
在Map出现之前, 我们用对象存储键值对数据, 但是用对象存储键值对有一些问题
- 键名( key )只能是字符串, ES6才新增了符号
- 获取数据的数量不方便 ( 基本上只能这样:
Object.keys.length
) - 键名( key )容易跟原型链属性冲突
在Map出来以后, 更好的解决了以上的问题, 但是并不代表我们以后就不用对象了, 结合实际情况自己怎么用着方便自己怎么用
Map集合的创建
跟Set一致, 我们直接使用构造函数Map创建
const map = new Map(); // 创建一个map
console.log(map);
我们也可以给Map传递一个iterator( 可迭代对象 )参数作为Map集合的初始值
注意: 虽然Map接收的是一个iterator, 但是他规定如下:
- 可迭代对象的每一项又必须是一个至少可以被迭代两次的可迭代对象( 通常我们会写一个长度为2的数组 )
- 每一项的可迭代对象的第一次迭代会被判定为Map成员的key, 第二次迭代结果会被判定为value( 长度为2数组的0索引位代表Map成员的key值, 1索引位为Map成员的value值 )
const map = new Map( [['name', 'loki'], ['age', 18] ] );
console.log(map);
对Map集合进行后续操作
-
set
给Map集合添加一个新的成员, 接收两个参数
- key: 代表该成员的键 ( 可以是任何类型 )
- value: 代表该成员的值( 可以是任何类型 )
如果键名已经存在( 使用Object.is比较 ), 则直接覆盖( 区别于Set )
const map = new Map(['name', 'loki']); map.set(3, 3); map.set(() => {console.log('hello')}, 'foo'); map.set([1, 2, 3], 'arr'); console.log(map);
由于在Map结构里成员的键是不能重复的, 所以在使用引用值做键名的时候要记得处理好
-
get
接收一个参数( key ), 根据传入的参数去Map数据结构中找到相对应的成员并返回成员值
const map = new Map(); map.set('key', 'value'); console.log(map.get('key')); // value console.log(map.get('name')); // undefined
-
has
接收一个参数( key ), 用于判断传入的参数键是否存在
const map = new Map(); map.set('key', 'value'); console.log(map.has('key')); // true
-
delete
接收一个参数( key ), 用于在Map中删除该key所在的Map成员
const map = new Map(); map.set('key', 'value'); console.log(map.get('key')); // value map.delete('key'); console.log(map.get('key')); // undefined
-
clear
清空Map结构所有成员
const map = new Map([['key', 'value']]); map.clear(); console.log(map.get('key')); // undefined
-
遍历
- for…of
const map = new Map([['a', 10], [1, 2]]) for(const [key, value] of map) { console.log(key, value); // 依次输出a 10, 1 2 }
- forEach
const map = new Map([['a', 10], [1, 2]]) map.forEach( (value, key, self) => { console.log(value, key, self); } )
-
size
返回当前Map集合的元素个数( 只读不可更改 )
// Map是允许用其他任何数据结构作为Map成员的key值的 const map = new Map([[{a: 10}, 20]]); console.log(map.size); // 1
【 扩展 】手写Map
有了上面Set的前车之鉴, 笔者这里就直接开始写喽
const MyMap = (function () {
// 判断传入的参数是不是迭代对象
function validateIterable(iterator) {
return typeof iterator[Symbol.iterator] === 'function';
}
const _data = Symbol('_data');
// 通过传入的key拿到对应的value值
function getMapValue( key ) {
for( const item of this[_data] ) {
if( isEqual( item.key, key ) ) {
return item;
}
}
}
// 判断两个数据是否相等
function isEqual(fst, sec) {
if (fst === 0 && sec === 0) {
return true;
}
return Object.is(fst, sec);
}
return class MyMap {
constructor(iterator = []) {
// 验证传入的参数是否为可迭代对象
const isIterable = validateIterable(iterator);
if (!isIterable) {
// 如果不是可迭代对象
throw new TypeError('The param you provided is not iterable')
}else {
this[_data] = [];
for( const item of iterator ) {
// Map要求每一个Map成员也必须是一个可迭代对象
if( !validateIterable( item ) ) {
throw new TypeError('The param child you provided is not iterable')
}else {
console.log( item );
const itemIterator = item[Symbol.iterator]();
const key = itemIterator.next().value; // 第一次迭代拿到key
const value = itemIterator.next().value; // 第二次迭代拿到value
this.set( key, value );
}
}
}
}
get size() {
return this[_data].length;
}
set(key, value) {
if(this.has( key )) {
// 如果当前键已经存在了
const item = getMapValue.call(this, key);
item.value = value;
}else {
this[_data].push( { key, value } );
}
}
has(key) {
// console.log(this[_data]);
for( const item of this[_data] ) {
return isEqual( item.key, key );
}
return false;
}
get(key) {
const result = getMapValue.call(this, key);
if( result !== undefined ) {
return result.value;
}
}
delete(key) {
const result = getMapValue.call(this, key);
if( result !== undefined ) {
const index = this[_data].indexOf( result );
this[_data].splice(index, 1);
}
}
// 清空的方法
clear() {
this[_data] = [];
}
}
}())
const map = new MyMap([[1, 2], [3, 4]]);
console.log(map);
map.set( 1, 5 );
map.set( 10, 2 );
console.log(map);
console.log(map.get(1))
map.delete( 1 );
【 扩展 】WeakMap和WeakSet
WeakSet
使用该集合可以直接拥有Set的基本功能( 有一些小区别 ), 最大的不同是它内部存储的地址不会影响JS的垃圾回收, 详细的区分笔者在稍后有写到
我们来看一个Set的例子, 你就知道了
const obj = {
name: 'loki',
age: 17
}
const set = new Set([obj]);
obj = null;
console.log(obj);
console.log(set);
我们发现我们将obj置空以后, set中的value还在, 这就是JS的回收机制导致的一些尴尬情况
为什么会这样呢? 因为我们创建了一个
obj
, 将引用值{ name: 'loki', age: 17 }
的地址给了他, 这个时候我们将obj
丢给set
, 于是set
的第零位也拿到了{name: 'loki', age: 17}
的地址, 这个时候我们将obj
置空, 但是JS不会释放掉{name: 'loki, age: 17'}
的内存, 为啥, 因为set
手里还攥着呢, 这个原因你要搞清楚就一定要理解引用值的真正原理
我们来看看WeakSet
的情况
const obj = {
name: 'loki',
age: 17
}
const set = new Set([obj]);
obj = null;
// 这儿我为什么要放进setTimeout, 因为我们如果直接输出的话, 可能JS的垃圾回收机制还未运行, 所以可能会有不太OK的控制台效果
setTimeout(() => {
console.log(obj);
console.log(set);
}, 8000)
WeakSet和Set的区别
- WeakSet不影响垃圾回收
- WeakSet只能添加对象
- WeakSet不能遍历, 没有size属性
- WeakSet只有add, delete, has实例方法
其实2, 3, 4点都是因为第一点的存在而产生的副作用
WeakMap
类似于Map的集合, 跟Set一样, WeakMap和Map最大的区别就是WeakMap的成员不会影响垃圾回收
let obj = {
name: 'loki',
age: 18
}
const map = new WeakMap([[obj, 123]]);
obj = null;
setTimeout(() => {
console.log(map);
}, 8000)
WeakMap和Set的区别
- WeakMap不影响垃圾回收
- WeakMap只能添加对象
- WeakMap不能遍历, 没有size属性
- WeakMap只有set, get, delete, has实例方法
反正WeakSet和WeakMap用的也比较少, 看着图个乐呵就好