JavaScript Map 入门指南 – wiki基地


JavaScript Map 入门指南:深入理解这种灵活的键值对存储结构

JavaScript 作为一门动态、灵活的语言,提供了多种内置的数据结构来帮助开发者组织和管理数据。我们最熟悉的可能是数组(Array)和对象(Object)。数组用于存储有序集合,而对象通常用于存储键值对。

然而,传统的 JavaScript 对象在用作键值对映射时存在一些局限性,尤其是在键的类型和迭代顺序方面。为了解决这些问题,ES6 (ECMAScript 2015) 引入了一种新的内置对象:Map

Map 提供了一种更强大、更灵活的方式来存储键值对,弥补了普通对象的一些不足。本指南将带你深入了解 JavaScript Map 的各个方面,从基础用法到高级特性,再到与普通对象的比较,帮助你完全掌握这一重要的数据结构。

目录

  1. 什么是 JavaScript Map?
    • 定义与基本概念
    • Map 的主要特点
  2. 为什么选择使用 Map?Map 的优势
    • 任意类型作为键
    • 保证键的顺序
    • 方便获取集合大小
    • 更清晰的迭代
    • 无原型链干扰
  3. 创建 Map
    • 创建空 Map
    • 使用可迭代对象初始化 Map
  4. Map 的基本操作:增、删、查、改
    • 添加或更新元素 (set())
    • 获取元素 (get())
    • 检查元素是否存在 (has())
    • 删除元素 (delete())
    • 清空 Map (clear())
  5. 获取 Map 的大小
    • 使用 size 属性
  6. 遍历 Map
    • 使用 for...of 循环
      • 遍历键值对 ([key, value])
      • 遍历键 (keys())
      • 遍历值 (values())
    • 使用 forEach() 方法
  7. Map 与 Object 的区别与选择
    • 键的类型
    • 键的顺序
    • 获取大小
    • 迭代方式
    • 原型链
    • 性能考量
    • JSON 序列化
    • 何时使用 Map?
    • 何时使用 Object?
  8. Map 的一些进阶概念
    • 键的相等性比较
    • 对象作为键的注意事项
  9. WeakMap 简介(与 Map 的关联)
    • 什么是 WeakMap?
    • WeakMap 的特点与用途
  10. 实际应用场景举例
    • 数据缓存
    • DOM 元素关联数据
    • 计数器
  11. 总结

1. 什么是 JavaScript Map?

定义与基本概念

Map 是一种新的数据结构,用于存储键值对(key-value pairs)。它类似于对象,因为它们都将值(values)关联到键(keys)。然而,Map 与普通对象在几个关键方面有所不同。

你可以将 Map 想象成一个字典或者查找表,你通过一个特定的“单词”(键)来找到它对应的“定义”(值)。

Map 的主要特点

  • 任意类型作为键: 这是 Map 最重要的特性之一。与对象不同,Map 的键可以是 任何 数据类型,包括原始值(字符串、数字、布尔值、nullundefinedSymbol)和对象(普通对象、函数、数组、DOM 元素等)。
  • 保证键的顺序: Map 中的键值对是按照它们被插入的顺序进行存储的。在迭代 Map 时,你会按照元素被 set() 方法添加的顺序获取它们。
  • 方便获取集合大小: Map 提供了一个直接的 size 属性,可以非常方便地获取 Map 中元素的数量。
  • 可迭代: Map 实例本身是可迭代的,你可以直接使用 for...of 循环来遍历它的键值对。

2. 为什么选择使用 Map?Map 的优势

了解了 Map 的基本定义后,我们来看看它解决了哪些问题,以及相比普通对象有哪些优势,这有助于我们理解为什么要在某些场景下选择 Map。

任意类型作为键 (Flexible Keys)

这是 Map 相比对象最核心的优势。在普通对象中,如果你使用非字符串或 Symbol 类型作为键,它们会被自动转换为字符串。

