10分钟了解set,map,weakset,weakmap
2024-12-09 11:13:16

面试官(朋友):看简历上,您对JS很熟悉,您了解set和map吗?

我:set我常用来做数据去重,map没了解过。

面试官(朋友):哈哈哈,您真是太幽默了,欢迎那您来参加面试,后续人事会通知您面试结果的。

正文

关于set和map的面试题,我个人确实没怎么看,除了一开始翻过ES6的书,记得要用set去重数组之外,后续就没有深入了解了。

我甚至记不得weakset和weakmap这两种。

这里今天朋友客串了一把面试官,然后狠狠地嘲笑了我一顿,想想真是糟糕。

Set和Map

Set和Map是ES6新增加的数据类型。其中Set被称作“集合”,Map被称作“映射”。

新增的这两个数据结构提供了更灵活和强大的方式来处理和存储数据。

Set

Set是一个简单值的集合,类似于数组。Set的特点:Set成员的值都是唯一的,不允许重复。

通过Set这一特性,可以运用Set进行去重操作。

应用场景
数组去重

通过数组的解构和Set无重复值的特性,可以产生一个数组去重的小妙招。

1
2
3
4
const arr = [1, 2, 3, 1, 2, 3, 1, 1]
const newArr = [...new Set(arr)]
console.log(newArr);
// 结果:[1,2,3]
字符串去重

数组可以通过Set特性去重,而且数组还有join方法可以将数组转换成字符串,这样我们可以联想到字符串去重的方法。

1
2
3
4
const str = 'abcabcaaa'
const newStr = [...new Set(str)].join('')
console.log(newStr);
// 结果:abc
Set常用方法

通过new Set()初始化Set对象

1
2
3
4
5
const set = new Set()

//或从一个数组创建
const arr = [1,2,3,4]
const set = new Set(arr)

调用add(value)方法向 set 集合 中添加指定元素

1
set.add(1)

调用has(value)方法检查集合中是否存在指定元素

1
set.has(2)

调用delete(value)方法删除集合中的指定元素

1
set.delete(1)

调用clear()方法清空集合内所有元素

1
set.clear()

set对象中的size属性可以返回集合中元素的数量:

1
2
3
set.add(1)
set.add(1)
console.log(set.size)//1(不存在重复值)
Set的遍历方法
  1. 调用keys()方法返回一个新的迭代器对象,该对象包含Set对象的每个元素的键。
  2. 调用values()方法返回一个新的迭代器对象,该对象包含Set对象的每个元素的值。
  3. 调用entries()方法返回一个新的迭代器对象,该对象包含Set对象的每个元素(值和键)。
1
2
3
4
5
6
7
8
9
const set = new Set([1, 2, 3, 1, 2, 3, 4])
console.log(set.keys());
console.log(set.values());
console.log(set.entries());

// 结果如下:
// SetIterator {1, 2, 3, 4}
// SetIterator {1, 2, 3, 4}
// SetIterator {1 => 1, 2 => 2, 3 => 3, 4 => 4}
遍历Set的方法

forEach方法

和数组一样,Set也有forEach方法。

1
2
3
4
5
6
7
8
9
const set = new Set([1, 2, 3, 1, 2, 3, 4])
set.forEach((item) => {
console.log(item)
})
// 结果为:
// 1
// 2
// 3
// 4

for...of循环

1
2
3
4
5
6
7
8
9
const set = new Set([1, 2, 3, 1, 2, 3, 4])
for (let item of set) {
console.log(item)
}
// 结果为:
// 1
// 2
// 3
// 4
Set的优缺点
  • 优点:
    1. Set中的元素都是唯一的,不会重复的值。
    2. 因为Set中的值是唯一的,所有查找、删除和添加的操作执行得更快。
  • 缺点:
    1. 在ES6中,Set保持了插入顺序,但是存在兼容问题。
    2. 与数组不同,Set不能通过索引来访问Set中的元素。

Map

在普通对象中,对象的键名的类型都是字符串,如果不是字符串类型也会被转换为字符串类型。

1
2
3
4
5
6
7
8
9
let n = 123
const obj = {
[n]: 1,
n: 1,
name: '张三',
[['测']]: '1'
}
console.log(obj);
// 结果:{123: 1, n: 1, name: '张三', "测": '1'}

这里我们注意到了,二维数组也被转为了空字符串。

然而随着需求的不断增加,出现了对象中的键名需要是其他类型的需求,因此JavaScript提供了Map这一数据结构。

Map这一数据结构允许使用任何值作为键。

注意:不要将数组的map方法和Map数据结构记混。

Map常用方法

通过new Map()初始化Map对象。

1
const map= new Map()

调用set(key,value)方法向Map中添加一个键值对。

注意:不要和上面的Set数据结构搞混喽。

1
map.set('name','张三')

调用get(key)方法根据提供的键名返回对应的值,如果不存在该键名则返回undefined。

1
map.get('name')

调用has(key)方法可以检查是否包含指定的键。

1
console.log(map.has('name'))

如果包含就返回true,如果不包含就返回false。

调用delete(key)方法可以删除指定的键值对。

1
map.delete('name')

调用clear()方法可以删除所有键值对。

1
map.clear()

Map的属性size可以返回键值对数量。(注意size不是方法,而是属性)

1
2
map.size
// 1
Map的遍历方法

