地图

基线 广泛可用

此功能非常成熟,并在许多设备和浏览器版本上运行良好。它自以下时间以来在所有浏览器中都可用 2015 年 7 月.

**Map** 对象保存键值对,并记住键的原始插入顺序。任何值(包括对象和原始值)都可作为键或值使用。

尝试一下

描述

Map 对象是键值对的集合。Map 中的键**只能出现一次**;它在 Map 的集合中是唯一的。Map 对象按键值对进行迭代 - for...of 循环在每次迭代中返回包含 [key, value] 的 2 元素数组。迭代按插入顺序进行,该顺序对应于每个键值对第一次通过 set() 方法插入到地图中的顺序(也就是说,在调用 set() 时,地图中不存在具有相同值的键)。

规范要求地图的实现“平均情况下提供访问时间,该访问时间对集合中的元素数量呈亚线性”。因此,它可以在内部表示为哈希表(具有 O(1) 查找)、搜索树(具有 O(log(N)) 查找)或任何其他数据结构,只要复杂度优于 O(N) 即可。

键相等

值相等基于 SameValueZero 算法。(它曾经使用 SameValue,该算法将 0-0 视为不同。请查看 浏览器兼容性。)这意味着 NaN 被视为与 NaN 相同(即使 NaN !== NaN),并且所有其他值根据 === 运算符的语义被视为相等。

对象与地图

ObjectMap 相似 - 两者都允许你将键设置为值,检索这些值,删除键,以及检测键是否存在值。出于这个原因(以及因为没有内置的替代方案),历史上 Object 一直用作 Map

但是,存在一些重要的差异,在某些情况下使 Map 更加可取。

地图 对象
意外键 Map 默认不包含任何键。它只包含显式放入其中的内容。

Object 有一个原型,因此它包含默认键,如果你不小心,这些键可能会与你自己的键发生冲突。

注意:这可以通过使用 Object.create(null) 来绕过,但这很少见。

安全性 Map 可以安全地与用户提供的键和值一起使用。

Object 上设置用户提供的键值对可能会允许攻击者覆盖对象原型,这可能导致对象注入攻击。与意外键问题一样,这也可以通过使用 null 原型对象来缓解。

键类型 Map 的键可以是任何值(包括函数、对象或任何原始值)。 Object 的键必须是 StringSymbol
键顺序

Map 中的键以一种简单直接的方式排序:Map 对象按条目、键和值的插入顺序进行迭代。

虽然普通 Object 的键现在是有序的,但情况并非总是如此,并且顺序很复杂。因此,最好不要依赖属性顺序。

顺序最初仅在 ECMAScript 2015 中为自有属性定义;ECMAScript 2020 还为继承的属性定义了顺序。但请注意,没有一种机制可以迭代对象的所有属性;各种机制分别包含属性的不同子集。(for-in 只包括可枚举的字符串键属性;Object.keys 只包括自有、可枚举的字符串键属性;Object.getOwnPropertyNames 包括自有、字符串键属性,即使不可枚举也是如此;Object.getOwnPropertySymbolsSymbol 键属性执行相同操作,等等。)

大小

Map 中的项目数量可以从它的 size 属性中轻松检索。 确定 Object 中的项目数量更加迂回且效率较低。一种常见的方法是通过从 Object.keys() 返回的数组的 length 来确定。
迭代 Map 是一个 可迭代的,因此它可以直接迭代。

Object 未实现 迭代协议,因此对象无法直接使用 JavaScript for...of 语句(默认情况下)进行迭代。

注意

  • 对象可以实现迭代协议,或者你可以使用 Object.keysObject.entries 为对象获取可迭代对象。
  • for...in 语句允许你迭代对象的可枚举属性。
性能

在涉及频繁添加和删除键值对的场景中表现更好。

未针对频繁添加和删除键值对进行优化。

序列化和解析

没有对序列化或解析的原生支持。

(但你可以通过使用 JSON.stringify()(及其替换器参数)以及通过使用 JSON.parse()(及其恢复器参数)为 Map 构建自己的序列化和解析支持。请查看 Stack Overflow 问题 如何使用 JSON.stringify 序列化 ES6 Map?)。

原生支持从 Object 到 JSON 的序列化,使用 JSON.stringify()

原生支持从 JSON 到 Object 的解析,使用 JSON.parse()

设置对象属性

设置对象属性也适用于 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 的对象(或“类似 Map 的对象”)是 Web API 接口,其行为在许多方面类似于 Map

Map 一样,条目可以以添加到对象的相同顺序进行迭代。类似 Map 的对象和 Map 也有名称和行为相同的属性和方法。但是,与 Map 不同,它们只允许为每个条目的键和值使用特定预定义类型。

