如何使用 TypeScript Map:创建、操作与遍历 – wiki基地


TypeScript Map 深度指南:创建、操作与遍历

引言

在现代 JavaScript 和 TypeScript 开发中,我们经常需要存储和管理键值对数据。最常见的工具是 JavaScript 的原生对象 ({})。然而,随着应用复杂度的增加,原生对象的局限性也逐渐显现,特别是在键的类型、迭代顺序以及性能方面。为了解决这些问题,ECMAScript 6 (ES6) 引入了 Map 数据结构,它提供了一种更强大、更灵活的方式来处理键值对集合。

TypeScript 作为 JavaScript 的超集,自然也完全支持 Map,并且通过其强大的类型系统,为 Map 的使用带来了额外的类型安全保障。本文将深入探讨 TypeScript 中的 Map,包括如何创建它,如何执行各种操作(添加、获取、修改、删除、检查、清空),以及如何高效地遍历其中的数据。我们将详细对比 Map 与原生对象的区别,并展示 TypeScript 的类型系统如何增强 Map 的可用性。

第一章:认识 Map —— 为什么选择 Map?

在深入技术细节之前,我们首先要理解 Map 是什么,以及它为何成为处理键值对的有力工具,尤其是在与原生对象进行对比时。

什么是 Map?

Map 是一种键值对的集合,其中的键可以是任意类型(包括对象、函数、nullundefined,甚至 NaN),而不仅仅是字符串或 Symbol。这与原生对象形成鲜明对比,原生对象的键会被强制转换为字符串(或 Symbol)。Map 维护键值对的插入顺序,这意味着当你遍历 Map 时,元素的顺序会按照它们被添加到 Map 中的顺序出现。

Map 与原生对象的区别

理解 Map 的价值,最好是通过与原生对象的对比:

  1. 键的类型 (Key Types):

    • 原生对象 ({}): 键只能是字符串或 Symbol。如果尝试使用其他类型作为键,它会被自动转换为字符串(例如,数字 1 会变成字符串 "1",对象 {} 会变成字符串 "[object Object]")。这意味着 { 1: 'value' }{ '1': 'value' } 是同一个键,并且 { {}: 'value1' }{ {}: 'value2' } 虽然使用了不同的对象实例作为键,但都会因为被转换为 "[object Object]" 字符串而覆盖前一个值。
    • Map: 键可以是任意类型。数字 1 和字符串 "1" 是不同的键。不同的对象实例,即使结构相同,也会被视为不同的键(基于引用相等性)。这使得 Map 在需要使用非字符串标识符作为键的场景下非常有用。
  2. 迭代顺序 (Iteration Order):

    • 原生对象 ({}): 历史上,原生对象的迭代顺序是不保证的(尽管现代 JavaScript 引擎为数字键和字符串键引入了一些规则,但总体来说,其迭代顺序不如 Map 可预测和稳定)。
    • Map: Map 保证按照键值对被插入的顺序进行迭代。这对于需要保持数据顺序的应用场景至关重要。
  3. 尺寸获取 (Getting Size):

    • 原生对象 ({}): 获取键值对的数量需要通过 Object.keys(obj).lengthObject.values(obj).lengthObject.entries(obj).length 来计算,或者手动维护一个计数器。这并不是一个直接属性。
    • Map: Map 有一个内置的 size 属性,可以直接获取键值对的数量,这是一个 O(1) 操作,非常高效。
  4. 原型链 (Prototype Chain):

    • 原生对象 ({}): 创建的对象会继承自 Object.prototype。这意味着对象可能包含一些来自原型链的默认属性(例如 toStringhasOwnProperty 等)。在使用 for...in 循环或直接访问属性时,需要注意这些继承属性,可能需要使用 hasOwnProperty 进行过滤。
    • Map: Map 实例不继承自任何原型对象,它是一个纯粹的键值对集合。这避免了原型链带来的潜在干扰。
  5. 性能 (Performance):

    • 对于频繁的添加和删除操作,Map 通常比原生对象表现更好,尤其是在处理大量键值对时。原生对象在键的查找和管理上可能会随着属性数量的增加而性能下降。
    • Mapsetgethasdelete 方法在大多数情况下具有接近 O(1) 的平均时间复杂度(取决于引擎实现和键的类型,特别是对象键的哈希)。

总结 Map 的优势

  • 灵活的键类型: 几乎任何值都可以作为键。
  • 可预测的迭代顺序: 按照插入顺序进行迭代。
  • 方便的尺寸获取: size 属性直接提供键值对数量。
  • 纯净的实例: 不受原型链干扰。
  • 更好的某些操作性能: 适用于频繁的添加/删除操作。