“`javascript
const obj = {};
const key1 = {};
const key2 = {};

obj[key1] = ‘value1’;
obj[key2] = ‘value2’;

console.log(obj); // { ‘[object Object]’: ‘value2’ }
console.log(obj[key1]); // ‘value2’
console.log(obj[key2]); // ‘value2’
// key1 和 key2 都被转换成了字符串 ‘[object Object]’,导致它们互相覆盖。

const map = new Map();
const mapKey1 = {};
const mapKey2 = {};

map.set(mapKey1, ‘value1’);
map.set(mapKey2, ‘value2’);

console.log(map); // Map { {} => ‘value1’, {} => ‘value2’ }
console.log(map.get(mapKey1)); // ‘value1’
console.log(map.get(mapKey2)); // ‘value2’
// 在 Map 中,对象 {} 和 {} 作为键是不同的,它们不会互相覆盖。
“`
这个特性使得 Map 非常适合将数据关联到特定的对象实例上,比如将配置信息关联到 DOM 元素,或者将计算结果缓存到函数参数对象上。

保证键的顺序 (Guaranteed Order)

Map 迭代的顺序就是元素插入的顺序。对于需要在特定顺序下处理键值对的场景,这非常有用。虽然现代 JavaScript 对象(ES2015 后对于非 Symbol 字符串键)在迭代时也倾向于保持插入顺序,但 Map 的规范明确保证了这一点,并且适用于所有键类型。

“`javascript
const map = new Map();
map.set(‘c’, 3);
map.set(‘a’, 1);
map.set(‘b’, 2);

for (const [key, value] of map) {
console.log(${key}: ${value});
}
// 输出:
// c: 3
// a: 1
// b: 2
// 顺序与插入顺序一致
“`

方便获取集合大小 (Easy Size)

Map 提供了一个 .size 属性来直接获取 Map 中元素的数量,这是一个 O(1) 操作(通常非常快)。而获取对象属性数量通常需要 Object.keys(obj).lengthObject.getOwnPropertyNames(obj).length,这涉及到先创建一个键的数组,然后获取其长度,效率上可能不如 Map 的 .size 直接。

“`javascript
const map = new Map([[‘a’, 1], [‘b’, 2]]);
console.log(map.size); // 2

const obj = { a: 1, b: 2 };
console.log(Object.keys(obj).length); // 2 (需要一个额外的步骤)
“`

更清晰的迭代 (Clearer Iteration)

Map 实例本身是可迭代的,可以直接与 for...of 循环配合使用,方便地同时获取键和值。它还提供了 keys(), values(), entries() 等方法,让迭代更加灵活和清晰。

“`javascript
const map = new Map([[‘name’, ‘Alice’], [‘age’, 30]]);

// 迭代键值对
for (const [key, value] of map) {
console.log(${key}: ${value});
}

// 迭代键
for (const key of map.keys()) {
console.log(key);
}

// 迭代值
for (const value of map.values()) {
console.log(value);
}
“`

无原型链干扰 (No Prototype Chain Interference)

普通对象继承自 Object.prototype,这意味着即使你没有显式定义,对象也可能有一些默认属性(如 toString, hasOwnProperty 等)。当你使用对象作为纯粹的键值对查找表时,这可能会导致意外的行为或需要使用 hasOwnProperty 等方法进行检查。Map 没有这样的原型链,它的键值对只包含你显式添加的数据,这使得 Map 作为集合使用时更加纯粹和安全。

“`javascript
const obj = {};
console.log(obj.toString); // function toString()… (来自原型链)

const map = new Map();
console.log(map.toString); // function toString() { [native code] } (Map 自己的 toString 方法)
console.log(map.hasOwnProperty); // undefined (Map 实例上没有这个属性,也不会从原型链继承同名、可能导致混淆的属性)
// 在 Map 中,你不用担心你 set 的键会与原型链上的属性冲突。
“`

性能考量 (Performance Considerations)

对于大量元素的频繁添加和删除操作,Map 通常比使用对象作为哈希表更高效。JavaScript 引擎在内部对 Map 进行了优化,使其在动态增删元素时表现更好。然而,对于简单的、静态的字符串键查找,普通对象的性能可能也非常高,甚至在某些特定场景下略优,但这通常不是选择 Map 的主要原因。Map 的优势在于其灵活性和特性集。

综合来看,Map 在需要使用非字符串/Symbol 键、需要保证迭代顺序、频繁增删元素或需要避免原型链干扰的场景下,是比普通对象更优的选择。

3. 创建 Map

