📚 TypeScript 教程系列

  1. 入门与配置
  2. 基础类型与变量声明
  3. 函数
  4. 流程控制与运算符
  5. 集合类型(本文)
  6. 异步编程与错误处理
  7. 接口与类
  8. 泛型与类型组合
  9. 高级类型
  10. 模块、装饰器与工程化
  11. TypeScript 与 Bun 实战
  12. TypeScript 与 React 实战

⚠️ 来源声明:本文内容参考自 菜鸟教程 TypeScript 教程,仅供学习交流,版权归原作者所有。
TypeScript 在 JavaScript 的基础上为各类集合类型加上了静态类型约束,使得数组、元组、枚举、Map、Set 以及对象在编写阶段就能获得类型检查与智能提示。本篇将这几种常用的集合类型串联起来,从声明、访问到常用方法逐一展开,帮助你建立完整的类型化数据结构心智模型。

数组(Array)

数组对象用于使用单独的变量名来存储一系列的值。例如,将多个网站名字各自存入单独变量会非常繁琐,而使用数组则简洁得多:

1
2
let sites:string[];
sites = ["Google","Runoob","Taobao"]

声明数组

TypeScript 声明数组的语法格式如下:

1
2
let array_name[:datatype];        // 声明
array_name = [val1,val2,valn..] // 初始化

或者直接在声明时初始化:

1
let array_name[:datatype] = [val1,val2…valn]

若声明数组时未指定类型但提供了初始值,TypeScript 会根据初始值推断数组类型(如 let arr = [1, 2, 3] 推断为 number[])。在 strict 模式(tsc --init 默认开启,含 noImplicitAny)下,未指定类型又无初始化值(如 let arr;)会报隐式 any 错误,需显式标注类型。

数组声明与初始化

创建一个 number 类型的数组:

1
const numlist:number[] = [2,4,6,8]

索引值从 0 开始,可通过索引访问元素:

1
2
3
4
let sites:string[];
sites = ["Google","Runoob","Taobao"]
console.log(sites[0]);
console.log(sites[1]);

输出:

1
2
Google
Runoob

声明时直接初始化的示例:

1
2
3
4
5
const nums:number[] = [1,2,3,4]
console.log(nums[0]);
console.log(nums[1]);
console.log(nums[2]);
console.log(nums[3]);

输出:

1
2
3
4
1
2
3
4

Array 对象

也可以使用 Array 对象来创建数组。其构造函数接受以下两种值:

  • 表示数组大小的数值
  • 初始化的数组列表(逗号分隔)

实例:指定数组初始化大小:

1
2
3
4
5
const arr_names:number[] = new Array(4)
for(let i = 0; i<arr_names.length; i++) {
arr_names[i] = i * 2
console.log(arr_names[i])
}

⚠️ new Array(size) 会创建含 empty 槽位的稀疏数组,是已知 footgun。现代写法推荐 Array.from({ length: 4 }, (_, i) => i * 2),或先 new Array(4).fill(0) 填充再赋值;优先使用字面量 []

输出:

1
2
3
4
0
2
4
6

实例:直接初始化数组元素:

1
2
3
4
const sites:string[] = new Array("Google","Runoob","Taobao","Facebook")
for(let i = 0;i<sites.length;i++) {
console.log(sites[i])
}

输出:

1
2
3
4
Google
Runoob
Taobao
Facebook

数组解构

可将数组元素赋值给变量:

1
2
3
4
const arr:number[] = [12,13]
let[x,y] = arr // 将数组的两个元素赋值给变量 x 和 y
console.log(x)
console.log(y)

输出:

1
2
12
13

数组迭代

使用 for...of 语句循环输出数组元素(for...in 会以字符串形式返回索引并可能枚举原型属性,不推荐用于数组):

1
2
3
4
const nums: number[] = [1001, 1002, 1003, 1004];
for (const num of nums) {
console.log(num);
}

输出:

1
2
3
4
1001
1002
1003
1004

展开运算符(Spread)

展开运算符 ... 可以把数组、对象或可迭代对象「展开」为独立元素,常用于拷贝、合并与函数调用传参。它与剩余参数(rest)共用 ... 语法,但方向相反:剩余参数是「收集」,展开是「拆开」。

数组展开:复制数组、合并数组。