基于这些优势,Map 在许多场景下是比原生对象更合适的选择,例如缓存、存储与特定对象关联的数据、需要维护插入顺序的集合等。

第二章:创建 Map —— 构建你的键值对集合

在 TypeScript 中创建 Map 非常简单,并且可以通过泛型来指定键和值的类型,从而增强类型安全。

基本创建方式

创建一个空的 Map

typescript
// 创建一个键为 string,值为 number 的空 Map
const myMap = new Map<string, number>();
console.log(myMap); // 输出: Map(0) {}
console.log(myMap.size); // 输出: 0

在 TypeScript 中,我们通常建议使用泛型 <K, V> 来明确指定 Map 的键类型 (K) 和值类型 (V)。这使得编译器能够在开发阶段检查你的代码,确保你使用了正确的键类型来获取或设置值,并知道 get 方法返回的值的类型。

如果不指定泛型类型,TypeScript 会尽力推断,或者默认为 Map<any, any>,这会丧失一部分类型安全的好处。

“`typescript
// 没有指定泛型,TypeScript 推断为 Map (基于后续操作)
// 但如果创建时没有操作,可能推断为 Map
const anotherMap = new Map();

// 尝试添加不同类型的键值对
anotherMap.set(‘name’, ‘Alice’); // Map
anotherMap.set(123, true); // Map
// anotherMap.set({ id: 1 }, [1, 2]); // Map — 推断变得复杂且不精确
“`

明确指定泛型是最佳实践:

“`typescript
interface User {
id: number;
name: string;
}

// 创建一个键是 User 对象,值是其 id 的 Map
const userMap = new Map();

// 创建一个键是 string,值是任意类型数组的 Map
const dataMap = new Map();
“`

使用迭代器初始化 Map

Map 的构造函数还可以接受一个可迭代对象(Iterable),该对象包含形如 [key, value] 的二元数组元素。这非常方便用于从现有数据结构(如数组或另一个 Map)初始化一个 Map

常见的可迭代对象包括:

  • 二维数组 ([[k1, v1], [k2, v2], ...])
  • 另一个 Map 实例
  • Set 实例的 entries() 方法等

示例:使用二维数组初始化

“`typescript
// 使用二维字符串数组初始化 Map
const cityMap = new Map([
[‘Beijing’, ‘China’],
[‘Tokyo’, ‘Japan’],
[‘Paris’, ‘France’]
]);

console.log(cityMap); // 输出: Map(3) { ‘Beijing’ => ‘China’, ‘Tokyo’ => ‘Japan’, ‘Paris’ => ‘France’ }
console.log(cityMap.size); // 输出: 3
“`

示例:从另一个 Map 初始化

“`typescript
const initialMap = new Map([
[1, ‘Apple’],
[2, ‘Banana’]
]);

// 创建一个 newMap,内容与 initialMap 相同
const newMap = new Map(initialMap);
console.log(newMap); // 输出: Map(2) { 1 => ‘Apple’, 2 => ‘Banana’ }
“`

使用迭代器初始化 Map 时,TypeScript 会根据传入的迭代器元素的类型来推断 Map 的泛型类型,或者你可以显式指定泛型来覆盖推断。

“`typescript
// TypeScript 可以推断出 Map
const ageMap = new Map([
[‘Alice’, 30],
[‘Bob’, 25]
]);

// 显式指定类型,即使初始化数据类型不同,也会报错
// const errorMap = new Map([
// [‘Alice’, 30],
// [‘Bob’, ‘twenty-five’] // 类型错误!’twenty-five’ 不是 number
// ]);
“`

通过构造函数初始化是创建带有初始数据的 Map 的一种简洁高效的方式。

第三章:操作 Map —— 管理你的键值对

Map 提供了多种方法来方便地操作其中的键值对:添加、获取、检查是否存在、删除和清空。

1. 添加或更新元素:map.set(key, value)

set(key, value) 方法用于向 Map 中添加一个新的键值对,或者在键已经存在时更新其对应的值。该方法返回 Map 实例本身,因此可以进行链式调用。

“`typescript
const studentScores = new Map();

// 添加新的键值对
studentScores.set(‘Alice’, 95);
studentScores.set(‘Bob’, 88);

console.log(studentScores); // 输出: Map(2) { ‘Alice’ => 95, ‘Bob’ => 88 }

// 更新现有键的值
studentScores.set(‘Alice’, 98); // Alice 的分数被更新
console.log(studentScores); // 输出: Map(2) { ‘Alice’ => 98, ‘Bob’ => 88 }

// 使用不同类型的键
const userObj = { id: 101, name: ‘Charlie’ };
const projectMap = new Map(); // 使用对象作为键
projectMap.set(userObj, ‘Project Alpha’);
projectMap.set({ id: 102, name: ‘David’ }, ‘Project Beta’); // 注意:这个对象是另一个实例,是不同的键

console.log(projectMap.size); // 输出: 2 (userObj 和 { id: 102, … } 是两个不同的键)

// 链式调用
const chainedMap = new Map();
chainedMap.set(1, ‘one’).set(2, ‘two’).set(3, ‘three’);
console.log(chainedMap); // 输出: Map(3) { 1 => ‘one’, 2 => ‘two’, 3 => ‘three’ }
“`

