各位朋友,大家好!今天咱们来聊聊 JavaScript 里一个非常实用的数据结构——Map
。 别看它名字简单,功能可一点都不含糊。 准备好了吗? 咱们这就开始!
开场白:为啥需要 Map?
想象一下,你有一个非常非常重要的任务:需要存储一些数据,并且这些数据的索引(也就是“键”)类型非常多,比如数字、字符串、甚至是对象。 你第一时间想到的是什么? 也许是传统的 JavaScript 对象 (Object)。
const obj = {};
obj['name'] = 'Alice';
obj[1] = 'Bob';
obj[{ key: 'value' }] = 'Charlie'; // 哎呦,报错了!
看起来不错,但是很快你会发现问题:
- 键的类型有限制: JavaScript 对象的键会被强制转换为字符串。这意味着
obj[1]
和obj['1']
实际上指向的是同一个属性! 对象的键必须是字符串或Symbol。 - 顺序问题: 虽然现代浏览器在一定程度上保留了对象属性插入的顺序,但并不能完全保证,尤其是在处理大量数据的时候。 依赖对象属性顺序是不靠谱的。
- 原型链污染: 对象会继承原型链上的属性和方法,这可能会导致一些意想不到的冲突。 比如,你想检查对象是否包含某个键,用
obj.hasOwnProperty('toString')
永远返回true
,因为toString
是从原型链上继承来的。 - 大小问题: 获取对象的大小(属性的数量)稍微麻烦一点,需要使用
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); }); // 输出同上
Map
与 Object
的比较
特性 | 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 还有 WeakMap
。 WeakMap
与 Map
的主要区别在于:
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 代码。
希望今天的讲座对大家有所帮助! 咱们下期再见!