Set类型
定义
本身是一个构造函数,用来生成Set数据结构,可以接受一个数组(具有iterable接口的其他数据结构)作为参数,用来初始化。允许存储任何类型的值,无论是原始值或者是对象引用
特点
- 值唯一:Set 自动去重,不会包含重复的值。(以下为判断恒等特殊值)
- +0 与 -0 在存储判断唯一性的时候是恒等的,所以不重复
undefined
与undefined
是恒等的,所以不重复NaN
与NaN
是不恒等的,但是在Set
中认为NaN
与NaN
相等,所有只能存在一个,不重复
- 值可以是任意数据类型:可以存储基本类型(如字符串、数字)和引用类型(如对象、函数)。
- 有序性:Set 的值按照插入顺序排列。
- 适合集合操作:可以高效实现交集、并集、差集等集合操作。
属性及方法
属性及方法 | 描述 |
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)
应用
Array.from
方法可以将Set
结构转为数组
- 数组去重
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]
- 检测集合中是否存在某值
比数组的 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.
- 实现并集
(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' }
- 数据订阅与去重
当需要动态管理订阅者(如事件监听器)时,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
- 表单数据校验
快速验证表单输入是否重复
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.
使用Set
的Leetcode
题
(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"
特点
- 键的类型灵活
- Map 的键可以是任何数据类型:对象、函数、基本类型(数字、字符串等)。
- 普通对象的键只能是字符串或 Symbol。
- 键值对有序
- 键值对按照插入顺序排列。
- 性能优化
- 在频繁插入、删除操作时,Map 的性能比普通对象更高。
- 继承性问题
- 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的区别
-
Object
对象有原型, 也就是说他有默认的key
值在对象上面, 除非我们使用Object.create(null)
创建一个没有原型的对象
- 在
Object
对象中, 只能把String
和Symbol
作为key
值, 但是在Map
中,key
值可以是任何基本类型(String
,Number
,Boolean
,undefined
,NaN
….),或者对象(Map
,Set
,Object
,Function
,Symbol
,null
….)
- 通过
Map
中的size
属性, 可以很方便地获取到Map
长度, 要获取Object
的长度, 你只能手动计算
前端应用
- 缓存管理
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));
- 记录组件状态
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'}`); }
- 管理复杂键值对数据
如果需要使用对象或数组作为键,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' }
- 数据去重并关联复杂信息
当需要对复杂对象去重并关联额外信息时,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' }]
使用Map
的Leetcode
题
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
除外),不接受其他类型的值作为键名
- 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
- 不能遍历,方法有
get
、set
、has
、delete
应用
- 储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏
<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>
tip:这里当 button 被移除,disabledElements 中的内容会因为是弱引用而直接变成空,也就是disabledElements被垃圾回收掉了其中的内存,避免了一个小小的内存泄漏的产生
- 一个用户对象作为键,其访问次数为值。当一个用户离开时(该用户对象将被垃圾回收机制回收),这时我们就不再需要他的访问次数了
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;
- 缓存计算的结果
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 的键查找相对慢,尤其在键很多时。