网站首页 > 技术文章 正文
各位技术同好们,有没有过这样的经历?你手里有一堆数据,需要快速判断某个元素是否存在,或者你得从一个长长的列表中找出所有不重复的项,又或者你需要在键值对存储中,用一个对象甚至一个函数作为“键”?在过去,我们可能习惯性地拿起数组和普通对象去“硬刚”这些需求。但很快你就会发现,当数据量一大,或者需求稍显复杂时,这些“老朋友”的性能瓶颈就暴露无遗:查找效率低下、重复数据处理麻烦、键的限制等等,都让人头疼不已。
难道就没有一种更优雅、更高效的方式来处理集合数据吗?答案当然是肯定的!今天,我要给大家揭秘JavaScript现代编程中的两位“数据魔术师”——Map和Set。它们就像给你的JavaScript代码装上了“涡轮增压器”,专门用来解决传统数组和对象在处理集合数据时的痛点。告别重复、告别慢查询,从今天开始,你的代码将焕然一新,变得更简洁、更强大,甚至在性能上实现质的飞跃!准备好了吗?让我们一起看看它们到底有何神通!
一、传统方式的“痛点”:为什么我们需要新工具?
在我们深入Map和Set之前,先来简单回顾一下用数组和普通对象处理集合时的几个“不那么爽”的地方:
- 数组去重慢:想从中取出不重复的?你可能会用循环加判断,或者用indexOf,再或者转成JSON字符串比较……这些方法往往需要O(n^2)或O(n)的复杂度,数据量一大就“卡壳”。
 - 查找元素效率低:在数组中判断一个元素是否存在,通常需要遍历(indexOf或includes),这也是O(n)的操作。
 - 普通对象做“键”的限制:普通对象的键只能是字符串或Symbol。如果你想用一个对象实例作为另一个对象的键,那可就犯了难,因为JavaScript会把对象自动转换为字符串[object Object],导致键冲突。
 - 对象属性顺序不保证:虽然现代JavaScript引擎在遍历对象属性时通常会保持插入顺序,但ES规范并没有强制要求,这在某些场景下可能会带来不确定性。
 
这些“痛点”是不是也曾让你皱眉?别担心,Map和Set正是为解决这些问题而生!
二、Set:宇宙中的“唯一俱乐部”——拒绝重复!
想象一下,有一个非常高大上的俱乐部,它只接收独一无二的成员。如果你是新来的,它会检查你是否已经入会了,如果是,直接让你出门;如果不是,热情欢迎你。Set在JavaScript中就扮演着这样一个“唯一俱乐部”的角色。
核心概念:
Set 是一种特殊的集合,它只存储唯一的值。当你尝试添加一个已经存在于 Set 中的值时,它不会被添加,也不会报错。
基本用法:
- 创建 Set:
 
    const mySet = new Set(); // 创建一个空的Set
    // 可以用一个可迭代对象(如数组)初始化Set,会自动去重
    const uniqueNumbers = new Set([1, 2, 3, 2, 1, 4]);
    console.log(uniqueNumbers); // Set {1, 2, 3, 4}
是不是很方便?一行代码就搞定了数组去重!
- 添加元素:add()
 
    mySet.add('apple');
    mySet.add('banana');
    mySet.add('apple'); // 尝试添加重复的值,无效
    console.log(mySet); // Set {'apple', 'banana'}
- 判断元素是否存在:has()
这是一个非常高效的操作,复杂度接近O(1)(常数时间)。 
    console.log(mySet.has('apple'));  // true
    console.log(mySet.has('orange')); // false
相较于数组的`includes`(O(n)),`has()`的效率简直是天壤之别!
- 删除元素:delete()
 
    mySet.delete('banana');
    console.log(mySet); // Set {'apple'}
- Set 的大小:size 属性
 
    console.log(mySet.size); // 1
- 清空 Set:clear()
 
    mySet.clear();
    console.log(mySet); // Set {}
- 遍历 Set:
Set 是可迭代的,所以可以用 for...of 循环。 
    for (const item of uniqueNumbers) {
      console.log(item);
    }
    // 1, 2, 3, 4
Set 的经典应用:数组去重
这几乎是 Set 最常被提及的用途,因为它太简单高效了:
const numbersWithDuplicates = [10, 20, 30, 20, 10, 40, 50, 30];
const uniqueArr = [...new Set(numbersWithDuplicates)]; // 利用Set自动去重,再用展开运算符转回数组
console.log(uniqueArr); // [10, 20, 30, 40, 50]
是不是比你之前写的去重代码简洁了N倍?
三、Map:无所不能的“超级字典”——键的自由!
如果说 Set 是一个只允许唯一成员的俱乐部,那么 Map 就是一个拥有无限灵活性的“超级字典”或“地址簿”。在这里,你可以用任意类型的值作为键(Key),而不仅仅是字符串或Symbol,这打破了传统JavaScript对象的束缚。
核心概念:
Map 是一种存储键值对的集合,但它的键可以是任何数据类型(对象、函数、数字、甚至null或undefined),并且它会保持键的插入顺序。
基本用法:
- 创建 Map:
 
    const myMap = new Map(); // 创建一个空的Map
    // 可以用一个包含键值对数组的数组初始化Map
    const fruitPrices = new Map([
      ['apple', 5],
      ['banana', 3],
      ['orange', 6]
    ]);
    console.log(fruitPrices); // Map(3) {'apple' => 5, 'banana' => 3, 'orange' => 6}
