JavaScript 中的 Map、Set、WeakMap、WeakSet:详细解析与实战应用

828 阅读8分钟

一、MapSetWeakMapWeakSet 的基本概念

1. Map(映射)

Map 是一种有序的键值对集合,允许任何类型的键,包括原始类型和对象。它比普通对象提供了更丰富的功能,尤其是在键值对管理方面。

  • 特点

    • 键可以是任何类型(对象、函数、原始类型等)。
    • 按插入顺序存储键值对。
    • 支持 size 属性来获取 Map 的大小。
    • 支持 set(), get(), has(), delete() 等方法。
  • 常见用法

    • 用来处理对象键值对,提供更加高效的插入、删除和查找操作。
    • 提供迭代能力,可以按插入顺序遍历 Map。
    const map = new Map();
    map.set('name', 'Alice');
    map.set(1, 'Number One');
    map.set(true, 'Boolean');
    console.log(map.get('name'));  // 输出 'Alice'
    console.log(map.get(1));       // 输出 'Number One'
    console.log(map.get(true));    // 输出 'Boolean'
    
2. Set(集合)

Set 是一个集合,它可以存储唯一值(没有重复元素),值的类型可以是原始类型或对象。

  • 特点

    • 存储值的唯一性:添加重复元素时,Set 会自动去重。
    • 支持迭代:可以按插入顺序遍历 Set 中的元素。
    • 支持 add(), delete(), has() 等方法。
  • 常见用法

    • 用于存储不重复的值,例如去重操作。
    • 用于集合运算,如交集、并集等。
    const set = new Set();
    set.add(1);
    set.add(2);
    set.add(2);  // 不会被添加,因为2已经存在
    console.log(set);  // 输出 Set { 1, 2 }
    
3. WeakMap(弱映射)

WeakMap 是一种特殊的 Map,它的键必须是对象,而且对这些键的引用是弱引用。弱引用意味着当对象没有其他引用时,WeakMap 会自动删除与该对象相关联的键值对。这使得 WeakMap 在避免内存泄漏方面非常有用。

  • 特点

    • 键只能是对象,值可以是任意类型。
    • 弱引用:如果 WeakMap 中的键所引用的对象被垃圾回收,WeakMap 中的键值对会被自动删除。
    • 不支持迭代:因为 WeakMap 的键可能随时被垃圾回收,无法确定其顺序。
  • 常见用法

    • 用于存储对象相关的数据或元数据,避免内存泄漏,特别是在 DOM 操作和事件监听中非常有用。
    let obj = {};
    const weakMap = new WeakMap();
    weakMap.set(obj, 'Object Metadata');
    console.log(weakMap.get(obj));  // 输出 'Object Metadata'
    
    obj = null;  // 键所引用的对象被垃圾回收,WeakMap 中的键值对也会被删除
    
4. WeakSet(弱集合)

WeakSetSet 的变种,存储的是对象的弱引用。和 Set 不同的是,WeakSet 中的元素必须是对象,当这些对象没有其他引用时,它们会被垃圾回收,从而避免内存泄漏。

  • 特点

    • 只能存储对象,不能存储原始类型。
    • 采用弱引用,类似于 WeakMap,当对象没有其他引用时,它会被自动从集合中删除。
    • 不支持迭代,因为元素的存在是依赖于对象是否被引用。
  • 常见用法

    • 用于标记或存储对象,确保对象在没有其他引用时会自动被垃圾回收。
    let obj = {};
    const weakSet = new WeakSet();
    weakSet.add(obj);
    console.log(weakSet.has(obj));  // 输出 true
    obj = null;  // 对象被垃圾回收
    

二、MapSetWeakMapWeakSet 的区别

特性MapSetWeakMapWeakSet
键(Key)类型可以是任何类型(对象、原始值)无键(只存值)只能是对象只能是对象
值(Value)类型可以是任何类型无值,只有元素可以是任何类型无值,只有元素
垃圾回收不会自动回收不会自动回收键为对象时会自动回收对象会自动回收
内存管理会保留引用,可能导致内存泄漏会保留引用,可能导致内存泄漏通过弱引用避免内存泄漏通过弱引用避免内存泄漏
迭代支持支持支持不支持不支持
常用方法set(), get(), has(), delete()add(), delete(), has()set(), get(), has(), delete()add(), has(), delete()

三、实战应用

1. Map 的实战应用:高效查找

假设我们需要实现一个缓存系统,存储用户信息。我们可以使用 Map 来存储这些信息,利用其高效的查找和插入性能。

class Cache {
  constructor() {
    this.cache = new Map();
  }

  getUserInfo(userId) {
    if (this.cache.has(userId)) {
      return this.cache.get(userId);
    }
    // 假设从数据库加载用户信息
    const userInfo = this.loadUserFromDatabase(userId);
    this.cache.set(userId, userInfo);
    return userInfo;
  }

