MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript中的Map和Set数据结构解析

2021-02-272.2k 阅读

JavaScript中的Map和Set数据结构解析

1. 简介

在JavaScript编程中,数据结构是存储和组织数据的方式,它们对程序的效率和功能起着至关重要的作用。MapSet是ES6(ECMAScript 2015)引入的两种新的数据结构,为开发者提供了更强大的数据管理能力。

Map是一种键值对的集合,与普通对象类似,但Map的键可以是任何类型,而普通对象的键只能是字符串或Symbol类型。Set则是一种不包含重复值的集合,类似于数学中的集合概念。

2. Map数据结构

2.1 创建Map

在JavaScript中,可以使用new Map()构造函数来创建一个空的Map实例,也可以在创建时传入一个可迭代对象(如数组),该数组的每个元素是一个包含两个元素的数组,第一个元素作为键,第二个元素作为值。

// 创建一个空的Map
let myMap1 = new Map();

// 使用可迭代对象创建Map
let arr = [['name', 'John'], ['age', 30]];
let myMap2 = new Map(arr);

2.2 Map的基本操作

设置键值对:使用set(key, value)方法可以向Map中添加或更新一个键值对。如果键已经存在,则更新其对应的值;如果键不存在,则添加新的键值对。

let map = new Map();
map.set('name', 'Alice');
map.set('age', 25);

获取值:通过get(key)方法可以根据键获取对应的值。如果键不存在,则返回undefined

let name = map.get('name');
console.log(name); // 输出: Alice

检查键是否存在:利用has(key)方法可以判断Map中是否存在指定的键,返回一个布尔值。

let hasAge = map.has('age');
console.log(hasAge); // 输出: true

删除键值对:使用delete(key)方法可以从Map中删除指定键的键值对,返回一个布尔值表示删除是否成功。

let isDeleted = map.delete('age');
console.log(isDeleted); // 输出: true

获取Map的大小size属性返回Map中键值对的数量。

let mapSize = map.size;
console.log(mapSize); // 输出: 1

2.3 Map的遍历

Map提供了多种遍历方法,包括keys()values()entries()

keys():返回一个包含Map中所有键的迭代器。

let map = new Map([['name', 'Bob'], ['city', 'New York']]);
let keysIterator = map.keys();
for (let key of keysIterator) {
    console.log(key);
}
// 输出:
// name
// city

values():返回一个包含Map中所有值的迭代器。

let valuesIterator = map.values();
for (let value of valuesIterator) {
    console.log(value);
}
// 输出:
// Bob
// New York

entries():返回一个包含Map中所有键值对的迭代器,每个元素是一个包含两个元素的数组,第一个元素为键,第二个元素为值。

let entriesIterator = map.entries();
for (let [key, value] of entriesIterator) {
    console.log(key, value);
}
// 输出:
// name Bob
// city New York

Map还支持forEach()方法,用于对Map中的每个键值对执行一个给定的函数。

map.forEach((value, key) => {
    console.log(`${key}: ${value}`);
});
// 输出:
// name: Bob
// city: New York

2.4 Map与普通对象的区别

虽然普通对象也可以用来存储键值对,但Map有一些独特的优势。

  • 键的类型:普通对象的键只能是字符串或Symbol类型,而Map的键可以是任何类型,包括对象、数组等。
let obj = {};
let keyObj = { id: 1 };
obj[keyObj] = 'value';
console.log(obj[Object.prototype.toString.call(keyObj)]); // 输出: value
// 这里实际上是将对象转换为字符串作为键

let map = new Map();
map.set(keyObj, 'value');
console.log(map.get(keyObj)); // 输出: value
  • 迭代顺序Map会按照插入的顺序进行迭代,而普通对象的属性迭代顺序是不确定的(在ES6之后,对象属性的遍历顺序是按照创建时的顺序,但这不适用于通过for...in遍历继承属性的情况)。
let obj = { a: 1, b: 2 };
for (let key in obj) {
    console.log(key);
}
// 输出顺序可能是 'a' 'b' 也可能是 'b' 'a'