与Set的遍历方法相似。

  1. keys():返回一个新的迭代器对象,其中包含Map的所有键名。
  2. values():返回一个新的迭代器对象,其中包含Map的所有键值。
  3. entries():返回一个新的迭代器对象,其中包含Map的所有键值对。
1
2
3
4
5
6
7
8
9
10
const map = new Map();
map.set('1', '2')
map.set(['1'], '1')
console.log(map.keys());
console.log(map.values());
console.log(map.entries());

// MapIterator {'1', Array(1)}
// MapIterator {'2', '1'}
// MapIterator {'1' => '2', Array(1) => '1'}
遍历Map的方法

forEach方法

1
2
3
4
5
6
7
8
9
10
11
const map = new Map()
map.set('name', '张三')
map.set('age', 18)
map.set('gender', '男')
map.forEach((value, key) => {
console.log(key, value)
})

// name 张三
// age 18
// gender 男

for...of循环

1
2
3
4
5
6
7
8
9
10
const map = new Map()
map.set('name', '张三')
map.set('age', 18)
map.set('gender', '男')
for (let [key, value] of map) {
console.log(key, value)
}
// name 张三
// age 18
// gender 男
Map的优缺点
  • 优点:
    • Map可以存储任何类型的键和值
    • Map的键也是唯一的,不存在重复的键
    • Map内的顺序和插入顺序一致
  • 缺点:
    • Map不能和数组一样通过索引直接访问
    • 存在兼容IE的问题
Map的应用场景

Map因为是一组键值对的结构,具有极快的查找速度。

举个例子,假设要根据同学的名字查找对应的成绩,如果用Array实现,需要两个Array

1
2
var names = ['Michael', 'Bob', 'Tracy'];
var scores = [95, 75, 85];

给定一个名字,要查找对应的成绩,就先要在names中找到对应的位置,再从scores取出对应的成绩,Array越长,耗时越长。

这里我们用Map实现,只需要一个“名字”-“成绩”的对照表,直接根据名字查找成绩,无论这个表有多大,查找速度都不会变慢。

1
2
var m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
m.get('Michael'); // 95

初始化Map需要一个二维数组,或者直接初始化一个空MapMap具有以下方法

1
2
3
4
5
6
7
var m = new Map(); // 空Map
m.set('Adam', 67); // 添加新的key-value
m.set('Bob', 59);
m.has('Adam'); // 是否存在key 'Adam': true
m.get('Adam'); // 67
m.delete('Adam'); // 删除key 'Adam'
m.get('Adam'); // undefined

由于一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的值冲掉

1
2
3
4
var m = new Map();
m.set('Adam', 67);
m.set('Adam', 88);
m.get('Adam'); // 88

弱引用和强引用

WeakSet和WeakMap都是弱引用。

首先了解一下什么是弱引用。

弱引用是不能确保其引用的对象不会被垃圾回收器回收的引用,而强引用是确保其引用的对象不会被垃圾回收器回收的引用。

也就是说,JavaScript引擎在执行代码时,对象通过变量直接赋值形成的引用会被视为强引用,垃圾回收器就不会回收这类对象;

通过WeakMapWeakSet建立的引用会被视为弱引用,这类引用无法阻止垃圾回收器的回收。

当一个变量被设置为null时,会断开该变量与原对象间的引用,该对象就会变成垃圾回收器的回收目标。

强引用

1
2
3
4
let person = { name: "张三" };
const person1 = [person];
person = null;
console.log(person1);

创建一个叫person的对象,并将该对象存储到person1中;

然后将person设置为null,断开引用,但是因为变量person1存在对person对象的强引用,所以该对象不会被垃圾回收器给盯上。

弱引用

1
2
3
4
5
6
let person1 = new WeakMap();
let person = { name: "张三" };
person1.set(person, "张三");
person = null;
// 等待垃圾回收后
console.log(person1);

创建一个weakMap对象person1和一个对象person,并且将该对象作为键,键值为"张三"添加到person1中;

然后将变量person设置为null,断开了变量与对象的引用,然而person1person是弱引用,所以垃圾回收器可以回收person对象。

WeakSet和WeakMap

WeakSet

WeakSet和Set非常相似,但是有一些不同之处:

  1. 成员类型:WeakSet的成员只能是Symbol值和对象,不能是其他的数据类型;Set的成员可以是任意数据类型。
  2. 引用类型:WeakSet是弱引用;Set是强引用。
  3. 方法和属性:WeakSet不支持迭代,所以没有forEach, values, keys, entries方法,并且也没有size属性;Set有forEach, values, keys, entries方法,也有size属性。
  4. 作用:WeakSet可以实现自动清理回收;Set可以通过元素的唯一性用于实现去重工作。
WeakMap

WeakMap和Map也非常相似,但是也是有一些不同:

  1. 键的类型:WeakMap的键类型只能是对象和Symbol值;Map的键类型可以是任意数据类型。
  2. 引用类型:WeakMap是弱引用;Map是强引用。
  3. 方法和属性:WeakMap不支持迭代,所以没有forEach, values, keys, entries方法,并且也没有size属性;Map有forEach, values, keys, entries方法,也有size属性。
  4. 作用:WeakMap也可以实现自动清理回收;Map提供需要高效键值对的操作。

结语

set和Map是常用的数据去重的类型,我个人平时没怎么用过去重的方法,所以接触的不深。

今天了解了一下,依然感觉没什么太广阔的用途,作为知识点了解下吧。

参考

ES6数据结构深度解析:Set, Map, WeakSet 和 WeakMap

Map和Set的应用场景(搬运)