理解JavaScript中的Map和Set对象

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 Open Internet/Free Speech Fund 来接受捐赠。

在 JavaScript 中,开发人员经常花费大量时间来决定要使用的正确数据结构。 这是因为选择正确的数据结构可以使以后更容易操作该数据,从而节省时间并使代码更易于理解。 用于存储数据集合的两种主要数据结构是 ObjectsArrays(一种对象)。 开发人员使用对象来存储键/值对和数组来存储索引列表。 然而,为了给开发人员更多的灵活性,ECMAScript 2015 规范引入了两种新的可迭代对象类型:Maps,它们是键/值对的有序集合,和 Sets,它们是集合的独特价值。

在本文中,您将了解 Map 和 Set 对象,它们与对象和数组的相似或不同之处,它们可用的属性和方法,以及一些实际用途的示例。

地图

Map 是键/值对的集合,可以使用任何 数据类型 作为键,并且可以维护其条目的顺序。 Maps 具有 Objects(唯一的键/值对集合)和 Arrays(有序集合)的元素,但在概念上更类似于 Objects。 这是因为,尽管条目的大小和顺序像数组一样保留,但条目本身是键/值对,如对象。

可以使用 new Map() 语法初始化地图:

const map = new Map()

这给了我们一个空地图:

OutputMap(0) {}

向地图添加值

您可以使用 set() 方法向地图添加值。 第一个参数是键,第二个参数是值。

下面将三个键/值对添加到 map

map.set('firstName', 'Luke')
map.set('lastName', 'Skywalker')
map.set('occupation', 'Jedi Knight')

在这里,我们开始了解 Maps 如何同时具有 Objects 和 Arrays 的元素。 像数组一样,我们有一个零索引的集合,我们还可以看到默认情况下 Map 中有多少项。 映射使用 => 语法将键/值对表示为 key => value

OutputMap(3)
0: {"firstName" => "Luke"}
1: {"lastName" => "Skywalker"}
2: {"occupation" => "Jedi Knight"}

此示例看起来类似于具有基于字符串的键的常规对象,但我们可以使用任何数据类型作为 Maps 的键。

除了在 Map 上手动设置值之外,我们还可以使用值初始化 Map。 我们使用包含两个元素的数组来执行此操作,每个元素都是键/值对,如下所示:

[ [ 'key1', 'value1'], ['key2', 'value2'] ]

使用以下语法,我们可以重新创建相同的 Map:

const map = new Map([
  ['firstName', 'Luke'],
  ['lastName', 'Skywalker'],
  ['occupation', 'Jedi Knight'],
])

注:本例使用尾逗号,也称为悬空逗号。 这是一种 JavaScript 格式化实践,在声明数据集合时,系列中的最后一项以逗号结尾。 尽管这种格式选择可用于更清晰的差异和更轻松的代码操作,但是否使用它是一个偏好问题。 有关尾随逗号的更多信息,请参阅 MDN Web 文档中的这篇 Trailing Comma 文章


顺便说一句,此语法与在 Object 上调用 Object.entries() 的结果相同。 这提供了一种将 Object 转换为 Map 的现成方法,如以下代码块所示:

const luke = {
  firstName: 'Luke',
  lastName: 'Skywalker',
  occupation: 'Jedi Knight',
}

const map = new Map(Object.entries(luke))

或者,您可以使用一行代码将 Map 转换回 Object 或 Array。

下面将 Map 转换为 Object:

const obj = Object.fromEntries(map)

这将导致 obj 的以下值:

Output{firstName: "Luke", lastName: "Skywalker", occupation: "Jedi Knight"}

现在,让我们将 Map 转换为 Array:

const arr = Array.from(map)

这将导致 arr 的以下数组:

Output[ ['firstName', 'Luke'],
  ['lastName', 'Skywalker'],
  ['occupation', 'Jedi Knight'] ]

地图键

Maps 接受任何数据类型作为键,并且不允许重复键值。 我们可以通过创建一个映射并使用非字符串值作为键,以及将两个值设置为同一个键来证明这一点。

首先,让我们用非字符串键初始化一个映射:

const map = new Map()

map.set('1', 'String one')
map.set(1, 'This will be overwritten')
map.set(1, 'Number one')
map.set(true, 'A Boolean')