- 设置键值对:set(key, value)
 
    myMap.set('name', '张三');
    myMap.set(123, '是一个数字键');
    const myObjectKey = { id: 1 };
    myMap.set(myObjectKey, '这是一个对象键'); // 重点来了!用对象作为键!
    myMap.set(console.log, '这是一个函数键'); // 甚至可以用函数作为键!
    console.log(myMap);
    /*
    Map(4) {
      'name' => '张三',
      123 => '是一个数字键',
      { id: 1 } => '这是一个对象键',
      f console.log() => '这是一个函数键'
    }
    */
是不是惊呆了?传统对象根本做不到这一点!这打开了多少新的可能性啊!
- 获取值:get(key)
和 Set 的 has() 类似,Map 的 get() 查找效率也非常高。 
    console.log(myMap.get('name'));       // '张三'
    console.log(myMap.get(123));          // '是一个数字键'
    console.log(myMap.get(myObjectKey));  // '这是一个对象键' (注意这里是同一个引用对象)
    console.log(myMap.get({ id: 1 }));    // undefined (因为这是另一个不同的对象引用)
- 判断键是否存在:has(key)
 
    console.log(myMap.has('name'));    // true
    console.log(myMap.has('gender'));  // false
- 删除键值对:delete(key)
 
    myMap.delete('name');
    console.log(myMap.has('name')); // false
- Map 的大小:size 属性
 
    console.log(myMap.size); // 3 (因为我们删除了一个)
- 清空 Map:clear()
 
    myMap.clear();
    console.log(myMap); // Map(0) {}
- 遍历 Map:
Map 提供了多种遍历方式,因为它的键和值都是可迭代的。 
    // 遍历所有键值对
    for (const [key, value] of fruitPrices) { // 注意解构赋值的妙用!
      console.log(`${key}的价格是${value}元`);
    }
    // apple的价格是5元
    // banana的价格是3元
    // orange的价格是6元
    // 遍历所有键
    for (const key of fruitPrices.keys()) {
      console.log(key);
    }
    // 遍历所有值
    for (const value of fruitPrices.values()) {
      console.log(value);
    }
这些遍历方式都比传统对象更加灵活和直观。
Map 的典型应用:
- 用DOM元素作为键存储数据: 你可以将某个DOM元素与它的附加数据关联起来,而不会污染DOM本身的属性。
 
    const elementData = new Map();
    const myDiv = document.createElement('div');
    elementData.set(myDiv, { clicks: 0, lastClick: new Date() });
    // 后续可以通过myDiv直接获取其关联数据
    elementData.get(myDiv).clicks++;
- 缓存计算结果: 如果一个函数计算开销大,你可以用Map来缓存其结果,避免重复计算。
 
    const cache = new Map();
    function expensiveCalculation(param) {
      if (cache.has(param)) {
        console.log('从缓存获取...');
        return cache.get(param);
      }
      // 模拟耗时计算
      const result = param * 2 + 100;
      cache.set(param, result);
      console.log('新计算结果并缓存...');
      return result;
    }
    expensiveCalculation(5); // 新计算结果并缓存...
    expensiveCalculation(5); // 从缓存获取...
四、Map vs. Object,Set vs. Array:何时选择谁?
理解了 Map 和 Set 的基础,那么什么时候用它们,什么时候继续用老朋友呢?
Set vs. Array (处理唯一性/存在性):
- 选择 Set:你需要存储一组唯一的元素,并且去重是主要需求。你需要频繁地检查某个元素是否存在(has()的O(1)性能)。你对元素的顺序不敏感(或者说,顺序保持,但不依赖索引访问)。
 - 选择 Array:你需要存储有序的元素集合,且允许重复。你需要通过索引(arr)来访问元素。你需要对元素进行排序、过滤、映射等数组特有的操作。
 
Map vs. Object (处理键值对):
- 选择 Map:你需要用非字符串(或Symbol)类型的值作为键,例如对象、DOM元素、函数。你需要保持键值对的插入顺序。你需要频繁地添加、删除键值对,或进行大量查询,因为Map通常在这些操作上性能更优。键值对数量会动态变化且可能很大。
 - 选择 Object:你主要使用字符串或Symbol作为键。你需要通过字面量语法({ key: value })快速创建。你需要利用JSON进行序列化和反序列化(Map需要手动转换)。你主要是存储配置数据或结构固定的数据,且不需要频繁增删或查询。
 
