avatar
【ES6系列】Map类型和Set类型详解
🌑

【ES6系列】Map类型和Set类型详解

keywords
JavaScript, Set, WeakSet, Map, WeakMap、JavaScript技巧、代码优化
published_date
最新编辑 2024年11月21日
Created time
2021年12月21日
slug
本文介绍了ECMAScript中的Set、WeakSet、Map和WeakMap类型,包括它们的定义、属性、方法及应用。Set用于存储唯一值,支持操作如添加、删除和查找,且时间复杂度为O(1)。WeakSet仅存储对象并具有弱引用特性。Map允许使用任意类型的值作为键,提供了丰富的方法来操作键值对。WeakMap与Map类似,但键名是弱引用,适合存储临时数据,避免内存泄漏。
tags
JavaScript

Set类型

定义

本身是一个构造函数,用来生成Set数据结构,可以接受一个数组(具有iterable接口的其他数据结构)作为参数,用来初始化。允许存储任何类型的值,无论是原始值或者是对象引用

特点

  1. 值唯一:Set 自动去重,不会包含重复的值。(以下为判断恒等特殊值)
    1. +0 与 -0 在存储判断唯一性的时候是恒等的,所以不重复
    2. undefined 与 undefined 是恒等的,所以不重复
    3. NaN 与 NaN 是不恒等的,但是在 Set 中认为 NaN 与 NaN 相等,所有只能存在一个,不重复
  1. 值可以是任意数据类型:可以存储基本类型(如字符串、数字)和引用类型(如对象、函数)。
  1. 有序性:Set 的值按照插入顺序排列。
  1. 适合集合操作:可以高效实现交集、并集、差集等集合操作。

属性及方法

属性及方法
描述
new Set()
创建一个空的 Set 实例
add(value)
添加某个值,返回 Set 结构本身(可以链式调用)
delete(value)
删除某个值,删除成功返回 true,否则返false
has(value)
检查集合中是否存在指定值
clear()
清除所有成员,没有返回值。
keys()
返回包含所有值的迭代器(与 .values() 相同)
values()
返回包含所有值的迭代器
entries()
返回 [value, value] 的迭代器
forEach(callback)
遍历集合中的每个值,执行回调函数
示例代码
const set = new Set(); // 添加值 set.add(1); set.add(5); set.add(1); // 重复值会被忽略 set.add('hello'); // 检查值是否存在 console.log(set.has(5)); // 输出: true console.log(set.has(10)); // 输出: false // 获取集合大小 console.log(set.size); // 输出: 3 // 遍历 Set set.forEach(value => { console.log(value); }); // 输出: // 1 // 5 // hello // 删除值 set.delete(5); console.log(set.size); // 输出: 2 // 清空集合 set.clear(); console.log(set.size); // 输出: 0

优势(相对数组)

set中的每一项都必须是唯一的
  • 查看元素
  • 删除元素:可以直接使用每项的value来删除该项
  • 保存NaN:不能使用indexOf()或者includes()来查找NaN,而Set可以保存该值
  • 删除重复项
  • Set的时间复杂度为O(1),而数组为O(n)

应用

  1. Array.from 方法可以将 Set 结构转为数组
  1. 数组去重
Set 是去重操作的天然工具,比传统 filter 或 indexOf 操作更简洁高效。
const arr = [1, 2, 3, 3, 4, 4, 5]; const uniqueArr = [...new Set(arr)]; console.log(uniqueArr); // [1, 2, 3, 4, 5]
  1. 检测集合中是否存在某值
比数组的 includes 方法性能更优,适合大量数据的快速查找。
const visitedPages = new Set(); function visitPage(page) { if (visitedPages.has(page)) { console.log(`${page} already visited.`); } else { visitedPages.add(page); console.log(`${page} is visited for the first time.`); } } // 示例 visitPage('home'); // 输出: home is visited for the first time. visitPage('home'); // 输出: home already visited.
  1. 实现并集 (Union)、交集 (Intersect) 和差集,例如权限管理、标签筛选等场景。
