Skip to content

TypeScript Map 对象

在 TypeScript 中,Map 是一种特殊的数据结构,用于存储键值对,其中键可以是任何类型(包括对象、数组、函数等)。Map 是 ES6 引入的新特性,它提供了比普通对象更灵活的键值存储方式。本文将详细介绍 TypeScript 中的 Map 对象。

1. 基本用法

创建 Map

typescript
// 创建空 Map
let map1: Map<string, number> = new Map();

// 创建带有初始值的 Map
let map2: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

// 使用对象作为键
let objKey = { id: 1 };
let map3: Map<object, string> = new Map([
  [objKey, "value"]
]);

// 使用数组作为键
let arrKey = [1, 2, 3];
let map4: Map<number[], string> = new Map([
  [arrKey, "value"]
]);

// 使用函数作为键
let funcKey = () => console.log("hello");
let map5: Map<Function, string> = new Map([
  [funcKey, "value"]
]);

添加和获取元素

typescript
let map: Map<string, number> = new Map();

// 添加元素
map.set("a", 1);
map.set("b", 2);
map.set("c", 3);

// 获取元素
console.log(map.get("a")); // 输出:1
console.log(map.get("b")); // 输出:2
console.log(map.get("c")); // 输出:3
console.log(map.get("d")); // 输出:undefined

检查元素是否存在

typescript
let map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

console.log(map.has("a")); // 输出:true
console.log(map.has("d")); // 输出:false

删除元素

typescript
let map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

// 删除元素
map.delete("b");
console.log(map.has("b")); // 输出:false

// 清除所有元素
map.clear();
console.log(map.size); // 输出:0

获取 Map 大小

typescript
let map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

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

2. 类型注解

基本类型键值对

typescript
// 字符串键,数字值
let map1: Map<string, number> = new Map();

// 数字键,字符串值
let map2: Map<number, string> = new Map();

// 布尔键,对象值
let map3: Map<boolean, object> = new Map();

复杂类型键值对

typescript
// 对象键,字符串值
let map1: Map<object, string> = new Map();

// 数组键,数字值
let map2: Map<number[], number> = new Map();

// 函数键,布尔值
let map3: Map<Function, boolean> = new Map();

联合类型键值对

typescript
// 字符串或数字键,字符串值
let map1: Map<string | number, string> = new Map();

// 字符串键,数字或布尔值
let map2: Map<string, number | boolean> = new Map();

泛型类型

typescript
// 泛型 Map 类型
interface User {
  id: number;
  name: string;
}

let users: Map<number, User> = new Map([
  [1, { id: 1, name: "John" }],
  [2, { id: 2, name: "Jane" }]
]);

3. Map 方法

set(key, value)

添加或更新键值对,返回 Map 实例。

typescript
let map: Map<string, number> = new Map();

map.set("a", 1).set("b", 2).set("c", 3);
console.log(map.get("a")); // 输出:1
console.log(map.get("b")); // 输出:2
console.log(map.get("c")); // 输出:3

// 更新值
map.set("a", 10);
console.log(map.get("a")); // 输出:10

get(key)

获取指定键的值,如果不存在则返回 undefined。

typescript
let map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

console.log(map.get("a")); // 输出:1
console.log(map.get("d")); // 输出:undefined

has(key)

检查指定键是否存在,返回布尔值。

typescript
let map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

console.log(map.has("a")); // 输出:true
console.log(map.has("d")); // 输出:false

delete(key)

删除指定键的键值对,返回布尔值表示是否删除成功。

typescript
let map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

console.log(map.delete("b")); // 输出:true
console.log(map.has("b")); // 输出:false
console.log(map.delete("d")); // 输出:false

clear()

清除所有键值对,无返回值。

typescript
let map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

map.clear();
console.log(map.size); // 输出:0
console.log(map.has("a")); // 输出:false

keys()

返回一个包含所有键的迭代器。

typescript
let map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

for (const key of map.keys()) {
  console.log(key); // 输出:a, b, c
}

// 转换为数组
let keys: string[] = Array.from(map.keys());
console.log(keys); // 输出:["a", "b", "c"]

values()

返回一个包含所有值的迭代器。

typescript
let map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