let map = new Map([['a', 1], ['b', 2]]);
for (let key of map.keys()) {
    console.log(key);
}
// 输出: 'a' 'b'
  • 内存管理Map在内存管理方面可能更高效,尤其是当键值对数量较多时。因为Map有自己的内部数据结构优化存储,而普通对象可能会因为原型链等因素产生一些额外的内存开销。

3. Set数据结构

3.1 创建Set

Map类似,Set可以使用new Set()构造函数创建一个空的Set实例,也可以传入一个可迭代对象(如数组)来初始化Set,该可迭代对象中的所有元素将被添加到Set中,重复的元素会被自动去除。

// 创建一个空的Set
let mySet1 = new Set();

// 使用数组创建Set
let arr = [1, 2, 2, 3];
let mySet2 = new Set(arr);
console.log([...mySet2]); // 输出: [1, 2, 3]

3.2 Set的基本操作

添加值:使用add(value)方法可以向Set中添加一个值,如果该值已经存在,则不会重复添加,add方法会返回Set实例本身,这使得可以链式调用。

let set = new Set();
set.add(1).add(2).add(1);
console.log([...set]); // 输出: [1, 2]

检查值是否存在:通过has(value)方法可以判断Set中是否存在指定的值,返回一个布尔值。

let hasTwo = set.has(2);
console.log(hasTwo); // 输出: true

删除值delete(value)方法用于从Set中删除指定的值,返回一个布尔值表示删除是否成功。

let isDeleted = set.delete(2);
console.log(isDeleted); // 输出: true

获取Set的大小size属性返回Set中元素的数量。

let setSize = set.size;
console.log(setSize); // 输出: 1

3.3 Set的遍历

Set提供了keys()values()entries()方法用于遍历。需要注意的是,在Set中,keys()values()方法返回的迭代器是相同的,因为Set中的值就是键。

keys() 和 values():返回一个包含Set中所有值的迭代器。

let set = new Set([1, 2, 3]);
let keysIterator = set.keys();
let valuesIterator = set.values();
for (let value of keysIterator) {
    console.log(value);
}
// 输出:
// 1
// 2
// 3

for (let value of valuesIterator) {
    console.log(value);
}
// 输出:
// 1
// 2
// 3

entries():返回一个包含Set中所有键值对的迭代器,每个元素是一个包含两个相同元素的数组,第一个元素为键,第二个元素为值(因为Set中键值相同)。

let entriesIterator = set.entries();
for (let [key, value] of entriesIterator) {
    console.log(key, value);
}
// 输出:
// 1 1
// 2 2
// 3 3

Set同样支持forEach()方法,用于对Set中的每个值执行一个给定的函数。

set.forEach((value) => {
    console.log(value);
});
// 输出:
// 1
// 2
// 3

3.4 Set的应用场景

  • 去重:这是Set最常见的应用场景,如前面示例中通过将数组传入Set构造函数,可以轻松去除数组中的重复元素。
let arr = [1, 2, 2, 3, 4, 4];
let uniqueSet = new Set(arr);
let uniqueArray = [...uniqueSet];
console.log(uniqueArray); // 输出: [1, 2, 3, 4]
  • 集合运算:可以利用Set来实现一些集合运算,如交集、并集、差集。

交集:两个Set的交集是同时存在于两个Set中的元素组成的集合。

let set1 = new Set([1, 2, 3]);
let set2 = new Set([2, 3, 4]);
let intersection = new Set([...set1].filter(value => set2.has(value)));
console.log([...intersection]); // 输出: [2, 3]

并集:两个Set的并集是包含两个Set中所有元素的集合(去除重复元素)。

let union = new Set([...set1, ...set2]);
console.log([...union]); // 输出: [1, 2, 3, 4]

差集:集合1相对于集合2的差集是在集合1中但不在集合2中的元素组成的集合。

let difference = new Set([...set1].filter(value =>!set2.has(value)));
console.log([...difference]); // 输出: [1]

