什么是 JavaScript 中的 Set 和 Map 数据结构?它们与数组和对象相比有何优势和应用场景?

各位观众,掌声欢迎!我是今天的主讲人,人称“代码界的段子手”(其实是自己封的)。今天咱们不讲枯燥的理论,来聊聊JavaScript里两个好玩儿的数据结构:Set和Map。

先别皱眉头,我知道你们可能觉得数组和对象已经够用了,干嘛还要学这些“花里胡哨”的东西?但相信我,学完之后你会发现,它们就像你工具箱里的瑞士军刀,关键时刻能帮你解决很多麻烦。

一、Set:不允许重复元素的集合,专注“唯一”

你可以把Set想象成一个非常挑剔的俱乐部,只允许独一无二的会员加入。如果有人想重复加入,对不起,直接拒之门外。

  • 特点:

    • 不允许重复元素。
    • 元素没有顺序(虽然遍历时按照插入顺序)。
    • 可以存储任何类型的数据。
  • 基本用法:

    • 创建Set:

      let mySet = new Set(); // 创建一个空Set
      let initialSet = new Set([1, 2, 3, 4, 5]); // 用数组初始化Set
    • 添加元素:

      mySet.add(1);
      mySet.add(2);
      mySet.add(2); // 重复添加,Set会自动忽略
      console.log(mySet); // 输出: Set(2) {1, 2}
    • 删除元素:

      mySet.delete(1);
      console.log(mySet); // 输出: Set(1) {2}
    • 检查元素是否存在:

      console.log(mySet.has(2)); // 输出: true
      console.log(mySet.has(1)); // 输出: false
    • 获取Set的大小:

      console.log(mySet.size); // 输出: 1
    • 清空Set:

      mySet.clear();
      console.log(mySet.size); // 输出: 0
  • Set的遍历:

    Set提供了多种遍历方式:for...of 循环、forEach() 方法和 values() 方法。

    let mySet = new Set([1, 2, 3]);
    
    // 使用 for...of 循环
    for (let item of mySet) {
      console.log(item);
    }
    // 输出:
    // 1
    // 2
    // 3
    
    // 使用 forEach() 方法
    mySet.forEach(function(value) {
      console.log(value);
    });
    // 输出:
    // 1
    // 2
    // 3
    
    // 使用 values() 方法
    for (let value of mySet.values()) {
      console.log(value);
    }
    // 输出:
    // 1
    // 2
    // 3
  • Set的优势和应用场景:

    • 数组去重: 这是Set最常见的应用场景。将数组转换为Set,再转换回数组,就能轻松去除重复元素。

      let arr = [1, 2, 2, 3, 4, 4, 5];
      let uniqueArr = [...new Set(arr)]; // 使用扩展运算符
      console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]

      相比于传统的循环判断方法,Set去重更加简洁高效。

    • 判断元素是否存在: has() 方法的查找速度非常快,接近O(1)复杂度。这在需要频繁判断元素是否存在的场景下非常有用。

    • 数学集合运算: Set可以方便地进行并集、交集、差集等运算。

      • 并集:

        let setA = new Set([1, 2, 3]);
        let setB = new Set([3, 4, 5]);
        
        let union = new Set([...setA, ...setB]);
        console.log(union); // 输出: Set(5) {1, 2, 3, 4, 5}
      • 交集:

        let setA = new Set([1, 2, 3]);
        let setB = new Set([3, 4, 5]);
        
        let intersection = new Set([...setA].filter(x => setB.has(x)));
        console.log(intersection); // 输出: Set(1) {3}
      • 差集:

        let setA = new Set([1, 2, 3]);
        let setB = new Set([3, 4, 5]);
        
        let difference = new Set([...setA].filter(x => !setB.has(x))); // A - B
        console.log(difference); // 输出: Set(2) {1, 2}
    • 记录访问过的元素: 比如,在爬虫程序中,可以使用Set来记录已经访问过的URL,防止重复抓取。

  • Set 与数组的比较:

    特性 Set 数组
    是否允许重复
    元素顺序 插入顺序 (虽然没有索引) 有索引,严格按照插入顺序
    查找速度 has() 方法接近 O(1) indexOf() 或循环查找,平均 O(n)
    应用场景 去重、判断元素是否存在、集合运算等 存储有序数据、频繁访问元素、数组操作等