for (const value of map.values()) {
  console.log(value); // 输出:1, 2, 3
}

// 转换为数组
let values: number[] = Array.from(map.values());
console.log(values); // 输出:[1, 2, 3]

entries()

返回一个包含所有键值对的迭代器。

typescript
let map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

for (const [key, value] of map.entries()) {
  console.log(`${key}: ${value}`); // 输出:a: 1, b: 2, c: 3
}

// 转换为数组
let entries: [string, number][] = Array.from(map.entries());
console.log(entries); // 输出:[["a", 1], ["b", 2], ["c", 3]]

forEach(callback, thisArg?)

遍历 Map 中的每个键值对,执行回调函数。

typescript
let map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

map.forEach((value, key, map) => {
  console.log(`${key}: ${value}`); // 输出:a: 1, b: 2, c: 3
});

// 使用 thisArg
const obj = { prefix: "Value: " };
map.forEach(function(value, key) {
  console.log(`${this.prefix}${key}: ${value}`); // 输出:Value: a: 1, Value: b: 2, Value: c: 3
}, obj);

4. 类型守卫

在 TypeScript 中,可以使用类型守卫来检查一个值是否是 Map 类型。

instanceof 类型守卫

typescript
function processValue(value: any) {
  if (value instanceof Map) {
    // 在这个分支中,TypeScript 知道 value 是 Map 类型
    console.log(`Map size: ${value.size}`);
    value.forEach((val, key) => console.log(`${key}: ${val}`));
  } else {
    console.log(`Not a Map: ${value}`);
  }
}

processValue(new Map([["a", 1], ["b", 2]])); // 输出:Map size: 2  a: 1  b: 2
processValue({ a: 1, b: 2 }); // 输出:Not a Map: [object Object]
processValue([1, 2, 3]); // 输出:Not a Map: 1,2,3

自定义类型守卫

typescript
function isMap(value: any): value is Map<any, any> {
  return value instanceof Map;
}

function isStringNumberMap(value: any): value is Map<string, number> {
  if (!(value instanceof Map)) return false;
  for (const [key, val] of value.entries()) {
    if (typeof key !== "string" || typeof val !== "number") {
      return false;
    }
  }
  return true;
}

function processValue(value: any) {
  if (isStringNumberMap(value)) {
    // 在这个分支中,TypeScript 知道 value 是 Map<string, number> 类型
    let sum = 0;
    value.forEach((val) => sum += val);
    console.log(`Sum of values: ${sum}`);
  } else {
    console.log(`Not a Map<string, number>: ${value}`);
  }
}

processValue(new Map([["a", 1], ["b", 2]])); // 输出:Sum of values: 3
processValue(new Map([["a", "1"], ["b", "2"]])); // 输出:Not a Map<string, number>: [object Map]
processValue({ a: 1, b: 2 }); // 输出:Not a Map<string, number>: [object Object]

5. Map 操作的最佳实践

1. 使用类型注解

为 Map 添加类型注解可以提高代码的可读性和类型安全性。

typescript
// 推荐
let map: Map<string, number> = new Map();

// 不推荐
let map = new Map(); // 类型推断为 Map<unknown, unknown>

2. 使用 const 声明只读 Map

对于不需要修改的 Map,使用 const 声明可以防止意外修改。

typescript
// 推荐
const map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

// 不推荐
let map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]); // 可能被意外修改

3. 优先使用 Map 而非对象

当键不是字符串或需要保持插入顺序时,优先使用 Map。

typescript
// 推荐:使用 Map 存储非字符串键
const objKey = { id: 1 };
const map: Map<object, string> = new Map([
  [objKey, "value"]
]);

// 不推荐:使用对象存储非字符串键(会被转换为字符串)
const objKey = { id: 1 };
const obj: Record<string, string> = {};
obj[objKey] = "value"; // 键会被转换为 "[object Object]"
console.log(obj[objKey]); // 输出:undefined
console.log(obj["[object Object]"]); // 输出:value

4. 注意 Map 的键比较

Map 使用严格相等(===)来比较键,包括对象引用。

typescript
const map: Map<object, string> = new Map();

const obj1 = { id: 1 };
const obj2 = { id: 1 };