此示例将用后续的键覆盖 1 的第一个键,并将 '1' 字符串和 1 数字视为唯一键:

Output0: {"1" => "String one"}
1: {1 => "Number one"}
2: {true => "A Boolean"}

尽管人们普遍认为常规 JavaScript 对象已经可以将数字、布尔值和其他原始数据类型作为键来处理,但实际上并非如此,因为对象将所有键都更改为字符串。

例如,使用数字键初始化对象,并比较数字键 1 和字符串化 "1" 键的值:

// Initialize an object with a numerical key
const obj = { 1: 'One' }

// The key is actually a string
obj[1] === obj['1']  // true

这就是为什么如果您尝试使用 Object 作为键,它将打印出字符串 object Object 代替。

例如,创建一个 Object,然后将其用作另一个 Object 的键:

// Create an object
const objAsKey = { foo: 'bar' }

// Use this object as the key of another object
const obj = {
  [objAsKey]: 'What will happen?'
} 

这将产生以下结果:

Output{[object Object]: "What will happen?"}

这不是地图的情况。 尝试创建一个 Object 并将其设置为 Map 的键:

// Create an object
const objAsKey = { foo: 'bar' }

const map = new Map()

// Set this object as the key of a Map
map.set(objAsKey, 'What will happen?')

Map 元素的 key 现在是我们创建的对象。

Outputkey: {foo: "bar"}
value: "What will happen?"

使用 Object 或 Array 作为键需要注意一件重要的事情:Map 使用对 Object 的引用来比较相等性,而不是 Object 的字面值。 在 JavaScript 中 {} === {} 返回 false,因为这两个对象不是相同的两个对象,尽管具有相同的(空)值。

这意味着添加两个具有相同值的唯一对象将创建一个包含两个条目的 Map:

// Add two unique but similar objects as keys to a Map
map.set({}, 'One')
map.set({}, 'Two')

这将产生以下结果:

OutputMap(2) {{…} => "One", {…} => "Two"}

但是两次使用相同的 Object 引用将创建一个包含一个条目的 Map。

// Add the same exact object twice as keys to a Map
const obj = {}

map.set(obj, 'One')
map.set(obj, 'Two')

这将导致以下结果:

OutputMap(1) {{…} => "Two"}

第二个 set() 正在更新与第一个完全相同的键,因此我们最终得到一个只有一个值的 Map。

从地图中获取和删除项目

使用对象的缺点之一是难以枚举它们,或者使用所有键或值。 相比之下,Map 结构具有许多内置属性,可以更直接地使用它们的元素。

我们可以初始化一个新的 Map 来演示以下方法和属性:delete()has()get()size

// Initialize a new Map
const map = new Map([
  ['animal', 'otter'],
  ['shape', 'triangle'],
  ['city', 'New York'],
  ['country', 'Bulgaria'],
])

使用 has() 方法检查地图中是否存在项目。 has() 将返回一个布尔值。

// Check if a key exists in a Map
map.has('shark') // false
map.has('country') // true

使用 get() 方法通过键检索值。

// Get an item from a Map
map.get('animal') // "otter"

Maps 相对于 Objects 的一个特别好处是,您可以随时找到 Map 的大小,就像使用 Array 一样。 您可以使用 size 属性获取地图中的项目数。 这比将 Object 转换为 Array 来查找长度所涉及的步骤更少。

// Get the count of items in a Map
map.size // 4

使用 delete() 方法通过键从 Map 中删除项目。 该方法将返回一个布尔值——如果项目存在并被删除,则返回 true,如果不匹配任何项目,则返回 false

// Delete an item from a Map by key
map.delete('city') // true

这将产生以下地图:

OutputMap(3) {"animal" => "otter", "shape" => "triangle", "country" => "Bulgaria"}

最后,可以使用 map.clear() 清除地图中的所有值。

// Empty a Map
map.clear()

这将产生:

OutputMap(0) {}

映射的键、值和条目

对象可以使用 Object 构造函数的属性来检索键、值和条目。 另一方面,Map 具有原型方法,允许我们直接获取 Map 实例的键、值和条目。

keys()values()entries() 方法都返回一个 MapIterator,它类似于一个数组,您可以使用 for...of循环遍历这些值。

