全面解析 Map、WeakMap、Set、WeakSet

目录

[Map 是什么?](#Map 是什么?)

[Map 实例属性和方法](#Map 实例属性和方法)

[WeakMap 是什么?](#WeakMap 是什么?)

[WeakMap 常用方法](#WeakMap 常用方法)

[WeakMap 使用场景举例](#WeakMap 使用场景举例)

[Set 是什么?](#Set 是什么?)

[创建 Set](#创建 Set)

[Set 实例属性和方法](#Set 实例属性和方法)

[遍历 Set](#遍历 Set)

[Set 与数组的转换](#Set 与数组的转换)

[Set 使用场景](#Set 使用场景)

[WeakSet 是什么?](#WeakSet 是什么?)

[WeakSet 特点](#WeakSet 特点)

WeakSet实例方法

[WeakSet 使用场景举例](#WeakSet 使用场景举例)

总结

[回顾 WeakMap 和 WeakSet 的重要概念和特点](#回顾 WeakMap 和 WeakSet 的重要概念和特点)

强调它们在处理内存泄漏和避免循环引用方面的优势


Map 是什么?

Map 本质上是一个键值对的集合。相比于普通的对象(Object),Map提供了一些额外的功能和优势,解决了一些对象的限制和问题,包括:

  • 键的数据类型不受限制:普通对象的键只能是字符串或符号(Symbol),而 Map 的键可以是任意数据类型,包括基本类型和对象等
  • 键值对的顺序保证: Map保持键值对的插入顺序,迭代时会按照插入的顺序进行
  • 方便获取键值对数量: Map提供了 size 属性,可以方便地获取键值对的数量
  • 更灵活的迭代方式: Map提供了更灵活的迭代方式,包括 keys()、values() 和 entries() 等方法,使得对键、值或键值对的遍历更加方便
  • 易于判断是否包含某个键: 与对象相比,Map 提供了 has 方法,用于判断是否包含指定的键
  • 更好的性能: 在某些场景下,使用 Map 可能具有更好的性能,尤其是在频繁增删键值对的情况下,因为 Map 在这方面优于普通对象

Map 实例属性和方法

  • set()

设置键名 key 对应的键值为 value,然后会返回整个 Map 结构,如果设置的 key 已经存在,则会更新 value 值,否则会新生成该键

javascript 复制代码
let map = new Map()
map.set('name', 'Jasmine')
// 也可采用链式写法设置多组数据
map.set('sex', 'Female').set('age', 31)
// 更新 name 值
map.set('name', 'Sue')
console.log(map)
// Map(3) {'name' => 'Sue', 'sex' => 'Female', 'age' => 31}
  • get()

通过 get 方法读取 key 对应的键值,如果传入的键值不存在,则会返回 undefined

javascript 复制代码
let map = new Map()
map.set('name', 'Jasmine')
// 获取 name 的值
let name = map.get('name')
console.log(name)
// Jasmine
  • has()

判断传入的键是否存在当前 Map 对象中,该方法返回一个布尔值

javascript 复制代码
let map = new Map()
map.set('name', 'Jasmine')
console.log(map.has('name')) // true
console.log(map.has('age')) // false
  • delete()

删除传入的键,返回 true,如果删除失败,则返回 false

javascript 复制代码
let map = new Map()
map.set('name', 'Jasmine')
// 删除 name
map.delete('name')
// 再次查看是否还存在 name
console.log(map.has('name')) // false
  • clear()

清空 Map,删除所有键值对

javascript 复制代码
let map = new Map()
map.set('name', 'Jasmine').set('sex', 'Female').set('age', 31)
// 清空所有键值对
map.clear()
console.log(map)
// Map(0) {size: 0}
  • size

返回 Map 中键值对的数量

javascript 复制代码
let map = new Map()
map.set('name', 'Jasmine').set('sex', 'Female').set('age', 31)
console.log(map.size)
// 3
  • forEach(cb)

遍历 Map 中的每一个键值对,cb 函数依次接收三个参数:值、键、Map 本身

javascript 复制代码
let map = new Map()
map.set('name', 'Jasmine').set('sex', 'Female').set('age', 31)
map.forEach((value, key) => {
  console.log(key, value)
})
// name: Jasmine
// sex: Female
// age: 31
  • keys()

返回 Map 中所有键的迭代器(Iterator),可以用 for...of 来遍历

javascript 复制代码
let map = new Map()
map.set('name', 'Jasmine').set('sex', 'Female').set('age', 31)
for (let key of map.keys()) {
  console.log(key);
}
// name
// sex
// age
  • values()

返回 Map 中所有值的迭代器(Iterator

javascript 复制代码
let map = new Map()
map.set('name', 'Jasmine').set('sex', 'Female').set('age', 31)
for (let key of map.values()) {
  console.log(key);
}
// Jasmine
// Female
// 31
  • entries()

返回 Map 中所有键值对的迭代器,每个键值对会以 [key, value] 的形式返回

javascript 复制代码
let map = new Map()
map.set('name', 'Jasmine').set('sex', 'Female').set('age', 31)
for (let entry of map.entries()) {
  console.log(entry);
}

// (2) ['name', 'Jasmine']
// (2) ['sex', 'Female']
// (2) ['age', 31]
  • for...of 遍历 Map
javascript 复制代码
let map = new Map()
map.set('name', 'Jasmine').set('sex', 'Female').set('age', 31)
for (let [key, value] of map) {
  console.log(key, value);
}
// name: Jasmine
// sex: Female
// age: 31
  • map 转为数组
javascript 复制代码
let map = new Map()
map.set('name', 'Jasmine').set('sex', 'Female').set('age', 31)
let arr = [...map]
console.log(arr)
/*
(3) [Array(2), Array(2), Array(2)]
0: (2) ['name', 'Jasmine']
1: (2) ['sex', 'Female']
2: (2) ['age', 31]
length: 3
*/

WeakMap 是什么?

Map 可以解决对象的 key 不能为对象的缺陷,但是又随之而来一个缺点:耗费内存,强引用

ES6(ECMAScript 2015)考虑到这一点,推出了 WeakMap,用于存储键值对,其中键必须是对象,值可以是任意类型。与Map不同的是,WeakMap 的键是弱引用,也就是说,如果键对象没有被其他地方引用,则它们可以被垃圾回收。这使得WeakMap非常适合缓存数据,因为当对象不再需要时,它们可以自动从 WeakMap 中删除,从而释放内存。

强引用:创建引用之后,无法被 GC(垃圾回收机制) 进行回收,强到设置了 null 也分不开。

弱引用:对对象的弱引用是指当该对象应该被 GC 回收时,不会阻止 GC 的回收行为。

javascript 复制代码
let obj1 = {name: 'Jasmine'}
let obj2 = {name: 'Sue'}

// 申明两个变量,分别是Map类型与WeakMap类型
let map = new Map()
let weakMap = new WeakMap()

map.set(obj1, 1)
weakMap.set(obj2, 2)


// 将obj1与obj2置为null,obj1与obj2会被垃圾回收机制回收
obj1 = null
obj2 = null

/**
    虽然obj1设为了空,但由于obj1与map还存在引用关系,故无法分开
    WeakMap中,如果键对象没有被其他地方引用,则它们可以被垃圾回收
**/
console.log(map) // Map(1) {{...} => 1}
console.log(weakMap) // WeakMap {}

注意:WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

WeakMap 常用方法

WeakMap 中,每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象时,该对象将会被垃圾回收(相应的 key 则变成无效的),故 WeakMap 的 key 是不可枚举的。

  • set(key, value)
  • get(key)
  • has(key)
  • delete(key)
  • clear()

上述方法具体使用可参考 Map 的实例属性和方法

注意事项

  • 键必须是对象:如果尝试使用非对象作为键,将抛出 TypeError
  • 不可迭代:由于键的弱引用特性,WeakMap无法被遍历,因此没有 entries()keys()values()等方法

WeakMap 使用场景举例

  • 解决内存泄漏问题

内存泄漏是指程序中不再使用的对象仍然保留在内存中,导致内存占用过高,甚至可能导致程序崩溃。WeakMap 可以用来解决这个问题,因为它的键是弱引用的,当键不再被其他对象引用时,WeakMap 会自动释放对应的键值对,从而避免内存泄漏。

javascript 复制代码
// 示例:使用 WeakMap 解决内存泄漏问题
class LeakyClass {
  constructor() {
    this.data = new Map();
  }
  // 添加数据
  setData(key, value) {
    this.data.set(key, value);
  }
  // 获取数据
  getData(key) {
    return this.data.get(key);
  }
  // 移除数据
  removeData(key) {
    this.data.delete(key);
  }
}
// 创建 LeakyClass 的实例
let leakyObject = new LeakyClass();
// 将对象添加到 WeakMap 中,并将其作为键
let weakMap = new WeakMap();
weakMap.set(leakyObject, 'some data');
// 断开对 leakyObject 的引用
leakyObject = null;
// 检查 WeakMap 中是否仍然存在对应键值对
console.log(weakMap.has(leakyObject));  // false

在上述示例中,我们创建了一个LeakyClass类,它有一个data属性,用于存储数据。我们将leakyObject添加到weakMap中,并将其作为键。然后,我们断开对leakyObject的引用,此时leakyObject成为垃圾回收的候选对象。最后,我们检查weakMap中是否仍然存在对应键值对。由于leakyObject已经不再被引用,它将被垃圾回收,因此weakMap.has(leakyObject)返回false

  • 临时数据存储

WeakMap 可以用于存储临时数据,这些数据只在特定的时间段内有用。由于它们的弱引用特性,当不再需要这些数据时,它们会被自动释放,不会造成内存泄漏。

javascript 复制代码
// 示例:使用 WeakMap 存储临时数据
let weakMap = new WeakMap();
// 创建一个临时对象,并将其添加到 WeakMap 中
let tempObject = {key: 'value'};
weakMap.set(tempObject, 'some data');
// 使用临时对象
console.log(weakMap.get(tempObject)); 
// 断开对临时对象的引用
tempObject = null;
// 等待垃圾回收
setTimeout(() => {
  // 检查 WeakMap 中是否仍然存在对应键值对
  console.log(weakMap.has(tempObject)); 
}, 1000);

在上述示例中,我们使用WeakMap存储了一个临时对象和相关的数据。然后,我们断开对临时对象的引用,并等待垃圾回收。最后,WeakMap.has(tempObject)返回false,因为临时对象已经被回收。

  • 缓存

WeakMap 也可以用于缓存数据,尤其是在一些数据可能会变得很大的情况下。由于它们的弱引用特性,可以确保在不再需要这些数据时,它们会被自动释放,避免占用过多的内存。

javascript 复制代码
// 示例:使用 WeakMap 进行缓存
let weakMap = new WeakMap();
// 创建一个大型对象,并将其添加到 WeakMap 中
let largeObject = {key: 'value'};
weakMap.set(largeObject, 'cached data');
// 使用缓存的数据
console.log(weakMap.get(largeObject)); 
// 断开对大型对象的引用
largeObject = null;
// 等待垃圾回收
setTimeout(() => {
  // 检查 WeakMap 中是否仍然存在对应键值对
  console.log(weakMap.has(largeObject)); 
}, 1000);

在上述示例中,我们使用WeakMap缓存了一个大型对象。然后,我们断开对大型对象的引用,并等待垃圾回收。最后,WeakMap.has(largeObject)返回false,因为大型对象已经被回收。

这些只是 WeakMap 和 WeakSet 的一些常见应用场景。实际使用中,具体的场景可能会有所不同,需要根据具体情况选择合适的数据结构来解决问题。

Set 是什么?

Set 是一种集合数据结构,用于存储唯一值的集合,这意味着在一个Set中,任何值都只能出现一次,重复的值会被自动忽略。Set 类似于数组,但与数组不同的是,它确保了所有元素的唯一性,并且没有索引。Set 也是元素的有序集合,这意味着元素的检索顺序将与插入顺序相同。Set 适合用于当你需要从数组或其他可迭代对象中过滤掉重复项,或者当你需要检查一个值是否存在于集合中时。

创建 Set

可以使用 Set 构造函数创建一个新的集合:

javascript 复制代码
let mySet = new Set();

还可以传递一个可迭代对象(如数组)来初始化集合:

javascript 复制代码
let mySet = new Set([1, 2, 3, 4]);

Set 实例属性和方法

  • add()

向集合中添加一个值。如果值已经存在,则不执行任何操作。

javascript 复制代码
let mySet = new Set([1, 2, 3, 4]);
// 添加值5
mySet.add(5);  
/**
    1已经存在,不会重复添加,重复添加不会报错
    返回WeakSet对象本身,可以链式调用
**/
mySet.add(1).add(6);  

console.log(mySet) // Set(6) {1, 2, 3, 4, 5, 6}
  • delete()

从集合中删除一个值。如果该值存在,则返回 true,否则返回 false。

javascript 复制代码
let mySet = new Set([1,2,3,4,5])
mySet.delete(2);  // 删除值2
console.log(mySet) // Set(4) {1, 3, 4, 5}
  • has(value)

检查集合中是否存在某个值。返回 true 或 false。

javascript 复制代码
let mySet = new Set([1,2,3,4,5])
console.log(mySet.has(3));  // true
console.log(mySet.has(6));  // false
  • clear()

清空集合,删除所有元素。

javascript 复制代码
let mySet = new Set([1,2,3,4,5])
mySet.clear();  // 清空集合
console.log(mySet) // Set(0) {size: 0}
  • size

返回集合中元素的个数。

javascript 复制代码
let mySet = new Set([1,2,3,4])
console.log(mySet.size); // 4

遍历 Set

  • for...of 循环

    javascript 复制代码
    for (const value of mySet) {
        console.log(value);
    }
  • forEach 方法

    javascript 复制代码
    mySet.forEach((value) => {
        console.log(value);
    });
  • 结合解构赋值

    javascript 复制代码
    /***
    因为Set方法返回的数据结构是类数组,
    所以我们要使用Array.form()去将其转化为数组,
    也可以用ES6的结构将其转化为数组
    
    因为 Set 是值的集合,它没有键,只有值,所以遍历键和值的结果是一样
    ***/
    
    let mySet = new Set([1, 2, 3, 4])
    
    // keys(): 返回键名的遍历器
    console.log(Array.from(mySet.keys())) // (4) [1, 2, 3, 4]
    
    // values(): 返回键值的遍历器
    console.log(Array.from(mySet.values())) // (4) [1, 2, 3, 4]
    
    // entries(): 返回键值对的遍历器(解构类数组)
    console.log(Array.from(mySet.entries()))
    /***
    (4) [Array(2), Array(2), Array(2), Array(2)]
    0: (2) [1, 1]
    1: (2) [2, 2]
    2: (2) [3, 3]
    3: (2) [4, 4]
    ***/

Set 与数组的转换

  • 数组转换为 Set

    javascript 复制代码
    const myArray = [1, 2, 3, 4];
    const mySet = new Set(myArray);
  • Set 转换为数组

    javascript 复制代码
    let mySet = new Set([1, 2, 3, 4]);
    // Array.from 转换
    let myArr1 = Array.from(mySet);
    // 解构赋值
    let myArr1 = [...mySet]

Set 使用场景

  • 数组去重

    javascript 复制代码
    let arr = [2, 3, 4, 5, 6, 2, 5]
    console.log([... new Set(arr)]) // (5) [2, 3, 4, 5, 6]
  • 得到交集并集差集

    javascript 复制代码
    let aSet = new Set([1, 2, 3])
    let bSet = new Set([5, 6, 3, 4, 2])
    
    // 并集
    let union = [...new Set([...aSet, ...bSet])]
    console.log(union) // (6) [1, 2, 3, 5, 6, 4]
    
    // 交集
    let intersect = [...aSet].filter(x => bSet.has(x))
    console.log(intersect) // [2, 3]
    
    // 差集
    /**
    定义:设A,B是两个集合,由所有属于A且不属于B的元素构成的集合,叫做集合A与集合B的差集
    (同理,由所有属于B且不属于A的元素构成的集合,叫做集合B与集合A的差集)
    **/
    // aSet与bSet的差集
    let differA = [...aSet].filter(x => !bSet.has(x))
    console.log(differA) // [1]
    // bSet与aSet的差集
    let differB = [...bSet].filter(x => !aSet.has(x))
    console.log(differB) // (3) [5, 6, 4]

WeakSet 是什么?

WeakSet是ES6(ECMAScript 2015)引入的一种新的集合类型,用于存储对象的集合,并且这些对象都是弱引用。弱引用的含义是,如果没有其他引用指向集合中的对象,垃圾回收器可以自动回收这些对象,而不会因为它们存在于WeakSet中而阻止回收

WeakSet 特点

  • 只能存储对象:WeakSet 只能包含对象引用,不能包含原始值(如字符串、数字、布尔值等),尝试添加非对象类型的值将抛出TypeError
  • 对象的弱引用:集合中的对象不计入垃圾回收器的引用计数中,如果没有其他引用,垃圾回收器可以回收这些对象。
  • 不可迭代:WeakSet无法被遍历,没有entries()keys()values()forEach()等方法。
  • 没有size属性:由于弱引用的特性,WeakSet 无法提供集合的大小信息。

WeakSet实例方法

WeakSet对象提供了以下实例方法:

  • add(value)
  • delete(value)
  • has(value)

上述方法具体使用可参考 Set 的实例属性和方法

WeakSet 使用场景举例

  • 追踪对象状态

WeakSet可以用于追踪一组对象是否存在或是否被处理过,而不会阻止对象被垃圾回收。

示例:防止重复处理

javascript 复制代码
const processedObjects = new WeakSet();

function process(obj) {
  if (processedObjects.has(obj)) {
    console.log('已经处理过该对象');
    return;
  }

  // 执行处理逻辑
  console.log('处理对象:', obj);

  // 将对象添加到WeakSet中,标记为已处理
  processedObjects.add(obj);
}

const obj1 = { name: 'Alice' };
const obj2 = { name: 'Bob' };

process(obj1); // 处理对象: { name: 'Alice' }
process(obj1); // 已经处理过该对象
process(obj2); // 处理对象: { name: 'Bob' }
  • 关联元数据

可以使用WeakSet为对象关联一些元数据信息,而不需要修改对象本身。

javascript 复制代码
const visited = new WeakSet();

function traverse(node) {
  if (visited.has(node)) {
    return;
  }

  visited.add(node);

  // 处理节点逻辑
  console.log('访问节点:', node);

  // 假设节点有子节点
  if (node.children) {
    node.children.forEach(traverse);
  }
}

const node1 = { id: 1 };
const node2 = { id: 2 };
const node3 = { id: 3 };

node1.children = [node2, node3];
node2.children = [node3]; // node3被多个节点引用

traverse(node1);

// 输出:
// 访问节点: { id: 1, children: [ { id: 2, children: [ { id: 3 } ] }, { id: 3 } ] }
// 访问节点: { id: 2, children: [ { id: 3 } ] }
// 访问节点: { id: 3 }
  • 避免循环引用

循环引用是指两个或多个对象之间相互引用,形成一个循环,导致这些对象无法被垃圾回收。WeakSet 可以用来避免循环引用,因为它的成员是弱引用的,不会阻止垃圾回收。

javascript 复制代码
// 示例:使用 WeakSet 避免循环引用
class Node {
  constructor(value) {
    this.value = value;
    this.children = new WeakSet();
  }
  addChild(node) {
    this.children.add(node);
  }
  removeChild(node) {
    this.children.delete(node);
  }
}
// 创建两个 Node 对象,并形成循环引用
let node1 = new Node(1);
let node2 = new Node(2);
node1.addChild(node2);
node2.addChild(node1);
// 将 node1 添加到 WeakSet 中
let weakSet = new WeakSet();
weakSet.add(node1);
// 断开对 node1 的引用
node1 = null;
// 等待垃圾回收
setTimeout(() => {
  // 检查 WeakSet 中是否仍然存在 node1
  console.log(weakSet.has(node1)); 
}, 1000);

在上述示例中,我们创建了两个Node对象,并通过addChild方法形成循环引用。然后,我们将node1添加到weakSet中。最后,我们断开对node1的引用,并等待垃圾回收。在垃圾回收之后,weakSet.has(node1)返回false,因为node1已经被回收。

总结

回顾 WeakMap 和 WeakSet 的重要概念和特点

WeakMap 和 WeakSet 是ES6提供的新的数据结构,它们的特点和概念如下:

  • WeakMap:类似于Map,但是键只能是对象类型,且键名所指向的对象是弱引用,这意味着如果这个对象在其他地方没有被引用,那么它将会被垃圾回收,这也是 WeakMap 的主要应用场景。
  • WeakSet:类似于Set,但成员只能是对象类型,且成员对象是弱引用,这意味着如果这个对象在其他地方没有被引用,那么它将会被垃圾回收,这也是 WeakSet 的主要应用场景。

需要注意的是,WeakMap 和 WeakSet 的键是弱引用,这意味着垃圾回收机制可以自动回收不再被引用的键所对应的对象,而不用手动删除键或者值。

强调它们在处理内存泄漏和避免循环引用方面的优势

WeakMap 和 WeakSet 在处理内存泄漏和避免循环引用方面具有以下优势:

  1. 内存泄漏:WeakMap 和 WeakSet 的键是弱引用,这意味着如果一个对象不再被其他地方引用,那么它所对应的键也将不再被 WeakMap 或 WeakSet 引用,从而可以被垃圾回收器回收,避免了内存泄漏的问题。
  2. 避免循环引用:循环引用是指两个或多个对象相互引用,导致它们无法被垃圾回收器回收。WeakMap 和 WeakSet 的弱引用特性可以帮助避免循环引用的问题,因为它们不会阻止垃圾回收器回收其他对象。
  3. 性能优势:由于 WeakMap 和 WeakSet 的键是弱引用,它们不会对对象的生存时间产生影响,因此在某些情况下可以提高性能,尤其是在处理大量对象时。

总之,WeakMap 和 WeakSet 在处理内存泄漏和避免循环引用方面具有优势,可以帮助开发人员更好地管理内存和避免潜在的问题。

WeakMap 和 WeakSet:解决内存泄漏&避免循环引用(下)-阿里云开发者社区

相关推荐
阿珊和她的猫17 分钟前
React中事件处理和合成事件:理解与使用
前端·react.js·前端框架
打小就很皮...20 分钟前
深入理解React Hooks:使用useState和useEffect
前端·react.js
小周同学:25 分钟前
CSS:怎么把网站都变成灰色
前端·css
发现你走远了35 分钟前
『VUE』elementUI dialog的子组件created生命周期不刷新(详细图文注释)
javascript·vue.js·elementui
225082490736 分钟前
【无标题】
javascript·vue.js·elementui
音仔小瓜皮37 分钟前
【bug记录10】同一iOS webview页面中相同的两个svg图标出现部分显示或全部不显示的情况
前端·bug·html5·xhtml·webview
涔溪1 小时前
css3弹性布局
前端·css·css3
Array[赵]1 小时前
npm 最新国内淘宝镜像地址源 (旧版已不能用)
前端·npm·node.js
于慨1 小时前
pnpm报错如Runing this command will add the dependency to the workspace root所示
前端·npm
李豆豆喵2 小时前
第29天:安全开发-JS应用&DOM树&加密编码库&断点调试&逆向分析&元素属性操作
开发语言·javascript·安全