创建 Map 主要有两种方式:使用构造函数 new Map()

创建空 Map

最简单的方式是直接调用 Map 构造函数,创建一个空的 Map。

javascript
const emptyMap = new Map();
console.log(emptyMap); // Map {}
console.log(emptyMap.size); // 0

使用可迭代对象初始化 Map

Map 构造函数还可以接受一个可选的参数,该参数必须是一个可迭代对象(如数组),其元素是表示键值对的 [key, value] 数组。

“`javascript
// 使用数组初始化 Map
const initialData = [
[‘name’, ‘Bob’],
[‘age’, 25],
[‘city’, ‘New York’]
];
const myMap = new Map(initialData);
console.log(myMap);
// Map { ‘name’ => ‘Bob’, ‘age’ => 25, ‘city’ => ‘New York’ }
console.log(myMap.size); // 3

// 也可以使用其他可迭代对象,例如另一个 Map
const anotherMap = new Map(myMap);
console.log(anotherMap);
// Map { ‘name’ => ‘Bob’, ‘age’ => 25, ‘city’ => ‘New York’ }

// 使用由 Object.entries() 生成的数组初始化 Map (将对象转换为 Map)
const objToMap = { id: 123, status: ‘active’ };
const mapFromObject = new Map(Object.entries(objToMap));
console.log(mapFromObject);
// Map { ‘id’ => 123, ‘status’ => ‘active’ }
``
这种方式非常方便,特别是当你已经有一个包含键值对数组的数据源,或者想将一个普通对象快速转换为 Map 时(借助于
Object.entries()` 方法)。

4. Map 的基本操作:增、删、查、改

Map 提供了直观的方法来执行常见的集合操作。

添加或更新元素 (set(key, value))

使用 set(key, value) 方法可以将一个键值对添加到 Map 中。
如果 key 已经存在于 Map 中,set() 方法会更新该键对应的值。
set() 方法返回 Map 实例本身,这意味着你可以链式调用 set()

“`javascript
const map = new Map();

map.set(‘greeting’, ‘Hello’); // 添加一个字符串键值对
map.set(1, ‘One’); // 添加一个数字键值对
map.set(true, ‘Boolean key’); // 添加一个布尔键值对

const objKey = {};
map.set(objKey, ‘Value associated with object’); // 添加一个对象键值对

console.log(map);
// Map {
// ‘greeting’ => ‘Hello’,
// 1 => ‘One’,
// true => ‘Boolean key’,
// {} => ‘Value associated with object’
// }

// 更新一个已存在的键
map.set(‘greeting’, ‘Hi’);
console.log(map.get(‘greeting’)); // ‘Hi’

// 链式调用
map.set(‘a’, 1).set(‘b’, 2).set(‘c’, 3);
console.log(map);
// Map {
// ‘greeting’ => ‘Hi’,
// 1 => ‘One’,
// true => ‘Boolean key’,
// {} => ‘Value associated with object’,
// ‘a’ => 1,
// ‘b’ => 2,
// ‘c’ => 3
// }
“`

获取元素 (get(key))

使用 get(key) 方法可以根据键获取 Map 中对应的值。
如果 Map 中不存在该键,get() 方法返回 undefined

“`javascript
const map = new Map([
[‘name’, ‘Alice’],
[‘age’, 30]
]);

console.log(map.get(‘name’)); // ‘Alice’
console.log(map.get(‘age’)); // 30
console.log(map.get(‘city’)); // undefined (键不存在)

const objKey = {};
map.set(objKey, ‘data’);
console.log(map.get(objKey)); // ‘data’

const anotherObj = {};
console.log(map.get(anotherObj)); // undefined (虽然内容相同,但这是另一个对象引用)
“`

检查元素是否存在 (has(key))

使用 has(key) 方法可以检查 Map 中是否存在指定的键。
它返回一个布尔值:如果存在键则返回 true,否则返回 false
使用 has() 方法来检查键是否存在比使用 get(key) !== undefined 更好,因为 undefined 可能是一个合法存储在 Map 中的值。

