28.1 使用映射
Map
的实例将键映射到值。单个键值映射称为 条目
(entry)。
28.1.1 创建映射
创建映射有三种常用方法。
- 可以使用不带任何参数的构造函数来创建空 Map:
const emptyMap = new Map(); assert.equal(emptyMap.size, 0);
- 通过给构造函数传入一个拥有键值对的迭代器(例如数组):
const map = new Map([ [ 1, 'one' ], [ 2, 'two' ], [ 3, 'three' ], // trailing comma is ignored ]);
- 通过链式调用
.set()
方法向 Map 添加条目
(entry):const map = new Map() .set(1, 'one') .set(2, 'two') .set(3, 'three');
28.1.2 拷贝map
- 稍后我们可以看到,Map也是一个拥有键值对的迭代器,因此你可以使用构造函数来创造一个map的克隆,但仅仅是浅拷贝。
const original = new Map() .set(false, 'no') .set(true, 'yes'); const copy = new Map(original); assert.deepEqual(original, copy);
28.1.3 使用单个条目
.set()
.get()
分别用来添加成员和读取值(给定键)。const map = new Map(); map.set('foo', 123); assert.equal(map.get('foo'), 123); // 为定义的key: assert.equal(map.get('bar'), undefined); //如果条目不存在,则会使返回默认空字符串"" assert.equal(map.get('bar') || '', '');
.has()
检查 Map 是否具有给定键的条目。.delete()
删除条目。const map = new Map([['foo', 123]]); assert.equal(map.has('foo'), true); assert.equal(map.delete('foo'), true) assert.equal(map.has('foo'), false)
28.1.4 得到map的大小和清空map
.size
返回 Map 中的条目数。.clear()
删除 Map 的所有条目。const map = new Map() .set('foo', true) .set('bar', false) ; assert.equal(map.size, 2) map.clear(); assert.equal(map.size, 0)
28.1.5 获取 map 的键和值
.keys()
返回 Map 中key
的iterable
:const map = new Map() .set(false, 'no') .set(true, 'yes') ; for (const key of map.keys()) { console.log(key); } // Output: // false // true
- 我们可以使用 spread(
...
)将.keys()
返回的iterable
转换为数组:assert.deepEqual( [...map.keys()], [false, true]);
.values()
的作用类似于.keys()
,它返回的是value
的iterable
。
28.1.6 获取映射的条目
.entries()
在 Map 上返回一个entries
的iterable
:const map = new Map() .set(false, 'no') .set(true, 'yes') ; for (const entry of map.entries()) { console.log(entry); } // Output: // [false, 'no'] // [true, 'yes']
- Spreading(
...
)将.entries()
返回的iterable
转换为数组:assert.deepEqual( [...map.entrs()], [[false, 'no'], [true, 'yes']]);
- Map 实例是
entry
上的迭代器。在下面的代码中,我们使用解构来访问map
的键和值:for (const [key, value] of map) { console.log(key, value); } // Output: // false, 'no' // true, 'yes'
28.1.7 entries, keys, values
会按插入的顺序罗列出
- Map会记录
entry
创建的顺序,entries, keys,values
遍历的顺序就是插入顺序。const map1 = new Map([ ['a', 1], ['b', 2], ]); assert.deepEqual( [...map1.keys()], ['a', 'b']); const map2 = new Map([ ['b', 2], ['a', 1], ]); assert.deepEqual( [...map2.keys()], ['b', 'a']);
28.1.8 对象与Map之间的转换
- 只要
Map
的键是字符串或者Symbols
,你可以将它转换成对象(通过Object.fromEntries())const map = new Map([ ['a', 1], ['b', 2], ]); const obj = Object.fromEntries(map); assert.deepEqual( obj, {a: 1, b: 2});
- 也可以将对象转化成
map
它的键不是字符串就是Symbols
const obj = { a: 1, b: 2, }; const map = new Map(Object.entries(obj)); assert.deepEqual( map, new Map([['a', 1], ['b', 2]]);
28.2 示例:计算字符数
countChars()
返回一个map
会记录每个字符出现的次数。function countChars(chars) { const charCounts = new Map(); for (let ch of chars) { ch = ch.toLowerCase(); const prevCount = charCounts.get(ch) || 0; charCounts.set(ch, prevCount+1); } return charCounts; } const result = countChars('AaBccc'); assert.deepEqual( [...result], [ ['a', 2], ['b', 1], ['c', 3], ] );
28.3 关于 map
键的更多细节(高级)
- 任何值都可以当作键,甚至是对象:
const map = new Map(); const KEY1 = {}; const KEY2 = {}; map.set(KEY1, 'hello'); map.set(KEY2, 'world'); assert.equal(map.get(KEY1), 'hello'); assert.equal(map.get(KEY2), 'world');
28.3.1 什么键被认为是一样的?
大多数 Map 操作需要检查值是否对应其中一个键。它们是通过内部的 SameValueZero 来实现的,它的作用类似于===
,但认为NaN
等于自身。
- 因此,您可以在映射中使用
NaN
作为键,就像任何其他值一样:const map = new Map(); map.set(NaN, 123); map.get(NaN) 123
- 不同的对象总是被认为是不同的。这是无法配置的东西(但是 - TC39 意识到这是重要的功能)。
new Map().set({}, 1).set({}, 2).size 2
28.4 Map缺少的方法
28.4.1 fitter和map
你可以在数组上使用 .map()
和 .filter()
,但 map
没有这样的方法。解决方案如下:
- 将
Map
转换为[key,value]的数组。 - 得到的数组就可以使用
map
和filter
。 - 将结果转为Map。
下面演示它是如何工作的。
-
const originalMap = new Map() .set(1, 'a') .set(2, 'b') .set(3, 'c');
- 映射
originalMap
:const mappedMap = new Map( // step 3 [...originalMap] // step 1 .map(([k, v]) => [k * 2, '_' + v]) // step 2 ); assert.deepEqual([...mappedMap], [[2,'_a'], [4,'_b'], [6,'_c']]);
- 过滤
originalMap
:const filteredMap = new Map( // step 3 [...originalMap] // step 1 .filter(([k, v]) => k < 3) // step 2 ); assert.deepEqual([...filteredMap], [[1,'a'], [2,'b']]);
- 步骤 1 由扩展运算符(
...
)执行。
28.4.2 组合 map
map
没有组合的方法,因此我们需要把 map
转为数组。
- 让我们组合下面两个
map
:const map1 = new Map() .set(1, '1a') .set(2, '1b') .set(3, '1c') ; const map2 = new Map() .set(2, '2b') .set(3, '2c') .set(4, '2d') ;
- 要组合
map1
和map2
,我们通过扩展运算符(...
)将它们转换为数组然后连接这些数组。然后,我们将结果转换回 Map。所有这一切都在 A 行完成。const combinedMap = new Map([...map1, ...map2]); // (A) assert.deepEqual( [...combinedMap], // convert to Array for comparison [ [ 1, '1a' ], [ 2, '2b' ], [ 3, '2c' ], [ 4, '2d' ] ] );
练习:结合两张映射
exercises/maps-sets/combine_maps_test.js
28.5 快速参考:Map<K,V>
注意:为了简洁起见,我假装所有键具有相同的类型K
并且所有值具有相同的类型V
。
28.5.1 构造函数
new Map<K, V>(entries?: Iterable<[K, V]>)
^[ES6]^ 如果未提供参数entries
,则会创建空 Map。如果确实提供可迭代的[key,value],那么这些就会被添加到 Map。例如:const map = new Map([ [ 1, 'one' ], [ 2, 'two' ], [ 3, 'three' ], // trailing comma is ignored ]); ···
28.5.2 Map<K,V>.prototype
:处理单个条目
.get(key: K): V
^[ES6]^ 返回 Map 中key
映射的value
。如果此 Map 中没有这个key
,则返回undefined
。const map = new Map([[1, 'one'], [2, 'two']]); assert.equal(map.get(1), 'one'); assert.equal(map.get(5), undefined);
.set(key: K, value: V): this
^[ES6]^ 将给定的键值对添加到map
。如果已存在其键为key
的条目,则会更新该条目。否则,将创建一个新条目。该方法会返回this
,这意味着您可以链接它。const map = new Map([[1, 'one'], [2, 'two']]); map.set(1, 'ONE!'); map.set(3, 'THREE!'); assert.deepEqual( [...map.entries()], [[1, 'ONE!'], [2, 'two'], [3, 'THREE!']]);
.has(key: K): boolean
^[ES6]^ 返回布尔值,判断Map
中是否存在该key
。const map = new Map([[1, 'one'], [2, 'two']]); assert.equal(map.has(1), true); // key exists assert.equal(map.has(5), false); // key does not exist
.delete(key: K): boolean
^[ES6]^ 如果存在其键为key
的条目,则将其删除并返回true
。没有的话,没有任何反应,并返回false
。const map = new Map([[1, 'one'], [2, 'two']]); assert.equal(map.delete(1), true); assert.equal(map.delete(5), false); // nothing happens assert.deepEqual( [...map.entries()], [[2, 'two']]);
28.5.3 Map<K,V>.prototype
:处理所有条目
get .size: number
^[ES6]^ 返回Map
中的条目数。const map = new Map([[1, 'one'], [2, 'two']]); assert.equal(map.size, 2);
.clear(): void
^[ES6]^ 删除Map
中所有条目。const map = new Map([[1, 'one'], [2, 'two']]); assert.equal(map.size, 2); map.clear(); assert.equal(map.size, 0);
28.5.4 Map<K,V>.prototype
:迭代和循环
迭代和循环都按照条目添加到 Map 的顺序发生。
.entries(): Iterable<[K,V]>
^[ES6]^ 返回一个迭代器包含了所有的键值对条目。他们是长度为 2 的数组。const map = new Map([[1, 'one'], [2, 'two']]); for (const entry of map.entries()) { console.log(entry); } // Output: // [1, 'one'] // [2, 'two']
.forEach(callback: (value: V, key: K, theMap: Map<K,V>) => void, thisArg?: any): void
^[ES6]^ 第一个参数是回调函数,对于每个项目都会执行一次回调。如果提供了thisArg
,则每次回调的执行this
都会指向该它。否则this
为undefined
。const map = new Map([[1, 'one'], [2, 'two']]); map.forEach((value, key) => console.log(value, key)); // Output: // 'one', 1 // 'two', 2
.keys(): Iterable<K>
^[ES6]^ 返回 Map 中所有键名的迭代器。const map = new Map([[1, 'one'], [2, 'two']]); for (const key of map.keys()) { console.log(key); } // Output: // 1 // 2
.values(): Iterable<V>
^[ES6]^ 返回此 Map 中所有键值的迭代器。const map = new Map([[1, 'one'], [2, 'two']]); for (const value of map.values()) { console.log(value); } // Output: // 'one' // 'two'
[Symbol.iterator](): Iterable<[K,V]>
^[ES6]^ 迭代映射的默认方式。与.entries()
相同。const map = new Map([[1, 'one'], [2, 'two']]); for (const [key, value] of map) { console.log(key, value); } // Output: // 1, 'one' // 2, 'two'
28.5.5 来源
28.6 常问问题
28.6.1 什么时候使用 Map
,什么时候使用 对象
?
如果你需要字典这样的数据结构,他的键值不是 字符串
也不是symbol
,那么你不得不选择 Map
。但是,如果要将 字符串
和 symbol
作为键值的话,则必须决定是否使用 对象
。下面是一些简单的通用指南:
- 是否有一组固定不变的键(在开发时已知)?
那么你可以通过不变的键来访问对象的值
const value = obj.key
- 有一组键在程序执行的时候会改变? 然后使用 Map 并通过存储在变量中的键访问值:
const theKey = 123; map.get(theKey);
28.6.2 什么时候该用 object
作为 map
的 key
?
您通常希望按值比较Map键(如果它们具有相同的内容,则认为两个键相等)。这不包括对象。但是这个有一个用例可以将对象作为键:外部的数据跟对象有联系。但 WeakMaps
更好地服务于该用例,WeakMaps
条目不会阻止 key
被垃圾回收机制回收(详情请参阅下一章)。
28.6.3 为什么 Map
会保留条目插入的顺序
原则上来说, Maps
是无序的。排序条目的主要原因是列出条目,键或值的操作是确定性的。这对于测试是非常有用的。
28.6.4 为什么 map
有sizes属性,数组却是length属性
在 Javascript
中,可索引序列(像数组)有 length
属性,无序集合(如 Map
)有 size
属性。
下一节:WeakMaps 与 Maps 类似,但有以下区别:
1. 它们可用于将数据附加到对象,而不会阻止这些对象被垃圾回收。
2. 它们是黑盒子,只有拥有 WeakMap 和密钥才能访问值。
接下来的两节将更详细地研究这意味着什么。