数据结构---Set和Map
1.Set数据结构
Set本质上是一个没有重复数据,但是具有irerator接口可以遍历的一种集合。
Set本身也是一种数据结构的构造函数。
1.Set的初始化
var obj = new Set(参数);
上面生成一个Set的实例,obj是集合对象,可以通过for...of遍历。
参数可以是数组,也可以是类数组(具有iterator接口的数据,如字符串)
var obj = [...new Set([1,3,3,3])]; // [1,3]
var obj = [...new Set('hellohello')]; // ['h','e','l','o'].join('')--'helo'
注意new Set()生成的对象是类数组,通过[...]转为数组。
⚠️:[...new Set(数组或者类数组)] 可以去重!!!
// +0/-0是一个值 NaN等于自身 [...new Set([+0,-0,NaN, NaN])]; // [0, NaN]
两个对象总是不相等,该去重方法不适用于对象(任何形式的对象)!
[...new Set([{}, {}, {}])]; // [{}, {}, {}] // 对象指的的是存储地址,所以每个对象都不一样,所以,不会被认为是重复数据
⚠️将Set结构转为数组还有一个方法Array.from(set对象);Array.from可以将所有的类数组(含length)转为数组
let obj = Array.from(new Set([1,23,3,3]))
扩展运算符(...)和Array.from() 方法本质也是调用for...of循环。
2.Set的实例属性和方法
1.实例属性
1. Set.prototype.constructor
实例对象的构造函数,继承自原型对象
const obj = new Set('a'); obj.constructor; //Set Set.prototype.constructor === Set
2. Set.prototype.size
实例对象的元素去重后的个数;相当于数组的长度属性
const obj = new Set([1,2,1,2,3]); obj.size; // 3 const obj = new Set('aabbccdd'); obj.size; // 4
2. 操作方法
1. Set.prototype.add(value)
向Set集合添加元素
参数:只能传入一个参数;参数可以是任意类型
返回:添加元素后的Set实例
const set = new Set(); const result = set.add(1); result.size; // 1 说明result是Set结构,且是添加元素之后的Set结构
由返回值可知:add方法可以连写
const set = new Set(); const result = set.add(1).add('a').add(true); [...result]; //[1,'a',true]
2. Set.prototype.delete(value)
删除Set集合内的值
参数:待删除的元素
返回: 布尔值;表示是否删除成功
const set = new Set(); set.add(1).add(2).add(3); const result = set.delete(2); result; // true 删除成功 set.size; // 2 const result2 = set.delete(5); result2; // false Set集合不包括5,删除失败
3. Set.prototype.has(value)
判断Set数据集合中是否含有某元素
参数: 待判断数据
返回: 布尔值;表示是否包含该数据
const set = new Set(); set.add(1).add(2).add(3); set.has(1); // true set.has(5); // false
4. Set.prototype.clear()
清空Set集合
参数: 无
返回:无(undefined)
const set = new Set(); set.add(1).add(2).add(3); set.size; //3 set.clear(); set.size; //0
3. 遍历方法
Set集合的遍历顺序是元素的插入顺序。和元素的数据类型无关。
不同于对象的属性遍历顺序,根据属性遍历的先后顺序和属性的数据类型有关。
1. Set.prototype.keys()
同2
2.Set.prototype.values()
由于Set集合中只有value,没有key,所以默认keys和values都返回成员的值。
返回:一个遍历器;遍历器成员是集合的值; set.keys()/set.values()返回值一摸一样
values()方法是Set默认的[Symbol.iterator]属性对应的函数。
const set = new Set([1,2,4]); const keys = set.keys(); // SetIterator for (let key of keys()) { console.log(key) } // 运行结果如下: 1 2 4 const values = set.values(); //SetIterator [...values]; //[1,2,4] ...扩展运算符本质上调用的是for...of
3. Set.prototype.entries()
返回: 一个遍历器;遍历器每个成员是一个数组,数组含有相同的两个值。
let set = new Set(); set.add({a:1}).add(1).add(true); const entries = set.entries(); for(let entity of entries) { console.log(entity); } // 运行结果如下: [{a:1}, {a:1}] [1,1] [true, true]
4. Set.prototype.forEach(fn)
用法和数组的 forEach基本一致
const set = new Set([1,2,3]); set.forEach(function(value, key){ //value和key永远相等;数组中的key是index console.log(value,key); }, thisObj) // 运行结果如下: 1,1 2,2 3,3
3. Set应用
1. 去重
// 数组去重 [...new Set([1,2,3,1,3,5])]; //[1,2,3,5] Array.from(new Set([1,2,3,1,3,5])); // [1,2,3,5] // Set的每个成员*2;先变为数组 new Set([...new Set([1,2,3,1,3,5])].map(i => i*2)); // Set{2,4,6,10} new Set(Array.from(new Set([1,2,3,1,3,5]), i => i*2)); //Set{2,4,6,10}
2.求集合
假如现有两个集合:let setA = new Set([1,2,3]); let setB = new Set([1,3,5]);
1. 并集
const union = new Set([...setA, ...setB]) // Set{1,2,3,5}
2. 交集
const intersect = new Set([...setA].filter(i => setB.has(i))); // Set{1,3}
3. 差集
const difference = new Set([...setA].filter(i => !setB.has(i))); // Set{2} setA有;setB没有
二. WeakSet数据结构
1. 初始化
大体和Set相同;也是一个构造方法。
const ws = new WeakSet();
可以通过数组或者类数组传参,元素类型必须是对象
const ws = new WeakSet([{a:1}, {b:2}]); // 即数组内的每个元素必须是对象类型
2. 和Set不同点:
1. 元素只能是对象
WeakSet数据集合中元素只能是对象(普通对象,函数,数组等);不能是原始类型的值。
因此,add(value)的参数必须是对象,delete/has的参数数据类型可以是任意类型
const ws = new WeakSet(); // add(value)参数只能是对象 ws.add({a:1}); // 普通对象 ws.add([1,2]); // 数组 // delele/ has不要求参数数据类型
2. 不能遍历。
1. 实例对象不含[Symbol.iterator]属性,不能遍历;
所以,不能通过...或者Array.from将实例WeakSet数据转为数组
2. 不含size属性;
3. 没有forEach(), keys(), values(), entries()遍历方法。
原因:
该数据结构中的对象引用是弱引用,垃圾回收机制不考虑WeakSet对对象的引用。
即某对象在WeakSet之外没有其他的引用,则该对象即使在WeakSet中有引用,也会被垃圾回收处理掉。
因为元素对象的个数会根据外部引用情况而变化,所以不允许其遍历。
3. 不能清空
没有clear方法
3. 应用
对于引用的一些已经删除的DOM节点,已经不再需要,对于强引用的Set结构,
这些垃圾数据不会被垃圾回收机制自动清理掉,仍占用内存, 需要手动delete删除来释放内存;
但是WeakSet允许垃圾回收机制清理掉这些垃圾数据,释放内存。
三. Map数据结构
1. Map基础
1. 含义
Map是一种键值对的数据结构;不同于对象的键值只能是字符串和Symbol,
Map结构的key可以是任意类型的数据。
是一种更广义的值-值的映射。
2. 初始化
1. 初始化空值
const map = new Map(); const keyObj = {a:1}; map.set(keyObj, 'content');
2. 带参数初始化;可以传入元素是长度为2的数组或者具有iterator接口的数据类型
const map = new Map([[1,2],[{a:2}, 5]]); // 等同于 const set = new Set([[1,2],[{a:2}, 5]]); const map = new Map(set)
2. Map实例属性和方法
1. 实例属性
1.Map.prototype.size
元素的个数
const map = new Map(); map.set('foo', true); map.set('bar', false); map.size // 2
2. 操作方法
1. Map.prototype.set(key, value)
向Map结构添加值
返回map实例,可以链式写法;
当添加相同的键名时,后面的键名对应的值会覆盖前面的。
const map = new Map(); map.set('foo', true).set('foo', 2); map.size; //1 map.get('foo'); //2
当key值是对象类型时,注意除非该对象赋值给一个遍历,使用时通过变量时会认为是同一个值;
直接写对象(任何对象类型),都相当于新建了一个对象,它代表的是内存地址。
// 赋值给遍历 const map = new Map(); const arrObj = ['a'] map.set(arrObj, 555); map.get(arrObj) // 555 // 直接使用 map.set(['b'], 666); map.get(['b']) //undefined set,get中的['b']是不同的内存地址地址对应的值 // 即使值一样也对应不同的地址,对应Map的key来说是不同的值 const k1 = ['c']; const k2 = ['c']; //k1,k2值相同,但是内存地址不同 map.set(k1, 5); map.get(k2) //undefined
2. Map.prototype.get(key)
获取键名对应的值
3. Map.prototype.has(key)
判断某个键名是否存在
4. Map.prototype.delete(key)
删除某个键名对应的键值对
5.Map.prototype.clear()
清空整个Map数据结构
3. 遍历方法
1. Map.prototype.keys()
返回键名的遍历器
2. Map.prototype.values()
返回值的遍历器
3.Map.prototype.entries()
返回键值对的遍历器
Map结构的[Symbol.iterator]属性对应的是entries()方法
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] //Map函数生成的实例本身也可遍历 // [[1,'one'], [2, 'two'], [3, 'three']]
4.Map.prototype.forEach(fn)
同Set
4. 和其他数据结构互换
1. Map->数组
const map = new Map([[1,2],[{a:2}, 5]]); [...map]; // [[1,2],[{a:2}, 5]]
2. 数组->Map
数组必须满足每个成员都是长度为2的数组
new Map([ [true, 7], [{foo: 3}, ['abc']] ])
3. Map->对象
Object.fromEntries(new Map([
[true, 7],
[{foo: 3}, ['abc']]
])) //{true: 7, [object Object]: ['abc']}
// 键值是对象时,会自动将对象转为对应的字符串
4. 对象->Map
new Map([...Object.entries({yes: true, no: false})])
5. Map->JSON
1)键名都是字符串
先转为对象,再转JSON
const set = new Map().set('yes', true).set('no', false); function toJSON() { return JSON.stringify(Object.fromEntries(set)) }
2)键名有非字符串
转数组,再转JSON
const set = new Map().set('yes', true).set('no', false); function toJSON() { return JSON.stringify([...set])) }
6. JSON->Map
5的逆运算
都需要先JSON.parse()后再根据情况转换。
四.WeakMap
1. 初始化
const wp = new WeakMap(); wp.set({a:1}, 1) //键名必须是对象 // const wp = new WeakMap([[{a:1}, 1],[{a:2}, 2]]) //键名必须是对象
2.和Map差别
1. 键名只能是对象
2. 不能遍历。
原因:第4项
1. 没有size属性
2.没有forEach,keys,values,entries的遍历方法
3. 只有set,get,delete,has四个方法
3.无法清空
没有clear方法
4.键名对应的对象不计入垃圾回收机制。
3.应用-防止内存泄漏。
键名对应的对象是弱引用,不计入垃圾回收机制。只是键名!!
对于值是对象的情况,即使外部删除了引用,WeakMap内存仍然存在
const wm = new WeakMap(); let key = {}; let obj = {foo: 1}; wm.set(key, obj); obj = null; // 将对象设置为null, 手动删除引用 wm.get(key);// {foo: 1}内部仍存在
首先,对象用在普通数据结构中,是强引用,不手动删除的话,会一直占用内存。
const e1 = document.getElementById('foo'); const e2 = document.getElementById('bar'); const arr = [ [e1, 'foo 元素'], [e2, 'bar 元素'], ]; // e1, e2是两个NodeList对象,用于二维数组中,相当于被二维数组引用,就会占用内存存放; // arr之外其实e1,e2对应的DOM已经不存在,也不会自动被垃圾回收机制回收掉,只能手动删除,清空数组,arr.length = 0;
而WeakMap结构是弱引用,如果出现上面例子中的情况,会自动被垃圾回收机制回收掉,释放内存。
验证:
使用process.memoryUsage()查看内存; // 首先打开node命令行 node --expose-gc // 允许手动执行垃圾回收机制 // 手动执行一次垃圾回收,保证获取的内存使用状态准确 > global.gc(); undefined // 查看内存占用的初始状态,heapUsed 为 4M 左右 > process.memoryUsage(); { rss: 21106688, heapTotal: 7376896, heapUsed: 4153936, external: 9059 } > let wm = new WeakMap(); undefined // 新建一个变量 key,指向一个 5*1024*1024 的数组 > let key = new Array(5 * 1024 * 1024); undefined // 设置 WeakMap 实例的键名,也指向 key 数组 // 这时,key 数组实际被引用了两次, // 变量 key 引用一次,WeakMap 的键名引用了第二次 // 但是,WeakMap 是弱引用,对于引擎来说,引用计数还是1 > wm.set(key, 1); WeakMap {} > global.gc(); undefined // 这时内存占用 heapUsed 增加到 45M 了 > process.memoryUsage(); { rss: 67538944, heapTotal: 7376896, heapUsed: 45782816, external: 8945 } // 清除变量 key 对数组的引用, // 但没有手动清除 WeakMap 实例的键名对数组的引用 > key = null; null // 再次执行垃圾回收 > global.gc(); undefined // 内存占用 heapUsed 变回 4M 左右, // 可以看到 WeakMap 的键名引用没有阻止 gc 对内存的回收 > process.memoryUsage(); { rss: 20639744, heapTotal: 8425472, heapUsed: 3979792, external: 8956 }
示例1: 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);
//一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。
示例二:部署类的私有属性
// Countdown类的两个内部属性_counter和_action,是实例的弱引用,所以如果删除实例,它们也就随之消失,不会造成内存泄漏 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('DONE')); c.dec() c.dec() // DONE