30.映射(Map)

优质
小牛编辑
131浏览
2023-12-01

原文: http://exploringjs.com/impatient-js/ch_maps.html

在 ES6 之前,JavaScript 没有字典的数据结构和(ab)使用的对象作为从字符串到任意值的字典。 ES6 带来了映射,这些映射是从任意值到任意值的字典。

30.1。使用映射

Map的实例将键映射到值。单个键值映射称为 _ 条目 _。映射记录创建订单条目并在返回时遵守该订单,例如密钥或条目。

30.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
]);

第三,.set()方法向 Map 添加条目并且是可链接的:

const map = new Map()
.set(1, 'one')
.set(2, 'two')
.set(3, 'three');

30.1.2。使用单个条目

.set().get()用于写入和读取值(给定键)。

const map = new Map();

map.set('foo', 123);

assert.equal(map.get('foo'), 123);
// Unknown key:
assert.equal(map.get('bar'), undefined);
// Use the default value '' if an entry is missing:
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)

30.1.3。确定映射的大小并清除它

.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)

30.1.4。获取映射的键和值

.keys()在 Map 的键上返回一个 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()返回的可迭代转换为数组:

assert.deepEqual(
  [...map.keys()], 
  [false, true]);

.values()的作用类似于.keys(),但是对于值而不是键。

30.1.5。获取映射的条目

.entries()在 Map 的条目上返回一个 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()返回的可迭代转换为数组:

assert.deepEqual(
  [...map.entries()], 
  [[false, 'no'], [true, 'yes']]);

映射实例也是条目上的迭代。在下面的代码中,我们使用解构来访问map的键和值:

for (const [key, value] of map) {
  console.log(key, value);
}
// Output:
// false, 'no'
// true, 'yes'

30.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],
  ]
);

30.3。关于映射键的更多细节(高级)

任何值都可以是键,甚至是对象:

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');

30.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

30.4。缺少映射操作

30.4.1。映射和过滤映射

你可以.map().filter()数组,但映射没有这样的操作。解决方案是:

  1. 将 Map 转换为[key,value]对数组。
  2. 映射或过滤数组。
  3. 将结果转换回 Map。

我将使用以下 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 由扩展运算符(...)执行。

30.4.2。结合映射

没有组合映射的方法,这就是为什么必须使用上一节的方法来实现这一点。

让我们结合以下两个映射:

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

30.5。快速参考:Map<K,V>

注意:为了简洁起见,我假装所有键具有相同的类型K并且所有值具有相同的类型V

30.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
    ]);
    ···
    
    

#### 30.5.2。 Map<K,V>.prototype:处理单个条目

  • .get(key: K): V [ES6]

    返回key在此 Map 中映射到的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]

    将给定键映射到给定值。如果已存在其键为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 中是否存在给定键。

    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']]);
    

30.5.3。 Map<K,V>.prototype:处理所有条目

  • get .size: number [ES6]

    返回此 Map 中有多少条目。

    const map = new Map([[1, 'one'], [2, 'two']]);
    assert.equal(map.size, 2);
    

    在 JavaScript 中,可索引序列(例如 Arrays)具有.length,而主要是无序集合(例如 Maps)具有.size

  • .clear(): void [ES6]

    从此 Map 中删除所有条目。

    const map = new Map([[1, 'one'], [2, 'two']]);
    assert.equal(map.size, 2);
    map.clear();
    assert.equal(map.size, 0);
    

30.5.4。 Map<K,V>.prototype:迭代和循环

迭代和循环都按照将条目添加到 Map 的顺序发生。

  • .entries(): Iterable<[K,V]> [ES6]

    为此 Map 中的每个条目返回一个具有一个[key,value]对的 iterable。这些对是长度为 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]

    第一个参数是为此 Map 中的每个条目调用一次的回调。如果提供了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'
    

30.5.5。来源

30.6。常问问题

30.6.1。什么时候我应该使用 Map,当一个对象?

如果将除字符串以外的任何内容映射到任何类型的数据,则别无选择:必须使用 Map。

但是,如果要将字符串映射到任意数据,则必须决定是否使用对象。粗略的一般准则是:

  • 是否有一组固定的密钥(在开发时已知)? 然后使用一个对象并通过固定键访问这些值:obj.key

  • 键组可以在运行时更改吗? 然后使用 Map 并通过存储在变量中的键访问值:map.get(theKey)

30.6.2。我何时将对象用作映射中的键?

如果通过值比较映射键主要是有意义的(相同的“内容”意味着两个值被认为是相同的,而不是相同的身份)。这排除了对象。有一个用例 - 外部将数据附加到对象,但 WeakMaps 更好地服务于用例,其中条目不会阻止垃圾收集(有关详细信息,请参阅下一章)。

测验

参见测验应用程序。