map.set(obj1, "value1");
console.log(map.get(obj1)); // 输出:value1
console.log(map.get(obj2)); // 输出:undefined(obj1 和 obj2 是不同的引用)

5. 合理使用 Map 方法

了解 Map 方法的使用场景,选择合适的方法。

typescript
// 推荐:使用 forEach 遍历 Map
const map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

map.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});

// 推荐:使用 entries() 遍历 Map
for (const [key, value] of map.entries()) {
  console.log(`${key}: ${value}`);
}

6. 注意 Map 的性能

对于频繁查找的场景,Map 的性能可能不如对象,因为对象的属性访问是直接的,而 Map 需要通过方法调用。

typescript
// 对于频繁查找的场景,对象可能更高效
const obj: Record<string, number> = {
  a: 1,
  b: 2,
  c: 3
};

console.log(obj.a); // 直接访问,性能更好

// 对于需要保持插入顺序或非字符串键的场景,Map 更合适
const map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);

console.log(map.get("a")); // 方法调用,性能略差

6. 常见错误

1. 类型不匹配

在 TypeScript 中,Map 的键和值类型必须与类型注解匹配。

typescript
// 错误:类型不匹配
let map: Map<string, number> = new Map([
  ["a", 1],
  ["b", "2"] // 类型 'string' 不能赋值给类型 'number'
]);

// 正确
let map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2]
]);

let map2: Map<string, number | string> = new Map([
  ["a", 1],
  ["b", "2"]
]);

2. 键比较错误

Map 使用严格相等(===)来比较键,包括对象引用。

typescript
const map: Map<object, string> = new Map();

const obj1 = { id: 1 };
const obj2 = { id: 1 };

map.set(obj1, "value");
console.log(map.get(obj2)); // 输出:undefined(错误:期望输出 value)

3. 忘记使用 Map 方法

使用 Map 时,必须使用 Map 提供的方法来操作,而不是像对象一样直接访问。

typescript
const map: Map<string, number> = new Map();

// 错误:直接赋值(不会添加到 Map 中)
map["a"] = 1;
console.log(map.get("a")); // 输出:undefined

// 正确:使用 set 方法
map.set("a", 1);
console.log(map.get("a")); // 输出:1

4. 混淆 Map 和对象

Map 和对象有不同的使用方式,混淆它们会导致错误。

typescript
// 错误:使用对象的方式访问 Map
const map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2]
]);

console.log(map.a); // 输出:undefined

// 正确:使用 get 方法
console.log(map.get("a")); // 输出:1

5. 错误使用 Map 的 size 属性

Map 的 size 是一个属性,不是方法,不需要括号。

typescript
const map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2]
]);

// 错误:使用方法调用
console.log(map.size()); // 运行时错误:map.size is not a function

// 正确:直接访问属性
console.log(map.size); // 输出:2

7. Map 与对象的比较

1. 键的类型

  • Map:键可以是任何类型(字符串、数字、对象、数组、函数等)。
  • 对象:键只能是字符串、数字或 Symbol。

2. 键的顺序

  • Map:保持插入顺序,遍历顺序与插入顺序一致。
  • 对象:在 ES6 之前不保证顺序,ES6 之后基本保持插入顺序,但数字键会被排序。

3. 大小

  • Map:有 size 属性,可以直接获取键值对数量。
  • 对象:需要手动计算,使用 Object.keys(obj).length

4. 迭代

  • Map:可以直接使用 for...of 循环或 forEach 方法遍历。
  • 对象:需要使用 Object.keys(), Object.values(), Object.entries() 等方法。

5. 性能

  • Map:对于频繁添加和删除键值对的场景,性能更好。
  • 对象:对于频繁查找的场景,性能可能更好。

6. 序列化

  • Map:默认情况下不能直接 JSON 序列化,需要手动转换。
  • 对象:可以直接使用 JSON.stringify() 序列化。

8. 实际应用场景

1. 存储用户数据

typescript
interface User {
  id: number;
  name: string;
  email: string;
}

const users: Map<number, User> = new Map([
  [1, { id: 1, name: "John", email: "john@example.com" }],
  [2, { id: 2, name: "Jane", email: "jane@example.com" }],
  [3, { id: 3, name: "Bob", email: "bob@example.com" }]
]);