const setA = new Set([1, 2, 3]); const setB = new Set([2, 3, 4]); // 并集 const union = new Set([...setA, ...setB]); console.log(union); // 输出: Set(4) {1, 2, 3, 4} // 交集 const intersection = new Set([...setA].filter(x => setB.has(x))); console.log(intersection); // 输出: Set(2) {2, 3} // 差集 const difference = new Set([...setA].filter(x => !setB.has(x))); console.log(difference); // 输出: Set(1) {1} const userPermissions = new Set(['read', 'write']); const requiredPermissions = new Set(['read', 'delete']); // 交集:用户实际拥有的权限 const validPermissions = new Set([...userPermissions].filter(x => requiredPermissions.has(x))); console.log(validPermissions); // Set { 'read' } // 差集:缺失的权限 const missingPermissions = new Set([...requiredPermissions].filter(x => !userPermissions.has(x))); console.log(missingPermissions); // Set { 'delete' }
  1. 数据订阅与去重
当需要动态管理订阅者(如事件监听器)时,Set 是一个高效的选择。
const subscribers = new Set(); function subscribe(listener) { subscribers.add(listener); // 确保唯一 } function notify(data) { subscribers.forEach(listener => listener(data)); } // 示例 subscribe(data => console.log(`Listener 1 received: ${data}`)); subscribe(data => console.log(`Listener 2 received: ${data}`)); notify('Event fired'); // 输出: // Listener 1 received: Event fired // Listener 2 received: Event fired
  1. 表单数据校验
快速验证表单输入是否重复
const emailSet = new Set(); function addEmail(email) { if (emailSet.has(email)) { console.log('This email is already registered.'); } else { emailSet.add(email); console.log('Email registered successfully.'); } } // 示例 addEmail('test@example.com'); // Email registered successfully. addEmail('test@example.com'); // This email is already registered.

使用SetLeetcode

(1) LeetCode 217: 存在重复元素 (Contains Duplicate)
题目描述:判断一个数组中是否存在重复元素。
关键点:用 Set 来检查是否有重复元素。
var containsDuplicate = function(nums) { const set = new Set(); for (const num of nums) { if (set.has(num)) { return true; } set.add(num); } return false; };
(2) LeetCode 349: 两个数组的交集 (Intersection of Two Arrays)
题目描述:求两个数组的交集,结果中每个元素不重复。
关键点:用 Set 来存储唯一值并进行集合运算。
var intersection = function(nums1, nums2) { const set1 = new Set(nums1); const set2 = new Set(nums2); return [...set1].filter(num => set2.has(num)); };
(3) LeetCode 128: 最长连续序列 (Longest Consecutive Sequence)
题目描述:找到数组中最长连续的元素序列,时间复杂度要求为 O(n)。
关键点:用 Set 存储数组并通过查找优化。
var longestConsecutive = function(nums) { const set = new Set(nums); let maxLength = 0; for (const num of set) { if (!set.has(num - 1)) { // 只有当 num 是序列的起点时才检查 let currentNum = num; let currentLength = 1; while (set.has(currentNum + 1)) { currentNum += 1; currentLength += 1; } maxLength = Math.max(maxLength, currentLength); } } return maxLength; };

WeakSet类型

定义

结构与Set类似,不重复的值的集合
💡
WeakSet 的成员只能是对象,而不能是其他类型的值
🧐特点:WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用
  • 如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中
  • WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。
  • 不可遍历⚠️
  • 用来存这个对象相关的数据,与数据共存亡

方法

  • add(value):添加某个值
  • delete(value):删除某个值,删除成功返回 true,否则返false
  • has(value):返回一个布尔值

Map类型

定义

类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,与普通对象类似,但功能更强大,性能更优。
const map = new Map([ ['name', '张三'], ['title', 'Author'] ]); map.size // 2 map.has('name') // true map.get('name') // "张三" map.has('title') // true map.get('title') // "Author"

特点

  1. 键的类型灵活
    1. Map 的键可以是任何数据类型:对象、函数、基本类型(数字、字符串等)。
    2. 普通对象的键只能是字符串或 Symbol。
  1. 键值对有序
    1. 键值对按照插入顺序排列。
  1. 性能优化
    1. 在频繁插入、删除操作时,Map 的性能比普通对象更高。
  1. 继承性问题
    1. Map 不会从原型链继承任何属性,因此不会受到原型上的默认属性(如 Object.prototype 的影响)。

方法和属性

方法和属性
描述
new Map()
创建一个空的 Map 实例
set(key, val)
Map 中添加新元素
get(key)
通过键值查找特定的数值并返回
has(key)
判断 Map 对象中是否有 Key 所对应的值,有返回 true,否则返回 false
delete(key)
通过键值从 Map 中移除对应的数据
size
返回所包含元素的数量
clear()
将这个 Map 中的所有元素删除
keys()
返回键名的遍历器
values()
返回键值的遍历器
entries()
返回键值对迭代器([key, value] 数组)
forEach(callback)
遍历 Map,对每个键值对执行回调函数
示例代码
const map = new Map(); // 添加键值对 map.set('name', 'Jessie'); map.set(1, 'Number Key'); map.set(true, 'Boolean Key'); // 读取键值对 console.log(map.get('name')); // 输出: Jessie console.log(map.get(1)); // 输出: Number Key // 检查键是否存在 console.log(map.has(true)); // 输出: true // 遍历 Map map.forEach((value, key) => { console.log(`Key: ${key}, Value: ${value}`); }); // 输出: // Key: name, Value: Jessie // Key: 1, Value: Number Key // Key: true, Value: Boolean Key // 删除键值对 map.delete(1); console.log(map.size); // 输出: 2 // 清空所有键值对 map.clear(); console.log(map.size); // 输出: 0

转化

// Map 转为数组 let map = new Map() let arr = [...map] // 数组转为 Map Map: map = new Map(arr) // Map 转为对象 let obj = {} for (let [k, v] of map) { obj[k] = v } // 对象转为 Map for( let k of Object.keys(obj)){ map.set(k,obj[k]) }

Map和Object的区别

  1. Object 对象有原型, 也就是说他有默认的 key 值在对象上面, 除非我们使用 Object.create(null)创建一个没有原型的对象
  1. Object 对象中, 只能把 StringSymbol 作为 key 值, 但是在 Map 中,key 值可以是任何基本类型(String, Number, Boolean, undefined, NaN….),或者对象(Map, Set, Object, Function , Symbol , null….)
  1. 通过 Map 中的 size 属性, 可以很方便地获取到 Map 长度, 要获取 Object 的长度, 你只能手动计算

前端应用

  1. 缓存管理
Map 可用于缓存数据,例如保存 API 的请求结果,避免重复请求。
const cache = new Map(); async function fetchData(url) { if (cache.has(url)) { return cache.get(url); // 返回缓存结果 } const response = await fetch(url); const data = await response.json(); cache.set(url, data); // 缓存结果 return data; } // 示例 fetchData('/api/data').then(data => console.log(data));
  1. 记录组件状态
Map 可以用于存储动态组件的状态,比如记录列表项的展开/折叠状态。
const expandState = new Map(); function toggleItem(itemId) { const currentState = expandState.get(itemId) || false; expandState.set(itemId, !currentState); console.log(`Item ${itemId} is now ${!currentState ? 'expanded' : 'collapsed'}`); }
  1. 管理复杂键值对数据
如果需要使用对象或数组作为键,Map 是首选,因为普通对象不支持这种操作。
const userSettings = new Map(); const user1 = { id: 1, name: 'Jessie' }; const user2 = { id: 2, name: 'Alex' }; userSettings.set(user1, { theme: 'dark', language: 'en' }); userSettings.set(user2, { theme: 'light', language: 'fr' }); console.log(userSettings.get(user1)); // { theme: 'dark', language: 'en' }
  1. 数据去重并关联复杂信息
当需要对复杂对象去重并关联额外信息时,Map 提供了良好的支持
const items = [ { id: 1, name: 'Apple' }, { id: 2, name: 'Banana' }, { id: 1, name: 'Apple' }, ]; const itemMap = new Map(); items.forEach(item => { itemMap.set(item.id, item); // 使用 id 去重 }); console.log([...itemMap.values()]); // 输出: [{ id: 1, name: 'Apple' }, { id: 2, name: 'Banana' }]

使用MapLeetcode

1、LeetCode 1: 两数之和 (Two Sum)
题目描述:给定一个整数数组 nums 和一个目标值 target,在数组中找到两个数,使它们的和为目标值。
关键点:使用 Map 来记录已经遍历的数字及其索引,减少时间复杂度。
var twoSum = function(nums, target) { const map = new Map(); for (let i = 0; i < nums.length; i++) { const complement = target - nums[i]; if (map.has(complement)) { return [map.get(complement), i]; } map.set(nums[i], i); } return []; };
2、LeetCode 205: 同构字符串 (Isomorphic Strings)
题目描述:判断两个字符串是否是同构的,即字符的映射关系是否唯一。
关键点:用两个 Map 分别记录字符的映射关系,双向验证。
var isIsomorphic = function(s, t) { const mapS = new Map(); const mapT = new Map(); for (let i = 0; i < s.length; i++) { const charS = s[i], charT = t[i]; if ((mapS.has(charS) && mapS.get(charS) !== charT) || (mapT.has(charT) && mapT.get(charT) !== charS)) { return false; } mapS.set(charS, charT); mapT.set(charT, charS); } return true; };
3、LeetCode 451: 根据字符出现频率排序 (Sort Characters By Frequency)
题目描述:根据字符出现的频率降序排序字符串。
关键点:用 Map 统计频率,并使用排序优化。
var frequencySort = function(s) { const map = new Map(); for (const char of s) { map.set(char, (map.get(char) || 0) + 1); } return [...map.entries()] .sort((a, b) => b[1] - a[1]) // 按频率降序排序 .map(([char, freq]) => char.repeat(freq)) // 重复字符 .join(''); };

WeakMap

WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。
  • 只接受对象作为键名(null 除外),不接受其他类型的值作为键名
  • 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
  • 不能遍历,方法有 getsethasdelete

应用

  1. 储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏
    1. <div id="root"> <button id="btn">11111</button> </div> <script> let wrap = document.getElementById("root"); let btn = document.getElementById("btn"); let element = new WeakSet(); console.log("btn", btn, wrap); element.add(btn); btn.addEventListener("click", () => { wrap.removeChild(btn); console.log("element", element); }); </script>
      notion image
      notion image
      💡
      tip:这里当 button 被移除,disabledElements 中的内容会因为是弱引用而直接变成空,也就是disabledElements被垃圾回收掉了其中的内存,避免了一个小小的内存泄漏的产生
  1. 一个用户对象作为键,其访问次数为值。当一个用户离开时(该用户对象将被垃圾回收机制回收),这时我们就不再需要他的访问次数了
    1. let visitsCountMap = new WeakMap() // 递归用户来访次数 function countUser(user){ let count = visitsCountMap.get(user) || 0 visitsCountMap.set(user, count + 1) } // 📁 main.js let john = { name: "John" }; countUser(john); // count his visits // 不久之后,john 离开了 john = null;
  1. 缓存计算的结果
    1. let cache = new WeakMap() // 与obj 嘻嘻相关的结果 function process(obj){ if(!cache.has(obj)) { let result = `与obj有关的计算` cache.set(obj, result) } return cache.get(obj) } // other.js let obj = {} let result1 = process(obj) let result2 = process(obj) obj = null // 如果是Map 就cache 里不可被回收

常见面试题

(1)Map和普通对象的区别?

考察点
  • Map 的键可以是任何类型(包括对象、函数)
  • Object 的键会被强制转换为字符串(Symbol 除外)
  • Map 的遍历顺序按照插入顺序,而 Object 无此保证
  • Map 有内建方法(如 size、get),而 Object 需要手动扩展或使用工具函数

(2) Set 和数组的区别?

考察点
  • Set 是无序且不重复的集合,而数组可以包含重复元素。
  • Set 提供内建的查重能力,而数组需要额外操作(如 includes 或 indexOf)。
  • 时间复杂度:Set 中的操作(如 has)是 O(1),而数组查找是 O(n)。

(3) 如何用 Set 对数组去重?

const arr = [1, 2, 2, 3, 4, 4]; const uniqueArr = [...new Set(arr)]; console.log(uniqueArr); // [1, 2, 3, 4]

(4) 如何用 Map 实现 LRU 缓存?

题目描述:设计一个数据结构,支持最近最少使用(Least Recently Used)缓存策略
class LRUCache { constructor(capacity) { this.capacity = capacity; this.cache = new Map(); } get(key) { if (!this.cache.has(key)) return -1; const value = this.cache.get(key); this.cache.delete(key); // 更新最近使用 this.cache.set(key, value); return value; } put(key, value) { if (this.cache.has(key)) { this.cache.delete(key); // 更新最近使用 } else if (this.cache.size >= this.capacity) { // 删除最少使用的 const oldestKey = this.cache.keys().next().value; this.cache.delete(oldestKey); } this.cache.set(key, value); } }

(5)为什么 Set 的查找性能比数组好?

  • Set 基于哈希表实现,查找时间复杂度是 O(1)
  • 数组需要遍历所有元素,查找时间复杂度是 O(n)

(6) Map 和 Object 的性能区别?

  • Map 适合频繁增删查改操作,因为其键的查找时间复杂度是 O(1)。
  • Object 的键查找相对慢,尤其在键很多时。

关于我

My desire to practice my skills and share my acquired knowledge fuels my endeavors.

联系 : znjessie858@gmail.com

订阅

订阅我的文章,收到我的最新发布通知,可随时取消订阅!