TypeScript 中的类型安全:

使用 set 方法时,TypeScript 会检查你传入的 keyvalue 的类型是否与 Map 定义的泛型类型兼容。

“`typescript
const numMap = new Map();

numMap.set(1, ‘one’); // OK
// numMap.set(‘2’, ‘two’); // 错误:Type ‘string’ is not assignable to type ‘number’.
// numMap.set(3, 3); // 错误:Type ‘number’ is not assignable to type ‘string’.
“`

2. 获取元素:map.get(key)

get(key) 方法用于获取与指定键关联的值。如果键存在于 Map 中,则返回其对应的值;如果键不存在,则返回 undefined

“`typescript
const fruitColors = new Map([
[‘Apple’, ‘Red’],
[‘Banana’, ‘Yellow’]
]);

console.log(fruitColors.get(‘Apple’)); // 输出: Red
console.log(fruitColors.get(‘Banana’)); // 输出: Yellow
console.log(fruitColors.get(‘Orange’)); // 输出: undefined (键不存在)

// 使用对象作为键的例子
const objKey1 = { id: 1 };
const objKey2 = { id: 2 };
const objMap = new Map();
objMap.set(objKey1, ‘Value 1’);

console.log(objMap.get(objKey1)); // 输出: Value 1 (使用同一个对象引用获取)
console.log(objMap.get({ id: 1 })); // 输出: undefined (这是另一个不同的对象实例)
console.log(objMap.get(objKey2)); // 输出: undefined (这个键从未被设置)
“`

TypeScript 中的类型安全:

get 方法的返回类型是 V | undefined,其中 VMap 定义的值类型。这提醒开发者在使用 get 的结果时,需要考虑键可能不存在的情况。

“`typescript
const productPrices = new Map([
[‘Laptop’, 1200],
[‘Mouse’, 25]
]);

const laptopPrice: number | undefined = productPrices.get(‘Laptop’);
const keyboardPrice: number | undefined = productPrices.get(‘Keyboard’);

// 可以使用类型守卫来处理 undefined
if (laptopPrice !== undefined) {
console.log(Laptop price: $${laptopPrice});
} else {
console.log(‘Laptop price not found.’);
}

// keyboardPrice 是 undefined,这里不会进入 if 块
if (keyboardPrice !== undefined) {
console.log(Keyboard price: $${keyboardPrice});
} else {
console.log(‘Keyboard price not found.’);
}
“`

3. 检查元素是否存在:map.has(key)

has(key) 方法用于检查 Map 中是否存在指定的键。它返回一个布尔值:如果键存在则返回 true,否则返回 false

“`typescript
const userRoles = new Map([
[‘Alice’, ‘Admin’],
[‘Bob’, ‘Editor’]
]);

console.log(userRoles.has(‘Alice’)); // 输出: true
console.log(userRoles.has(‘Charlie’)); // 输出: false

// 使用对象作为键的例子
const settingsMap = new Map();
const configObj = { key: ‘appConfig’ };
settingsMap.set(configObj, { theme: ‘dark’ });

console.log(settingsMap.has(configObj)); // 输出: true (使用同一个对象引用检查)
console.log(settingsMap.has({ key: ‘appConfig’ })); // 输出: false (这是另一个不同的对象实例)
“`

为什么 has()get() !== undefined 更好?

虽然 map.get(key) !== undefined 也能判断键是否存在,但 has() 方法是更推荐的方式,原因如下:

  • 语义清晰: has() 方法明确表达了“检查键是否存在”的意图。
  • 处理 undefined 值: 如果 Map 中某个键的值恰好是 undefinedmap.get(key) 将返回 undefined。此时,map.get(key) !== undefined 将评估为 false,错误地认为键不存在。而 map.has(key) 会正确地返回 true

“`typescript
const undefinedValueMap = new Map();
undefinedValueMap.set(‘keyWithUndefinedValue’, undefined);
undefinedValueMap.set(‘keyWithValue’, ‘some value’);
undefinedValueMap.set(‘nonExistentKey’, undefined); // 这个键不存在,get自然返回undefined

console.log(undefinedValueMap.get(‘keyWithUndefinedValue’)); // 输出: undefined
console.log(undefinedValueMap.has(‘keyWithUndefinedValue’)); // 输出: true (正确判断键存在)

console.log(undefinedValueMap.get(‘nonExistentKey’)); // 输出: undefined
console.log(undefinedValueMap.has(‘nonExistentKey’)); // 输出: false (正确判断键不存在)
“`

