JS `Map`:键值对集合,支持任意类型键,保持插入顺序

各位朋友,大家好!今天咱们来聊聊 JavaScript 里一个非常实用的数据结构——Map。 别看它名字简单,功能可一点都不含糊。 准备好了吗? 咱们这就开始!

开场白:为啥需要 Map?

想象一下,你有一个非常非常重要的任务:需要存储一些数据,并且这些数据的索引(也就是“键”)类型非常多,比如数字、字符串、甚至是对象。 你第一时间想到的是什么? 也许是传统的 JavaScript 对象 (Object)。

const obj = {};
obj['name'] = 'Alice';
obj[1] = 'Bob';
obj[{ key: 'value' }] = 'Charlie'; // 哎呦,报错了!

看起来不错,但是很快你会发现问题:

  1. 键的类型有限制: JavaScript 对象的键会被强制转换为字符串。这意味着 obj[1]obj['1'] 实际上指向的是同一个属性! 对象的键必须是字符串或Symbol。
  2. 顺序问题: 虽然现代浏览器在一定程度上保留了对象属性插入的顺序,但并不能完全保证,尤其是在处理大量数据的时候。 依赖对象属性顺序是不靠谱的。
  3. 原型链污染: 对象会继承原型链上的属性和方法,这可能会导致一些意想不到的冲突。 比如,你想检查对象是否包含某个键,用 obj.hasOwnProperty('toString') 永远返回 true,因为 toString 是从原型链上继承来的。
  4. 大小问题: 获取对象的大小(属性的数量)稍微麻烦一点,需要使用 Object.keys(obj).length

这时候,Map 就闪亮登场了! 它可以完美解决上述问题。

Map 的基本用法

Map 是一个键值对集合,它允许你使用任意类型的值作为键,并且会记住键值对的插入顺序。

  • 创建 Map:

    const myMap = new Map();
  • 添加键值对: 使用 set() 方法。

    myMap.set('name', 'Alice');
    myMap.set(1, 'Bob');
    myMap.set({ key: 'value' }, 'Charlie'); // 没问题!
    myMap.set(NaN, 'Nancy');  // 甚至 NaN 也可以作为键!
  • 获取值: 使用 get() 方法。

    console.log(myMap.get('name')); // 输出: Alice
    console.log(myMap.get(1));      // 输出: Bob
    console.log(myMap.get({ key: 'value' }));  // 输出: undefined  (注意:对象是引用类型,必须是同一个对象引用)
    const keyObj = { key: 'value' };
    myMap.set(keyObj, 'Charlie');
    console.log(myMap.get(keyObj));  // 输出: Charlie
    console.log(myMap.get(NaN));  // 输出: Nancy
  • 检查是否包含键: 使用 has() 方法。

    console.log(myMap.has('name')); // 输出: true
    console.log(myMap.has(2));      // 输出: false
  • 删除键值对: 使用 delete() 方法。

    myMap.delete('name');
    console.log(myMap.has('name')); // 输出: false
  • 获取 Map 的大小: 使用 size 属性。

    console.log(myMap.size); // 输出: 3 (因为 'name' 已经被删除)
  • 清空 Map: 使用 clear() 方法。

    myMap.clear();
    console.log(myMap.size); // 输出: 0

Map 的迭代

Map 提供了多种迭代方法,让你方便地遍历其中的键值对。

  • keys() 返回一个包含所有键的迭代器。

    const myMap = new Map([
        ['name', 'Alice'],
        [1, 'Bob'],
        [{ key: 'value' }, 'Charlie']
    ]);
    
    for (const key of myMap.keys()) {
        console.log('Key:', key);
    }
    // 输出:
    // Key: name
    // Key: 1
    // Key: { key: 'value' }
  • values() 返回一个包含所有值的迭代器。

    for (const value of myMap.values()) {
        console.log('Value:', value);
    }
    // 输出:
    // Value: Alice
    // Value: Bob
    // Value: Charlie
  • entries() 返回一个包含所有键值对的迭代器,每个元素都是一个 [key, value] 数组。 这是最常用的迭代方式。

    for (const [key, value] of myMap.entries()) {
        console.log('Key:', key, 'Value:', value);
    }
    // 输出:
    // Key: name Value: Alice
    // Key: 1 Value: Bob
    // Key: { key: 'value' } Value: Charlie

    你也可以直接使用 for...of 迭代 Map 对象,效果与 myMap.entries() 相同。

    for (const [key, value] of myMap) {
        console.log('Key:', key, 'Value:', value);
    }
  • forEach() 提供一个回调函数,对每个键值对执行操作。

    myMap.forEach((value, key) => {
        console.log('Key:', key, 'Value:', value);
    });
    // 输出同上

MapObject 的比较

特性 Object Map
键的类型 字符串或 Symbol 任意类型
键的顺序 不保证 (现代浏览器基本能保持插入顺序) 保证插入顺序
大小 需要手动计算 (Object.keys(obj).length) 直接获取 (map.size)
迭代 相对复杂 (Object.keys(), for...in) 简单直接 (for...of, forEach())
原型链 会继承原型链上的属性和方法 没有原型链污染
性能 (添加/删除) 在大型对象中可能较慢 通常更快,尤其是在频繁添加和删除键值对的情况下

