2-2、TS 实战

元数据

定义:描述数据的数据
通过给类、方法指定/定义属性进一步丰富它的形态
元数据的使用范围通常为对象、类、方法
作用:

  • 扩展已有的属性形态
  • 不改变本身的代码逻辑

场景举例:
在实际业务中,存在老业务的迭代或扩展,这种情况下可以使用元数据进行扩展

1
2
3
4
5
6
7
// 老业务:course 函数返回一个字符串,代表课程名称
let course = function() {
return 'ts 实战'
}

// 新业务:course 函数要拥有课程时长、上课老师等属性

扩展方法:

  1. 采用原型链的思路来实现,通过 Function.prototype 实现
    1. 隐蔽性太高,不易查找
    2. 维护成本大,协作效率低
    3. 对象的操作不统一

在 JS 中,对象的操作一直都是不统一的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建对象
let obj = {}

// 新增属性
obj.name = 'lisi'

// 更改属性
obj.name = '张三'

// 删除属性
delete obj.name

// 获取属性
obj.name

// 合并对象
Object.assign(obj1, obj2)

// 可以发现,对象的不同操作语法是各式各样的

在 TS 里面,就有一种统一的操作对象的方式:Reflect
元数据的实现是通过Reflect + metadata

Reflect

官方文档:Reflect - JavaScript | MDN
一种统一的操作对象的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const obj = {}

// 新增/更改属性;参数:目标对象,属性名称,属性描述;返回值:boolean(是否操作成功)
// Reflect.defineProperty(target, propertyKey, attributes)
Reflect.defineProperty(obj, 'name', { value: 'lisi' }) // true
Reflect.defineProperty(obj, 'age', { value: 27 }) // true
// obj: { name: 'lisi', age: 27 }
// 其中的 name|age 是不可操作的(configurable 默认为 false)

// 新增/更改属性;参数:目标对象,属性名称,属性值;返回值:boolean(是否操作成功)
// Reflect.set(target, propertyKey, value)
Reflect.set(obj, 'address', '四川成都')
// obj: { name: 'lisi', age: 27, address: '四川成都'}
// 其中的 address 是可操作的(configurable 默认为 true)

// 获取属性;参数:目标对象,属性名称;返回值:属性的值
// Reflect.get(target, propertyKey)
Reflect.get(obj, 'name') // 'lisi'

// 删除属性;参数:目标对象,属性名称;返回值:boolean(是否操作成功)
// Reflect.deleteProperty(target, propertyKey)
Reflect.deleteProperty(obj, 'age') // false
Reflect.deleteProperty(obj, 'address') // true
// obj: { name: 'lisi', age: 27}

这样对象的所有操作都统一使用Reflect来完成:
增(Reflect.set(...))、删(Reflect.deleteProperty(...))、改(Reflect.set(...))、查(Reflect.get(...))

Metadata

在 TS 中元数据的具体实现,需要引入一个第三方库
再次强调元数据的使用范围可以为对象、类、属性(变量/方法)

1
import 'reflect-metadata'

用的前提是:tsconfig.json 开启配置

1
2
3
4
5
{
"compilerOptions" : {
"emitDecoratorMetadata": true
}
}

设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
Reflect.defineMetadata(
metadataKey: any,
metadataValue: any,
target: Object,
propertyKey: string | symbol
): void
// metadataKey:存储 metadata 的 Key,require
// metadataValue:存储 metadata 的 值,require
// target:metadata 要绑的目标对象,require
// propertyKey:目标对象的属性名,optional

class Test {
static oldName = "zhangsan";

static sayYes() {
return "好的";
}

name = "lisi";

sayHello() {
return `你好,我是${this.name}`;
}
}


Reflect.defineMetadata("Test_metadataKey1", "Test_metadataValue1", Test);
Reflect.defineMetadata(
"Test_public_metadataKey2",
"Test_public_metadataValue2",
Test,
"name"
);

