各位观众老爷们,大家好!今天咱们来聊聊JavaScript里一对好基友:Map
和Set
。 别看它们名字有点陌生,其实它们在某些场合比老朋友Object
和Array
更好使,甚至能让你的代码跑得更快!咱们今天就扒一扒它们的底裤,看看它们到底有啥本事。
开场白:Object和Array的局限性
在JavaScript的世界里,Object
和Array
是元老级的存在。 咱们天天用,用得那叫一个溜。 但是,时间长了,你有没有觉得它们有点不够劲儿?
- Object的Key只能是字符串或Symbol: 想用数字、对象当Key? 没门! Object会默默地把你转换成字符串,然后告诉你:“我只能帮你到这儿了”。
- Array的indexOf查找效率: 想在数组里找个东西?
indexOf
跑一遍,效率嘛… 尤其是数组很大的时候,简直慢到怀疑人生。 - Object遍历顺序不确定: 你想按顺序遍历Object的属性? 呵呵,JavaScript引擎表示: “我心情好就给你按顺序,心情不好就随机”。 (ES2015后对于非数字键的遍历顺序是按照插入顺序,但是依旧不能保证全部情况)
- Array删除元素产生空洞: 用
delete
删除Array里的元素? 会留下一个undefined
的空洞,数组的长度不变,遍历的时候还得小心翼翼地避开它。
正是在这些局限性的刺激下,Map
和Set
应运而生,它们就是来解决这些问题的!
第一节:Map——更灵活的键值对存储
Map
,顾名思义,地图。 你可以把它想象成一张地图,每个地点(Key)都对应着一个目的地(Value)。 和Object
最大的区别在于,Map
的Key可以是任何类型,包括数字、字符串、对象、甚至是另一个Map
!
1. Map的基本用法
// 创建一个Map
const myMap = new Map();
// 设置键值对
myMap.set('name', '张三');
myMap.set(123, '数字');
myMap.set({ a: 1 }, '对象'); // 对象的引用作为key
// 获取值
console.log(myMap.get('name')); // 输出: 张三
console.log(myMap.get(123)); // 输出: 数字
console.log(myMap.get({ a: 1 })); // 输出: undefined (因为这是另一个对象)
const obj = { a: 1 };
myMap.set(obj, '对象引用');
console.log(myMap.get(obj)); // 输出: 对象引用 (使用相同的引用)
// 检查是否存在某个键
console.log(myMap.has('name')); // 输出: true
console.log(myMap.has('age')); // 输出: false
// 删除键值对
myMap.delete('name');
console.log(myMap.has('name')); // 输出: false
// 获取Map的大小
console.log(myMap.size); // 输出: 2 (删除'name'后)
// 清空Map
myMap.clear();
console.log(myMap.size); // 输出: 0
2. Map的遍历
Map
提供了多种遍历方式,让你可以轻松地访问所有的键值对。
const myMap = new Map([
['name', '张三'],
[123, '数字'],
[{ a: 1 }, '对象']
]);
// 1. 使用 for...of 循环
for (const [key, value] of myMap) {
console.log(key, value);
}
// 2. 使用 forEach 方法
myMap.forEach((value, key) => {
console.log(key, value);
});
// 3. 获取所有的键
for (const key of myMap.keys()) {
console.log(key);
}
// 4. 获取所有的值
for (const value of myMap.values()) {
console.log(value);
}
// 5. 获取所有的键值对条目
for (const entry of myMap.entries()) {
console.log(entry[0], entry[1]);
}
3. Map和Object的性能对比
特性 | Object | Map |
---|---|---|
Key的类型 | 字符串或Symbol | 任意类型 |
大小 | 手动计算 (没有直接的属性) | size 属性 |
遍历 | for...in (遍历原型链) , Object.keys() |
for...of , forEach |
性能 (添加/删除) | O(1) (平均情况,取决于哈希函数) | O(1) (平均情况,取决于哈希函数实现) |
查找 | O(1) (平均情况,取决于哈希函数) | O(1) (平均情况,取决于哈希函数实现) |
键冲突 | 可能发生 (需要考虑原型链) | 不会发生 (Map内部处理) |
插入顺序 | ES2015+ 按照插入顺序 (非数字键),之前不保证 | 保持插入顺序 |
- Key的类型:
Map
完胜。 想用啥当Key就用啥,不用担心类型转换的问题。 - 大小:
Map
有size
属性,直接获取大小,方便快捷。Object
需要自己遍历统计,麻烦! - 遍历:
Map
的遍历方式更多,更灵活。Object
的for...in
还会遍历原型链,需要用hasOwnProperty
过滤一下,略显繁琐。 - 性能: 一般情况下,
Map
在频繁添加和删除键值对的场景下,性能更好。 特别是当Key不是字符串的时候,Map
的优势更明显。Object
在Key是字符串,并且只需要简单存储和访问的时候,性能可能略好。 - 键冲突:
Object
的键可能会和原型链上的属性冲突,而Map
不会。
代码说话:性能测试
// Map 和 Object 的性能对比
const ITERATIONS = 100000;
// Map
console.time('Map Set');
const map = new Map();
for (let i = 0; i < ITERATIONS; i++) {
map.set(i, i);
}
console.timeEnd('Map Set');
console.time('Map Get');
for (let i = 0; i < ITERATIONS; i++) {
map.get(i);
}
console.timeEnd('Map Get');
// Object
console.time('Object Set');
const obj = {};
for (let i = 0; i < ITERATIONS; i++) {
obj[i] = i;
}
console.timeEnd('Object Set');
console.time('Object Get');
for (let i = 0; i < ITERATIONS; i++) {
obj[i];
}
console.timeEnd('Object Get');
(运行结果会因环境而异,但通常在大量操作时,Map在非字符串Key的情况下会表现出优势)
第二节:Set——集合的艺术
Set
,集合。 你可以把它想象成一个不允许重复元素的数组。 Set
最大的特点就是元素唯一,它可以用来去重,或者判断某个元素是否存在。
1. Set的基本用法
// 创建一个Set
const mySet = new Set();
// 添加元素
mySet.add(1);
mySet.add(2);
mySet.add(3);
mySet.add(1); // 重复添加,会被忽略
console.log(mySet); // 输出: Set(3) { 1, 2, 3 }
// 检查是否存在某个元素
console.log(mySet.has(2)); // 输出: true
console.log(mySet.has(4)); // 输出: false
// 删除元素
mySet.delete(2);
console.log(mySet.has(2)); // 输出: false
// 获取Set的大小
console.log(mySet.size); // 输出: 2 (删除2后)
// 清空Set
mySet.clear();
console.log(mySet.size); // 输出: 0
2. Set的遍历
Set
也提供了多种遍历方式,和Map
类似。
const mySet = new Set([1, 2, 3, 4, 5]);
// 1. 使用 for...of 循环
for (const item of mySet) {
console.log(item);
}
// 2. 使用 forEach 方法
mySet.forEach(item => {
console.log(item);
});
// 3. 获取所有的值 (Set没有键的概念,所以keys()和values()方法返回的是相同的结果)
for (const value of mySet.values()) {
console.log(value);
}
// 4. 获取所有的键 (Set没有键的概念,所以keys()和values()方法返回的是相同的结果)
for (const key of mySet.keys()) {
console.log(key);
}
// 5. 获取所有的键值对条目 (Set没有键的概念,所以entries()方法返回的键和值是相同的)
for (const entry of mySet.entries()) {
console.log(entry[0], entry[1]); // entry[0] 和 entry[1] 相同
}
3. Set和Array的性能对比
特性 | Array | Set |
---|---|---|
元素唯一性 | 需要手动去重 | 自动去重 |
查找 | indexOf , includes (O(n)) |
has (O(1) 平均情况) |
删除 | splice , filter (可能产生空洞) |
delete (不会产生空洞) |
大小 | length 属性 |
size 属性 |
性能 | 查找、删除元素效率较低 (特别是大型数组) | 查找、删除元素效率较高 (基于哈希表) |
- 元素唯一性:
Set
完胜。Array
需要自己写代码去重,麻烦不说,效率还低。 - 查找:
Set
的has
方法查找效率更高,特别是当数组很大的时候。Array
的indexOf
和includes
需要遍历整个数组,效率较低。 - 删除:
Set
的delete
方法不会产生空洞,Array
的delete
会留下undefined
,需要小心处理。 - 性能: 在需要频繁查找和删除元素的场景下,
Set
的性能更好。
代码说话:去重和查找的性能测试
const ITERATIONS = 100000;
const array = Array.from({ length: ITERATIONS }, () => Math.floor(Math.random() * ITERATIONS / 2)); // 创建一个包含重复元素的数组
// Array 去重
console.time('Array 去重');
const uniqueArray = [...new Set(array)];
console.timeEnd('Array 去重');
// Set 去重
console.time('Set 去重');
const uniqueSet = new Set(array);
console.timeEnd('Set 去重');
const searchValue = Math.floor(Math.random() * ITERATIONS / 2);
// Array 查找
console.time('Array 查找');
array.includes(searchValue);
console.timeEnd('Array 查找');
// Set 查找
console.time('Set 查找');
uniqueSet.has(searchValue);
console.timeEnd('Set 查找');
(运行结果会因环境而异,但通常Set在去重和查找方面的性能优势明显)
第三节:Map和Set的应用场景
说了这么多,Map
和Set
到底能干啥呢?
-
Map的应用场景:
- 存储配置信息: 可以用对象作为Key,存储不同配置对应的参数。
- 缓存: 可以用URL作为Key,缓存请求的结果。
- 统计: 可以用元素作为Key,统计出现的次数。
- DOM元素和数据的关联: 将DOM元素作为Key,存储相关数据,避免使用
data-*
属性。
-
Set的应用场景:
- 数据去重: 简单粗暴,一键去重。
- 判断元素是否存在: 快速判断某个元素是否已经存在。
- 跟踪访问过的URL: 防止重复访问相同的URL。
- 实现数学上的集合运算: 例如并集、交集、差集等。
举个栗子:统计字符出现的次数
const str = 'hello world';
// 使用Object
const charCountObj = {};
for (const char of str) {
charCountObj[char] = (charCountObj[char] || 0) + 1;
}
console.log(charCountObj);
// 使用Map
const charCountMap = new Map();
for (const char of str) {
charCountMap.set(char, (charCountMap.get(char) || 0) + 1);
}
console.log(charCountMap);
在这个例子里,Map
和Object
都可以实现统计字符出现次数的功能。 但是,如果字符串包含特殊字符,Map
的处理方式更优雅,不会出现属性命名冲突的问题。
总结:选择合适的工具
Map
和Set
是JavaScript里非常有用的数据结构,它们在某些场景下比Object
和Array
更高效、更方便。 但是,这并不意味着Object
和Array
就没用了。 选择合适的工具,取决于你的具体需求。
- 如果Key是字符串或Symbol,只需要简单存储和访问,
Object
可能更合适。 - 如果Key需要是任意类型,需要频繁添加和删除键值对,
Map
更合适。 - 如果需要存储唯一值,需要频繁查找和删除元素,
Set
更合适。 - 如果只需要简单存储和访问,并且对元素唯一性没有要求,
Array
可能更合适。
希望今天的讲座能让你对Map
和Set
有更深入的了解。 记住,没有最好的工具,只有最合适的工具! 下次写代码的时候,不妨考虑一下Map
和Set
,说不定它们能给你带来惊喜! 咱们下期再见!