允许的类型在规范 IDL 定义中设置。例如,RTCStatsReport 是一个类似 Map 的对象,它必须使用字符串作为键,使用对象作为值。这在下面的规范 IDL 中定义

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

类似 Map 的对象是只读的或可读写的(参见上面 IDL 中的 readonly 关键字)。

这些方法和属性与 Map 中的等效实体具有相同的行为,只是对键和值的类型的限制不同。

以下是一些只读类似 Map 的浏览器对象的示例

构造函数

Map()

创建一个新的 Map 对象。

静态属性

Map[Symbol.species]

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

静态方法

Map.groupBy()

使用提供的回调函数返回的值对给定可迭代对象中的元素进行分组。最终返回的 Map 使用测试函数中的唯一值作为键,这些键可以用来获取每个组中的元素数组。

实例属性

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

Map.prototype.constructor

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

Map.prototype.size

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

Map.prototype[Symbol.toStringTag]

[Symbol.toStringTag] 属性的初始值为字符串 "Map"。此属性在 Object.prototype.toString() 中使用。

实例方法

Map.prototype.clear()

Map 对象中删除所有键值对。

Map.prototype.delete()

如果 Map 对象中存在元素并且已删除,则返回 true,否则返回 false。之后,map.has(key) 将返回 false

Map.prototype.entries()

返回一个新的 Iterator 对象,该对象包含 Map 对象中每个元素的 [key, value] 的两个元素数组,顺序为插入顺序。

Map.prototype.forEach()

Map 对象中存在的每个键值对调用 callbackFn 一次,顺序为插入顺序。如果为 forEach 提供了 thisArg 参数,它将用作每个回调的 this 值。

Map.prototype.get()

返回与传递的键关联的值,如果没有则返回 undefined

Map.prototype.has()

返回一个布尔值,指示 Map 对象中是否已将值与传递的键关联起来。

Map.prototype.keys()

返回一个新的 Iterator 对象,该对象包含 Map 对象中每个元素的键,顺序为插入顺序。

Map.prototype.set()

Map 对象中设置传递的键的值。返回 Map 对象。

Map.prototype.values()

返回一个新的 Iterator 对象,该对象包含 Map 对象中每个元素的值,顺序为插入顺序。

Map.prototype[Symbol.iterator]()

返回一个新的 Iterator 对象,该对象包含 Map 对象中每个元素的 [key, value] 的两个元素数组,顺序为插入顺序。

示例

使用 Map 对象

js
const myMap = new Map();

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

// setting the values
myMap.set(keyString, "value associated with 'a string'");
myMap.set(keyObj, "value associated with keyObj");
myMap.set(keyFunc, "value associated with keyFunc");

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

// getting the values
console.log(myMap.get(keyString)); // "value associated with 'a string'"
console.log(myMap.get(keyObj)); // "value associated with keyObj"
console.log(myMap.get(keyFunc)); // "value associated with keyFunc"

console.log(myMap.get("a string")); // "value associated with 'a string'", because keyString === 'a string'
console.log(myMap.get({})); // undefined, because keyObj !== {}
console.log(myMap.get(function () {})); // undefined, because keyFunc !== function () {}

使用 NaN 作为 Map 键

NaN 也可以用作键。尽管每个 NaN 都不等于自身(NaN !== NaN 为真),但以下示例有效,因为 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

可以使用 for...of 循环迭代 Map

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

可以使用 forEach() 方法迭代 Map

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

与数组对象的关联

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

// Use the regular Map constructor to transform a 2D key-value Array into a map
const myMap = new Map(kvArray);

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

// Use Array.from() to transform a map into a 2D key-value Array
console.log(Array.from(myMap)); // Will show you exactly the same Array as kvArray

// A succinct way to do the same, using the spread syntax
console.log([...myMap]);

// Or use the keys() or values() iterators, and convert them to an array
console.log(Array.from(myMap.keys())); // ["key1", "key2"]

克隆和合并 Map

就像 Array 一样,Map 也可以克隆

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

const clone = new Map(original);

console.log(clone.get(1)); // one
console.log(original === clone); // false (useful for shallow comparison)

注意:请记住,数据本身并没有被克隆。

Map 可以合并,保持键的唯一性

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

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

// Merge two maps. The last repeated key wins.
// Spread syntax essentially converts a Map to an Array
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"],
]);

// Merge maps with an array. The last repeated key wins.
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

规范

规范
ECMAScript 语言规范
# sec-map-objects

浏览器兼容性

BCD 表格仅在浏览器中加载

另请参阅