“`javascript
const map = new Map([
[‘name’, ‘Alice’],
[‘age’, 30],
[‘occupation’, undefined] // 存储一个 undefined 值
]);

console.log(map.has(‘name’)); // true
console.log(map.has(‘city’)); // false
console.log(map.has(‘occupation’)); // true (键存在,值是 undefined)

// 不好的做法:
console.log(map.get(‘occupation’) !== undefined); // false (错误判断为不存在)
console.log(map.get(‘city’) !== undefined); // false (正确判断为不存在)
// 好的做法:
console.log(map.has(‘occupation’)); // true (正确判断为存在)
console.log(map.has(‘city’)); // false (正确判断为不存在)
“`

删除元素 (delete(key))

使用 delete(key) 方法可以从 Map 中移除指定键的元素。
如果成功删除了元素,该方法返回 true;如果 Map 中不存在该键,则返回 false

“`javascript
const map = new Map([
[‘a’, 1],
[‘b’, 2],
[‘c’, 3]
]);

console.log(map.delete(‘b’)); // true (成功删除)
console.log(map); // Map { ‘a’ => 1, ‘c’ => 3 }
console.log(map.size); // 2

console.log(map.delete(‘d’)); // false (键不存在,删除失败)
console.log(map); // Map { ‘a’ => 1, ‘c’ => 3 }
console.log(map.size); // 2
“`

清空 Map (clear())

使用 clear() 方法可以移除 Map 中的所有元素,使其变为空 Map。该方法没有返回值(或者说返回 undefined)。

“`javascript
const map = new Map([
[‘a’, 1],
[‘b’, 2]
]);

console.log(map.size); // 2

map.clear();

console.log(map); // Map {}
console.log(map.size); // 0
“`

5. 获取 Map 的大小

使用 size 属性可以非常方便地获取 Map 中元素的数量。

“`javascript
const map = new Map();
console.log(map.size); // 0

map.set(‘x’, 1).set(‘y’, 2).set(‘z’, 3);
console.log(map.size); // 3

map.delete(‘y’);
console.log(map.size); // 2

map.clear();
console.log(map.size); // 0
“`
正如前面提到的,这是 Map 相较于计算对象属性数量的一个优势。

6. 遍历 Map

Map 提供了多种灵活的迭代方式,包括 for...of 循环和 forEach 方法,并且可以分别获取键、值或键值对。

使用 for...of 循环

Map 实例本身是可迭代的,默认情况下,for...of 循环会迭代 Map 的键值对,每个迭代项是一个 [key, value] 数组。这使得使用解构赋值非常方便。

“`javascript
const map = new Map([
[‘name’, ‘Alice’],
[‘age’, 30],
[‘city’, ‘Paris’]
]);

// 默认迭代 (键值对)
console.log(“— Iterating [key, value] pairs —“);
for (const [key, value] of map) {
console.log(${key}: ${value});
}
// 输出:
// — Iterating [key, value] pairs —
// name: Alice
// age: 30
// city: Paris
“`

Map 还提供了专门的迭代器方法:

  • map.keys(): 返回一个迭代器,用于遍历 Map 中的所有
  • map.values(): 返回一个迭代器,用于遍历 Map 中的所有
  • map.entries(): 返回一个迭代器,用于遍历 Map 中的所有键值对(与默认迭代相同)。

你可以结合 for...of 循环使用这些方法:

“`javascript
const map = new Map([
[‘name’, ‘Alice’],
[‘age’, 30]
]);

// 遍历键
console.log(“\n— Iterating keys —“);
for (const key of map.keys()) {
console.log(key);
}
// 输出:
// — Iterating keys —
// name
// age

// 遍历值
console.log(“\n— Iterating values —“);
for (const value of map.values()) {
console.log(value);
}
// 输出:
// — Iterating values —
// Alice
// 30

// 遍历键值对 (与默认迭代相同)
console.log(“\n— Iterating entries —“);
for (const entry of map.entries()) {
console.log(entry); // entry 是一个 [key, value] 数组
console.log(Key: ${entry[0]}, Value: ${entry[1]});
}
// 输出:
// — Iterating entries —
// [ ‘name’, ‘Alice’ ]
// Key: name, Value: Alice
// [ ‘age’, 30 ]
// Key: age, Value: 30
“`

使用 forEach() 方法

Map 也提供了 forEach() 方法,类似于数组的 forEach。它接受一个回调函数作为参数,该函数会对 Map 中的每个键值对执行一次。

