28. 映射(Map)

28.1 使用映射

Map的实例将键映射到值。单个键值映射称为 条目(entry)。

28.1.1 创建映射

创建映射有三种常用方法。

  1. 可以使用不带任何参数的构造函数来创建空 Map:
    const emptyMap = new Map();
    assert.equal(emptyMap.size, 0);
    
  2. 通过给构造函数传入一个拥有键值对的迭代器(例如数组):
    const map = new Map([
      [ 1, 'one' ],
      [ 2, 'two' ],
      [ 3, 'three' ], // trailing comma is ignored
    ]);
    
  3. 通过链式调用.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 中keyiterable
    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(),它返回的是 valueiterable

28.1.6 获取映射的条目

  • .entries()在 Map 上返回一个 entriesiterable
    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 没有这样的方法。解决方案如下:

  1. Map 转换为[key,value]的数组。
  2. 得到的数组就可以使用 mapfilter
  3. 将结果转为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')
    ;
    
  • 要组合map1map2,我们通过扩展运算符(...)将它们转换为数组然后连接这些数组。然后,我们将结果转换回 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 都会指向该它。否则 thisundefined
    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 作为 mapkey

您通常希望按值比较Map键(如果它们具有相同的内容,则认为两个键相等)。这不包括对象。但是这个有一个用例可以将对象作为键:外部的数据跟对象有联系。但 WeakMaps 更好地服务于该用例,WeakMaps 条目不会阻止 key 被垃圾回收机制回收(详情请参阅下一章)。

28.6.3 为什么 Map 会保留条目插入的顺序

原则上来说, Maps 是无序的。排序条目的主要原因是列出条目,键或值的操作是确定性的。这对于测试是非常有用的。

28.6.4 为什么 map 有sizes属性,数组却是length属性

Javascript 中,可索引序列(像数组)有 length 属性,无序集合(如 Map)有 size属性。

下一节:WeakMaps 与 Maps 类似,但有以下区别:

1. 它们可用于将数据附加到对象,而不会阻止这些对象被垃圾回收。
2. 它们是黑盒子,只有拥有 WeakMap 和密钥才能访问值。

接下来的两节将更详细地研究这意味着什么。