1
2
3
4
5
6
const a = [1, 2, 3];
const b = [4, 5];

const copy = [...a]; // [1, 2, 3],浅拷贝
const merged = [...a, ...b]; // [1, 2, 3, 4, 5]
const withExtra = [0, ...a, 6]; // [0, 1, 2, 3, 6]

对象展开:浅拷贝对象、合并属性(后展开的同名属性会覆盖前者)。

1
2
3
4
const user = { name: "Alice", age: 30 };
const copy = { ...user }; // { name: "Alice", age: 30 }
const updated = { ...user, age: 31 }; // { name: "Alice", age: 31 }
const withRole = { ...user, role: "admin" }; // 合并新属性

函数调用展开:把数组元素作为独立参数传入函数。

1
2
3
4
5
function sum(a: number, b: number, c: number): number {
return a + b + c;
}
const nums: [number, number, number] = [1, 2, 3];
console.log(sum(...nums)); // 6,等价于 sum(1, 2, 3)

⚠️ 展开运算符做的是浅拷贝:嵌套的数组或对象内层仍是引用,修改副本的内层会影响原对象。需要深拷贝时请使用 structuredClone() 或递归处理。

多维数组

一个数组的元素可以是另外一个数组,这样就构成了多维数组。最简单的二维数组定义方式:

1
let arr_name:datatype[][]=[ [val1,val2,val3],[v1,v2,v3] ]

多维数组

实例:

1
2
3
4
5
6
7
const multi:number[][] = [[1,2,3],[23,24,25]]
console.log(multi[0][0])
console.log(multi[0][1])
console.log(multi[0][2])
console.log(multi[1][0])
console.log(multi[1][1])
console.log(multi[1][2])

输出:

1
2
3
4
5
6
1
2
3
23
24
25

数组在函数中的使用

作为参数传递给函数:

1
2
3
4
5
6
7
const sites:string[] = new Array("Google","Runoob","Taobao","Facebook")
function disp(arr_sites:string[]) {
for(let i = 0; i < arr_sites.length; i++) {
console.log(arr_sites[i])
}
}
disp(sites);

输出:

1
2
3
4
Google
Runoob
Taobao
Facebook

作为函数的返回值:

1
2
3
4
5
6
7
function disp():string[] {
return new Array("Google", "Runoob", "Taobao", "Facebook");
}
const sites: string[] = disp();
for (const site of sites) {
console.log(site);
}

输出:

1
2
3
4
Google
Runoob
Taobao
Facebook

数组方法

下表列出了一些常用的数组方法:

方法 & 描述实例
concat()
连接两个或更多的数组,返回结果
const alpha = ["a", "b", "c"];
const numeric = [1, 2, 3];
const alphaNumeric = alpha.concat(numeric);
→ a,b,c,1,2,3
every()
检测每个元素是否都符合条件
function isBigEnough(element) {
return element >= 10;
}
const passed = [12, 5, 8, 130, 44].every(isBigEnough);
→ false
filter()
返回符合条件所有元素的数组
const passed = [12, 5, 8, 130, 44].filter(isBigEnough);
→ 12,130,44
forEach()
每个元素执行一次回调函数
let num = [7, 8, 9];
num.forEach(function (value) {
console.log(value);
});
→ 7 8 9
indexOf()
搜索元素并返回位置,未找到返回 -1
const index = [12, 5, 8, 130, 44].indexOf(8);
→ 2
join()
将所有元素放入一个字符串
const arr = new Array("Google","Runoob","Taobao");
const str = arr.join();
→ Google,Runoob,Taobao
arr.join(" + ")
→ Google + Runoob + Taobao
lastIndexOf()
返回指定值最后出现的位置,从后向前搜索
const index = [12, 5, 8, 130, 44].lastIndexOf(8);
→ 2
map()
通过指定函数处理每个元素,返回处理后的数组
const numbers = [1, 4, 9];
const roots = numbers.map(Math.sqrt);
→ 1,2,3
pop()
删除最后一个元素并返回它
const numbers = [1, 4, 9];
const element = numbers.pop();
→ 9
push()
向末尾添加一个或多个元素,返回新长度
const length = numbers.push(10);
→ 1,4,9,10
reduce()
将数组元素计算为一个值(从左到右)
const total = [0, 1, 2, 3].reduce(function(a, b) {
return a + b;
});
→ 6
reduceRight()
将数组元素计算为一个值(从右到左)
const total = [0, 1, 2, 3].reduceRight(function(a, b) {
return a + b;
});
→ 6
reverse()
反转数组元素顺序
const arr = [0, 1, 2, 3].reverse();
→ 3,2,1,0
shift()
删除并返回第一个元素
const arr = [10, 1, 2, 3].shift();
→ 10
slice()
选取一部分并返回新数组
const arr = ["orange", "mango", "banana", "sugar", "tea"];
arr.slice(1, 2);
→ mango
some()
检测是否有元素符合条件
const retval = [2, 5, 8, 1, 4].some(isBigEnough);
→ false
改为 [12, 5, 8, 1, 4] → true
sort()
对元素排序
const sorted = arr.sort();
→ banana,mango,orange,sugar
splice()
从数组中添加或删除元素
const removed = arr.splice(2, 0, "water");
插入 water
toString()
将数组转换为字符串
const str = arr.toString();
→ orange,mango,banana,sugar
unshift()
向开头添加一个或多个元素,返回新长度
const length = arr.unshift("water");
→ water,orange,mango,banana,sugar