二、Map:键值对的集合,更灵活的“对象”

Map 是一种存储键值对的数据结构,类似于对象,但它比对象更灵活。对象的键只能是字符串或 Symbol,而 Map 的键可以是任何类型的数据,包括对象、函数等。

  • 特点:

    • 键值对存储,键可以是任何类型。
    • 键是唯一的,值可以重复。
    • 保持键的插入顺序。
  • 基本用法:

    • 创建Map:

      let myMap = new Map(); // 创建一个空Map
      let initialMap = new Map([
        ['name', 'Alice'],
        ['age', 30],
        [true, 'isStudent']
      ]); // 用数组初始化Map
    • 添加键值对:

      myMap.set('name', 'Bob');
      myMap.set(1, 'number');
      myMap.set({}, 'object'); // 键可以是对象
      console.log(myMap);
      // 输出:
      // Map(3) {
      //   'name' => 'Bob',
      //   1 => 'number',
      //   {} => 'object'
      // }
    • 获取值:

      console.log(myMap.get('name')); // 输出: Bob
      console.log(myMap.get(1)); // 输出: number
    • 删除键值对:

      myMap.delete('name');
      console.log(myMap);
      // 输出:
      // Map(2) {
      //   1 => 'number',
      //   {} => 'object'
      // }
    • 检查键是否存在:

      console.log(myMap.has(1)); // 输出: true
      console.log(myMap.has('name')); // 输出: false
    • 获取Map的大小:

      console.log(myMap.size); // 输出: 2
    • 清空Map:

      myMap.clear();
      console.log(myMap.size); // 输出: 0
  • Map的遍历:

    Map 提供了多种遍历方式:for...of 循环、forEach() 方法、keys() 方法、values() 方法和 entries() 方法。

    let myMap = new Map([
      ['name', 'Alice'],
      ['age', 30],
      ['city', 'New York']
    ]);
    
    // 使用 for...of 循环 (entries())
    for (let [key, value] of myMap) {
      console.log(key, value);
    }
    // 输出:
    // name Alice
    // age 30
    // city New York
    
    // 使用 forEach() 方法
    myMap.forEach(function(value, key) {
      console.log(key, value);
    });
    // 输出:
    // name Alice
    // age 30
    // city New York
    
    // 使用 keys() 方法
    for (let key of myMap.keys()) {
      console.log(key);
    }
    // 输出:
    // name
    // age
    // city
    
    // 使用 values() 方法
    for (let value of myMap.values()) {
      console.log(value);
    }
    // 输出:
    // Alice
    // 30
    // New York
    
    // 使用 entries() 方法
    for (let entry of myMap.entries()) {
      console.log(entry);
    }
    // 输出:
    // [ 'name', 'Alice' ]
    // [ 'age', 30 ]
    // [ 'city', 'New York' ]
  • Map的优势和应用场景:

    • 键可以是任何类型: 这是Map最显著的优势。当需要使用对象、函数等作为键时,Map是唯一的选择。

      let obj1 = { id: 1 };
      let obj2 = { id: 2 };
      
      let myMap = new Map();
      myMap.set(obj1, 'Object 1');
      myMap.set(obj2, 'Object 2');
      
      console.log(myMap.get(obj1)); // 输出: Object 1
    • 保持键的插入顺序: Map 会记住键的插入顺序,这在某些需要按照特定顺序处理数据的场景下非常有用。

    • 更好的性能: 在频繁添加和删除键值对的场景下,Map 的性能通常比对象更好。

    • 存储元数据: 可以使用 Map 来存储与 DOM 元素或其他对象相关的元数据,而不会污染这些对象本身。

      let element = document.createElement('div');
      let metadata = new Map();
      metadata.set(element, {
        tooltip: 'This is a div element',
        className: 'highlighted'
      });
      
      // 获取元素的元数据
      let elementMetadata = metadata.get(element);
      console.log(elementMetadata.tooltip); // 输出: This is a div element
    • 缓存: Map 可以用作缓存,存储计算结果,避免重复计算。键可以是函数的参数,值可以是函数的返回值。

  • Map 与对象的比较:

    特性 Map 对象
    键的类型 任何类型 字符串或 Symbol
    键的顺序 保持插入顺序 不保证顺序 (ES6 之后大部分浏览器保持插入顺序,但不应该依赖)
    性能 在频繁添加和删除键值对时通常更好 在访问已知属性时通常更快
    迭代 内置迭代器,易于遍历 需要使用 Object.keys(), Object.values() 等方法
    应用场景 需要使用非字符串键、需要保持键的顺序等 存储简单数据、配置对象等