这是另一个 Map 示例,我们可以使用它来演示这些方法:

const map = new Map([
  [1970, 'bell bottoms'],
  [1980, 'leg warmers'],
  [1990, 'flannel'],
])

keys() 方法返回键:

map.keys()
OutputMapIterator {1970, 1980, 1990}

values() 方法返回值:

map.values()
OutputMapIterator {"bell bottoms", "leg warmers", "flannel"}

entries() 方法返回一个键/值对数组:

map.entries()
OutputMapIterator {1970 => "bell bottoms", 1980 => "leg warmers", 1990 => "flannel"}

使用地图迭代

Map 有一个内置的 forEach 方法,类似于 Array,用于内置迭代。 但是,它们迭代的内容有些不同。 Map 的 forEach 的回调会遍历 valuekeymap 本身,而 Array 版本会遍历 itemindexarray 本身。

// Map 
Map.prototype.forEach((value, key, map) = () => {})

// Array
Array.prototype.forEach((item, index, array) = () => {})

这是 Maps 优于 Objects 的一大优势,因为 Objects 需要使用 keys()values()entries() 进行转换,并且没有简单的方法来检索属性对象而不转换它。

为了演示这一点,让我们遍历我们的 Map 并将键/值对记录到控制台:

// Log the keys and values of the Map with forEach
map.forEach((value, key) => {
  console.log(`${key}: ${value}`)
})

这将给出:

Output1970: bell bottoms
1980: leg warmers
1990: flannel

由于 for...of 循环会迭代 Map 和 Array 等可迭代对象,因此我们可以通过解构 Map 项数组来获得完全相同的结果:

// Destructure the key and value out of the Map item
for (const [key, value] of map) {
  // Log the keys and values of the Map with for...of
  console.log(`${key}: ${value}`)
}

地图属性和方法

下表显示了 Map 属性和方法的列表,以供快速参考:

属性/方法 描述 退货
set(key, value) 将键/值对附加到 Map Map 对象
delete(key) 通过键从 Map 中删除键/值对 布尔值
get(key) 按键返回一个值 价值
has(key) 按键检查 Map 中是否存在元素 布尔值
clear() 从地图中删除所有项目 不适用
keys() 返回 Map 中的所有键 MapIterator 对象
values() 返回 Map 中的所有值 MapIterator 对象
entries() 将 Map 中的所有键和值返回为 [key, value] MapIterator 对象
forEach() 按插入顺序遍历 Map 不适用
size 返回 Map 中的项目数 数字

何时使用地图

总而言之,Maps 与 Objects 的相似之处在于它们持有键/值对,但 Maps 与对象相比有几个优点:

  • Size - 地图具有 size 属性,而对象没有内置方法来检索其大小。
  • Iteration - Maps 是可直接迭代的,而 Objects 不是。
  • 灵活性 - 映射可以有任何数据类型(原始或对象)作为值的键,而对象只能有字符串。
  • Ordered - 映射保留其插入顺序,而对象没有保证顺序。

由于这些因素,地图是需要考虑的强大数据结构。 但是,Objects 也有一些重要的优点:

  • JSON - 对象可以完美地与 JSON.parse()JSON.stringify() 一起工作,这两个基本函数用于处理 JSON,这是许多 REST API 处理的常见数据格式.
  • 使用单个元素 - 使用Object中的已知值,您可以直接使用键访问它,而无需使用方法,例如Map的get()

此列表将帮助您确定 Map 或 Object 是否适合您的用例。

Set 是唯一值的集合。 与 Map 不同,Set 在概念上更类似于 Array 而不是 Object,因为它是值列表而不是键/值对。 但是,Set 不是 Arrays 的替代品,而是为处理重复数据提供额外支持的补充。

您可以使用 new Set() 语法初始化 Set。

const set = new Set()

这给了我们一个空集:

OutputSet(0) {}

可以使用 add() 方法将项目添加到 Set。 (不要与 Map 可用的 set() 方法混淆,尽管它们相似。)

// Add items to a Set
set.add('Beethoven')
set.add('Mozart')
set.add('Chopin')

由于 Set 只能包含唯一值,因此任何添加已存在值的尝试都将被忽略。

set.add('Chopin') // Set will still contain 3 unique values