4. Map和Set的性能

在性能方面,MapSet在大多数操作上都具有较好的时间复杂度。

对于Mapsetgethasdelete操作的平均时间复杂度为O(1),这意味着这些操作的执行时间不会随着Map中键值对数量的增加而显著增加(在理想情况下)。在最坏情况下,时间复杂度可能会退化为O(n),例如当发生哈希冲突时。

对于Setaddhasdelete操作的平均时间复杂度也是O(1),最坏情况下为O(n)。Set在去重和检查元素是否存在方面效率较高,因为它利用了内部的哈希表结构。

相比之下,普通对象在执行类似操作时,由于需要处理原型链等因素,在某些情况下性能可能不如MapSet。例如,普通对象的hasOwnProperty方法虽然可以检查对象自身是否有某个属性,但在性能上可能不如Maphas方法和Sethas方法。

5. 结合使用Map和Set

在实际编程中,MapSet常常可以结合使用,以实现更复杂的数据管理和逻辑。

例如,假设我们有一个需求,统计一个数组中每个单词出现的次数,并按照出现次数从高到低排序。我们可以使用Map来存储单词及其出现的次数,然后使用Set来辅助去重。

let words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple'];
let wordCountMap = new Map();
words.forEach(word => {
    if (wordCountMap.has(word)) {
        wordCountMap.set(word, wordCountMap.get(word) + 1);
    } else {
        wordCountMap.set(word, 1);
    }
});

let sortedWords = Array.from(wordCountMap.entries()).sort((a, b) => b[1] - a[1]);
console.log(sortedWords);
// 输出: [['apple', 3], ['banana', 2], ['cherry', 1]]

在这个例子中,Map用于存储单词及其出现次数,Set虽然没有直接使用,但在整个逻辑中,如果我们需要确保单词不重复添加(例如从不同数据源获取单词时),Set可以发挥作用。

再比如,我们可以使用Set来存储一组唯一的标识符,然后使用Map来存储与这些标识符相关联的详细信息。

let idSet = new Set([1, 2, 3]);
let infoMap = new Map();
idSet.forEach(id => {
    let info = { name: `User${id}`, age: id * 10 };
    infoMap.set(id, info);
});
console.log(infoMap.get(2));
// 输出: { name: 'User2', age: 20 }

6. 兼容性与Polyfill

虽然MapSet是ES6引入的特性,但在一些旧版本的JavaScript环境(如IE浏览器)中不支持。为了在这些环境中使用MapSet,可以使用Polyfill。

Polyfill是一段代码(通常是JavaScript),用于在旧环境中实现新的JavaScript特性。对于MapSet,有一些第三方库提供了Polyfill,如core-js

使用core-js时,只需要在项目中引入相关的Polyfill代码即可。例如,在Node.js项目中,可以先安装core-js

npm install core-js

然后在代码中引入:

import 'core-js/stable/map';
import 'core-js/stable/set';

// 现在可以在不支持Map和Set的环境中使用它们了
let map = new Map();
map.set('key', 'value');
let set = new Set();
set.add(1);

在浏览器环境中,可以通过<script>标签引入core-js的相关脚本文件,以实现对MapSet的支持。

7. 总结

MapSet是JavaScript中非常有用的数据结构,它们为开发者提供了更灵活、高效的数据管理方式。Map适用于需要存储键值对且键可以是任意类型的场景,Set则在处理唯一值集合和集合运算方面表现出色。了解它们的特性、操作方法和性能特点,能够帮助开发者在编写JavaScript代码时,根据具体需求选择最合适的数据结构,从而提升程序的效率和可维护性。同时,通过Polyfill技术,即使在旧版本的JavaScript环境中,也能够享受到MapSet带来的便利。在实际项目开发中,合理运用MapSet,结合其他JavaScript特性,可以构建出更加健壮和高效的应用程序。无论是前端开发、后端开发还是数据处理等领域,MapSet都有着广泛的应用场景,值得开发者深入学习和掌握。