Map
===

**`Map`** 对象保存键值对，并且能够记住键的原始插入顺序。任何值（对象或者原始值）都可以作为键或值。

尝试一下
----

描述
--

`Map` 对象是键值对的集合。`Map` 中的一个键**只能出现一次**；它在 `Map` 的集合中是独一无二的。`Map` 对象按键值对迭代——一个 `for...of` 循环在每次迭代后会返回一个形式为 `[key, value]` 的数组。迭代按*插入顺序*进行，即键值对按 `set()` 方法首次插入到集合中的顺序（也就是说，当调用 `set()` 时，map 中没有具有相同值的键）进行迭代。

规范要求 map 实现“平均访问时间与集合中的元素数量呈次线性关系”。因此，它可以在内部表示为哈希表（使用 O(1) 查找）、搜索树（使用 O(log(N)) 查找）或任何其他数据结构，只要复杂度小于 O(N)。

### 键的相等

键的比较基于零值相等算法。（它曾经使用同值相等，将 `0` 和 `-0` 视为不同。检查浏览器兼容性。）这意味着 `NaN` 是与 `NaN` 相等的（虽然 `NaN !== NaN`），剩下所有其他的值是根据 `===` 运算符的结果判断是否相等。

### `Object` 和 `Map` 的比较

`Object` 和 `Map` 类似的是，它们都允许你按键存取一个值、删除键、检测一个键是否绑定了值。因此（并且也没有其他内建的替代方式了）过去我们一直都把对象当成 `Map` 使用。

不过 `Map` 和 `Object` 有一些重要的区别，在下列情况中使用 `Map` 会是更好的选择：

|  | Map | Object |
| --- | --- | --- |
| 意外的键 | `Map` 默认不包含任何键。它只包含显式存入的键值对。 | `Object` 有原型，因此它包含默认的键，如果不小心的话，它们可能会与你自己的键相冲突。

**备注：**这可以通过使用 `Object.create(null)` 来绕过，但很少这样做。
 |
| 安全性 | `Map` 可以安全地与用户提供的键值一起使用。 | 在 `Object` 上设置用户提供的键值对可能会允许攻击者覆盖对象的原型，这可能会导致对象注入攻击。就像意外的键问题一样，这也可以通过使用 `null` 原型对象来缓解。 |
| 键的类型 | `Map` 的键可以为任何值（包括函数、对象或任何原始值）。 | `Object` 的键必须为 `String` 或 `Symbol`。 |
| 键的顺序 | `Map` 中的键以简单、直接的方式排序：`Map` 对象按照插入的顺序迭代条目、键和值。 | 尽管现在普通的 `Object` 的键是有序的，但情况并非总是如此，并且其排序比较复杂的。因此，最好不要依赖属性的顺序。
该顺序最初仅在 ECMAScript 2015 中为自有属性定义；ECMAScript 2020 还定义了继承属性的顺序。但请注意，没有单一机制可以迭代对象的**所有**属性；各种机制各自包含不同的属性子集。（`for-in` 仅包含可枚举的字符串键属性；`Object.keys` 仅包含可枚举的自有字符串键属性；`Object.getOwnPropertyNames` 包括自有的字符串键属性，即使是不可枚举的；`Object.getOwnPropertySymbols` 仅对 `Symbol` 键属性执行相同的操作，等等。） |
| 大小 | `Map` 中的项目数量很容易从其 `size` 属性中获得。 | 确定 `Object` 中的项目数量通常更麻烦，效率也较低。一种常见的方法是通过获取 `Object.keys()` 返回的数组的长度。 |
| 迭代 | `Map` 是可迭代对象，所以它可以直接迭代。 | `Object` 没有实现迭代协议，因此对象默认情况下不能直接通过 JavaScript 的 for...of 语句进行迭代。

**备注：**
* 一个对象可以实现迭代协议，或者你可以使用 `Object.keys` 或 `Object.entries` 来获取一个对象的可迭代对象。
* for...in 语句允许你迭代对象的*可枚举*属性。

 |
| 性能 | 在涉及频繁添加和删除键值对的场景中表现更好。 | 未针对频繁添加和删除键值对进行优化。 |
| 序列化和解析 | 没有对序列化或解析的原生支持。
（但你可以通过使用 `JSON.stringify()` 及其 *replacer* 参数和 `JSON.parse()` 及其 *reviver* 参数来为 `Map` 构建自己的序列化和解析支持。参见 Stack Overflow 问题 How do you JSON.stringify an ES6 Map?）。 | 原生支持使用 `JSON.stringify()` 序列化 `Object` 到 JSON。
原生支持使用 `JSON.parse()` 解析 JSON 为 `Object`。 |

### 设置对象属性

设置对象属性同样适用于 Map 对象，但容易造成困扰。

即，以下的代码能够正常运行（但不推荐）：


```js
const wrongMap = new Map();
wrongMap["bla"] = "blaa";
wrongMap["bla2"] = "blaaa2";

console.log(wrongMap); // Map { bla: 'blaa', bla2: 'blaaa2' }
```


但这种设置属性的方式不会改变 Map 的数据结构。它使用的是通用对象的特性。`'bla'` 的值未被存储在 Map 中，无法被查询到。其他的对这一数据的操作也会失败：


```js
wrongMap.has("bla"); // false
wrongMap.delete("bla"); // false
console.log(wrongMap); // Map { bla: 'blaa', bla2: 'blaaa2' }
```


正确的存储数据到 Map 中的方式是使用 `set(key, value)` 方法。


```js
const contacts = new Map();
contacts.set("Jessie", { phone: "213-555-1234", address: "123 N 1st Ave" });
contacts.has("Jessie"); // true
contacts.get("Hilary"); // undefined
contacts.set("Hilary", { phone: "617-555-4321", address: "321 S 2nd St" });
contacts.get("Jessie"); // {phone: "213-555-1234", address: "123 N 1st Ave"}
contacts.delete("Raymond"); // false
contacts.delete("Jessie"); // true
console.log(contacts.size); // 1
```

### 类 Map 浏览器 API

**浏览器类 Map 对象**（或称为“maplike 对象”）是其行为在很多方面都类似于 `Map` 的 Web API 接口。

就像 `Map` 一样，对象中的条目可以以添加的顺序迭代。类似 `Map` 的对象和 `Map` 具有相同的属性和方法。但是，与 `Map` 不同的是，它们仅允许每个条目中的键和值具有特定预定义的类型。

允许的类型规范的 IDL 定义给出。例如，`RTCStatsReport` 是一个类似 `Map` 的对象，必须使用字符串作为键，对象作为值。这是在规范 IDL 中定义的：


```webidl
interface RTCStatsReport {
  readonly maplike<DOMString, object>;
};
```


类 `Map` 对象可以是只读的，也可以是可写的（参见上面 IDL 中的 `readonly` 关键字）。

* 只读的类 `Map` 对象具有 `size` 属性，以及这些方法：`entries()`、`forEach()`、`keys()`、`values()` 和 `@@iterator` 。
* 可写的类 `Map` 对象还额外具有这些方法：`clear()`、`delete()` 和 `set()`。

除了对键和值类型的限制外，其方法和属性的行为与 `Map` 中的对应实体相同。

以下是浏览器中只读的类 `Map` 对象的示例：

* `AudioParamMap` (en-US)
* `RTCStatsReport`
* `EventCounts` (en-US)
* `KeyboardLayoutMap` (en-US)
* `MIDIInputMap` (en-US)
* `MIDIOutputMap` (en-US)
构造函数
----

`Map()`

创建 `Map` 对象。

静态属性
----

`Map[@@species]`

用于创建派生对象的构造函数。

静态方法
----

`Map.groupBy()`

根据提供的回调函数返回的值将给定的可迭代对象分组。最终返回的 `Map` 对象使用测试函数返回的唯一值作为键，可用于获取每个组的元素数组。

实例属性
----

这些属性在 `Map.prototype` 上定义，并由所有 `Map` 实例共享。

`Map.prototype.constructor`

创建实例对象的构造函数。对于 `Map` 实例，初始值为 `Map` 构造函数。

`Map.prototype.size`

返回 `Map` 对象中的键值对数量。

`Map.prototype[@@toStringTag]`

`@@toStringTag` 属性的初始值是字符串 `"Map"`。该属性在 `Object.prototype.toString()` 中使用。

实例方法
----

`Map.prototype.clear()`

移除 `Map` 对象中所有的键值对。

`Map.prototype.delete()`

移除 `Map` 对象中指定的键值对，如果键值对存在并成功被移除，返回 `true`，否则返回 `false`。调用 `delete` 后再调用 `map.has(key)` 将返回 `false`。

`Map.prototype.entries()`

返回一个新的迭代器对象，其包含 `Map` 对象中所有键值对 `[key, value]` 二元数组，以插入顺序排列。

`Map.prototype.forEach()`

以插入顺序为 `Map` 对象中的每个键值对调用一次 `callbackFn`。如果为 `forEach` 提供了 `thisArg` 参数，则它将作为每一次 callback 的 `this` 值。

`Map.prototype.get()`

返回与指定的键 `key` 关联的值，若不存在关联的值，则返回 `undefined`。

`Map.prototype.has()`

返回一个布尔值，用来表明 `Map` 对象中是否存在与指定的键 `key` 关联的值。

`Map.prototype.keys()`

返回一个新的迭代器对象，其包含 `Map` 对象中所有元素的键，以插入顺序排列。

`Map.prototype.set()`

在 `Map` 对象中设置与指定的键 `key` 关联的值，并返回 `Map` 对象。

`Map.prototype.values()`

返回一个新的迭代对象，其中包含 `Map` 对象中所有的值，并以插入 `Map` 对象的顺序排列。

`Map.prototype[@@iterator]()`

返回一个新的迭代器对象，其包含 `Map` 对象中所有元素 `[key, value]` 二元数组，以插入顺序排列。

示例
--

### 使用 Map 对象


```js
const myMap = new Map();

const keyString = "a string";
const keyObj = {};
const keyFunc = function () {};

// 添加键
myMap.set(keyString, "和键'a string'关联的值");
myMap.set(keyObj, "和键 keyObj 关联的值");
myMap.set(keyFunc, "和键 keyFunc 关联的值");

console.log(myMap.size); // 3

// 读取值
console.log(myMap.get(keyString)); // "和键'a string'关联的值"
console.log(myMap.get(keyObj)); // "和键 keyObj 关联的值"
console.log(myMap.get(keyFunc)); // "和键 keyFunc 关联的值"

console.log(myMap.get("a string")); // "和键'a string'关联的值"，因为 keyString === 'a string'
console.log(myMap.get({})); // undefined，因为 keyObj !== {}
console.log(myMap.get(function () {})); // undefined，因为 keyFunc !== function () {}
```

### 将 NaN 作为 Map 的键

`NaN` 也可以作为键。虽然 `NaN` 与任何值甚至与自己都不相等（`NaN !== NaN` 返回 true），但是因为所有的 `NaN` 的值都是无法区分的，所以下面的例子成立：


```js
const myMap = new Map();
myMap.set(NaN, "not a number");

myMap.get(NaN);
// "not a number"

const otherNaN = Number("foo");
myMap.get(otherNaN);
// "not a number"
```

### 使用 for...of 迭代 Map

`Map` 可以使用 `for...of` 循环来实现迭代：


```js
const myMap = new Map();
myMap.set(0, "zero");
myMap.set(1, "one");

for (const [key, value] of myMap) {
  console.log(`${key} = ${value}`);
}
// 0 = zero
// 1 = one

for (const key of myMap.keys()) {
  console.log(key);
}
// 0
// 1

for (const value of myMap.values()) {
  console.log(value);
}
// zero
// one

for (const [key, value] of myMap.entries()) {
  console.log(`${key} = ${value}`);
}
// 0 = zero
// 1 = one
```

### 使用 forEach() 迭代 Map

`Map` 也可以通过 `forEach()` 方法迭代：


```js
myMap.forEach((value, key) => {
  console.log(`${key} = ${value}`);
});
// 0 = zero
// 1 = one
```

### Map 与数组对象的关系


```js
const kvArray = [
  ["key1", "value1"],
  ["key2", "value2"],
];

// 使用常规的 Map 构造函数可以将一个二维的键值对数组转换成一个 Map 对象
const myMap = new Map(kvArray);

console.log(myMap.get("key1")); // "value1"

// 使用 Array.from 函数可以将一个 Map 对象转换成一个二维的键值对数组
console.log(Array.from(myMap)); // 输出和 kvArray 相同的数组

// 更简洁的方法来做如上同样的事情，使用展开运算符
console.log([...myMap]);

// 或者在键或者值的迭代器上使用 Array.from，进而得到只含有键或者值的数组
console.log(Array.from(myMap.keys())); // 输出 ["key1", "key2"]
```

### 复制或合并 Maps

`Map` 能像数组一样被复制：


```js
const original = new Map([[1, "one"]]);

const clone = new Map(original);

console.log(clone.get(1)); // one
console.log(original === clone); // false. 浅比较 不为同一个对象的引用
```


**备注：** 请记住，*数据本身*未被克隆。

`Map` 对象间可以进行合并，但是会保持键的唯一性。


```js
const first = new Map([
  [1, "one"],
  [2, "two"],
  [3, "three"],
]);

const second = new Map([
  [1, "uno"],
  [2, "dos"],
]);

// 合并两个 Map 对象时，如果有重复的键值，则后面的会覆盖前面的。
// 展开语法本质上是将 Map 对象转换成数组。
const merged = new Map([...first, ...second]);

console.log(merged.get(1)); // uno
console.log(merged.get(2)); // dos
console.log(merged.get(3)); // three
```


`Map` 对象也能与数组合并：


```js
const first = new Map([
  [1, "one"],
  [2, "two"],
  [3, "three"],
]);

const second = new Map([
  [1, "uno"],
  [2, "dos"],
]);

// Map 对象同数组进行合并时，如果有重复的键值，则后面的会覆盖前面的。
const merged = new Map([...first, ...second, [1, "eins"]]);

console.log(merged.get(1)); // eins
console.log(merged.get(2)); // dos
console.log(merged.get(3)); // three
```

规范
--

| Specification |
| --- |
| ECMAScript Language Specification # sec-map-objects |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

参见
--

* `core-js` 中 `Map` 的 polyfill
* `Set`
* `WeakMap`
* `WeakSet`