前言
最近学习vue3
源码时发现响应式原理是用了 WeakMap
做缓存处理 ,而实际上工作中也是常用到 Set
去重,于是我决定彻底弄懂ES6
中的Map
和Set
、WeakSet
、WeakMap
,废话不多说,接下来我们就一起来了解一下这几种数据集合类型
Map
和Set
是ES6
新增的两个数据类型;都是属于内置构造函数;都使用new的方式来实例化
Map
是一组键值对的结构,具有极快的查找速度。Set
是一组key
的集合,但不存储value
, 而且key
不重复,可自动排重
如果要用一句话来描述,我们可以说
Set
是一种叫做集合的数据结构,Map
是一种叫做字典的数据结构
那什么是集合?什么又是字典?使用后端的定义
- 集合是由一堆无序的、相关联的,且不重复的内存结构【数学中称为元素】组成的组合
- 字典是一些元素的集合。每个元素有一个称作
key
的域,不同元素的key
各不相同
那集合和字典有哪些异同?
- 共同点:集合、字典都可以存储不重复的值
- 不同点:集合是以[值,值]的形式存储元素,字典是以[键,值]的形式存储
Set
Set
对象允许你存储任何类型的值,无论是原始值或者是对象引用。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set
本身是一个构造函数,用来生成Set
数据结构
Set
函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化
// 创建 Set 集合
const s = new Set(iterator);
// iterator 为一个可迭代数据,参数可以为空
Set
对象存储的值总是唯一的,所以需要判断两个值是否恒等,在es6
之前判断恒等一般是采用 ===
三个等号进行判断。
但是这个判断有些特殊值进行比较如 NaN
、+0
、-0
就不合人意, 所以 Set
判断比较采用的是 es6
出来的 Object.is()
方法进行。
但是有几个特殊值需要特殊对待那就是 +0
与 -0
在 Set
是判断相等。
Set
实例对象的属性和方法
size
:返回Set
实例的成员总数,只读属性add(value)
:向Set
中添加某个值,返回Set
结构本身(可以链式调用)delete(value)
:向Set
中删除某个值,删除成功返回true
,否则为false
has(value)
:查询Set
中是否有某个值,返回一个布尔值clear()
:清除所有成员,没有返回值
const s = new Set([1, 2, 3, 4]);
console.log(s); // Set(4) { 1, 2, 3, 4 }
console.log(s.add(5).add({ a: 6 }).add(1)); // Set(6) { 1, 2, 3, 4, 5, { a: 6 } } 返回 Set 本身
console.log(s); // Set(6) { 1, 2, 3, 4, 5, { a: 6 } } 重复项没有新增会自动去重
console.log(s.size); // 6
s.size = 5; // 设置无效只读属性
console.log(s.size); // Set(6) { 1, 2, 3, 4, 5, { a: 6 } }
console.log(s.delete(1)); // true
console.log(s); // Set(5) { 2, 3, 4, 5, { a: 6 } }
console.log(s.has(2)) // true
console.log(s.clear()); // undefined 没有返回值
console.log(s); // Set(0) {}
Set
的遍历方法
keys()
:返回键名的遍历器values()
:返回键值的遍历器entries()
:返回键值对的遍历器forEach()
:使用回调函数遍历每个成员
由于Set
结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys
方法和values
方法的行为完全一致
如下代码
const s = new Set(["a", "b", "c"]);
for (const item of s.keys()) {
console.log(item)
}
// a, b, c 依次打印
for (const item of s.values()) {
console.log(item)
}
// a, b, c 依次打印
for (const item of s.entries()) {
console.log(item)
}
// ["a","a"], ["b", "b"], ["c", "c"]
// 直接遍历set实例,等同于遍历set实例的values方法
for (let item of s) {
console.log(item)
}
// a, b, c 依次打印
s.forEach((item, index, self) => {
console.log(item, index, self);
})
// a a Set(3) { 'a', 'b', 'c' } , b b Set(3) { 'a', 'b', 'c' } , c c Set(3) { 'a', 'b', 'c' }
// set集合中不存在下标,因此forEach中的回调的第二个参数和第一个参数是一致的,均表示set中的每一项
Set
默认遍历器生成函数就是它的 values
方法
console.log(Set.prototype[Symbol.iterator] === Set.prototype.values) // true
Set
与数组间相互转化
Set
转化为数组可以使用es6
中最简单的展开运算符- 数组转
Set
因为数组就是一个可迭代数据直接将数组放入Set
构造函数参数中
// 数组转化为 Set
const arr = [1,2,3,4];
const s = new Set(arr);
console.log(s); // Set(4) { 1, 2, 3, 4 }
// Set 转化为数组
const a = [...s]
console.log(a); // [1,2,3,4]
Set
与Array
的区别
Set
中的元素不可重复,而Array
中的元素是可重复的Array
支持多种构建方式(构造函数、字面量),而Set
只支持使用构造函数来构建Set
不支持像Array
一样通过索引随机访问元素Set
只提供了一些基本的操作数据的方法,Array
提供了更多实用的原生方法
Set
的作用
- 数组去重
const s= new Set([1, 2, 3, 4, 4, 3])
console.log([...s]) // [1, 2, 3, 4]
- 合并
set
对象
let a = new Set([1, 2, 3])
let b = new Set([4, 3, 2])
let union = new Set([...a, ...b]) // Set(4){1, 2, 3, 4}
- 交集
let a = new Set([1, 2, 3])
let b = new Set([4, 3, 2])
let intersect = new Set([...a].filter(x => b.has(x)))
console.log(difference);console.log(difference); // Set(2){2, 3}
// 先除去自身的相同元素然后然后再利用数组的filter过滤方法
- 差集
let a = new Set([1, 2, 3])
let b = new Set([4, 3, 2])
const cross = new Set([...new Set(a)].filter(item => b.has(item))); // 交集
let difference = new Set([...a, ...b].filter(x => !cross.has(x)))
console.log(difference); // Set(2) { 1, 4 }
// 先得到交集,然后两个数组合并过滤相同在过滤交集
手写实现 Set
class MySet {
// 给定一个默认值因为使用时可以不传入值
constructor(iterator = []) {
// 判断是否传入一个可迭代数据
if (typeof iterator[Symbol.iterator] !== "function") {
throw new TypeError(`${typeof iterator} ${iterator} is not iterable (cannot read property Symbol(Symbol.iterator))`);
}
// 用于存储数据
this._datas = [];
// 遍历迭代器数据
for (const item of iterator) {
this.add(item);
}
}
// 添加方法
add(data) {
if (!this.has(data)) {
this._datas.push(data);
}
}
// 是否存在
has(data) {
for (const item of this._datas) {
if (this._isEqual(data, item)) { // 进行比较
return true;
}
}
return false;
}
// 删除方法
delete(data) {
for (const item of this._datas) {
if (this._isEqual(data, item)) {
this._datas = this._datas.filter(ele => ele != item); // 过滤掉重新赋值
return true
}
}
return false;
}
// 清除方法
clear() {
this._datas.length = 0;
}
//! set 长度由于不能设置长度只能获取长度所以不是方法而是访问器属性
get size() {
return this._datas.length;
}
//! 遍历方法
forEach(callback) {
for (const item of this._datas) {
// 参数一和二都是每一项
callback(item, item, this)
}
}
// keys
keys(){
return this._data;
}
// values
values() {
return this._data;
}
// entries
entries() {
let result = [];
this._data.forEach((item) => {
result.push([item, item]);
})
return result;
}
//! 迭代器实现
*[Symbol.iterator]() {
for (const item of this._datas) {
yield item;
}
}
// 用于比较数据是否相同
_isEqual(data1, data2) {
// 进行特殊比较
if (data1 === 0 && data2 === 0) { // 进行特殊比较,使用 Object.is 时 +0 与 -0 为 false
return true
}
return Object.is(data1, data2);
}
}
// 测试代码
var s = new MySet([1, 2, 4, 5, 6, 4]);
// set 中设置长度大小不能像数组一样改变内容设置无效
s.size = 3;
console.log(s);
for (var item of s) {
console.log(item);
}
console.log(new MySet(0))
Map
Map
对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。构造函数Map可以接受一个数组作为参数
// 创建一个 map 数据
const map = new Map(iterator);
// iterator 为一个可迭代数据,参数可以为空
Map数据结构它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键
Map结构提供了值 — 值
的对应,是一种更完善的Hash结构实现,所有具有极快的查找速度
Map
和Object
的区别
Map
中的键值是有序的(FIFO
原则),而添加到对象中的键则不是Map
的键值对个数可以从size
属性获取,而Object
的键值对个数只能手动计算Object.keys()
Object
都有自己的原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突Map
自身支持迭代,而Object
不支持Map
可以使用任何数据类型作为键,而Object
只能使用数值、字符串或symbol
Map
与Object
选择使用
- 「内存占用」 :固定大小的内存,
Map
约比Object
多存储 50%的键值对 - 「插入性能」 :涉及大量插入操作,
Map
性能更佳 - 「查找性能」 :涉及大量查找操作,
Object
更适合 - 「删除性能」 :涉及大量删除操作,选择
Map
Map
的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键
这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名
Map
实例对象的属性和方法
size
:返回Map
实例的成员总数,只读属性set(key,value)
:向Map
中添加某个简直对,如果key
存在则更新,否则添加,返回Map
结构本身(可以链式调用)get(key)
: 通过键值查找特定的数值返回,如果找不到key
那么返回undefined
has(key)
:判断Map
对象中是否有key
所对应的值,有返回true
否则false
delete(key)
:通过键值从Map
中移除对应的数据,删除成功返回true
删除失败返回false
clear()
: 将Map
中所有元素删除,没有返回值
const map = new Map();
console.log(map.set({}, 1).set(false, 2).set("abc", 4).set("abc", 5)); // Map(3) { {} => 1, false => 2, 'abc' => 5 } 返回 Map
console.log(map); // Map(3) { {} => 1, false => 2, 'abc' => 5 } // 添加相同的键为更新,不同则添加
console.log(map.get("abc")) // 5 get 获取值使用到自身 has 方法是否含有
console.log(map.get({})); // undefined
console.log(map.has(false)); // true has方法也是采用 Object.is() 进行比较
map.size = 5;
console.log(map.size); // 3 只读属性设置无效
console.log(map.delete({})); // false 引用类型比较时地址的比较
console.log(map.delete(false)); // true
console.log(map.clear()); // undefined
console.log(map); // Map(0) {}
Map
的遍历方法
keys()
:返回键名的遍历器values()
:返回键值的遍历器entries()
:返回键值对的遍历器forEach()
:使用回调函数遍历每个成员
// 创建一个 map
const map = new Map([['a', 1], ['b', 2]])
for (let key of map.keys()) {
console.log(key)
}
// "a" , "b"
for (let value of map.values()) {
console.log(value)
}
// 1, 2
for (let item of map.entries()) {
console.log(item)
}
// ["a", 1], ["b", 2]
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value)
}
// "a" 1, "b" 2
// for...of...遍历map等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value)
}
// "a" 1, "b" 2
Map的遍历器生成函数就是它的entries()方法
console.log(Map.prototype[Symbol.iterator] === Map.prototype.entries) //true
Map
与其他数据互相转换
-
Map
与对象的转换
// map 转对象
const obj = {}
const map = new Map(['a', 111], ['b', 222])
for(let [key,value] of map) {
obj[key] = value
}
console.log(obj) // {a:111, b: 222}
// 对象转map
const m = new Map();
for(const key in obj.keys()){
m.set(key,obj[key]);
}
-
Map
与 数组的转换
// 数组转 map
const arr = [["a", 3], ["b", 5], ["e", 6]];
console.log(new Map(arr)); // Map(3) { 'a' => 3, 'b' => 5, 'e' => 6 }
// Map 转数组
console.log([...arr]); // [["a", 3], ["b", 5], ["e", 6]] 展开运算符
-
JSON
字符串要转换成Map
可以先利用JSON.parse()
转换成数组或者对象,然后再转换即可
手写实现 Map
class MyMap {
constructor(iterable = []) {
// 判断是否为一个可迭代的对象
if (typeof iterable[Symbol.iterator] !== "function") {
throw new TypeError(`${typeof iterable} ${iterable} is not iterable (cannot read property Symbol(Symbol.iterator))`);
}
this._datas = []; // 存储数据
for (const item of iterable) {
// 再次判断 item 是否为可迭代对象
if (typeof item[Symbol.iterator] !== "function") {
throw new TypeError(`${typeof item} ${item} is not iterable (cannot read property Symbol(Symbol.iterator))`)
}
// 获取迭代器的前两项(注意这里不能使用数组方法因为迭代器不一定数组)
const iterator = item[Symbol.iterator]();
const key = iterator.next().value; // key 值
const value = iterator.next().value; // value 值
// 存储数据 key ==> value
this.set(key, value);
}
}
// 设置值(没有存储,含有进行覆盖修改)
set(key, value) {
if (this.has(key)) {
this._datas.forEach((item, index) => {
if (this._isEqual(item.key, key)) {
item.value = value;
}
});
} else {
this._datas.push({ key, value });
}
}
// 查找值
get(key) {
for (const item of this._datas) {
if (this._isEqual(item.key, key)) {
return item.value
}
}
return undefined; // 可以省略以为默认返回为 undefined
}
// 是否含有
has(key) {
return this.get(key) !== undefined;
}
// 删除
delete(key) {
this._datas.forEach((item, index) => {
if (this._isEqual(item.key, key)) {
this._datas.splice(index, 1)
return true;
}
});
return false;
}
// 清空 map
clear() {
this._datas.length = 0;
}
// map 长度只读属性
get size() {
return this._datas.length;
}
// 迭代器对象
*[Symbol.iterator]() {
for (const item of this._datas) {
yield [item.key, item.value]
}
}
// keys
keys() {
const result = [];
this._datas.forEach((item) => {
result.push(item.key);
});
return result;
}
// values
values() {
const result = [];
this._datas.forEach((item) => {
result.push(item.value);
});
return result;
}
// entries
entries() {
return this._datas;
}
// 遍历方法
forEach(callback) {
for (const item of this._datas) {
callback(item.value, item.key, this);
}
}
// 比较值是否相同
_isEqual(data1, data2) {
if (data1 === 0 && data2 === 0) {
return true;
}
return Object.is(data1, data2);
}
}
// 测试
var m1 = new MyMap([["a", 2], [{}, 4], [false, 6], ["a", 5]]);
console.log(m1);
console.log(m1.get("a"));
console.log(m1.size);
m1.forEach((value, key, self) => {
console.log(value, key, self._datas);
})
WeakSet
WeakSet
对象允许你将弱保持对象存储在一个集合中
WeakSet
语法使用跟 Set
一样,使用该集合,可以实现和Set
一样的功能
WeakSet
可以接受一个具有 Iterable
接口的可迭代对象作为参数,该对象的所有迭代值都会被自动添加进生成的 WeakSet
对象中,null
被认为是 undefined
var ws = new WeakSet([iterable]);
var ws = new WeakSet([])
WeakSet
与 Set
两个区别
WeakSet
内部存储的对象地址不会影响垃圾回收WeakSet
只能添加对象引用类型的集合,而不能是任何类型的任意值,Set
可以为任意类型- 不能遍历(不是可迭代的对象)、没有
size
属性、没有forEach
方法(因为不使用的数据会回收遍历无效) WeakSet
是不可枚举的,WeakSet
中没有存储当前对象的列表
WeakSet
含有的 API
如下图
WeakSet
里面的引用只要在外部消失,它在 WeakSet
里面的引用就会自动消失, 所有可以作为依赖收集
WeakSet
对象是一些对象值的集合, 并且其中的每个对象值都只能出现一次。在WeakSet
的集合中是唯一的
let ws = new WeakSet([2,3])
console.log(ws) //报错
let obj = {
name: "yj",
age: 18
};
const set = new WeakSet();
set.add(obj);
obj = null;
console.log(set); // WeakSet { <items unknown> }
// 由上面结果可以得知当 obj 中数据找不到时会进行垃圾回收则 WeakSet 中数据也为空
WeakSet
的应用场景
可以用于存储DOM节点,而不用担心节点从文档移除时引发内存泄漏
const wes= new WeakSet();
const h = document.querySelector('h1');
wes.add(h);
aa.addEventListener("click",function(){
// 这里进行代码操作删除自己之后,wes 中指向这数据直接释放
})
// 只要 WeakSet 中任何元素从 DOM 树中被删除,垃圾回收程序就可以忽略其存在,而立即释放其内存(假设没有其他地方引用这个对象)。
WeakMap
WeakMap
对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的
WeakMa
p结构与Map
结构类似,也是用于生成键值对的集合
WeakMap
与Map
两个区别
WeakMap
它的键存储的地址不会影响垃圾回收WeakMap
只接受对象作为键名,而Map
的键名可以是任何数据类型- 不能遍历(不是可迭代的对象)、没有size属性、没有forEach方法 (没有遍历
API
没有clear
方法
WeakMap
含有的 API
如下图
WeakMap
的键名所指向的对象,一旦不再需要,里面的键名对象和所对应的键值对会自动消失,不用手动删除引用
注意:WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用 ,如下示例
const wm = new WeakMap();
let key = {};
let obj = { foo: 1 };
wm.set(key, obj);
obj = null;
console.log(wm.get(key)); // {foo: 1}
WeakMap
实际应用
-
DOM
对象上保存数据
使用 jQuery
的时候获取一些元素身上相关信息,我们会通过$.data()
方法在 DOM
对象上储存的数据,jQuery
内部会使用一个对象管理 DOM
和对应的数据,当你将 DOM
元素删除,DOM
对象置为空的时候,相关联的数据并不会被删除,你必须手动调用 $.removeData()
方法才能删除掉相关联的数据,而如果我们使用 WeakMap
就可以简化删除结构操作后删除数据
let wm = new WeakMap(), element = document.querySelector(".element")
wm.set(element, "data")
let value = wm.get(elemet)
console.log(value) // data
element.parentNode.removeChild(element)
element = null
- 类中私有变量
const dongName = new WeakMap();
const dongAge = new WeakMap();
const classPrivateFieldSet = function(receiver, state, value) {
state.set(receiver, value);
}
const classPrivateFieldGet = function(receiver, state) {
return state.get(receiver);
}
class Dong {
constructor() {
dongName.set(this, void 0);
dongAge.set(this, void 0);
classPrivateFieldSet(this, dongName, 'dong');
classPrivateFieldSet(this, dongAge, 20);
}
hello() {
return 'I\'m ' + classPrivateFieldGet(this, dongName) + ', ' + classPrivateFieldGet(this, dongAge) + ' years old';
}
}
// 测试
const dong = new Dong();
console.log(dong.hello()); // I'm dong, 20 years old
console.log(dong.name); // undefind
console.log(dong.age); // undefined
通过使用WeakMap
完美的实现类中私有变量,私有属性保存在 WeakMap
中,因为是用对象作为 key 的,那不同的对象是放在不同的键值对上的,相互没影响,对象销毁的时候,对应的键值对就销毁,不需要手动管理
每个属性定义了一个 WeakMap 来维护,key 为当前对象,值为属性值,get 和 set 使用 classPrivateFieldSet 和 classPrivateFieldGet 这两个方法,最终是通过从 WeakMap 中存取的
在构造器里初始化下当前对象对应的属性值,也就是 dongName.set(this, void 0),这里的 void 0 的返回值是 undefined
总结
Set
对象允许你存储任何类型的值,无论是原始值或者是对象引用。成员的值都是唯一的,没有重复的值 (可做去重)Map
对象保存键值对形式,键名键值可以是任何值(对象或者原始值) 都可以作为一个键或一个值WeakSet
对象允许你将弱保持对象存储在一个集合中,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏WeakMap
对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象(null除外),而值可以是任意的