前言

最近学习vue3 源码时发现响应式原理是用了 WeakMap 做缓存处理 ,而实际上工作中也是常用到 Set 去重,于是我决定彻底弄懂ES6中的MapSetWeakSetWeakMap ,废话不多说,接下来我们就一起来了解一下这几种数据集合类型

es6map 下表_es6map 下表

MapSetES6新增的两个数据类型;都是属于内置构造函数;都使用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-0Set 是判断相等。

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]

SetArray的区别

  • 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结构实现,所有具有极快的查找速度

MapObject的区别

  • Map中的键值是有序的(FIFO原则),而添加到对象中的键则不是
  • Map的键值对个数可以从 size 属性获取,而 Object的键值对个数只能手动计算Object.keys()
  • Object 都有自己的原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突
  • Map自身支持迭代,而Object不支持
  • Map可以使用任何数据类型作为键,而Object只能使用数值、字符串或symbol

MapObject选择使用

  • 「内存占用」 :固定大小的内存,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([])

WeakSetSet 两个区别

  • WeakSet内部存储的对象地址不会影响垃圾回收
  • WeakSet只能添加对象引用类型的集合,而不能是任何类型的任意值, Set 可以为任意类型
  • 不能遍历(不是可迭代的对象)、没有size属性、没有forEach方法(因为不使用的数据会回收遍历无效)
  • WeakSet 是不可枚举的,WeakSet中没有存储当前对象的列表

WeakSet 含有的 API 如下图

es6map 下表_es6map 下表_02

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 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的

WeakMap结构与Map结构类似,也是用于生成键值对的集合

WeakMapMap两个区别

  • WeakMap 它的键存储的地址不会影响垃圾回收
  • WeakMap只接受对象作为键名,而Map的键名可以是任何数据类型
  • 不能遍历(不是可迭代的对象)、没有size属性、没有forEach方法 (没有遍历 API 没有 clear 方法

WeakMap 含有的 API 如下图

es6map 下表_数组_03

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除外),而值可以是任意的