19. 映射和集合
- 19.1. 概述
- 19.1.1. 映射
- 19.1.2. 集合
- 19.1.3. 弱映射
- 19.2. 映射
- 19.2.1. 基本操作
- 19.2.2. 设置映射
- 19.2.3. 键
- 19.2.4. 迭代映射
- 19.2.5. 循环遍历映射条目
- 19.2.6. 映射和过滤映射
- 19.2.7. 合并映射
- 19.2.8. 通过键值对数组将任意映射转换为 JSON
- 19.2.9. 通过对象将字符串映射转换为 JSON
- 19.2.10. 映射 API
- 19.3. 弱映射
- 19.3.1. 弱映射键是对象
- 19.3.2. 弱映射键是弱引用的
- 19.3.3. 您无法获取弱映射的概览或清除它
- 19.3.4. 弱映射的用例
- 19.3.5. 弱映射 API
- 19.4. 集合
- 19.4.1. 基本操作
- 19.4.2. 设置集合
- 19.4.3. 比较集合元素
- 19.4.4. 迭代
- 19.4.5. 映射和过滤
- 19.4.6. 并集、交集、差集
- 19.4.7. 集合 API
- 19.5. 弱集合
- 19.5.1. 弱集合的用例
- 19.5.2. 弱集合 API
- 19.6. 常见问题解答:映射和集合
- 19.6.1. 为什么映射和集合具有属性
size
而不是 length
?
- 19.6.2. 为什么我不能配置映射和集合如何比较键和值?
- 19.6.3. 有没有办法在从映射中获取内容时指定默认值?
- 19.6.4. 我应该何时使用映射,何时使用对象?
- 19.6.5. 我什么时候会在映射中使用对象作为键?
19.1 概述
ECMAScript 6 中新增了以下四种数据结构:Map
、WeakMap
、Set
和 WeakSet
。
19.1.1 映射
映射的键可以是任意值
您可以使用包含 [键,值] 对的数组(或任何可迭代对象)来设置映射中的初始数据
19.1.2 集合
集合是唯一元素的集合
如您所见,如果将可迭代对象(示例中的 arr
)传递给构造函数,则可以使用元素初始化集合。
19.1.3 弱映射
弱映射是一种映射,它不会阻止对其键进行垃圾回收。这意味着您可以将数据与对象相关联,而不必担心内存泄漏。例如
19.2 映射
JavaScript 一直拥有非常简陋的标准库。非常缺少的是用于将值映射到值的数据结构。在 ECMAScript 5 中,您能获得的最佳选择是字符串到任意值的映射,方法是滥用对象。即使那样,也有一些陷阱可能会让您绊倒。
ECMAScript 6 中的 Map
数据结构允许您使用任意值作为键,这是非常受欢迎的。
19.2.1 基本操作
处理单个条目
确定映射的大小并清除它
19.2.2 设置映射
您可以通过键值“对”(包含 2 个元素的数组)的可迭代对象来设置映射。一种可能性是使用数组(它是可迭代的)
或者,set()
方法是可链接的
19.2.3 键
任何值都可以是键,甚至是对象
19.2.3.1 哪些键被视为相等?
大多数映射操作都需要检查一个值是否等于其中一个键。它们通过内部操作 SameValueZero 来实现,该操作的工作方式类似于 ===
,但将 NaN
视为与其自身相等。
让我们首先看看 ===
如何处理 NaN
相反,您可以像使用任何其他值一样在映射中使用 NaN
作为键
与 ===
一样,-0
和 +0
被视为相同的值。这通常是处理两个零的最佳方式(详细信息在“Speaking JavaScript”中解释)。
不同的对象始终被视为不同的。这是无法配置的(目前),如常见问题解答中稍后所述。
获取未知键会产生 undefined
19.2.4 迭代映射
让我们设置一个映射来演示如何对其进行迭代。
映射记录插入元素的顺序,并在迭代键、值或条目时遵循该顺序。
19.2.4.1 键和值的可迭代对象
keys()
返回映射中键的可迭代对象
values()
返回映射中值的可迭代对象
19.2.4.2 条目的可迭代对象
entries()
将映射的条目作为 [键,值] 对(数组)的可迭代对象返回。
解构使您能够直接访问键和值
迭代映射的默认方式是 entries()
因此,您可以使前面的代码段更短
19.2.4.3 将可迭代对象(包括映射)转换为数组
展开运算符 (...
) 可以将可迭代对象转换为数组。这使我们能够将 Map.prototype.keys()
的结果(一个可迭代对象)转换为数组
映射也是可迭代的,这意味着展开运算符可以将映射转换为数组
19.2.5 循环遍历映射条目
Map
方法 forEach
具有以下签名
第一个参数的签名反映了 Array.prototype.forEach
的回调的签名,这就是为什么值首先出现的原因。
19.2.6 映射和过滤映射
您可以 map()
和 filter()
数组,但映射没有这样的操作。解决方案是
- 将映射转换为 [键,值] 对的数组。
- 映射或过滤数组。
- 将结果转换回映射。
我将使用以下映射来演示它是如何工作的。
映射 originalMap
过滤 originalMap
步骤 1 由展开运算符 (...
) 执行,我之前已经解释过。
19.2.7 合并映射
没有用于合并映射的方法,这就是为什么必须使用上一节中的方法来实现的原因。
让我们合并以下两个映射
为了合并 map1
和 map2
,我通过展开运算符 (...
) 将它们转换为数组,并将这些数组连接起来。之后,我将结果转换回映射。所有这些都在第一行完成。
19.2.8 通过键值对数组将任意映射转换为 JSON
如果映射包含任意(JSON 兼容)数据,我们可以通过将其编码为键值对数组(2 元素数组)将其转换为 JSON。让我们首先研究如何实现这种编码。
19.2.8.1 在映射和键值对数组之间转换
展开运算符 允许您将映射转换为键值对数组
Map
构造函数允许您将键值对数组转换为映射
19.2.8.2 与 JSON 的转换
让我们使用这些知识将任何包含 JSON 兼容数据的映射转换为 JSON 并返回
以下交互演示了如何使用这些函数
19.2.9 通过对象将字符串映射转换为 JSON
只要映射仅包含字符串作为键,就可以通过将其编码为对象将其转换为 JSON。让我们首先研究如何实现这种编码。
19.2.9.1 在字符串映射和对象之间转换
以下两个函数在字符串映射和对象之间转换
让我们使用这两个函数
19.2.9.2 与 JSON 的转换
使用这些辅助函数,转换为 JSON 的工作方式如下
这是使用这些函数的示例
19.2.10 映射 API
构造函数
-
new Map(entries? : Iterable<[any,any]>)
如果您不提供参数 iterable
,则会创建一个空映射。如果您确实提供了 [键,值] 对的可迭代对象,则这些对将用于向映射添加条目。例如
处理单个条目
-
Map.prototype.get(key) : any
返回在此映射中 key
映射到的 value
。如果此映射中没有键 key
,则返回 undefined
。
-
Map.prototype.set(key, value) : this
将给定键映射到给定值。如果已经存在一个键为 key
的条目,则更新该条目。否则,将创建一个新条目。此方法返回 this
,这意味着您可以链接它。
-
Map.prototype.has(key) : boolean
返回给定键是否存在于此映射中。
-
Map.prototype.delete(key) : boolean
如果存在一个键为 key
的条目,则删除该条目并返回 true
。否则,不执行任何操作并返回 false
。
处理所有条目
-
get Map.prototype.size : number
返回此映射中有多少个条目。
-
Map.prototype.clear() : void
从此映射中删除所有条目。
**迭代和循环:** 按将条目添加到映射中的顺序进行。
-
Map.prototype.entries() : Iterable<[any,any]>
返回一个可迭代对象,其中包含此映射中每个条目的一个 [键,值] 对。这些对是长度为 2 的数组。
-
Map.prototype.forEach((value, key, collection) => void, thisArg?) : void
第一个参数是一个回调,它为此映射中的每个条目调用一次。如果提供了 thisArg
,则每次调用都会将 this
设置为它。否则,将 this
设置为 undefined
。
-
Map.prototype.keys() : Iterable<any>
返回此映射中所有键的可迭代对象。
-
Map.prototype.values() : Iterable<any>
返回此映射中所有值的可迭代对象。
-
Map.prototype[Symbol.iterator]() : Iterable<[any,any]>
迭代映射的默认方式。引用 Map.prototype.entries
。
19.3 弱映射
弱映射的工作方式与映射基本相同,但有以下区别
- 弱映射键是对象(值可以是任意值)
- 弱映射键是弱引用的
- 您无法获取弱映射内容的概览
- 您无法清除弱映射
以下各节将解释这些差异。
19.3.1 WeakMap 键是对象
如果向 WeakMap 添加条目,则键必须是对象。
19.3.2 WeakMap 键是弱引用的
WeakMap 中的键是弱引用的:通常,任何存储位置(变量、属性等)都没有引用的对象都可以被垃圾回收。从这个意义上说,WeakMap 键不计为存储位置。换句话说:一个对象作为 WeakMap 中的键不会阻止该对象被垃圾回收。
此外,一旦键消失,其条目也将消失(最终会消失,但无论如何都无法检测到何时消失)。
19.3.3 您无法获取 WeakMap 的概览或清除它
无法检查 WeakMap 的内部结构,也无法获取它们的概览。这包括无法迭代键、值或条目。换句话说:要从 WeakMap 中获取内容,您需要一个键。也没有办法清除 WeakMap(作为一种解决方法,您可以创建一个全新的实例)。
这些限制实现了一种安全属性。引用 Mark Miller 的话:“只有同时拥有 WeakMap 和键的人才能观察或影响从 WeakMap/键对到值的映射。使用 clear()
,只有 WeakMap 的人就可以影响 WeakMap 和键到值的映射。”
此外,迭代将难以实现,因为您必须保证键保持弱引用。
19.3.4 WeakMap 的用例
WeakMap 对于将数据与您无法(或不想)控制其生命周期的对象相关联非常有用。在本节中,我们将看两个例子
19.3.4.1 通过 WeakMap 缓存计算结果
使用 WeakMap,您可以将先前计算的结果与对象相关联,而不必担心内存管理。以下函数 countOwnKeys
就是一个例子:它将先前结果缓存在 WeakMap cache
中。
如果我们将此函数与对象 obj
一起使用,您可以看到结果仅在第一次调用时计算,而第二次调用则使用缓存值
19.3.4.2 管理监听器
假设我们想在不更改对象的情况下将监听器附加到对象。您可以将监听器添加到对象 obj
并且您将能够触发监听器
这两个函数 addListener()
和 triggerListeners()
可以如下实现。
在这里使用 WeakMap 的优势在于,一旦一个对象被垃圾回收,它的监听器也会被垃圾回收。换句话说:不会有任何内存泄漏。
19.3.4.3 通过 WeakMap 保存私有数据
在以下代码中,WeakMap _counter
和 _action
用于存储 Countdown
实例的虚拟属性的数据
有关此技术的更多信息,请参见 关于类的章节。
19.3.5 WeakMap API
WeakMap
的构造函数和四种方法的工作方式与其 Map
等效项相同
19.4 Set
ECMAScript 5 也没有 Set 数据结构。有两种可能的解决方法
- 使用对象的键来存储字符串集的元素。
- 将(任意)集合元素存储在数组中:通过
indexOf()
检查它是否包含元素,通过 filter()
删除元素等。这不是一个非常快的解决方案,但它很容易实现。需要注意的一个问题是 indexOf()
找不到值 NaN
。
ECMAScript 6 具有数据结构 Set
,它适用于任意值,速度快并且可以正确处理 NaN
。
19.4.1 基本操作
管理单个元素
确定 Set 的大小并清除它
19.4.2 设置 Set
您可以通过构成 Set 的元素的可迭代对象来设置 Set。例如,通过数组
或者,add
方法是可链接的
19.4.2.1 陷阱:new Set()
最多只有一个参数
Set
构造函数有零个或一个参数
- 零个参数:创建一个空的 Set。
- 一个参数:该参数需要是可迭代的;迭代的项目定义了 Set 的元素。
其他参数将被忽略,这可能会导致意外结果
对于第二个 Set,仅使用 'foo'
(它是可迭代的)来定义 Set。
19.4.3 比较 Set 元素
与 Map 一样,元素的比较方式类似于 ===
,但 NaN
与任何其他值一样。
再次添加元素无效
与 ===
类似,两个不同的对象永远不会被认为是相等的(目前无法自定义,稍后将在常见问题解答中解释)
19.4.4 迭代
Set 是可迭代的,并且 for-of
循环的工作方式与您预期的一样
如您所见,Set 保留了迭代顺序。也就是说,始终按照插入元素的顺序迭代元素。
前面解释的扩展运算符 (...
) 适用于可迭代对象,因此您可以将 Set 转换为数组
我们现在有一种简洁的方法可以将数组转换为 Set 并返回,其作用是从数组中消除重复项
19.4.5 映射和过滤
与数组相比,Set 没有 map()
和 filter()
方法。一种解决方法是将它们转换为数组并返回。
映射
过滤
19.4.6 并集、交集、差集
ECMAScript 6 Set 没有用于计算并集(例如 addAll
)、交集(例如 retainAll
)或差集(例如 removeAll
)的方法。本节介绍如何解决该限制。
19.4.6.1 并集
并集 (a
∪ b
):创建一个包含 Set a
和 Set b
的元素的 Set。
模式始终相同
- 将一个或两个 Set 转换为数组。
- 执行操作。
- 将结果转换回 Set。
扩展运算符 (...
) 将可迭代对象(例如 Set)的元素插入到数组中。因此,[...a, ...b]
意味着 a
和 b
被转换为数组并连接起来。它等效于 [...a].concat([...b])
。
19.4.6.2 交集
交集 (a
∩ b
):创建一个 Set,其中包含 Set a
中也存在于 Set b
中的元素。
步骤:将 a
转换为数组,过滤元素,将结果转换为 Set。
19.4.6.3 差集
差集 (a
\ b
):创建一个 Set,其中包含 Set a
中不存在于 Set b
中的元素。此操作有时也称为减法 (-
)。
19.4.7 Set API
构造函数
-
new Set(elements? : Iterable<any>)
如果您不提供参数 iterable
,则会创建一个空的 Set。如果提供,则迭代的值将作为元素添加到 Set 中。例如
单个 Set 元素
-
Set.prototype.add(value) : this
将 value
添加到此 Set。此方法返回 this
,这意味着它可以链接。
-
Set.prototype.has(value) : boolean
检查 value
是否在此 Set 中。
-
Set.prototype.delete(value) : boolean
从此 Set 中删除 value
。
所有 Set 元素
-
get Set.prototype.size : number
返回此 Set 中有多少个元素。
-
Set.prototype.clear() : void
从此 Set 中删除所有元素。
迭代和循环
-
Set.prototype.values() : Iterable<any>
返回此 Set 中所有元素的可迭代对象。
-
Set.prototype[Symbol.iterator]() : Iterable<any>
迭代 Set 的默认方式。指向 Set.prototype.values
。
-
Set.prototype.forEach((value, key, collection) => void, thisArg?)
循环遍历此 Set 的元素,并为每个元素调用回调(第一个参数)。value
和 key
都设置为该元素,因此此方法的工作方式类似于 Map.prototype.forEach
。如果提供了 thisArg
,则每次调用都会将 this
设置为它。否则,this
将设置为 undefined
。
与 Map
的对称性:以下两种方法仅存在是为了使 Set 的接口类似于 Map 的接口。每个 Set 元素的处理方式就好像它是一个 Map 条目,其键和值都是该元素。
Set.prototype.entries() : Iterable<[any,any]>
Set.prototype.keys() : Iterable<any>
entries()
允许您将 Set 转换为 Map
19.5 WeakSet
WeakSet
是一个 Set,它不会阻止其元素被垃圾回收。有关 WeakSet 不允许迭代、循环和清除的原因,请参阅有关 WeakMap
的部分。
19.5.1 WeakSet 的用例
鉴于您无法迭代它们的元素,因此 WeakSet 的用例并不多。它们确实允许您标记对象。
19.5.1.1 标记由工厂函数创建的对象
例如,如果您有一个用于代理的工厂函数,则可以使用 WeakSet 来记录哪些对象是由该工厂创建的
完整示例显示在 关于代理的章节 中。
_proxies
必须是 WeakSet,因为一旦不再引用代理,正常的 Set 将阻止代理被垃圾回收。
19.5.1.2 将对象标记为可安全地与方法一起使用
Domenic Denicola 展示了 类 Foo
如何确保其方法仅应用于由它创建的实例
19.5.2 WeakSet API
WeakSet
的构造函数和三种方法的工作方式与其 Set
等效项相同
19.6 常见问题解答:Map 和 Set
19.6.1 为什么 Map 和 Set 具有属性 size
而不是 length
?
数组具有属性 length
来计算条目的数量。Map 和 Set 具有不同的属性 size
。
之所以会有这种差异,是因为 length
用于序列,即可以索引的数据结构,例如数组。而 size
用于主要无序的集合,例如映射和集合。
19.6.2 为什么我不能配置映射和集合如何比较键和值?
如果有一种方法可以配置哪些映射键和哪些集合元素被认为是相等的,那就太好了。但该功能已被推迟,因为它难以正确有效地实现。
19.6.3 有没有一种方法可以在从映射中获取内容时指定默认值?
如果您使用键从映射中获取内容,您有时希望指定一个默认值,以便在键不在映射中时返回该值。ES6 映射不允许您直接这样做。但您可以使用 Or
运算符 (||
),如下面的代码所示。countChars
返回一个将字符映射到出现次数的映射。
在 A 行中,如果 ch
不在 charCounts
中并且 get()
返回 undefined
,则使用默认值 0
。
19.6.4 我应该何时使用映射,何时使用对象?
如果您将字符串以外的任何内容映射到任何类型的数据,则别无选择:您必须使用映射。
但是,如果您要将字符串映射到任意数据,则必须决定是否使用对象。一个粗略的通用准则是
- 是否存在一组固定的键(在开发时已知)?
如果是,则使用对象并通过固定键访问值:obj.key
- 键集是否可以在运行时更改?
如果是,则使用映射并通过存储在变量中的键访问值:map.get(theKey)
19.6.5 我什么时候会在映射中使用对象作为键?
如果映射键是按值比较的(相同的“内容”意味着两个值被认为是相等的,而不是相同的身份),那么它们才有意义。这排除了对象。有一种用例——将数据外部附加到对象,但这种用例最好由 WeakMap 来处理,在 WeakMap 中,当键消失时,条目也会消失。