三、Set 和 Map 的实际应用例子

为了更好地理解 Set 和 Map 的应用,我们来看几个实际的例子。

  1. 使用 Set 实现简单的投票系统:

    let voters = new Set();
    
    function vote(voterId) {
      if (voters.has(voterId)) {
        console.log(`Voter ${voterId} has already voted.`);
      } else {
        voters.add(voterId);
        console.log(`Voter ${voterId} voted successfully.`);
      }
    }
    
    vote(123); // 输出: Voter 123 voted successfully.
    vote(456); // 输出: Voter 456 voted successfully.
    vote(123); // 输出: Voter 123 has already voted.
    console.log(`Total votes: ${voters.size}`); // 输出: Total votes: 2
  2. 使用 Map 实现一个简单的缓存系统:

    let cache = new Map();
    
    function expensiveCalculation(input) {
      console.log(`Calculating for input: ${input}`);
      // 模拟耗时计算
      let result = input * 2;
      return result;
    }
    
    function cachedCalculation(input) {
      if (cache.has(input)) {
        console.log(`Fetching from cache for input: ${input}`);
        return cache.get(input);
      } else {
        let result = expensiveCalculation(input);
        cache.set(input, result);
        return result;
      }
    }
    
    console.log(cachedCalculation(5)); // 输出: Calculating for input: 5  10
    console.log(cachedCalculation(5)); // 输出: Fetching from cache for input: 5  10
    console.log(cachedCalculation(10)); // 输出: Calculating for input: 10  20
    console.log(cachedCalculation(10)); // 输出: Fetching from cache for input: 10  20
  3. 使用 Map 存储数据条目的访问次数

    let data = [
        { id: 1, name: 'Item A' },
        { id: 2, name: 'Item B' },
        { id: 3, name: 'Item C' }
    ];
    
    let accessCounts = new Map();
    
    // 初始化访问计数
    data.forEach(item => {
        accessCounts.set(item.id, 0);
    });
    
    function accessItem(itemId) {
        if (accessCounts.has(itemId)) {
            let count = accessCounts.get(itemId);
            accessCounts.set(itemId, count + 1);
            console.log(`Item ${itemId} accessed. Access count: ${count + 1}`);
        } else {
            console.log(`Item ${itemId} not found.`);
        }
    }
    
    accessItem(1);
    accessItem(2);
    accessItem(1);
    accessItem(3);
    
    console.log(accessCounts);
    // 输出 Map(3) {
    //  1 => 2,
    //  2 => 1,
    //  3 => 1
    // }

四、总结

Set 和 Map 是 JavaScript 中非常有用的数据结构,它们分别提供了存储唯一元素和键值对的强大功能。相比于数组和对象,它们在某些场景下具有更高的效率和灵活性。

  • Set 适合: 需要存储唯一值,进行集合运算,或者需要快速判断元素是否存在的情况。
  • Map 适合: 需要使用非字符串键,需要保持键的插入顺序,或者需要在键值对之间建立复杂关系的情况。

希望今天的讲解能帮助大家更好地理解和使用 Set 和 Map。记住,它们不是万能的,但绝对是你的工具箱里不可或缺的利器。

好啦,今天的讲座就到这里,感谢大家的收听!下次再见,拜拜! 记住,编程的路上,多学习,多实践,多思考,才能成为真正的 “代码段子手”!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注