Reflect.defineMetadata(
"Test_static_metadataKey2",
"Test_static_metadataValue2",
Test,
"oldName"
);
Reflect.defineMetadata(
"Test_public_metadataKey3",
"Test_public_metadataValue3",
Test,
"sayHello"
);
Reflect.defineMetadata(
"Test_static_metadataKey4",
"Test_static_metadataValue4",
Test,
"sayYes"
);

小细节

当设置元数据的时候,可以有两种写法:

  1. 函数调用形式:Reflect.defineMetadata(...)
  2. 装饰器形式:@Reflect.metatda(...)

这两种写法都能设置元数据,但针对装饰器形式设置的,对应的获取时就存在一些注意事项

1
2
3
4
5
6
7
8
9
@Reflect.metadata("Test_metadataKey1", "Test_metadataValue1")
class Test {}

// @Reflect.metadata 的写法就是 2-1 TS 详解里面的"类装饰器",其原理代码大致如下:
Reflect.metadata = function(metadataKey: string, metadataValue: any) {
return function(target: Object, propertyKey: string) {
// 具体的逻辑暂且忽略.........
}
}
操作本身
设置
1
2
3
4
5
6
7
8
9
// Reflect.defineMetadata(...) 设置
class Test {}
Reflect.defineMetadata("Test_metadataKey1", "Test_metadataValue1", Test);

// @Reflect.metadata(...) 设置
@Reflect.metadata("Test_metadataKey1", "Test_metadataValue1")
class Test {}

// 以上两种方式是等价的,都是设置元数据到"类"
取值
1
2
// 相同取法
Reflect.getMetadata("Test_metadataKey1", Test); // "Test_metadataValue1"
操作属性
设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Reflect.defineMetadata(...) 设置
class Test {
static oldName = 'zhangsan'
name = "lisi"
}

Reflect.defineMetadata(
"Test_public_metadataKey2",
"Test_public_metadataValue2",
Test,
"name"
);

Reflect.defineMetadata(
"Test_static_metadataKey2",
"Test_static_metadataValue2",
Test,
"oldName"
);

// @Reflect.metadata(...) 设置
class Test {
@Reflect.metadata("Test_static_metadataKey2", "Test_static_metadataValue2")
static oldName = "zhangsan";

@Reflect.metadata("Test_public_metadataKey2", "Test_public_metadataValue2")
name = "lisi";
}

// 咋一看操作是一样的,但其取值对象是不一样

静态属性定义了元数据,取值target类本身
动态属性定义了元数据,取值target类的实例

取值
1
2
3
4
5
6
7
8
9
10
11
Reflect.getMetadata(
"Test_static_metadataKey2",
Test, // 这里就必须为类,若为类的实例,则取值结果为 undefined
"oldName"
); // "Test_static_metadataValue2"

Reflect.getMetadata(
"Test_public_metadataKey2",
new Test(), // 这里就必须为类的实例,若为类,则取值结果为 undefined
"name"
); // "Test_public_metadataValue2"

获取

1
2
3
4
5
6
7
8
9
10
11
12
13
Reflect.getMetadata(
metadataKey: any,
target: Object,
propertyKey: string | symbol
): any
// metadataKey:存储 metadata 的 Key,require
// target:metadata 要绑的目标对象,require
// propertyKey:目标对象的属性名,optional

Reflect.getMetadata("Test_metadataKey1", Test); // "Test_metadataValue1"
Reflect.getMetadata("Test_public_metadataKey2", Test, "name"); // "Test_public_metadataValue2"
Reflect.getMetadata("Test_static_metadataKey2", Test, "oldName"); // "Test_static_metadataValue2"
Reflect.getMetadata("Test_public_metadataKey3", Test, "sayHello"); // "Test_public_metadataValue3"

删除

1
2
3
4
5
6
7
8
9
10
11
12
13
Reflect.deleteMetadata(
metadataKey: any,
target: Object,
propertyKey: string | symbol
): boolean
// metadataKey:存储 metadata 的 Key,require
// target:metadata 要绑的目标对象,require
// propertyKey:目标对象的属性名,optional