💡 TS 5.5+ 类型谓词推断:当 filter 回调返回布尔表达式或类型谓词时,TypeScript 能自动推断结果元素类型。例如 .filter(x => x != null) 无需手写 (x): x is T 即可从 (T | null)[] 收窄为 T[]

元组(Tuple)

我们知道数组中元素的数据类型一般相同(any[] 类型的数组可以不同),如果存储的元素数据类型不同,则需要使用元组。元组是一种特殊数组,允许存储不同类型的元素,每个元素都有明确的类型和位置,适合表示固定长度且各元素类型已知的数据结构。

创建元组

语法格式:

1
let tuple: [类型1, 类型2, 类型3, ...];

实例:

1
2
let mytuple: [number, string];
mytuple = [42, "Runoob"];

访问元组

通过索引访问,索引从 0 开始,语法:tuple_name[index]

1
2
3
4
5
6
7
let mytuple: [number, string, boolean] = [42, "Runoob", true];
let num = mytuple[0];
let str = mytuple[1];
let bool = mytuple[2];
console.log(num);
console.log(str);
console.log(bool);

输出:

1
2
3
42
Runoob
true

元组运算

  • push():向元组末尾添加元素
  • pop():从元组末尾移除元素并返回该元素

push 实例:

1
2
3
let tuple = [42, "Hello"];
tuple.push("World");
console.log(tuple); // 输出: [42, "Hello", "World"]

pop 实例:

1
2
3
4
let tuple: [number, string, boolean] = [42, "Hello", true];
let lastElement = tuple.pop();
console.log(lastElement); // 输出: true
console.log(tuple); // 输出: [42, "Hello"]

更新元组

元组是可变的,可通过索引直接赋值更新。

1
2
3
4
let mytuple = [42, "Runoob", "Taobao", "Google"];
console.log("元组的第一个元素为:" + mytuple[0])
mytuple[0] = 121
console.log("元组中的第一个元素更新为:" + mytuple[0])

输出:

1
2
元组的第一个元素为:42
元组中的第一个元素更新为:121

解构元组

可将元组元素赋值给变量:

1
2
3
4
let a: [number, string, boolean] = [42, "Hello", true];
const [b,c] = a
console.log(b)
console.log(c)

输出:

1
2
42
Hello

标签元组与其他操作

可为元组元素添加标签以提高可读性:

1
let tuple: [id: number, name: string] = [1, "John"];

元组常用于函数返回多个值的场景:

1
2
3
4
5
6
function getUserInfo(): [number, string] {
return [1, "John Doe"];
}
const [userId, userName] = getUserInfo();
console.log(userId); // 输出: 1
console.log(userName); // 输出: John Doe

使用 as const 将元组视为不可变常量元组:

1
let tuple = [42, "Hello"] as const; // 元组类型:[42, "Hello"]

使用 concat 方法连接元组,结果是一个普通数组而非元组:

1
2
3
4
let tuple1: [number, string] = [42, "Hello"];
let tuple2: [number, string] = [100, "World"];
let result = tuple1.concat(tuple2);
console.log(result); // 输出: [42, "Hello", 100, "World"]