Map 的应用场景

  • 缓存: 可以使用 Map 来存储计算结果,避免重复计算。 键可以是计算的参数,值是计算结果。

    const cache = new Map();
    
    function expensiveCalculation(arg) {
        if (cache.has(arg)) {
            console.log('从缓存中获取结果');
            return cache.get(arg);
        }
    
        console.log('进行耗时计算');
        // 模拟耗时计算
        const result = arg * 2;
        cache.set(arg, result);
        return result;
    }
    
    console.log(expensiveCalculation(5)); // 进行耗时计算,输出 10
    console.log(expensiveCalculation(5)); // 从缓存中获取结果,输出 10
    console.log(expensiveCalculation(10)); // 进行耗时计算,输出 20
  • 存储元数据: 可以将 DOM 元素或者其他对象与相关的元数据存储在 Map 中。

    const element = document.createElement('div');
    const metadata = new Map();
    
    metadata.set(element, {
        id: 'myDiv',
        className: 'highlighted'
    });
    
    const elementData = metadata.get(element);
    console.log(elementData.id); // 输出: myDiv
  • 统计词频: 可以使用 Map 来统计一段文本中每个单词出现的次数。

    function wordFrequency(text) {
        const words = text.toLowerCase().split(/s+/); // 将文本转换为小写,并按空格分割
        const frequencyMap = new Map();
    
        for (const word of words) {
            if (frequencyMap.has(word)) {
                frequencyMap.set(word, frequencyMap.get(word) + 1);
            } else {
                frequencyMap.set(word, 1);
            }
        }
    
        return frequencyMap;
    }
    
    const text = 'This is a test. This is only a test.';
    const frequencies = wordFrequency(text);
    
    for (const [word, count] of frequencies) {
        console.log(word, ':', count);
    }
    // 输出:
    // this : 2
    // is : 2
    // a : 2
    // test. : 2
    // only : 1
  • 替代复杂的 switch 语句: 如果你的 switch 语句有很多 case,可以考虑使用 Map 来替代,提高代码的可读性和可维护性。

    // 使用 switch 语句
    function handleAction(action) {
        switch (action) {
            case 'create':
                console.log('Creating...');
                break;
            case 'update':
                console.log('Updating...');
                break;
            case 'delete':
                console.log('Deleting...');
                break;
            default:
                console.log('Invalid action.');
        }
    }
    
    // 使用 Map 替代
    const actionMap = new Map([
        ['create', () => console.log('Creating...')],
        ['update', () => console.log('Updating...')],
        ['delete', () => console.log('Deleting...')]
    ]);
    
    function handleActionWithMap(action) {
        const actionFunction = actionMap.get(action);
        if (actionFunction) {
            actionFunction();
        } else {
            console.log('Invalid action.');
        }
    }
    
    handleAction('create');        // 输出: Creating...
    handleActionWithMap('create');  // 输出: Creating...

WeakMap

除了 Map 之外,JavaScript 还有 WeakMapWeakMapMap 的主要区别在于:

  • WeakMap 的键必须是对象。
  • WeakMap 的键是弱引用。 这意味着,如果一个对象只被 WeakMap 引用,当垃圾回收器运行时,该对象会被回收,并且 WeakMap 中对应的键值对也会被移除。 这可以防止内存泄漏。

WeakMap 的 API 与 Map 类似,但没有 size 属性,也不能进行迭代。

WeakMap 的主要应用场景是存储对象的元数据,并且不希望这些元数据阻止对象被垃圾回收。 例如,可以用 WeakMap 来存储 DOM 元素的事件监听器,当 DOM 元素被移除时,相关的事件监听器也会被自动移除,避免内存泄漏。

const weakMap = new WeakMap();

let element = document.createElement('div');
weakMap.set(element, { eventListener: () => console.log('Element clicked!') });

// 当 element 不再被其他地方引用时,它会被垃圾回收,并且 weakMap 中对应的键值对也会被移除。
element = null;

性能考量

虽然 Map 在很多方面优于 Object,但在某些情况下,Object 仍然可能更适合:

  • 简单对象: 如果只需要存储少量简单的键值对,并且键都是字符串或 Symbol,那么 Object 可能更轻量级。
  • 性能敏感的应用: 在某些高度优化的场景下,直接访问对象属性可能比使用 Map.get() 更快。 但是,这种差异通常很小,并且只有在进行大量操作时才会显现出来。

一般来说,除非有明确的性能瓶颈,否则建议优先使用 Map,因为它更灵活、更安全、更易于使用。

总结

Map 是 JavaScript 中一个非常强大的数据结构,它提供了比传统 Object 更加灵活和高效的键值对存储方式。 通过掌握 Map 的基本用法和应用场景,可以编写出更优雅、更健壮的 JavaScript 代码。

希望今天的讲座对大家有所帮助! 咱们下期再见!

发表回复

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