  loadUserFromDatabase(userId) {
    // 模拟数据库查询
    return { userId, name: `User ${userId}` };
  }
}

const cache = new Cache();
console.log(cache.getUserInfo(1));  // 第一次调用,从数据库加载
console.log(cache.getUserInfo(1));  // 第二次调用,从缓存中获取
2. Set 的实战应用:去重

Set 最常见的应用场景是去重。比如我们有一个包含重复数字的数组,想要去除重复项。

const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = new Set(numbers);
console.log([...uniqueNumbers]);  // 输出 [1, 2, 3, 4, 5]
3. WeakMap 的实战应用:DOM 元素关联数据存储

在 Web 开发中,通常需要在 DOM 元素和相关数据之间建立关联,而不希望这些数据阻止 DOM 元素的垃圾回收。这时 WeakMap 非常有用。

const elementData = new WeakMap();

function setElementData(element, data) {
  elementData.set(element, data);
}

function getElementData(element) {
  return elementData.get(element);
}

let div = document.createElement('div');
setElementData(div, { id: 1, name: 'My Div' });
console.log(getElementData(div));  // 输出 { id: 1, name: 'My Div' }
div = null;  // 当 div 被垃圾回收时,关联的数据会自动清除
4. WeakSet 的实战应用:标记已处理的对象

假设我们需要在遍历某个对象集合时,确保每个对象只被处理一次。我们可以使用 WeakSet 来标记已经处理过的对象。

const processedObjects = new WeakSet();

function processObject(obj) {
  if (processedObjects.has(obj)) {
    return;  // 如果对象已经处理过,跳过当前对象
  }

  // 处理对象的逻辑
  console.log('Processing object:', obj);
  
  // 标记为已处理
  processedObjects.add(obj);
}

let obj1 = { id: 1 };
let obj2 = { id: 2 };
let obj3 = { id: 1 };

processObject(obj1);  // 处理 obj1
processObject(obj2);  // 处理 obj2
processObject(obj3);  // obj3 具有相同的 id,仍会被处理一次

// 后续对 obj1 和 obj3 的调用将不会触发重复处理
processObject(obj1);  // 不会再次处理
processObject(obj3);  // 不会再次处理

在这个例子中,WeakSet 用于标记哪些对象已经被处理过,避免重复处理,尤其是在处理大量对象时。由于 WeakSet 的元素是弱引用的,如果 obj1obj2 没有其他引用,它们会被垃圾回收,WeakSet 会自动移除这些对象的记录。


四、何时使用 MapSetWeakMapWeakSet

选择正确的数据结构对于优化代码的效率和可维护性至关重要。以下是一些常见的使用场景:

  1. 使用 Map

    • 当你需要使用对象或其他非字符串类型作为键时(比如数组、函数、对象等)。
    • 需要快速查找键值对时(Map 提供了比普通对象更好的性能,特别是在键是对象时)。
    • 想要保持键值对的插入顺序时。

    例如,处理用户信息、配置设置或缓存数据时,Map 是非常合适的选择。

  2. 使用 Set

    • 当你需要存储一组唯一值时,特别是希望避免重复时。
    • 当你需要频繁检查某个值是否已存在时,Set 提供了 O(1) 的查找效率。
    • 需要集合操作(如并集、交集、差集)时,Set 非常适合。

    例如,在处理文件去重、唯一标识符等场景时,Set 很有用。

  3. 使用 WeakMap

    • 当你需要将数据与对象关联时,但不希望这个数据阻止对象被垃圾回收。
    • 适用于存储 DOM 元素的附加数据、对象元数据等场景,特别是当对象的生命周期由其他部分控制时。

    例如,开发 SPA(单页面应用)时,你可能需要将额外数据附加到 DOM 元素上,而不希望这些数据影响垃圾回收。

  4. 使用 WeakSet

    • 当你需要存储一组对象,并且希望对象在没有其他引用时能够被垃圾回收。
    • 适用于对象去重、标记处理过的对象等场景,特别是在需要大量对象管理时,WeakSet 可以有效防止内存泄漏。

    例如,处理一组 DOM 元素并确保每个元素只被处理一次时,WeakSet 非常有用。


总结

MapSetWeakMapWeakSet 都是 JavaScript 提供的强大数据结构,它们在不同的应用场景中能够提供高效、灵活的解决方案。

  • Map 是一个高效的键值对集合,支持任何类型的键,并保持插入顺序。它适合用于需要快速查找和存储数据的场景。
  • Set 用于存储唯一的值,自动去重,适合需要判断元素是否存在或进行集合运算的场景。
  • WeakMap 提供弱引用机制,适用于存储与对象相关的数据,并确保在对象没有其他引用时不会阻止垃圾回收。它常用于关联对象和元数据,避免内存泄漏。
  • WeakSetSet 的弱引用版本,适用于存储一组对象,并确保当对象没有其他引用时可以被垃圾回收。它适用于需要标记或去重对象的场景。