因此,总是优先使用 has() 方法来检查键是否存在。

4. 删除元素:map.delete(key)

delete(key) 方法用于从 Map 中移除指定的键及其关联的值。如果删除成功(即键存在并被移除),该方法返回 true;如果键不存在,则返回 false

“`typescript
const inventory = new Map([
[‘Laptop’, 10],
[‘Mouse’, 50],
[‘Keyboard’, 20]
]);

console.log(inventory.size); // 输出: 3

const deletedLaptop = inventory.delete(‘Laptop’); // 删除存在的键
console.log(deletedLaptop); // 输出: true
console.log(inventory.size); // 输出: 2
console.log(inventory); // 输出: Map(2) { ‘Mouse’ => 50, ‘Keyboard’ => 20 }

const deletedMonitor = inventory.delete(‘Monitor’); // 删除不存在的键
console.log(deletedMonitor); // 输出: false
console.log(inventory.size); // 输出: 2
“`

5. 获取 Map 的尺寸:map.size

size 属性(注意,它不是方法)返回 Map 中当前存储的键值对数量。这是一个高效的操作。

“`typescript
const myDataMap = new Map();
console.log(myDataMap.size); // 输出: 0

myDataMap.set(1, ‘a’);
myDataMap.set(2, ‘b’);
console.log(myDataMap.size); // 输出: 2

myDataMap.delete(1);
console.log(myDataMap.size); // 输出: 1
“`

6. 清空 Map:map.clear()

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

“`typescript
const settings = new Map([
[‘darkMode’, true],
[‘notifications’, false]
]);

console.log(settings.size); // 输出: 2
console.log(settings); // 输出: Map(2) { ‘darkMode’ => true, ‘notifications’ => false }

settings.clear(); // 清空 Map

console.log(settings.size); // 输出: 0
console.log(settings); // 输出: Map(0) {}
“`

通过上述方法,我们可以对 Map 中的数据进行全面的管理。

第四章:遍历 Map —— 访问集合中的数据

Map 提供了多种强大的方式来遍历其中的键值对,并且由于 Map 保证按照插入顺序进行迭代,这使得遍历结果非常可预测。

1. 使用 for...of 循环

Map 是一个可迭代对象,可以直接在 for...of 循环中使用。默认情况下,for...of 迭代 Map[key, value] 对。

“`typescript
const productQuantities = new Map([
[‘Laptop’, 10],
[‘Mouse’, 50],
[‘Keyboard’, 20]
]);

console.log(“— Iterating [key, value] pairs —“);
for (const entry of productQuantities) {
// entry 是一个包含 [key, value] 的数组
console.log(entry);
}
// 输出:
// — Iterating [key, value] pairs —
// [ ‘Laptop’, 10 ]
// [ ‘Mouse’, 50 ]
// [ ‘Keyboard’, 20 ]

console.log(“— Iterating with destructuring —“);
for (const [product, quantity] of productQuantities) {
// 使用数组解构直接获取 key 和 value
console.log(${product}: ${quantity});
}
// 输出:
// — Iterating with destructuring —
// Laptop: 10
// Mouse: 50
// Keyboard: 20
“`

使用 map.keys() 遍历键

keys() 方法返回一个迭代器,该迭代器按插入顺序产生 Map 中的所有键。

typescript
console.log("--- Iterating keys ---");
for (const key of productQuantities.keys()) {
console.log(key);
}
// 输出:
// --- Iterating keys ---
// Laptop
// Mouse
// Keyboard

使用 map.values() 遍历值

values() 方法返回一个迭代器,该迭代器按插入顺序产生 Map 中的所有值。

typescript
console.log("--- Iterating values ---");
for (const value of productQuantities.values()) {
console.log(value);
}
// 输出:
// --- Iterating values ---
// 10
// 50
// 20

使用 map.entries() 遍历键值对

entries() 方法返回一个迭代器,该迭代器按插入顺序产生 Map 中的 [key, value] 对。它是 for...of 默认迭代 Map 时内部调用的方法。

typescript
console.log("--- Iterating entries ---");
for (const entry of productQuantities.entries()) {
console.log(entry);
}
// 输出 (与直接使用 for...of 相同):
// --- Iterating entries ---
// [ 'Laptop', 10 ]
// [ 'Mouse', 50 ]
// [ 'Keyboard', 20 ]

2. 使用 map.forEach() 方法