回调函数接收三个参数:
1. value: 当前迭代项的值。
2. key: 当前迭代项的键。
3. map: 调用 forEach 的 Map 对象本身。

forEach() 方法还支持一个可选的第二个参数 thisArg,用于指定回调函数内部 this 的值。

“`javascript
const map = new Map([
[‘name’, ‘Bob’],
[‘location’, ‘London’]
]);

console.log(“\n— Using forEach —“);
map.forEach((value, key, callingMap) => {
console.log(Key: ${key}, Value: ${value});
console.log(Is this the same map? ${callingMap === map});
});
// 输出:
// — Using forEach —
// Key: name, Value: Bob
// Is this the same map? true
// Key: location, Value: London
// Is this the same map? true

// 使用 thisArg
const context = { prefix: ‘Data:’ };
map.forEach(function(value, key) {
console.log(${this.prefix} ${key} = ${value});
}, context);
// 输出:
// Data: name = Bob
// Data: location = London
``forEach` 方法在需要对 Map 中的每个元素执行一个特定操作时非常有用,特别是当这个操作不需要中断或者不需要提前退出循环时。

7. Map 与 Object 的区别与选择

深入理解 Map 和 Object 的区别是何时使用它们的关键。虽然它们都可以存储键值对,但它们的设计目的和特点有所不同。

特性 JavaScript Map JavaScript Object (用作键值对)
键的类型 可以是任何数据类型(原始值、对象、函数等) 主要是字符串或 Symbol 类型(其他类型会被转换)
键的顺序 保证按照插入顺序迭代 对于字符串键在 ES2015+ 通常保持插入顺序,Symbol 键无保证;早期版本无保证
获取大小 直接使用 .size 属性 (O(1)) 需要 Object.keys(obj).length 等 (需要额外步骤)
迭代方式 可迭代 (for...of), .keys(), .values(), .entries(), .forEach() 需要 .keys(), .values(), .entries() 配合迭代器/for...of, for...in (包含原型)
原型链 无原型链干扰,纯净的键值对 继承自 Object.prototype, 可能有默认属性需要注意
性能 对于频繁增删操作通常更优化 对于静态、简单的字符串键查找可能非常快,动态增删相对 Map 慢
JSON 序列化 不能直接使用 JSON.stringify() 序列化为 JSON 可以轻松序列化为 JSON

键的类型

  • Map: 这是最大的区别。Map 可以使用任何类型作为键,尤其是对象。两个不同的对象字面量 {} 在 Map 中是两个不同的键,即使它们看起来一样。
  • Object: 如果使用非字符串或 Symbol 作为对象键,它们会被转换为字符串。数字键也会被转换为字符串。这导致了前面例子中 {}{} 作为键会互相覆盖的问题。

键的顺序

  • Map: 迭代顺序就是插入顺序。可靠且一致。
  • Object: 对于字符串键,ES2015 后规范规定了顺序(先数字升序,然后创建顺序)。但 Symbol 键没有保证顺序,并且 for...in 遍历顺序更复杂(取决于实现和属性类型)。Map 的顺序保证更简单可靠。

获取大小

  • Map: map.size 属性直观且高效。
  • Object: 需要 Object.keys(obj).length 或类似方法,不够直接。

迭代方式

  • Map: 设计上就是为了方便迭代,提供了多种视图(键、值、键值对)和标准的迭代协议。
  • Object: 迭代不如 Map 直接。for...in 会遍历原型链上的可枚举属性(通常需要 hasOwnProperty 检查)。需要 Object.keys(), Object.values(), Object.entries() 来获取可迭代的数组进行遍历。

原型链

  • Map: 简洁干净,没有继承自原型的属性,不会与你的键冲突。
  • Object: 继承自 Object.prototype。这可能需要在使用对象作为通用键值对存储时额外小心,比如使用 hasOwnProperty 或创建空对象时不继承原型 (Object.create(null))。

性能考量

  • Map: 在需要频繁添加和删除大量键值对的场景下,Map 的性能通常优于 Object。
  • Object: 对于简单的静态结构的查找,或者当键已知且稳定时,Object 的属性访问通常非常快。

