Skip to content

Files

Latest commit

b7ab058 · Jul 7, 2019

History

History
554 lines (402 loc) · 13.2 KB

37.md

File metadata and controls

554 lines (402 loc) · 13.2 KB

30.映射(Map

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

贡献者:so-hard

30.1 使用映射

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

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

30.1.2 拷贝map

稍后我们可以看到,Map也是一个拥有键值对的迭代器,因此你可以使用构造函数来创造一个map的克隆,但仅仅是浅拷贝。

const original = new Map()
  .set(false, 'no')
  .set(true, 'yes');

const copy = new Map(original);
assert.deepEqual(original, copy);

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

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

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

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

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

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

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

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 Map缺少的方法

30.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 由扩展运算符(...)执行。

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

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]

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

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

    返回一个迭代器包含了所有的键值对条目。他们是长度为 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'

30.5.5 来源

30.6 常问问题

30.6.1 什么时候使用 Map,什么时候使用 对象

如果你需要字典这样的数据结构,他的键值不是 字符串 也不是symbol,那么你不得不选择 Map

但是,如果要将 字符串symbol作为键值的话,则必须决定是否使用 对象。下面是一些简单的通用指南:

  • 是否有一组固定不变的键(在开发时已知)?

    那么你可以通过不变的键来访问对象的值

    const value = obj.key
  • 有一组键在程序执行的时候会改变? 然后使用 Map 并通过存储在变量中的键访问值:

    const theKey = 123;
    map.get(theKey);

30.6.2 什么时候该用 object 作为 mapkey

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

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

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

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

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

测验

参见测验应用程序