之前写过一篇文章,讲的是es6的Map对象,而同为es6的新语法,Set却没有讲,如今看了数据结构,对Set/Map有了一些理解,正好就将之前所学联系起来,于是乎决定自己封装Set/Map
Set在我的理解中,完全符合数据结构集合的定义,即在集合中,每个值都是唯一值,且集合内的元素是无序的,如果我们想自己封装一个集合,可以参考Set
可以看到,封装一个Set,需要:
属性
- size: 元素个数
- Symbol(Symbol.iterator): 迭代器接口
方法
- add(): 添加元素
- clear(): 清空元素
- delete(): 删除元素
- entries(): 返回一个由所有元素组成的数组
- forEach(): 遍历元素
- has(): 判断是否存在该元素
- keys(): 返回其中元素的键的数组
- values(): 返回其中元素的键的数组
其他杂七杂八的算了,我们就封装这些主要功能好了
首先,我决定用对象来封装Set,因为我们知道对象的键是唯一的,因此,我们可以将键和值设置为相等的,这样我们就可以很轻易的实现集合 无序/元素唯一 的两大特点
Set本身是怎么实现的我也不懂,但道理是差不多的,可以看到内部的元素也是键值相等的键值对
// 使用class来封装MySet类
class MySet {
constructor(...element) {
this.size = 0;
this.items = {};
element.forEach((item) => this.add(item));
}
[Symbol.iterator]() {
let index = 0;
const keys = Object.keys(this.items);
const _this = this;
return {
next() {
return {
done: index>=keys.length,
// 注意这里的后缀式自增,返回自增前的值
value: _this.items[keys[index++]],
};
},
};
}
has(element) {
return !!this.items[element];
}
add(element) {
if (this.has(element)) return false;
Object.defineProperties(this,{
[element]:{
configurable: true,
get:(k)=>{
return this.items[element]
},
}
})
this.items[element] = element;
this.size++;
return this;
}
clear() {
this.items = {};
this.size = 0;
}
delete(element) {
if (this.has(element)) {
delete this.items[element];
this.size--;
return true;
}
return false;
}
entries() {
return Object.entries(this.items).map((item) => ({
key: item[0],
value: item[1],
}));
}
keys() {
return Object.entries(this.items).map((item) => item[0]);
}
values() {
return Object.entries(this.items).map((item) => item[1]);
}
forEach(callback) {
const iterator = this[Symbol.iterator]();
while (!iterator.done) {
callback(iterator.value, iterator.key);
iterator.next();
}
}
// 并集
static union(a, b) {
b.forEach((item) => {
a.add(item);
});
return a;
}
}
const myset = new MySet()
myset.add(10)
myset.add(20)
myset.add(30)
for(let k of myset) {
console.log(k);
}
对于集合的应用是非常常见的,这里也使用类的静态方法写一下常用的求并集/交集/差集的方法
求并集
static union(a, b) {
return new MySet(...a, ...b);
}
求交集
static intersect(a, b) {
return new MySet(
...a.values().filter((item) => b.values().includes(item))
);
}
求差集
static except(a, b) {
return new MySet(
...a.values().filter((item) => !b.values().includes(item))
);
}
写完试一下
const mysetA = new MySet(10, 20, 30);
const mysetB = new MySet(30, 40, 50);
const unionSet = MySet.union(mysetA, mysetB);
const intersectSet = MySet.intersect(mysetA, mysetB);
const exceptSet = MySet.except(mysetA, mysetB);
console.log(unionSet);
console.log(intersectSet);
console.log(exceptSet);
好的,MySet现在有了,再说说Map,Map显然是名为字典的数据结构,它的特点是他的键值都可以是任意数据类型,他也可以称之为映射/符号表/关联数组
他有的常见方法:
- toStrFn() 将对象转换为字符串
- hasKey() 判断是否已经有这个键
- set() 添加/修改值
- get() 获取键对应的值
- remove() 删除对应键的键值对
- keys() 获取键数组
- values() 获取值数组
- entries() 获取键值对数组
- size() 获取实例的属性数量
- isEmpty() 判断实例是否为空
- clear() 清空实例中的属性
- forEach() 遍历
注: es6的Map和我封装的不同,我使用对象转换为json字符串作为键,但Map使用的是引用地址作为键,所以必须保存键的引用才能通过键拿到值,同时,如果引用丢了,就会导致内存泄漏,为了解决这个问题,es6还有一个新的对象结构,weakMap,它的键必须是引用数据类型,并且一旦键的引用消失,其WeakMap内的键值就会被垃圾回收机制回收,也是因为这种自动的垃圾回收,WeakMap也无法使用各种遍历方法
class MyMap {
constructor(...element) {
this.table = {};
}
toStrFn(obj) {
if (obj === null) return "NULL";
else if (obj === undefined) return "UNDEFINED";
// 这里使用instanceof判断是否使用构造函数创建的字符串类型
else if (obj instanceof String || typeof obj === "string") return obj;
return JSON.stringify(obj);
}
hasKey(key) {
return !!this.table[this.toStrFn(key)];
}
set(key, value) {
// 为了保存初始的键,这里使用对象存储
this.table[this.toStrFn(key)] = { key, value };
}
get(key) {
if (this.hasKey(key)) return this.table[this.toStrFn(key)].value;
}
remove(key) {
if (!this.hasKey(key)) return;
delete this.table[this.toStrFn(key)];
return true;
}
entries() {
return Object.values(this.table);
}
keys() {
return this.entries().map((item) => item.key);
}
values() {
return this.entries().map((item) => item.value);
}
size() {
return Object.keys(this.table).length;
}
isEmpty() {
return this.size() === 0;
}
clear() {
this.table = {};
}
forEach(callback) {
const arr = this.entries();
for (let i = 0; i < arr.length; i++) {
callback(arr[i].key, arr[i].value);
}
}
}
注释,为什么要同时使用typeof和instanceof: