Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者{{Glossary("Primitive", "原始值")}})都可以作为键或值。
{{InteractiveExample("JavaScript 演示:Map", "taller")}}
```js interactive-example const map = new Map();
map.set("a", 1); map.set("b", 2); map.set("c", 3);
console.log(map.get("a")); // 期望输出:1
map.set("a", 97);
console.log(map.get("a")); // 期望输出:97
console.log(map.size); // 期望输出:3
map.delete("b");
console.log(map.size); // 期望输出:2
## 描述
`Map` 对象是键值对的集合。`Map` 中的一个键**只能出现一次**;它在 `Map` 的集合中是独一无二的。`Map` 对象按键值对迭代——一个 {{jsxref("Statements/for...of", "for...of")}} 循环在每次迭代后会返回一个形式为 `[key, value]` 的数组。迭代按*插入顺序*进行,即键值对按 [`set()`](/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Map/set) 方法首次插入到集合中的顺序(也就是说,当调用 `set()` 时,map 中没有具有相同值的键)进行迭代。
规范要求 map 实现“平均访问时间与集合中的元素数量呈次线性关系”。因此,它可以在内部表示为散列表(查找复杂度为 O(1))、搜索树(查找复杂度为 O(log(N)))或任何其他数据结构,只要复杂度小于 O(N)。
### 键的相等
键的比较基于[零值相等](/zh-CN/docs/Web/JavaScript/Guide/Equality_comparisons_and_sameness#零值相等)算法。(它曾经使用[同值相等](/zh-CN/docs/Web/JavaScript/Guide/Equality_comparisons_and_sameness#使用_object.is_进行同值相等比较),将 `0` 和 `-0` 视为不同。参见[浏览器兼容性](#浏览器兼容性)。)这意味着 <code>NaN</code> 是与 `NaN` 相等的(虽然 `NaN !== NaN`),剩下所有其他的值是根据 `===` 运算符的结果判断是否相等。此外,对于对象键,相等性基于对象标识。它们是通过引用进行比较,而非通过值。参见[使用 Map 对象](#使用_map_对象)示例。
### `Object` 和 `Map` 的比较
<code>Object</code> 和 `Map` 类似的是,它们都允许你按键存取一个值、删除键、检测一个键是否绑定了值。因此(并且也没有其他内建的替代方式了)过去我们一直都把对象(`Object`)当成 `Map` 使用。
不过,它们之间有一些重要的区别,是的在一些情况下使用 `Map` 会是更好的选择:
Map
Object
意外的键
Map 默认不包含任何键。它只包含显式存入的键值对。
Object 有原型,因此它包含默认的键,如果不小心的话,它们可能会与你自己的键相冲突。
备注:这可以通过使用 {{jsxref("Object.create", "Object.create(null)")}} 来绕过,但很少这样做。
安全性
Map 可以安全地与用户提供的键值一起使用。
在 Object 上设置用户提供的键值对可能会允许攻击者覆盖对象的原型,这可能会导致对象注入攻击或原型污染攻击。就像意外的键问题一样,这也可以通过使用 null 原型对象来缓解。
键的类型
Map 的键可以为任何值(包括函数、对象或任何原始值)。
Object 的键必须为 String 或 Symbol。
键的顺序
Map 中的键以简单、直接的方式排序:Map 对象按照插入的顺序迭代条目、键和值。
尽管现在普通的 Object 的键是有序的,但情况并非总是如此,并且其排序比较复杂的。因此,最好不要依赖属性的顺序。
该顺序最初仅在 ECMAScript 2015 中为自有属性定义;ECMAScript 2020 还定义了继承属性的顺序。但请注意,没有单一机制可以迭代对象的所有属性;各种机制各自包含不同的属性子集。({{jsxref("Statements/for...in", "for-in")}} 仅包含可枚举的字符串键属性;Object.keys 仅包含可枚举的自有字符串键属性;Object.getOwnPropertyNames 包括自有的字符串键属性,即使是不可枚举的;Object.getOwnPropertySymbols 仅对 Symbol 键属性执行相同的操作,等等。)
大小
Map 中的项目数量很容易从其 {{jsxref("Map.prototype.size", "size")}} 属性中获得。
确定 Object 中的项目数量通常更麻烦,效率也较低。一种常见的方法是通过获取 Object.keys() 返回的数组的{{jsxref("Array/length", "长度", "", 1)}}。
迭代
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 example-bad
const wrongMap = new Map();
wrongMap["bla"] = "blaa";
wrongMap["bla2"] = "blaaa2";
console.log(wrongMap); // Map { bla: 'blaa', bla2: 'blaaa2' }
但这种设置属性的方式不会改变 Map 的数据结构。它使用的是通用对象的特性。'bla' 的值未被存储在 Map 中,无法被查询到。其他的对这一数据的操作也会失败:
```js example-bad wrongMap.has("bla"); // false wrongMap.delete("bla"); // false console.log(wrongMap); // Map { bla: 'blaa', bla2: 'blaaa2' }
正确的存储数据到 Map 中的方式是使用 `set(key, value)` 方法。
```js example-good
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 中定义的:
interface RTCStatsReport {
readonly maplike<DOMString, object>;
};
类 Map 对象可以是只读的,也可以是可写的(参见上面 IDL 中的 readonly 关键字)。
- 只读的类
Map对象具有 {{jsxref("Map/size", "size")}} 属性,以及这些方法:{{jsxref("Map/entries", "entries()")}}、{{jsxref("Map/forEach", "forEach()")}}、{{jsxref("Map/get", "get()")}}、{{jsxref("Map/has", "has()")}}、{{jsxref("Map/keys", "keys()")}}、{{jsxref("Map/values", "values()")}} 和Symbol.iterator()。 - 可写的类
Map对象还额外具有这些方法:{{jsxref("Map/clear", "clear()")}}、{{jsxref("Map/delete", "delete()")}} 和 {{jsxref("Map/set", "set()")}}。
除了对键和值类型的限制外,其方法和属性的行为与 Map 中的对应实体相同。
以下是浏览器中只读的类 Map 对象的示例:
AudioParamMapCSSFontFeatureValuesMapEventCountsKeyboardLayoutMapMIDIInputMapMIDIOutputMapRTCStatsReport
构造函数
- {{jsxref("Map/Map", "Map()")}}
- : 创建
Map对象。
- : 创建
静态属性
Map[Symbol.species]- : 用于创建派生对象的构造函数。
静态方法
Map.groupBy()- : 根据提供的回调函数返回的值将给定的可迭代对象分组。最终返回的
Map对象使用测试函数返回的唯一值作为键,可用于获取每个组的元素数组。
- : 根据提供的回调函数返回的值将给定的可迭代对象分组。最终返回的
实例属性
这些属性在 Map.prototype 上定义,并由所有 Map 实例共享。
- {{jsxref("Object/constructor", "Map.prototype.constructor")}}
- : 创建实例对象的构造函数。对于
Map实例,初始值为 {{jsxref("Map/Map", "Map")}} 构造函数。
- : 创建实例对象的构造函数。对于
Map.prototype.size- : 返回
Map对象中的键值对数量。
- : 返回
Map.prototype[Symbol.toStringTag]- :
[Symbol.toStringTag]属性的初始值是字符串"Map"。该属性在Object.prototype.toString()中使用。
- :
实例方法
Map.prototype.clear()- : 移除
Map对象中所有的键值对。
- : 移除
Map.prototype.delete()- : 移除
Map对象中由指定的键标识的条目。
- : 移除
Map.prototype.entries()- : 返回一个新的迭代器对象,其包含
Map对象中所有键值对[key, value]二元数组,以插入顺序排列。
- : 返回一个新的迭代器对象,其包含
Map.prototype.forEach()- : 以插入顺序为
Map对象中的每个键值对调用一次callbackFn。如果为forEach提供了thisArg参数,则它将作为每一次 callback 的this值。
- : 以插入顺序为
Map.prototype.get()- : 返回
Map对象中与指定键对应的值,若不存在,则返回undefined。
- : 返回
Map.prototype.getOrInsert()- : 返回
Map对象中与指定的键对应的值。如果键不存在,则插入一个包含该键和给定默认值的新条目,并返回插入的值。
- : 返回
Map.prototype.getOrInsertComputed()- : 返回
Map对象中与指定键对应的值。如果键不存在,则插入一个新条目,其键为指定键,值由给定的回调函数计算得出,并返回插入的值。
- : 返回
Map.prototype.has()- : 返回一个布尔值,指示
Map对象中是否存在具有指定键的条目。
- : 返回一个布尔值,指示
Map.prototype.keys()- : 返回一个新的迭代器对象,其包含
Map对象中所有元素的键,以插入顺序排列。
- : 返回一个新的迭代器对象,其包含
Map.prototype.set()- : 向
Map对象添加一个具有指定键和值的新条目,如果键已存在则更新现有条目。
- : 向
Map.prototype.values()- : 返回一个新的迭代对象,其中按插入顺序包含
Map对象中每个元素的值。
- : 返回一个新的迭代对象,其中按插入顺序包含
Map.prototype[Symbol.iterator]()- : 返回一个新的迭代器对象,其包含
Map对象中所有元素[key, value]二元数组,以插入顺序排列。
- : 返回一个新的迭代器对象,其包含
示例
使用 Map 对象
const myMap = new Map();
const keyString = "a string";
const keyObj = {};
const keyFunc = () => {};
// 添加键
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(() => {})); // undefined,因为 keyFunc !== () => {}
将 NaN 作为 Map 的键
NaN 也可以作为键。虽然 NaN 与任何值甚至与自己都不相等(NaN !== NaN 返回 true),但是因为所有的 NaN 的值都是无法区分的,所以下面的例子成立:
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 循环来实现迭代:
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 也可以通过 {{jsxref("Map.prototype.forEach", "forEach()")}} 方法迭代:
myMap.forEach((value, key) => {
console.log(`${key} = ${value}`);
});
// 0 = zero
// 1 = one
Map 与数组对象的关系
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 能像数组一样被复制:
const original = new Map([[1, "one"]]);
const clone = new Map(original);
console.log(clone.get(1)); // one
console.log(original === clone); // false(适用于浅比较)
[!NOTE] 请记住,数据本身未被克隆。换句话说,这只是对
Map的浅拷贝。
Map 对象间可以进行合并,但是会保持键的唯一性。
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 对象也能与数组合并:
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