使用 slice 方法切片元组,返回新数组:

1
2
3
let tuple: [number, string, boolean] = [42, "Hello", true];
let sliced = tuple.slice(1);
console.log(sliced); // 输出: ["Hello", true]

遍历元组可使用 for...of 循环或 forEach 方法:

1
2
3
4
5
let tuple: [number, string, boolean] = [42, "Hello", true];
for (let item of tuple) {
console.log(item);
}
tuple.forEach(item => console.log(item));

通过 Array.from 转换为普通数组后可使用数组方法:

1
2
3
4
let tuple: [number, string, boolean] = [42, "Hello", true];
let array = Array.from(tuple);
array.push("New Element");
console.log(array); // 输出: [42, "Hello", true, "New Element"]

使用剩余运算符合并多个元组:

1
2
3
4
let tuple1: [number, string] = [42, "Hello"];
let tuple2: [boolean] = [true];
let extendedTuple: [number, string, ...typeof tuple2] = [42, "Hello", ...tuple2];
console.log(extendedTuple); // 输出: [42, "Hello", true]

枚举(Enum)

枚举(Enum)类型用于定义数值集合,让开发者为一组数值赋予友好的名字,从而避免在代码中出现"魔术数字",提升可读性与可维护性。

数字枚举

默认情况下,枚举成员从 0 开始编号:

1
2
3
4
5
6
7
8
9
10
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}

const dir: Direction = Direction.Up;
console.log("方向: " + dir);
console.log("方向名称: " + Direction[0]);

输出:

1
2
方向: 0
方向名称: Up

手动赋值

可以手动为枚举成员指定值:

1
2
3
4
5
6
7
8
enum Status {
Success = 1,
Error = 2,
Pending = 3
}

console.log("状态: " + Status.Success);
console.log("状态名称: " + Status[1]);

输出:

1
2
状态: 1
状态名称: Success

字符串枚举

字符串枚举要求每个成员都必须赋予字符串字面量值:

1
2
3
4
5
6
7
enum Message {
Success = "SUCCESS",
Error = "ERROR",
Warning = "WARNING"
}

console.log("消息: " + Message.Success);

输出:

1
消息: SUCCESS

常量枚举

使用 const 修饰符可以声明常量枚举,它会在编译时被内联,从而生成更优化的代码:

1
2
3
4
5
6
7
8
const enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}

const favoriteColor: Color = Color.Red;
console.log("喜欢的颜色: " + favoriteColor);

⚠️ 使用 Vite、esbuild、swc 等基于 isolatedModules 的工具链时,const enum 跨文件不会内联,可能产生运行时问题。新代码建议改用普通 enum 或联合字面量类型,const enum 仅在单文件内或确认编译器支持时使用。

异构枚举

枚举可以混合数字和字符串值,但这种用法并不推荐:

1
2
3
4
5
6
7
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES"
}

console.log("值: " + BooleanLikeHeterogeneousEnum.No);
console.log("字符串值: " + BooleanLikeHeterogeneousEnum.Yes);

枚举成员类型

当所有成员都是字面量值时,单个成员可以作为类型使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
enum ShapeKind {
Circle = "circle",
Square = "square"
}

interface Circle {
kind: ShapeKind.Circle;
radius: number;
}

interface Square {
kind: ShapeKind.Square;
sideLength: number;
}

const c: Circle = {
kind: ShapeKind.Circle,
radius: 10
};

console.log("圆形: " + JSON.stringify(c));

运行时常量枚举

普通枚举在运行时会作为真实对象保留:

1
2
3
4
5
6
7
enum FileAccess {
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write
}

console.log("文件访问: " + FileAccess.ReadWrite);

输出:

1
文件访问: 6

枚举类型小结:

  • 数字枚举:默认从 0 开始,支持手动赋值
  • 字符串枚举:每个成员必须是字符串字面量
  • 常量枚举:使用 const,编译时内联
  • 异构枚举:混合数字与字符串(不推荐)
  • 成员类型:字面量成员可作为类型使用

💡 更轻量的现代替代:当只需一组字符串状态值且不需要反向映射时,可用联合字面量类型代替 enum,更利于 tree-shaking:

1
2
type Status = "success" | "error" | "pending";
const current: Status = "success";

