dayLog02.“引用”类型——map与set

199 阅读6分钟

面向面试学习:“引用”类型——map与set

前言:原计划尝试写一些文章,过程中逐渐感觉前端在耦合与内聚这一块好像差点意思,在某一点上的理解往往会涉及到其余的联系;在以清晰的表达传递易于理解的结果这事上还不够水准,草稿箱里两三篇都不够成熟。从0到1是个漫长的过程,慢慢来吧。

“引用”

初识Object的时候我就有一个疑问——什么叫引用类型?在学习之后我把它理解为C系语言中的指针,可是如果二者相同,js在设计之初直接使用这个名词不是更好,显然这种理解并不完善。

首先,明确一个问题,在定义变量时,我们要定义的是数据或者说是值,也就是存储空间中实际存在的,这是行为逻辑,而实际的行为表现则是——变量类型 变量名 = 值

let a = 1;

这类单一的数据在js中称为简单数据类型,还有一类复杂的“数据堆”称之为引用类型。

let obj = {
    a:1,
    b:2
};
let arr = [1,2,3,4];

如果说变量名是语言提供给开发者的一种标识,内存实际并不在意这个标识长什么样,那么简单变量对于程序员与计算机就是这样一种对应关系——编辑器中的变量名实际就是变量地址的抽象,在使用a变量的时候,计算机的解读是操作‘a’地址中的变量。简单变量就是这种直接的对应关系,而对于复杂的引用类型“数据堆”,对于具体的变量显然没有直接的对应关系,而是间接的关系。

程序员视角:使用变量名使用变量

计算机视角:使用地址找到程序员要使用的变量

一组数据需要一组空间去组织存储,也就会产生一组地址信息,将这一组地址信息抽象为一个标记进行存储,再使用这个标识去建立直接的对应关系,这就是“引用”的含义——变量名是抽象标记是简单数据类型对应模式,使用抽象标记间接的使用数据本身。也就是说,表述b是a的引用实际的含义是“b”地址中的抽象标记与“a”地址中的抽象标记相同

总结:引用类型就好比楼房或是饭店,我们使用的单元号/酒店名,而数据具体要通过门牌号/房间名得到,这个门牌号可以是隐含的顺序结构(如数组下标0,1,2...),也可以是自定义的酒店雅间(如对象键名‘yyds’,‘aswl’...)

map与set

map与set都是引用数据类型,map的数据基本结构类似对象是键值对,set则是类似数组表现为值的集合,他们都只能通过new关键字来创建数据实例

注意:红宝书上将set称之为加强的map,原因是二者api和行为相同,本文是从数据的外在表现进行类比

let isMap = new Map();
let isSet = new Set();

对数据的操作无非就是增删改查,因此二者的api相似。

添加数据的方式有两种,第一种是在初始化时候填充,第二种方式是通过set()/add()函数添加,这个函数会返回数据结果实例。map的键/值可以是任意数据类型,set的可以是任意数据类型。这两种数据类型都使用size获取数据尺寸。注意,与对象类型不同,这两种类型内部数据的组织是有顺序的

let isMap = new Map(
    [
        ["key1","val1"],
        ["key2","val2"]
    ]
)
let key3 = {
    a:1,
    b:2
}
isMap.set(key3,"val3")
isMap.size; //3
let isSet = new Set(
    ["val1","val2"]
)
isSet.add(key3);
isSet.size;  //2

移除数据的方式有两种,删除单个元素delete()函数和清空整个集合的clear()函数。Map类型delete函数的参数为目标元素的键,Set类型则为元素值

isMap.delete('key1');
isMap.size; //2
isMap.clear();
isMap.size;  //0

isSet.delete('val1');
isSet.size;  //1
isSet.clear();
isSet.size;  //0

两种类型均可使用has()函数判断集合中是否存在目标元素,与delete()方法相同,Map使用键作为参数,Set使用值作为参数。并且二者都是可迭代类型,即可以通过“遍历”的方式获得内部元素,对应Map类型,可以使用forEach()函数依次获得每组键值对,或使用for...of方法结合keys()/values() 方法,取得每组元素的键/值。Set类型同理可以使用forEach()或for...of结合values() 方法,此外,两种类型还可以通过扩展操作(...)将集合转变为数组

let m = new Map(
    [
        ["key1","val1"],
        ["key2","val2"],
        ["key3","val3"],
    ]
)
m.has("key1");  //true;
m.forEach((key,val) => {
    console.log(`${key}--${val}`);
})
/*
*   key1--val1
*   key2--val2
*   key3--val3
*/
for(let key of m.keys()){
    console.log(key)
}
//key1
//key2
//key3
console.log([...m]) //[[key1,val1],[key2,val2],[key3,val3]]

let s = new Set(
    ["val1","val2","val3"]
)
s.has("val4");  //false
s.forEach((val) => {
    console.log(val)
})
//val1
//val2
//val3

for(let val of s.values()){
    console.log(val)
}
//val1
//val2
//val3

console.log([...s]) //["val1","val2","val3"];

在遍历的过程中可以对取到的元素进行修改,注意:假如修改的元素是一个引用类型,那么针对集合的修改操作也会影响到原来的引用类型的数据。现在应该不难理解,修改操作使用的是抽象标记,而无论是独立的引用数据,还是其在集合中的“副本”都是使用同一个抽象标记,因此,二者的操作是联动的。

let a = {
    id:1
}
let m = new Map(
    [a,"obj"]
)
for(let key of m.keys()){
    console.log(key.id) //1
    key.id = "2";
    console.log(m.get(a)) //obj
}
console.log(a.id) //2

weakMap/weakSet

这两个“弱映射”是对完全体的进一步约束。进一步约束表现在weakMap的键只能是Object类型或继承自Object的类型(比如通过new关键字创建的“简单数据”),weakSet的值只能是Object类型或继承自Object的类型;弱映射则表现在,当使用的Object类型不存在集合以外的其他引用时,这个键/值会被垃圾回收,集合中将不再存在与之相关的元素。

//初始化weakMap类型时创建了一个空对象,这个空对象不存在与之关联的其他引用,这个元素会被垃圾回收
let wm = new WeakMap()

wm.set({},"val");
console.log(wm.has({})) //false

let obj = {
    key:{}
}

wm.set(obj.key,"val");
//此时,obj.key所抽象标记的{}被集合引用,同时也在obj中被引用,因此集合中的元素不会被垃圾回收
console.log(wm.has(obj.key)) //true
obj.key = null;
//此时{}不再被obj.key引用,此时wm集合中的数据将被垃圾回收
console.log(wm.has(obj.key)) //false

小结

Map类型与Set类型的区别在于内部数据的组织形式是键值对或值,weak版本相较完全体对数据的类型具备更高的要求以及垃圾回收问题,此外,因为内部元素随时可能被垃圾回收,weak版本是不可迭代的,也就无法对他们施加“遍历”操作。

总结:这篇文章的由头是在面试题中看到Map与WeakMap的区别,于是就翻书补充了一下。因为作者修为尚浅,这其中涉及到的迭代相关的内容并没有深入研究,也就没有展开。说到底还是面向面试的功利性学习,对于应用场景的理解上充满了模糊的不安全感。自学到现在仍然处于很闭塞的状态,如果读到这里,欢迎留下你任何角度的想法,我很期望这样的交流。