Map 也提供了 forEach 方法,类似于数组的 forEach。它接受一个回调函数,该函数将为 Map 中的每个键值对执行一次。回调函数接收三个参数:valuekeymap 本身。

“`typescript
const userSettings = new Map([
[‘darkMode’, true],
[‘notifications’, false],
[’emailOptIn’, true]
]);

console.log(“— Iterating with forEach —“);
userSettings.forEach((value, key, map) => {
console.log(Key: ${key}, Value: ${value});
// console.log(Map size inside forEach: ${map.size}); // 可以访问原始 Map
});
// 输出:
// — Iterating with forEach —
// Key: darkMode, Value: true
// Key: notifications, Value: false
// Key: emailOptIn, Value: true
“`

forEach 方法还可以接受第二个参数,用于指定回调函数内部 this 的值。

``typescript
const processor = {
process(value: boolean, key: string) {
console.log(
Processing ${key}: ${value} using processor.`);
}
};

console.log(“— Iterating with forEach and ‘this’ context —“);
// 在回调函数中使用 this.process
userSettings.forEach(function(value, key) {
// 注意这里使用 function 关键字以确保 this 绑定
this.process(value, key);
}, processor); // 将 processor 对象作为 this 的上下文传入
// 输出:
// — Iterating with forEach and ‘this’ context —
// Processing darkMode: true using processor.
// Processing notifications: false using processor.
// Processing emailOptIn: true using processor.
“`

选择哪种遍历方式?

  • for...of (默认或 .entries()): 最常用和推荐的方式,结构清晰,适合解构键值对。
  • for...of (.keys().values()): 当你只关心键或值时使用。
  • forEach(): 适合执行带有副作用的操作(如打印、修改外部变量),或者需要访问原始 Map 实例的场景。它不支持 breakcontinue 中断循环。

所有这些遍历方法都严格遵守 Map 的插入顺序。

第五章:Map 的键类型详解

Map 最大的特点之一就是它的键可以是任意类型。这与原生对象只能使用字符串或 Symbol 作为键有着本质的区别。理解不同类型的键在 Map 中的行为至关重要。

1. 基本类型作为键

字符串、数字、布尔值、nullundefined 和 Symbol 都可以作为 Map 的键。它们是基于值相等性来比较的。

“`typescript
const primitiveKeyMap = new Map();

primitiveKeyMap.set(‘name’, ‘Alice’); // 字符串键
primitiveKeyMap.set(123, ‘a number’); // 数字键
primitiveKeyMap.set(true, ‘a boolean’); // 布尔值键
primitiveKeyMap.set(null, ‘a null value’); // null 键
primitiveKeyMap.set(undefined, ‘undefined value’); // undefined 键

const uniqueSymbol = Symbol(‘description’);
primitiveKeyMap.set(uniqueSymbol, ‘a symbol’); // Symbol 键

console.log(primitiveKeyMap.get(‘name’)); // Alice
console.log(primitiveKeyMap.get(123)); // a number
console.log(primitiveKeyMap.get(true)); // a boolean
console.log(primitiveKeyMap.get(null)); // a null value
console.log(primitiveKeyMap.get(undefined)); // undefined value
console.log(primitiveKeyMap.get(uniqueSymbol)); // a symbol

console.log(primitiveKeyMap.has(‘name’)); // true
console.log(primitiveKeyMap.has(999)); // false
“`

特殊的 NaN 键:

MapNaN 有特殊的处理:所有 NaN 值都被视为同一个键。这是为了解决 JavaScript 中 NaN === NaN 始终为 false 的问题。

“`typescript
const nanMap = new Map();

nanMap.set(NaN, ‘Not a Number’);

console.log(nanMap.get(NaN)); // 输出: Not a Number
console.log(nanMap.has(NaN)); // 输出: true

// 另一个 NaN 值也能获取到同一个值
const anotherNaN = 0/0; // anotherNaN 也是 NaN
console.log(nanMap.get(anotherNaN)); // 输出: Not a Number
console.log(nanMap.has(anotherNaN)); // 输出: true
“`

2. 对象和数组作为键

当使用对象(包括数组、函数、普通对象字面量等)作为 Map 的键时,键的比较是基于引用相等性(reference equality),而不是值相等性。这意味着即使两个不同的对象实例具有相同的结构和属性值,它们在 Map 中也会被视为不同的键。

“`typescript
const objectKeyMap = new Map();

const obj1 = { id: 1, name: ‘A’ };
const obj2 = { id: 2, name: ‘B’ };
const obj3 = { id: 1, name: ‘A’ }; // 结构与 obj1 相同,但它是不同的实例

objectKeyMap.set(obj1, ‘Value for obj1’);
objectKeyMap.set(obj2, ‘Value for obj2’);

console.log(objectKeyMap.get(obj1)); // 输出: Value for obj1 (使用 obj1 引用获取)
console.log(objectKeyMap.get(obj2)); // 输出: Value for obj2 (使用 obj2 引用获取)
console.log(objectKeyMap.get(obj3)); // 输出: undefined (obj3 是不同的引用,即使内容一样)

console.log(objectKeyMap.has(obj1)); // true
console.log(objectKeyMap.has(obj3)); // false

// 数组作为键也是基于引用
const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3]; // 与 arr1 内容相同,但不同引用
const arrayKeyMap = new Map(); // 或者 Map()
arrayKeyMap.set(arr1, ‘Array 1 value’);

console.log(arrayKeyMap.get(arr1)); // 输出: Array 1 value
console.log(arrayKeyMap.get(arr2)); // 输出: undefined (arr2 是不同的引用)
“`

这种基于引用的特性使得 Map 非常适合将数据或元信息与特定的对象实例关联起来,例如将 DOM 元素作为键存储其事件监听器,或者将数据模型对象作为键存储其视图状态。

TypeScript 中的对象键类型:

如果你的对象键类型比较具体,最好在泛型中指定,例如 Map<User, number> 而不是 Map<object, number>,这样可以获得更好的类型提示和检查。

“`typescript
interface Settings {
theme: string;
fontSize: number;
}

const elementSettings = new Map();

const header = document.getElementById(‘header’);
const footer = document.getElementById(‘footer’);

if (header) {
elementSettings.set(header, { theme: ‘dark’, fontSize: 16 });
}

if (footer) {
elementSettings.set(footer, { theme: ‘light’, fontSize: 14 });
}

// 获取设置
if (header) {
const headerSettings = elementSettings.get(header); // headerSettings 的类型是 Settings | undefined
if (headerSettings) {
console.log(Header theme: ${headerSettings.theme});
}
}
“`

第六章:Map 与 Plain Object 的对比总结

为了更好地理解何时使用 Map,让我们再次总结它与原生对象 ({}) 在关键方面的区别。

特性 Map Plain Object ({})
键的类型 任意类型 (包括对象、函数、nullNaN) 仅限字符串和 Symbol (其他类型会被转为字符串)
键的比较 基本类型基于值相等,对象基于引用相等 字符串和 Symbol 基于值相等 (其他类型转为字符串后再比较)
迭代顺序 保证按照插入顺序 不保证 (虽然现代 JS 有规则,但不如 Map 可靠)
获取尺寸 map.size (属性,O(1) 效率) 需要计算 (Object.keys().length 等)
原型链 无,纯净的键值对集合 继承自 Object.prototype (可能需要 hasOwnProperty 检查)
常用方法 set(), get(), has(), delete(), clear(), keys(), values(), entries(), forEach() 直接属性访问 (obj[key]), delete obj[key], Object.keys(), Object.values(), Object.entries()
删除性能 通常比对象删除属性更高效 (尤其对于大量属性) 性能可能受属性数量影响
序列化 不能直接 JSON.stringify() 序列化 可以直接 JSON.stringify() 序列化 (但键会被转为字符串)
使用场景 需要使用非字符串键,需要保证迭代顺序,频繁增删,避免原型链干扰 简单的键值对存储,用作结构体或记录 (Record),与 JSON 互操作

何时使用 Map?

  • 当你需要使用非字符串(尤其是对象)作为键时。
  • 当你需要保证键值对的插入顺序进行迭代时。
  • 当你需要频繁添加或删除键值对,并且集合较大时。
  • 当你需要快速获取集合的尺寸时。
  • 当你希望避免原型链对数据的影响时。

何时使用 Plain Object?

  • 当你只需要字符串或 Symbol 作为键时。
  • 当你主要将其用作简单的数据结构或记录,属性名固定且已知时。
  • 当你需要与 JSON 数据进行互操作时(JSON.parseJSON.stringify 直接处理对象)。
  • 当你不需要保证迭代顺序,或者只需要简单的属性访问时。

在 TypeScript 中,使用 Map 并结合泛型可以为你提供更强的类型保证,特别是在处理复杂键类型时。

第七章:TypeScript 如何增强 Map 的使用

TypeScript 通过泛型 <K, V>Map 带来了静态类型检查,这极大地提高了代码的可维护性和健壮性。

1. 明确的键和值类型

创建 Map 时指定泛型,可以明确 Map 存储的键和值应该是什么类型。

“`typescript
// 创建一个键为 number,值为 string 的 Map
const idNameMap = new Map();

// 创建一个键为对象 User,值为对象 Order 的 Map
interface User { id: number; name: string; }
interface Order { orderId: string; amount: number; }
const userOrderMap = new Map();
“`

2. 类型安全的 setget

当你调用 setget 方法时,TypeScript 会检查传入的参数类型是否与 Map 定义的泛型类型匹配。

“`typescript
const typedMap = new Map();

typedMap.set(‘age’, 30); // OK: string key, number value
// typedMap.set(123, 40); // 错误:123 不是 string 类型的键
// typedMap.set(‘score’, ‘excellent’); // 错误:’excellent’ 不是 number 类型的值

const retrievedValue: number | undefined = typedMap.get(‘age’); // 返回类型是 number | undefined

// 错误的使用获取到的值 (如果不知道它是 number)
// const result: string = retrievedValue; // 错误:Type ‘number | undefined’ is not assignable to type ‘string’.

// 正确处理 undefined
if (retrievedValue !== undefined) {
const double = retrievedValue * 2; // OK,因为在 if 块内 TypeScript 知道它是 number
console.log(double);
}
“`

3. 类型安全的迭代

Map 的迭代器方法 (keys(), values(), entries()) 和 forEach 方法的类型签名都利用了泛型,确保你在遍历时能获取到正确类型的键和值。

“`typescript
const typedIterateMap = new Map([
[1, ‘one’],
[2, ‘two’]
]);

// for…of 默认迭代 [key, value]
for (const [id, name] of typedIterateMap) {
const numericId: number = id; // OK, id 是 number
const stringName: string = name; // OK, name 是 string
// const booleanValue: boolean = name; // 错误:string 不能赋值给 boolean
}

// keys() 迭代器
for (const id of typedIterateMap.keys()) {
const numericId: number = id; // OK, id 是 number
}

// values() 迭代器
for (const name of typedIterateMap.values()) {
const stringName: string = name; // OK, name 是 string
}

// forEach
typedIterateMap.forEach((name, id) => {
const numericId: number = id; // OK
const stringName: string = name; // OK
});
“`

通过在 TypeScript 中使用 Map 并充分利用泛型,我们可以在编译时捕获许多潜在的类型错误,使得代码更加健壮和易于维护。

第八章:实际应用场景

Map 的特性使其在多种实际开发场景中非常有用:

  1. 缓存计算结果 (Memoization):
    当一个函数需要频繁计算某个值,而计算过程耗时,且计算结果只依赖于输入参数时,可以使用 Map 来缓存结果。如果输入参数是对象,Map 可以轻松地使用对象引用作为键。

    ``typescript
    // 假设有一个计算量大的函数
    function calculateExpensiveResult(inputObj: { id: number; value: string }): string {
    console.log('Calculating for', inputObj);
    // 模拟耗时计算
    let result = '';
    for (let i = 0; i < 1000000; i++) {
    result += inputObj.value[i % inputObj.value.length];
    }
    return
    Result for ID ${inputObj.id}: ${result.slice(0, 10)}…`;
    }

    const cache = new Map(); // 使用对象作为键

    function memoizedCalculation(inputObj: { id: number; value: string }): string {
    if (cache.has(inputObj)) {
    console.log(‘Cache hit!’);
    return cache.get(inputObj)!; // 知道有这个键,所以 get 不会返回 undefined
    } else {
    const result = calculateExpensiveResult(inputObj);
    cache.set(inputObj, result);
    return result;
    }
    }

    const input1 = { id: 1, value: ‘abcdefg’ };
    const input2 = { id: 2, value: ‘hijklmn’ };
    const input3 = { id: 1, value: ‘abcdefg’ }; // 内容与 input1 相同,但不同实例

    console.log(memoizedCalculation(input1)); // 第一次计算并缓存
    console.log(memoizedCalculation(input1)); // 缓存命中
    console.log(memoizedCalculation(input2)); // 第一次计算并缓存
    console.log(memoizedCalculation(input3)); // 第一次计算并缓存 (input3 是新键)
    console.log(memoizedCalculation(input1)); // 缓存命中

    console.log(‘Cache size:’, cache.size); // 输出: Cache size: 3
    ``
    注意:如果希望内容相同的对象被视为同一个键,你需要自己实现一个哈希函数,将对象转换为一个唯一的字符串作为
    Map的键,但这会丢失一些Map的原生优势。更常见且适合Map` 的是基于引用进行缓存。

  2. 存储与 DOM 元素关联的数据:
    在前端开发中,有时需要将一些额外的数据或状态与特定的 DOM 元素关联起来。使用 Map 可以很方便地以 DOM 元素作为键来存储这些数据,而不会污染 DOM 元素的属性。

    “`typescript
    const elementData = new Map();

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

    if (myButton) {
    elementData.set(myButton, { isActive: false, tooltip: ‘Click me’ });
    }

    if (myDiv) {
    elementData.set(myDiv, { isActive: true, tooltip: ‘Content area’ });
    }

    // 获取并使用关联的数据
    if (myButton) {
    const buttonData = elementData.get(myButton);
    if (buttonData) {
    console.log(Button tooltip: ${buttonData.tooltip});
    // 更新数据
    buttonData.isActive = true;
    }
    }

    // 检查是否存在
    console.log(myDiv has data? ${elementData.has(myDiv!)}); // 使用 ! 告诉 TypeScript myDiv 不是 null
    “`

  3. 实现唯一对象集合 (基于引用):
    如果需要一个集合,其中每个对象实例都是唯一的,可以使用 Map 的键来保证这一点。

    “`typescript
    class Entity {
    constructor(public id: number, public name: string) {}
    }

    const entityCollection = new Map(); // 值可以是任意占位符,如 true

    const entityA = new Entity(1, ‘Alpha’);
    const entityB = new Entity(2, ‘Beta’);
    const entityC = new Entity(1, ‘Alpha’); // 内容与 entityA 相同,但不同实例

    entityCollection.set(entityA, true);
    entityCollection.set(entityB, true);
    entityCollection.set(entityC, true); // C 是一个新键,因为引用不同

    console.log(‘Collection size:’, entityCollection.size); // 输出: 3 (A, B, C 都是不同的键)

    console.log(‘Has entityA?’, entityCollection.has(entityA)); // true
    console.log(‘Has entityC?’, entityCollection.has(entityC)); // true
    console.log(‘Has a new Entity(1, “Alpha”)?’, entityCollection.has(new Entity(1, ‘Alpha’))); // false (新实例)
    “`

  4. 维护需要保持插入顺序的记录:
    当键的顺序很重要,且需要频繁增删时,Map 是一个不错的选择。

    “`typescript
    const eventLog = new Map();

    eventLog.set(1, { type: ‘UserLogin’, timestamp: new Date() });
    // 稍后添加的事件
    eventLog.set(2, { type: ‘ProductView’, timestamp: new Date() });
    eventLog.set(3, { type: ‘AddToCart’, timestamp: new Date() });

    // 迭代时会按照添加的顺序
    console.log(‘— Event Log —‘);
    for (const [id, event] of eventLog) {
    console.log(Event ID: ${id}, Type: ${event.type});
    }
    // 输出将按照 1, 2, 3 的顺序
    “`

第九章:Map 与 WeakMap 的简要对比

在讨论 Map 时,值得简要提及相关的 WeakMapWeakMap 是一种特殊的 Map,它也存储键值对,但有两个关键区别:

  1. 键必须是对象: WeakMap 的键只能是对象类型,不能是基本类型。
  2. 弱引用键: WeakMap 对键是弱引用。这意味着如果一个对象只被 WeakMap 的键引用,而没有其他地方引用它,那么垃圾回收机制可以回收这个对象以及 WeakMap 中对应的键值对。Map 对键是强引用,只要键存在于 Map 中,对象就不会被垃圾回收。

何时使用 WeakMap?

当你想将数据与对象关联起来,并且希望在对象本身被垃圾回收时,关联的数据也自动被清理,以避免内存泄漏时,使用 WeakMap。例如,将私有数据或元数据与外部对象关联,或者存储 DOM 元素的事件监听器(当元素从 DOM 移除时,监听器也随之清理)。

WeakMap 的限制:

由于键是弱引用且可能随时被垃圾回收,WeakMap 不支持迭代 (keys(), values(), entries(), forEach()),也没有 size 属性。你只能通过 get(), set(), has(), delete() 方法来操作它。

理解 WeakMap 可以帮助你在特定场景下选择更合适的工具,但本文的重点是功能更全面的 Map

结论

Map 是 ECMAScript 6 引入的一项重要数据结构,它提供了比原生对象更加灵活和强大的键值对管理能力。其支持任意类型的键(尤其是对象键),保证插入顺序的迭代,以及方便的 size 属性,解决了原生对象在这些方面的局限性。

结合 TypeScript 的类型系统,使用 Map<K, V> 泛型可以极大地提高代码的类型安全性和可维护性,让你在开发阶段就能捕获许多潜在的错误。无论是用于构建缓存、存储对象关联数据,还是处理需要保持顺序的集合,Map 都是一个值得优先考虑的选择。

掌握 Map 的创建、操作和遍历方法,理解它与原生对象的区别以及 TypeScript 带来的类型优势,将使你能够编写更高效、更健壮的 JavaScript 和 TypeScript 代码。鼓励你在合适的场景下积极采用 Map,体验它带来的便利和强大。

希望本文能够帮助你深入理解和熟练运用 TypeScript 中的 Map


发表评论

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

滚动至顶部