需要反向映射(如 Status[1])或运行时对象迭代时,仍使用 enum

Map 对象

Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值)都可以作为一个键或一个值。Map 是 ES2015 引入的标准内置对象。

创建 Map

TypeScript 使用 Map 类型与 new 关键字来创建 Map:

1
let myMap = new Map();

初始化时可以传入键值对数组:

1
2
3
4
let myMap = new Map([
["key1", "value1"],
["key2", "value2"]
]);

Map 的属性与方法

方法 / 属性说明
map.clear()移除所有键值对
map.set(key, value)设置键值对,返回 Map 本身
map.get(key)返回键对应的值,不存在则返回 undefined
map.has(key)判断是否包含该键,返回布尔值
map.delete(key)删除键值对,返回 true/false
map.size返回键值对数量
map.keys()返回键的迭代器
map.values()返回值的迭代器
map.entries()返回所有 [key, value] 的迭代器

常用函数签名如下(TS 5.6 起,Mapkeys()/values()/entries() 返回 MapIterator 类型,Set 对应 SetIterator,均含迭代器辅助方法):

  • set(key: K, value: V): this — 添加或更新键值对
  • get(key: K): V | undefined — 根据键取值
  • has(key: K): boolean — 判断键是否存在
  • delete(key: K): boolean — 移除键值对
  • clear(): void — 清空所有条目
  • size: number — 条目数量
  • keys(): MapIterator<K> — 返回键的迭代器
  • values(): MapIterator<V> — 返回值的迭代器
  • entries(): MapIterator<[K, V]> — 返回 [key, value] 数组的迭代器
  • forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void — 对每个条目执行回调

带类型参数的 Map 示例:

1
2
3
4
5
6
7
8
9
10
const map = new Map<string, number>();
map.set('one', 1);
map.set('two', 2);
console.log(map.get('one')); // 1
console.log(map.has('two')); // true
map.delete('one');
console.log(map.size); // 1
map.forEach((value, key) => { console.log(key, value); }); // two 2
map.clear();
console.log(map.size); // 0

完整示例(test.ts),创建一个字符串键、数字值的 nameSiteMapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let nameSiteMapping = new Map<string, number>();

// 设置键值对
nameSiteMapping.set("Google", 1);
nameSiteMapping.set("Runoob", 2);
nameSiteMapping.set("Taobao", 3);

// 获取键对应的值
console.log(nameSiteMapping.get("Runoob")); // 2
// 判断 Map 中是否包含某键
console.log(nameSiteMapping.has("Taobao")); // true
console.log(nameSiteMapping.has("Zhihu")); // false
// 返回 Map 对象键/值对的数量
console.log(nameSiteMapping.size); // 3
// 删除某键
console.log(nameSiteMapping.delete("Runoob")); // true
console.log(nameSiteMapping); // Map { 'Google' => 1, 'Taobao' => 3 }
// 移除所有键值对,清空 Map
nameSiteMapping.clear();
console.log(nameSiteMapping); // Map {}

直接运行(tsx test.tsbun test.ts,Node 23.6+ 亦可原生执行 .ts),输出:

1
2
3
4
5
6
7
2
true
false
3
true
Map { 'Google' => 1, 'Taobao' => 3 }
Map {}

迭代 Map

Map 中的元素按插入顺序排列,TypeScript 使用 for...of 来迭代:

1
2
3
4
5
6
7
8
9
10
11
12
for (let key of nameSiteMapping.keys()) {
console.log(key);
}
for (let value of nameSiteMapping.values()) {
console.log(value);
}
for (let entry of nameSiteMapping.entries()) {
console.log(entry[0], entry[1]);
}
for (let [key, value] of nameSiteMapping) {
console.log(key, value);
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
Google
Runoob
Taobao
1
2
3
Google 1
Runoob 2
Taobao 3
Google 1
Runoob 2
Taobao 3

Map 的键可以是任意类型(对象、函数等),比使用普通对象作为键更灵活,是实现缓存、统计、索引等功能的理想选择。

分组:Object.groupBy 与 Map.groupBy

ES2024 引入了 Object.groupByMap.groupBy,TypeScript 5.4 起为其提供了类型支持,可按指定键把数组元素分组,无需手写 reduce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const inventory = [
{ name: "asparagus", type: "vegetables", quantity: 9 },
{ name: "banana", type: "fruit", quantity: 5 },
{ name: "goat", type: "meat", quantity: 3 },
{ name: "cherries", type: "fruit", quantity: 12 },
];

// Object.groupBy 返回以分组键为属性的对象(类型为 Partial<Record<...>>)
const byType = Object.groupBy(inventory, ({ type }) => type);
console.log(byType.vegetables); // [{ name: "asparagus", ... }]

// Map.groupBy 返回真正的 Map,键可以是任意类型
const bySize = Map.groupBy(inventory, ({ quantity }) =>
quantity > 5 ? "large" : "small"
);
console.log(bySize.get("large")); // asparagus、cherries

Object.groupBy 适合键为字符串/数字字面量的场景;当键是对象或其他类型时,改用 Map.groupBy

Set 与 WeakMap/WeakSet

TypeScript 继承自 JavaScript 的 Set 和 WeakMap 数据结构,提供了更强大的类型支持。这些数据结构在处理唯一值集合、键值对映射、缓存等场景非常有用。

数据结构对比

数据结构特点行为方法
Set值的集合值唯一,不重复,可遍历add/has/delete
WeakSet对象弱引用不影响 GC,不可遍历add/has/delete
Map键值对集合键可以是任意类型,可遍历set/get/has
WeakMap键弱引用键必须是对象或非注册 symbol,不影响 GC,不可遍历set/get/has

应用场景:Set 用于去重、唯一值集合;Map 用于键值映射、缓存;WeakMap/WeakSet 用于内存优化。

Set 提供了自动去重的集合功能,比数组更方便处理唯一值;WeakSet 和 WeakMap 使用弱引用,不会阻止垃圾回收,适用于避免内存泄漏的场景,如缓存 DOM 节点。

Set

Set 是值的集合,值唯一,不允许重复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建 Set,指定元素类型为 number
const numbers = new Set<number>();

// 添加元素
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(1); // 重复值会被忽略,不会添加

// 检查大小和包含
console.log("Set 大小: " + numbers.size);
console.log("是否包含 2: " + numbers.has(2));

// 遍历 Set
numbers.forEach(function(value) {
console.log("值: " + value);
});

// 转换为数组
const arr = Array.from(numbers);
console.log("转换为数组: " + arr);

运行结果:

1
2
3
4
5
6
Set 大小: 3
是否包含 2: true
: 1
: 2
: 3
转换为数组: 1,2,3

Set 会自动忽略重复的值,这使得它非常适合用于数组去重。

Set 类型注解

可以显式指定 Set 中值的类型,使用 Set<T> 语法指定 Set 中元素的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 字符串 Set
// 只能添加字符串类型的值
const stringSet: Set<string> = new Set();
stringSet.add("a");
stringSet.add("b");

// 对象 Set
// 定义 Person 接口
interface Person {
name: string;
}
// 创建存储 Person 对象的 Set
const personSet: Set<Person> = new Set();
personSet.add({ name: "Alice" });
personSet.add({ name: "Bob" });

console.log("字符串 Set: " + Array.from(stringSet));
console.log("对象 Set 大小: " + personSet.size);

WeakSet

WeakSet 存储对象引用,引用为弱引用(不影响垃圾回收)。WeakSet 只能存储对象或非注册 symbol(ES2023 起),不能存储其他原始值,且不能遍历。类型约束为 WeakKeyobject | symbol),其中对象是最常见用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// WeakSet 只能存储对象,不能存储原始值
const weakSet = new WeakSet();

// 创建对象
const obj1 = { name: "Alice" };
const obj2 = { name: "Bob" };

// 添加对象到 WeakSet
weakSet.add(obj1);
weakSet.add(obj2);

// 检查是否包含
console.log("是否包含 obj1: " + weakSet.has(obj1));

// 移除引用后,对象可能被垃圾回收
weakSet.delete(obj1);
console.log("删除后是否包含 obj1: " + weakSet.has(obj1));

WeakSet 适合存储需要被垃圾回收的对象。

WeakMap

WeakMap 的键是弱引用,不影响垃圾回收。WeakMap 的键必须是对象或非注册 symbol(ES2023 起),不可遍历,常用于缓存 DOM 节点数据——当 DOM 节点被移除时,缓存数据也会被自动清理,避免内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// WeakMap 的键必须是对象或非注册 symbol
// 键类型为 object,值类型为 string
const weakMap = new WeakMap<object, string>();