注意:适用于 Map 键的相同等式比较适用于 Set 项。 具有相同值但不共享相同引用的两个对象将不被视为相等。


您还可以使用 Array 值初始化 Sets。 如果数组中有重复的值,它们将从 Set 中删除。

// Initialize a Set from an Array
const set = new Set(['Beethoven', 'Mozart', 'Chopin', 'Chopin'])
OutputSet(3) {"Beethoven", "Mozart", "Chopin"}

相反,只需一行代码,就可以将 Set 转换为 Array:

const arr = [...set]
Output(3) ["Beethoven", "Mozart", "Chopin"]

Set 有许多与 Map 相同的方法和属性,包括 delete()has()clear()size

// Delete an item
set.delete('Beethoven') // true

// Check for the existence of an item
set.has('Beethoven') // false

// Clear a Set
set.clear()

// Check the size of a Set
set.size // 0

请注意,Set 无法通过键或索引访问值,例如 Map.get(key)arr[index]

集合的键、值和条目

Map 和 Set 都有返回 Iterator 的 keys()values()entries() 方法。 然而,虽然这些方法中的每一个在 Map 中都有不同的用途,但 Set 没有键,因此键是值的别名。 这意味着 keys()values() 都将返回相同的迭代器,而 entries() 将返回两次该值。 仅将 values() 与 Set 一起使用是最有意义的,因为存在其他两种方法是为了与 Map 保持一致性和交叉兼容性。

const set = new Set([1, 2, 3])
// Get the values of a set
set.values()
OutputSetIterator {1, 2, 3}

带集合的迭代

和 Map 一样,Set 有一个内置的 forEach() 方法。 由于 Set 没有键,因此 forEach() 回调的第一个和第二个参数返回相同的值,因此除了与 Map 兼容之外,它没有用例。 forEach()的参数为(value, key, set)

forEach()for...of 都可以在 Set 上使用。 首先,让我们看一下forEach()迭代:

const set = new Set(['hi', 'hello', 'good day'])

// Iterate a Set with forEach
set.forEach((value) => console.log(value))

然后我们可以写出for...of版本:

// Iterate a Set with for...of
for (const value of set) {  
    console.log(value);
}

这两种策略都会产生以下结果:

Outputhi
hello
good day

设置属性和方法

下表显示了 Set 属性和方法的列表以供快速参考:

属性/方法 描述 退货
add(value) 将新项目附加到 Set Set 对象
delete(value) 从 Set 中移除指定项 布尔值
has() 检查 Set 中是否存在项目 布尔值
clear() 从 Set 中移除所有项目 不适用
keys() 返回 Set 中的所有值(与 values() 相同) SetIterator 对象
values() 返回 Set 中的所有值(与 keys() 相同) SetIterator 对象
entries() 将 Set 中的所有值返回为 [value, value] SetIterator 对象
forEach() 按插入顺序遍历 Set 不适用
size 返回 Set 中的项目数 数字

何时使用集合

Set 是 JavaScript 工具包的一个有用的补充,特别是对于处理数据中的重复值。

在一行中,我们可以从具有重复值的 Array 创建一个没有重复值的新 Array。

const uniqueArray = [ ...new Set([1, 1, 2, 2, 2, 3])] // (3) [1, 2, 3]

这将给出:

Output(3) [1, 2, 3]

Set 可用于查找两组数据之间的并集、交集和差异。 但是,由于 sort()map()filter()reduce() 方法,Arrays 在对数据进行额外操作方面比 Sets 具有显着优势作为与 JSON 方法的直接兼容性。

结论

在本文中,您了解到 Map 是有序键/值对的集合,而 Set 是唯一值的集合。 这两种数据结构都为 JavaScript 添加了额外的功能并简化了常见任务,例如分别查找键/值对集合的长度和从数据集中删除重复项。 另一方面,对象和数组传统上用于 JavaScript 中的数据存储和操作,并且与 JSON 直接兼容,这继续使它们成为最重要的数据结构,尤其是在使用 REST API 时。 Maps 和 Sets 主要用于支持对象和数组的数据结构。

如果您想了解更多关于 JavaScript 的知识,请查看我们的主页以查看我们的 如何在 JavaScript 中编写代码系列,或者浏览我们的 如何在 Node.js 中编写代码系列 以获取后面的文章-结束开发。