Reflect.deleteMetadata("Test_metadataKey1", Test); // true
Reflect.deleteMetadata("Test_public_metadataKey2", Test, "name"); // true
Reflect.deleteMetadata("Test_static_metadataKey2", Test, "oldName"); // true
Reflect.deleteMetadata("Test_public_metadataKey3", Test, "sayHello"); // true

通过上面的设置、获取、删除方法,已基本了解了metadata的使用,并且也成功的在不改动原本数据的情况下,扩展了新的属性与值

实战

服务端场景 - 路由封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// app.ts

// 一个简单的 app.ts
import express from "express";

const app = express();
const port = 3000;

app.get("/", (req, res) => {
res.send("Hello World!");
});

app.get("/xxxx", (req, res) => {
res.send("xxxx");
});

app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
});

按照上述的写法,服务端的路由将会无比的多,并且臃肿和不好管理
封装思路:使用面向对象写法,进行更好的归类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// user.ts
// 封装用户相关的接口

import { Get, Post } from "../decorators/methods";
import { Path } from "../decorators/path";

export class User {
// 用户查询
@Get
@Path("/user/info")
info() {
// 接口逻辑...
return "info";
}

// 用户登录
@Post
@Path("/user/login")
login() {
// 接口逻辑...
return "login";
}

logout() {
// 接口逻辑...
return "logout";
}
}
1
2
3
4
5
6
7
8
9
10
// decorators/methods.ts

export const methodsKey = Symbol("router:methods");