// 根据 ID 获取用户
function getUserById(id: number): User | undefined {
  return users.get(id);
}

// 添加新用户
function addUser(user: User): void {
  users.set(user.id, user);
}

// 删除用户
function deleteUser(id: number): boolean {
  return users.delete(id);
}

2. 缓存数据

typescript
class Cache<K, V> {
  private map: Map<K, V>;
  private maxSize: number;

  constructor(maxSize: number = 100) {
    this.map = new Map();
    this.maxSize = maxSize;
  }

  get(key: K): V | undefined {
    return this.map.get(key);
  }

  set(key: K, value: V): void {
    // 如果缓存已满,删除最早的键值对
    if (this.map.size >= this.maxSize) {
      const firstKey = this.map.keys().next().value;
      this.map.delete(firstKey);
    }
    this.map.set(key, value);
  }

  has(key: K): boolean {
    return this.map.has(key);
  }

  delete(key: K): boolean {
    return this.map.delete(key);
  }

  clear(): void {
    this.map.clear();
  }

  get size(): number {
    return this.map.size;
  }
}

// 使用示例
const cache = new Cache<string, number>(3);
cache.set("a", 1);
cache.set("b", 2);
cache.set("c", 3);
console.log(cache.get("a")); // 输出:1

cache.set("d", 4); // 缓存已满,删除最早的 "a"
console.log(cache.get("a")); // 输出:undefined
console.log(cache.get("b")); // 输出:2

3. 事件管理

typescript
type EventHandler = (...args: any[]) => void;

class EventEmitter {
  private events: Map<string, Set<EventHandler>>;

  constructor() {
    this.events = new Map();
  }

  on(event: string, handler: EventHandler): void {
    if (!this.events.has(event)) {
      this.events.set(event, new Set());
    }
    this.events.get(event)!.add(handler);
  }

  off(event: string, handler: EventHandler): void {
    if (this.events.has(event)) {
      this.events.get(event)!.delete(handler);
      if (this.events.get(event)!.size === 0) {
        this.events.delete(event);
      }
    }
  }

  emit(event: string, ...args: any[]): void {
    if (this.events.has(event)) {
      this.events.get(event)!.forEach(handler => handler(...args));
    }
  }

  clear(event?: string): void {
    if (event) {
      this.events.delete(event);
    } else {
      this.events.clear();
    }
  }

  get eventNames(): string[] {
    return Array.from(this.events.keys());
  }
}

// 使用示例
const emitter = new EventEmitter();

const handler1 = (message: string) => console.log(`Handler 1: ${message}`);
const handler2 = (message: string) => console.log(`Handler 2: ${message}`);

emitter.on("message", handler1);
emitter.on("message", handler2);
emitter.emit("message", "Hello, world!");
// 输出:
// Handler 1: Hello, world!
// Handler 2: Hello, world!

emitter.off("message", handler1);
emitter.emit("message", "Hello again!");
// 输出:
// Handler 2: Hello again!

总结

TypeScript 中的 Map 对象是一种强大的数据结构,用于存储键值对,其中键可以是任何类型。它包括:

  • 基本用法:创建 Map,添加和获取元素,检查元素是否存在,删除元素,获取 Map 大小
  • 类型注解:基本类型键值对,复杂类型键值对,联合类型键值对,泛型类型
  • Map 方法:set, get, has, delete, clear, keys, values, entries, forEach
  • 类型守卫:使用 instanceof 和自定义类型守卫检查 Map 类型
  • 最佳实践:使用类型注解,使用 const 声明只读 Map,优先使用 Map 而非对象,注意 Map 的键比较,合理使用 Map 方法,注意 Map 的性能
  • 常见错误:类型不匹配,键比较错误,忘记使用 Map 方法,混淆 Map 和对象,错误使用 Map 的 size 属性
  • Map 与对象的比较:键的类型,键的顺序,大小,迭代,性能,序列化
  • 实际应用场景:存储用户数据,缓存数据,事件管理

通过合理使用 Map 对象,你可以在 TypeScript 中更灵活地处理键值对数据,特别是当键不是字符串或需要保持插入顺序时。