JSON 序列化

  • Map: JSON.stringify() 不会自动将 Map 转换为 JSON 对象。你需要手动将其转换为一个数组或对象,然后再进行序列化。
  • Object: 普通对象(不包含循环引用等)可以直接被 JSON.stringify() 序列化为 JSON 对象,这是其作为数据交换格式的天然优势。

“`javascript
const map = new Map([[‘a’, 1], [‘b’, 2]]);
const obj = { a: 1, b: 2 };

console.log(JSON.stringify(obj)); // ‘{“a”:1,”b”:2}’
console.log(JSON.stringify(map)); // {} (一个空对象)

// Map 转换为 JSON 的示例
const mapToJson = Object.fromEntries(map); // 需要 ES2017+
console.log(JSON.stringify(mapToJson)); // ‘{“a”:1,”b”:2}’

// 或者使用 spread syntax (ES2015+) 和 Array.from
const mapToArray = Array.from(map); // [[ ‘a’, 1 ], [ ‘b’, 2 ]]
const mapToArrayJson = JSON.stringify(mapToArray);
console.log(mapToArrayJson); // ‘[[“a”,1],[“b”,2]]’

// 从 JSON 恢复 Map
const jsonToMap = new Map(Object.entries(JSON.parse(‘{“a”:1,”b”:2}’)));
console.log(jsonToMap); // Map { ‘a’ => 1, ‘b’ => 2 }
“`

何时使用 Map?

基于以上区别,以下场景更适合使用 Map:

  • 你需要使用非字符串或 Symbol 类型作为键(尤其是对象、函数、DOM 元素等)。
  • 你需要依赖键值对的插入顺序进行迭代。
  • 你需要频繁地添加和删除键值对。
  • 你需要方便地获取集合中元素的数量。
  • 你需要一个纯净的键值对存储,不受原型链干扰。
  • 你存储的数据是动态的,键的集合不是固定的或预先知道的。

何时使用 Object?

以下场景更适合使用 Object:

  • 你的所有键都是字符串或 Symbol,且无需使用对象等作为键。
  • 你主要将对象用作一个记录(record)或结构体(struct),其中属性名是固定的或在代码中明确知道。
  • 你需要方便地使用点语法 (obj.key) 或方括号语法 (obj['key']) 进行访问(尽管 Map 也有类似语法,但点语法对非字符串键无效)。
  • 你需要将数据结构直接序列化为 JSON。
  • 你不需要保证键的插入顺序进行迭代(或者只关心字符串键的顺序,且现代 JS 引擎的行为可以接受)。
  • 你只需要简单的键值对存储,不涉及复杂的增删或使用非标准键。

总的来说,Map 提供了更强大的功能和更好的行为保证,特别是在处理动态或复杂键的场景下。Object 则更适合静态结构或简单的字符串键存储,并且在 JSON 序列化方面有优势。

8. Map 的一些进阶概念

键的相等性比较 (Key Equality)

Map 在比较键时使用的是 SameValueZero 算法。这个算法的特点是:

  • 除了 +0-0 被视为相等外,其他与 SameValue 算法(用于 Object.is())相同。
  • NaN 被视为与 NaN 相等。这与严格相等 (===) 不同,在严格相等中 NaN === NaNfalse

这意味着:

“`javascript
const map = new Map();

map.set(NaN, ‘Not a Number’);
console.log(map.get(NaN)); // ‘Not a Number’ (Map 认为 NaN 与 NaN 是同一个键)

map.set(0, ‘Zero’);
map.set(-0, ‘Negative Zero’);
console.log(map.get(0)); // ‘Negative Zero’ (Map 认为 0 和 -0 是同一个键,后设置的覆盖前设置的)
console.log(map.get(-0)); // ‘Negative Zero’

// 对象作为键的相等性比较:
const obj1 = {};
const obj2 = {};
map.set(obj1, ‘Object 1’);
console.log(map.get(obj1)); // ‘Object 1’
console.log(map.get(obj2)); // undefined (obj1 和 obj2 是不同的对象引用)
map.set({}, ‘New Object’); // 新的对象引用作为一个新键
console.log(map.size); // 3 (NaN, 0/-0, obj1, 新的 {})
“`
理解 SameValueZero 对于使用数字(尤其是 0 和 NaN)以及对象作为键时非常重要。对于对象,相等性是基于引用的,而不是基于内容的。