export const Get = (target: Object, propertyKey: string) => {
Reflect.defineMetadata(methodsKey, "get", target, propertyKey);
};
export const Post = (target: Object, propertyKey: string) => {
Reflect.defineMetadata(methodsKey, "post", target, propertyKey);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// decorators/path.ts

import { Request, Response } from "express";

export const pathKey = Symbol("router:path");

// Path 装饰器 - 用在 类的方法 上
export const Path = (path: string): Function => {
return (
target: Object,
propertyKey: string,
desicriptor: PropertyDescriptor
) => {
Reflect.defineMetadata(pathKey, path, target, propertyKey);

// 1、获取方法本身,存一份
const oldMethod = desicriptor.value;

// 2、边缘检测:无值则直接 return
if (!oldMethod) return;

// 3、覆盖原来的方法
desicriptor.value = function (req: Request, res: Response) {
// a. 获取到原方法的参数
const params = Object.assign({}, req.body, req.query);

// b. 触发原方法的调用
const result = oldMethod.call(this, params);

// c. 返回给客户端
res.send(result);
};
};
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// router.ts
import { User } from "./user";

import { methodsKey } from "../decorators/methods";
import { pathKey } from "../decorators/path";

export default (app: any) => {
const user = new User();

Object.keys(User).forEach((key) => {
// app.get(path, fun)

const method = Reflect.getMetadata(methodsKey, user, key);

const path = Reflect.getMetadata(pathKey, user, key);

app[method](path, user); // 挂载完成路由监听
});
};

最终实现的user.ts写法已经跟现在的 nodejs 框架nest.js、midday.js等类似了

客户端场景 - 倒计时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 需求:写一个自定义时间的倒计时器
// 比如:从现在起到明天这个时间点的倒计时,11:59:59

class Countdown {
// endTime: 倒计时的终点时间
// step: 倒计时间隔,单位毫秒
constructor(endTime: number, step: number){
// 待补充...
}

// 待补充...
}

// 使用
const countdown = new Countdown(Date.now() * 60 * 60 * 12, 1000)

countdown.on('running', time => {
// 只要倒计时还未结束,则该函数每间隔 X(初始化传入的值) 秒后会执行

// time: 剩余时间
// hour: 剩余小时
// minutes: 剩余分钟
// seconds: 剩余秒数
// count: 计时次数
const { hour, minutes, seconds, count } = time

console.log(`还剩:${hour}:${minutes}:${seconds}`)
})

// 打印结果:
// 还剩:11:59:59
// 还剩:11:59:58
// ...
// 还剩:00:09:09
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// 需求:写一个自定义时间的倒计时器
// 比如:从现在起到明天这个时间点的倒计时,11:59:59

// 发布订阅用的
import { EventEmitter } from "eventemitter3";

interface CountdownEventMap {
[CountdownEventName.START]: [];
[CountdownEventName.RUNNING]: [RemainTimeData];
[CountdownEventName.STOP]: [];
}

enum CountdownEventName {
START = "start",
STOP = "stop",
RUNNING = "running",
}

enum CountdownStatus {
running,
paused,
stoped,
}

interface RemainTimeData {
hours: number;
minutes: number;
seconds: number;
count: number;
}

class Countdown extends EventEmitter<CountdownEventMap> {
endTime: number;
step: number;
remainTime: number;
count: number;
status: CountdownStatus = CountdownStatus.stoped;

// endTime: 倒计时的终点时间
// step: 倒计时间隔,单位毫秒
constructor(endTime: number, step = 1e3) {
super();
// 待补充...

this.endTime = endTime;
this.step = step;
this.remainTime = 0;
this.count = 0;

this.start();
}

// 待补充...
start() {
this.emit(CountdownEventName.START);
this.status = CountdownStatus.running;

this.countdown();
}

// 计时操作
countdown() {
if (this.status === CountdownStatus.running) {
this.remainTime = Math.max(this.endTime - Date.now(), 0);

this.count++;

this.emit(CountdownEventName.RUNNING, this.calcRemainTimeData());

if (this.remainTime > 0) {
setTimeout(() => {
this.countdown();
}, this.step);
} else {
this.stop();
}
}
}

calcRemainTimeData(): RemainTimeData {
let hours, minutes, seconds, count;

count = this.count;

// 创建一个新的 Date 对象
let date = new Date(this.remainTime);

// 获取小时、分钟和秒
hours = date.getHours();
minutes = date.getMinutes();
seconds = date.getSeconds();

return { hours, minutes, seconds, count };
}

stop() {
this.emit(CountdownEventName.STOP);
this.status = CountdownStatus.stoped;
}
}

// 使用
const countdown = new Countdown(Date.now() * 60 * 60, 1000);

countdown.on(CountdownEventName.RUNNING, (remainTimeData: RemainTimeData) => {
// 只要倒计时还未结束,则该函数每间隔 X(初始化传入的值) 秒后会执行

// remainTimeData: 剩余时间
// hours: 剩余小时
// minutes: 剩余分钟
// seconds: 剩余秒数
// count: 计时次数
const { hours, minutes, seconds, count } = remainTimeData;

console.log(`还剩:${hours}:${minutes}:${seconds}`, count);
});

// 打印结果:
// 还剩:11:59:59
// 还剩:11:59:58
// ...
// 还剩:00:09:09

扩展知识

元编程

官方文档:元编程 - JavaScript | MDN
使用Reflect 和 Proxy,可以实现元级别的编程(可自定义基本语言操作(例如属性查找、赋值、枚举和函数调用等))

Reflect

官方文档:Reflect - JavaScript | MDN

Proxy

官方文档:Proxy - JavaScript | MDN
用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等),并且对应的操作也会转发到这个对象上
语法:new Proxy(target, handler)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const obj = {}

const handler = {
// 代理 get 操作(取值)
get(target, propertyKey){
console.log('Proxy get')
return target[propertyKey]
},
// 代理 set 操作(赋值)
set(target, propertyKey, value){
console.log('Proxy set')
target[propertyKey] = value
}
}

const p = new Proxy(obj, handler)
p.a = 1 // 赋值,打印:Proxy set,并且赋值操作也会转发到 obj 上
const a = p.a // 取值,打印:Proxy get
console.log(obj) // { a:1 }

2-2、TS 实战
https://mrhzq.github.io/职业上一二事/前端面试/前端八股文/2-2、TS 实战/
作者
黄智强
发布于
2024年1月13日
许可协议