简单来说,当你的“字典”或“列表”需要更高的灵活性、更好的性能或者独特的键类型时,Map和Set就是你的首选。而对于简单、静态的场景,传统对象和数组依然是便利的选择。
五、一些进阶思考与“彩蛋”:
- WeakMap 和 WeakSet: 还有两个“兄弟”叫 WeakMap 和 WeakSet。它们的主要区别在于对键的引用是“弱引用”。这意味着如果键(在WeakMap中)或值(在WeakSet中)没有其他地方被引用了,垃圾回收器就可以将其回收,从而防止内存泄漏。这对于DOM元素、大型对象等场景特别有用。不过它们没有 size 属性,也不能被遍历。
 - 性能考量: 对于少量数据,Map/Set 和 Object/Array 的性能差异可能不明显。但当数据量达到几千、几万甚至更多时,Map/Set 的优势就会凸显出来。
 - 互操作性: Map 和 Set 都可以很方便地与数组进行转换,比如 Array.from(mySet) 或 [...myMap.keys()]。
 
总结:拥抱新标准,让你的代码更“潮”!
Map和Set是ES6(ECMAScript 2015)引入的重要新特性,它们代表着JavaScript在数据结构处理上的现代化和进化。它们的出现,极大地弥补了传统对象和数组在处理复杂集合数据时的不足,让我们的代码变得更简洁、更高效、更具可维护性。
告别过去那些低效的去重算法和受限的键值对存储方式吧!从今天开始,将Map和Set融入你的日常编码习惯,你会发现,你的JavaScript代码不仅跑得更快,写起来也更顺手、更优雅。这不仅仅是学习新的API,更是一种思维方式的转变——从“怎么实现”到“如何更高效地实现”。
你有没有在实际项目中用过Map或Set,它们给你带来了哪些惊喜?或者你觉得还有哪些特别的应用场景?欢迎在评论区留言分享你的经验和看法,我们一起进步,让我们的代码会呼吸,能思考!
猜你喜欢
- 2025-09-03 网页数据如何获取,带你走近JS逆向,完全入门级!
 - 2025-09-03 【JavaScript】手撕前端面试题:手写new操作符
 - 2025-06-10 告别JSON.stringify,一行代码搞定JavaScript深拷贝的正确姿势!
 - 2025-06-10 常见web安全问题,SQL注入、XSS、CSRF,基本原理以及如何防御
 - 2025-06-10 web前端之null和undefined区别(前端null与undefined区别)
 - 2025-06-10 Why does Google prepend while(1); to their JSON responses?
 - 2025-06-10 JavaScript 数据类型详解:从基础到进阶,一篇全搞定!
 - 2025-06-10 JS对象判空的几种方式,你真的会了吗?
 - 2024-07-30 JavaScript面向对象核心知识归纳(javascript面向对象编程)
 - 2024-07-30 前端教程:Javascript Boolean对象
 
- 最近发表
 - 
- 聊一下 gRPC 的 C++ 异步编程_grpc 异步流模式
 - [原创首发]安全日志管理中心实战(3)——开源NIDS之suricata部署
 - 超详细手把手搭建在ubuntu系统的FFmpeg环境
 - Nginx运维之路(Docker多段构建新版本并增加第三方模
 - 92.1K小星星,一款开源免费的远程桌面,让你告别付费远程控制!
 - Go 人脸识别教程_piwigo人脸识别
 - 安卓手机安装Termux——搭建移动服务器
 - ubuntu 安装开发环境(c/c++ 15)_ubuntu安装c++编译器
 - Rust开发环境搭建指南:从安装到镜像配置的零坑实践
 - Windows系统安装VirtualBox构造本地Linux开发环境
 
 
- 标签列表
 - 
- cmd/c (90)
 - c++中::是什么意思 (84)
 - 标签用于 (71)
 - 主键只能有一个吗 (77)
 - c#console.writeline不显示 (95)
 - pythoncase语句 (88)
 - es6includes (74)
 - sqlset (76)
 - apt-getinstall-y (100)
 - node_modules怎么生成 (87)
 - chromepost (71)
 - flexdirection (73)
 - c++int转char (80)
 - mysqlany_value (79)
 - static函数和普通函数 (84)
 - el-date-picker开始日期早于结束日期 (76)
 - js判断是否是json字符串 (75)
 - c语言min函数头文件 (77)
 - asynccallback (87)
 - localstorage.removeitem (77)
 - vector线程安全吗 (73)
 - java (73)
 - js数组插入 (83)
 - mac安装java (72)
 - 无效的列索引 (74)
 
 