对象作为键的注意事项

当你使用对象作为 Map 的键时,Map 存储的是对该对象的引用。只有当你使用完全相同的对象引用作为参数调用 get(), has(), delete() 时,才能匹配到对应的键。

“`javascript
const map = new Map();

const user1 = { id: 1 };
const user2 = { id: 2 };
const user1Copy = { id: 1 }; // 这是一个新的对象引用

map.set(user1, ‘Data for user 1’);
map.set(user2, ‘Data for user 2’);

console.log(map.get(user1)); // ‘Data for user 1’
console.log(map.get(user2)); // ‘Data for user 2’
console.log(map.get(user1Copy)); // undefined (user1Copy 是不同的引用)
console.log(map.has({ id: 1 })); // false (一个新的对象引用)

// 如果你有一个对象的引用,并且该对象已经被用作 Map 的键,
// 那么你可以通过这个引用来操作 Map 中的对应项。
const existingUser = user1; // 引用 user1 的同一个对象
console.log(map.get(existingUser)); // ‘Data for user 1’
“`
这强调了 Map 使用对象键时是基于引用的相等性,而不是内容的相等性。

9. WeakMap 简介 (与 Map 的关联)

Map 密切相关的还有 WeakMap。虽然本指南主要关注 Map,但简单了解 WeakMap 可以帮助你更全面地理解 JavaScript 的键值对集合。

什么是 WeakMap?

WeakMap 也是一个键值对集合,但它有几个关键的区别:

  1. 键必须是对象: WeakMap 的键只能是对象(包括函数、数组等),不能是原始值。
  2. 弱引用: WeakMap 的键是弱引用(weakly referenced)。这意味着如果一个对象只作为 WeakMap 的键被引用,而没有其他地方对它有强的引用,那么垃圾回收机制就可以回收这个对象,同时 WeakMap 中对应的键值对也会被移除。

WeakMap 的特点与用途

  • 防止内存泄漏: 这是 WeakMap 最主要的用途。当你需要将一些数据附加到对象上,但又不想阻止这些对象被垃圾回收时,可以使用 WeakMap。例如,将一些元数据关联到 DOM 元素上。当 DOM 元素从文档中移除并被垃圾回收时,WeakMap 中对应的条目也会自动清理。
  • 不可迭代: WeakMap 没有 size 属性,也不支持迭代(如 for...offorEach)。这是因为其弱引用的特性,集合的大小和内容可能会随时因为垃圾回收而变化,提供这些功能没有太大意义。
  • 只有 get, set, has, delete 方法: WeakMap 只提供这四个基本方法,不能遍历或清空整个集合。

“`javascript
let obj = {};
const weakMap = new WeakMap();

weakMap.set(obj, ‘some data’);

console.log(weakMap.has(obj)); // true
console.log(weakMap.get(obj)); // ‘some data’

// 如果我们将对 obj 的强引用移除
obj = null;

// 此时,如果垃圾回收发生,原来的 {} 对象可能会被回收,
// WeakMap 中对应的条目也会被自动移除。
// 在实际代码中,这可能需要一些时间,并且我们无法直接检查 WeakMap 的内容。
// 但在概念上,当 obj 被回收后,weakMap.has(obj) 将变为 false。
``
总之,
WeakMapMap的一个变种,专门用于解决对象作为键时的内存管理问题,牺牲了可迭代性和获取大小的能力来换取弱引用特性。如果你只是需要一个通用的键值对存储,优先考虑使用Map`。

10. 实际应用场景举例

了解了 Map 的特性和优势后,我们来看几个实际的应用场景:

数据缓存 (Caching)

当你有一个计算成本较高的函数,并且你可能会用相同的对象或复杂参数多次调用它时,可以使用 Map 来缓存结果。