// 创建对象作为键
const keyObj = { id: 1 };
// 设置键值对
weakMap.set(keyObj, "value1");

// 获取值
console.log("获取值: " + weakMap.get(keyObj));
console.log("是否包含: " + weakMap.has(keyObj));

// 删除键值对
weakMap.delete(keyObj);
console.log("删除后: " + weakMap.has(keyObj));

实际应用场景

使用 Map 统计数组元素出现次数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 使用 Map 统计数组中每个元素的出现次数
function countElements(arr: string[]): Map<string, number> {
const counts = new Map<string, number>();
for (const item of arr) {
// 当前计数不存在时记为 0
const currentCount = counts.get(item) ?? 0;
counts.set(item, currentCount + 1);
}
return counts;
}

// 测试
const fruits = ["apple", "banana", "apple", "orange", "banana", "apple"];
const result = countElements(fruits);

// 遍历结果
result.forEach(function(count, fruit) {
console.log(fruit + ": " + count);
});

运行结果:

1
2
3
apple: 3
banana: 2
orange: 1

注意事项

  • Set 唯一性:Set 自动忽略重复值
  • WeakSet/WeakMap:键必须是对象或非注册 symbol,不能遍历
  • Map 键类型:Map 的键可以是任意类型
  • 内存管理:WeakSet/WeakMap 不阻止垃圾回收

选择建议:需要唯一值集合用 Set,需要键值映射用 Map,需要避免内存泄漏用 WeakSet/WeakMap。

对象(Object)

对象是包含一组键值对的实例。值可以是标量、函数、数组、对象等。

对象语法模板

1
2
3
4
5
6
7
const object_name = {
key1: "value1", // 标量
key2: "value",
key3: function() { // 函数
},
key4:["content1", "content2"] // 集合
}

对象实例:

1
2
3
4
5
6
7
const sites = {
site1:"Runoob",
site2:"Google"
};
// 访问对象的值
console.log(sites.site1)
console.log(sites.site2)

输出:

1
2
Runoob
Google

TypeScript 类型模板

在 JavaScript 中定义对象后可以动态添加方法,但 TypeScript 中的对象必须是特定类型的实例,因此需要预先在类型模板中声明方法。

1
2
3
4
5
6
7
8
9
const sites = {
site1: "Runoob",
site2: "Google",
sayHello: function () { } // 类型模板
};
sites.sayHello = function () {
console.log("hello " + sites.site1);
};
sites.sayHello();

输出:

1
hello Runoob

对象作为参数传递给函数

1
2
3
4
5
6
7
8
9
const sites = {
site1:"Runoob",
site2:"Google",
};
const invokesites = function(obj: { site1:string, site2 :string }) {
console.log("site1 :"+obj.site1)
console.log("site2 :"+obj.site2)
}
invokesites(sites)

输出:

1
2
site1 :Runoob
site2 :Google

鸭子类型(Duck Typing)

鸭子类型是动态类型的一种风格,是多态的一种形式。在这种风格中,一个对象有效的语义不是由继承自特定的类或实现特定的接口决定,而是由当前方法和属性的集合决定。

核心表述:当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。

在鸭子类型中,关注点在于对象的行为能做什么,而不是关注对象所属的类型。在不使用鸭子类型的语言中,可以编写一个函数接受类型为"鸭子"的对象并调用其"走"和"叫"方法;在使用鸭子类型的语言中,这样的函数可以接受任意类型的对象并调用其"走"和"叫"方法。如果这些方法不存在,将引发运行时错误。

TypeScript 接口与鸭子类型示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
interface IPoint {
x:number
y:number
}
function addPoints(p1:IPoint,p2:IPoint):IPoint {
const x = p1.x + p2.x
const y = p1.y + p2.y
return {x:x,y:y}
}
// 正确
const newPoint = addPoints({x:3,y:4},{x:5,y:1})
// 错误
const newPoint2 = addPoints({x:1},{x:4,y:3})

只要传入的对象具备接口所要求的 xy 属性,TypeScript 就认为它符合 IPoint 类型,这正是鸭子类型在静态类型系统中的体现。