“`javascript
const cache = new Map();

function processData(dataObject) {
// 检查缓存
if (cache.has(dataObject)) {
console.log(“Fetching from cache…”);
return cache.get(dataObject);
}

console.log(“Processing data…”);
// 模拟耗时计算
const result = JSON.stringify(dataObject).toUpperCase();

// 缓存结果,使用对象本身作为键
cache.set(dataObject, result);

return result;
}

const myData1 = { id: 1, value: ‘abc’ };
const myData2 = { id: 2, value: ‘xyz’ };
const myData1Copy = { id: 1, value: ‘abc’ }; // 内容相同,但引用不同

console.log(processData(myData1)); // Processing data…, 返回结果并缓存
console.log(processData(myData1)); // Fetching from cache…, 从缓存获取
console.log(processData(myData2)); // Processing data…, 返回结果并缓存
console.log(processData(myData1Copy)); // Processing data…, myData1Copy 是新键,重新计算并缓存
``
这里使用 Map 可以非常方便地将计算结果与输入的**具体对象实例**关联起来,而不是依赖于对象的字符串表示。如果使用普通对象,
{id: 1, value: ‘abc’}{id: 1, value: ‘abc’}` 作为键会被转换为同一个字符串,无法区分。

DOM 元素关联数据 (Associating Data with DOM Elements)

有时你需要将一些非标准数据附加到特定的 DOM 元素上,但又不想修改元素的 DOM 属性(可能导致命名冲突或不是标准用法)。Map 是一个很好的选择。如果你担心内存泄漏(当元素从 DOM 中移除时),WeakMap 可能是更好的选择。

“`javascript
const elementData = new Map(); // Or WeakMap if preferred

const myButton = document.getElementById(‘myButton’);
const myDiv = document.getElementById(‘myDiv’);

elementData.set(myButton, { clickCount: 0 });
elementData.set(myDiv, { visible: true });

// 在事件处理器中访问关联数据
myButton.addEventListener(‘click’, () => {
const data = elementData.get(myButton);
data.clickCount++;
console.log(Button clicked ${data.clickCount} times);
});

// 检查一个元素是否有关联数据
if (elementData.has(myButton)) {
console.log(‘myButton has associated data.’);
}
“`
这里,Map 的优势在于可以使用实际的 DOM 元素对象作为键,而不是依赖于元素的 ID 或其他属性。

计数器或频率统计 (Counters or Frequency Maps)

统计数组中元素的频率,特别是当元素可能是对象或混合类型时,Map 很有用。

“`javascript
const items = [‘apple’, ‘banana’, ‘apple’, ‘orange’, {}, {}, ‘banana’];
const frequency = new Map();

for (const item of items) {
// 使用 has() 检查键是否存在
if (frequency.has(item)) {
frequency.set(item, frequency.get(item) + 1);
} else {
frequency.set(item, 1);
}
}

console.log(frequency);
// Map {
// ‘apple’ => 2,
// ‘banana’ => 2,
// ‘orange’ => 1,
// {} => 1, // 第一个 {}
// {} => 1 // 第二个 {} (不同的对象引用)
// }

console.log(frequency.get(‘apple’)); // 2
console.log(frequency.get({})); // undefined (如果直接查找一个新的 {} 对象)
“`
这个例子再次展示了 Map 如何将不同的对象实例识别为不同的键,这在统计包含对象的集合时非常重要。

11. 总结

JavaScript Map 是一个强大且灵活的内置数据结构,它提供了比传统对象更优越的键值对存储能力,尤其是在以下方面:

  • 任意类型的键: 允许使用原始值、对象、函数等任何类型作为键,解决了对象键只能是字符串/Symbol 的限制。
  • 保证顺序: 迭代顺序严格按照元素的插入顺序。
  • 直接大小获取: .size 属性提供了高效的大小查询。
  • 清晰的迭代: 提供多种迭代器和 forEach 方法,方便遍历键、值或键值对。
  • 纯净的集合: 不受原型链干扰。

理解 Map 和 Object 的区别,以及各自适用的场景,是写出更健壮、更易维护的 JavaScript 代码的关键。当你需要一个通用的、键类型灵活、需要保持插入顺序或频繁增删元素的键值对集合时,Map 往往是比普通对象更好的选择。而当你处理固定结构的、需要 JSON 序列化的、或所有键都是简单字符串的场景时,普通对象可能更简单直观。

掌握 Map 的创建、基本操作和迭代方法,将极大地丰富你在 JavaScript 中处理数据集合的能力。现在,你可以尝试在你的代码中应用 Map,体验它带来的便利和强大之处!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部