装饰器metadata的英文是`Decorator Metadata`,装饰器大家用得应该很少,ECMAScript关于装饰器的提案也才是 Stage 3 阶段,期间经历了的较大的语法变更,现在终于快要成为正式标准了。而在装饰器提案的基础上,ECMAScript又有了新的扩展装饰器功能的提案:`装饰器metadata`。该提案也已经进入了 Stage 3,达到了稳定阶段。TypeScript在5.2版本中正式实现了该提案,下面咱们就来对它进行讲解。
装饰器是什么
详细的装饰器功能的讲解我们会另开一篇文章。这里我们只会简单讲解JavaScript的新语法-装饰器,目的是为了让大家更轻松地了解这篇文章的主角装饰器metadata
。
在此说明,我们这里讲解的是JavaScript的新语法装饰器,由于和旧的装饰器有很大的不同,所以不说明这一点,大家可能会很困惑。
一个简单的装饰器:
javascript
function logged(value, { kind, name }) {
// value:m方法
console.log('当前被装饰的类的元素', value);
// kind:method
console.log('被装饰元素在类中的角色', kind);
// name: 'm'字符串
console.log('被装饰的类的元素名称', name);
}
class C {
@logged
m(arg) {}
}
new C().m(1);
这里的@logged
就是一个装饰器,它本质是一个函数logged
,被装饰元素的信息会被传给logged
的函数。
上面这个例子看不出它能干啥,我们下面再举一个例子:
javascript
function logged(value, { kind, name }) {
return function (...args) {
// 该函数替换了m成员函数
console.log('装饰器中的函数被运行');
// value就是m成员函数,m会在这里得到调用
const ret = value.call(this, ...args);
return ret;
}
}
class C {
@logged
m(arg) {
console.log('m成员函数被运行', arg);
}
}
new C().m(1);
// 运行结果:
// 装饰器中的函数被运行
// m成员函数被运行 1
当调用new C().m(1)
时,成员函数m
并没有直接被C
的实例调用,而是调用的装饰器返回的匿名函数,然后匿名函数中可以决定该怎样调用m
函数。 假设我们的匿名函数叫f
,那我们用伪代码解释是这样:
javscript
// f是装饰器返回的函数
// newM是类实例真正调用的函数
newM = f(initialM) {
// initialM是类成员函数
initialM();
}
成员函数m
被装饰器中的函数包起来了,函数嵌套函数,就像装修一样,外面包一层装饰,所以叫装饰器。
这对于架构师来说又是一个大利好。 例如,架构师想把所有的登录判断逻辑抽象出来,而不用每次都让开发人员自己判断,那就可以这样:
javascript
// 装饰器由架构师统一封装
function needLogin(value, { kind, name }) {
return function (...args) {
if(isLogin) {
// 如果用户已登录,则继续调用m
return value.call(this, ...args);
}
// 如果用户未登录,则直接返回,不继续调用m
return {status: -1, message: '你没有登录'};
}
}
class C {
@needLogin
m(arg) {
// 这里不用再判断用户是否登录
}
}
new C().m(1);
这样以后如果某个功能需要登录,他只需要加一个@needLogin
的装饰器。
相信大家对装饰器已经有初步了解了。下面咱们看一下TypeScript这次的新特性。
装饰器metadata
这个名字有点太唬人了,其实质内容非常简单。 咱们还是先拿JavaScript举例:
javascript
function needLogin(value, {metadata}) {
// metadata就是这次要讲的装饰器metadata,他是一个普通JavaScript对象
console.log('metadata', metadata);
}
class C {
@needLogin
m(arg) {
}
}
needLogin
这个装饰器函数的第二个参数会返回被装饰元素的一些信息,以对象的形式进行组织,这个对象的成员如下: {kind, name, static, private, access, addInitializer, metadata}
其中metadata
就是这次的新特性:装饰器metadata。它有如下特点:
特点1
标准提案中说它是一个普通JavaScript对象,也就是{}
这样的形式,在TypeScript5.2中,如果所装饰的类没有自定义继承关系时,这个对象是由Object.create(null)
创建的。
特点2
metadata
对象除了可以在装饰器中访问,还可以通过类的静态属性Symbol.metadata
来访问,例如类C,那metadata
可以这样访问:C[Symbol.metadata]
,不过需要注意的是,由于需要通过类变量来访问,那就需要等类创建完成才能使用,装饰器内部是不能这样访问的,装饰器内部直接用传入的metadata
对象就可以了。 举例如下:
javascript
function needLogin(value, {metadata}) {
}
class C {
@needLogin
m(arg) {
}
}
// C类的metadata
console.log(C[Symbol.metadata]);
特点3
所有作用于同一个类的装饰器中的metadata
都是同一个对象。 举例如下:
javascript
let a, b;
function meta(value, {metadata}) {
a = metadata;
}
function needLogin(value, {metadata}) {
b = metadata;
}
class C {
@meta
foo = 123;
@needLogin
m(arg) {
}
}
// 相等
console.log(a == b == C[Symbol.metadata]);
这里的a
和b
是相等的,因为同一个类中的metadata
对象是同一个。
特点4
不同的类有不同的metadata
对象,不能共享。所以类和metadata
是一一对应关系,注意是和类而不是类的实例。 举例如下:
javascript
function needLogin(value, {metadata}) {
}
class C {
@needLogin
m(arg) {
}
}
class D {
@needLogin
n(arg) {
}
}
// 不相等
console.log(C[Symbol.metadata] != D[Symbol.metadata])
类C和类D的metadata
对象是不同的对象,尽管它们使用了相同的装饰器。
特点5
当一个类继承于父类,且父类也用了装饰器时,其metadata
则也继承于父类的metadata
。
javascript
function needLogin(value, {metadata}) {
}
class C {
@needLogin
m(arg) {
}
}
class D extends C{
@needLogin
n(arg) {
}
}
// 相等
console.log(Object.getPrototypeOf(D[Symbol.metadata]) == C[Symbol.metadata]);
类有继承关系时,它们的metadata
也有继承关系。
到这里JavaScript的这一新特性就讲完了。
Typescript中的用法
TypeScript关于这一特性的用法和JavaScript是一致的,只是多了类型支持。 TypeScript中,装饰器函数的第二个参数的类型有一个总类型叫做DecoratorContext
。 如下所示:
typescript
function needLogin(value, context: DecoratorContext) {
const metadata = context.metadata;
}
class C {
@needLogin
m(arg) {
}
}
不过TypeScript针对所装饰的类元素类型的不同,又分成了几个更细分的类型: 字段装饰器的Context:ClassFieldDecoratorContext
方法装饰器的Context:ClassMethodDecoratorContext
Setter装饰器的Context:ClassSetterDecoratorContext
等等。。。 它们都大同小异,都是下面的类型形式:
typescript
{
readonly kind: string;
readonly name: string | symbol;
readonly static: boolean;
readonly private: boolean;
readonly access: {
has(object: This): boolean;
set(object: This, value: Value): void;
};
addInitializer(initializer: (this: This) => void): void;
readonly metadata: DecoratorMetadata;
}
最下面的metadata
就是这次的新特性。大家可以下载TypeScript 5.2以上的版本,或者直接下载最新的VSCode,然后写一个装饰器,借助VSCode就可以看到相关的类型定义。
如果你不想分这么细,那也可以像上面的示例代码一样,用DecoratorContext
代替。
Polyfill
装饰器metadata这个特新性很多JavaScript运行环境还没有实现,即使使用TypeScript那也需要Polyfill,这里需要的Polyfill主要是: Symbol.metadata ??= Symbol("Symbol.metadata");
这个还是挺简单的,只需要把Symbol.metadata
赋上值就可以了。如果执行环境有版本较低的情况,可以把??=
换成if else
。这句话可以放到TypeScript中,也可以放到JavaScript中,放到TypeScript中时,需要在这行代码上面加上// @ts-ignore
,因为Symbol.metadata
在TypeScript被认为是只读属性,不能修改。
另外TypeScript配置文件也需要进行相应的设置:
typescript
{
"compilerOptions": {
"lib": ["esnext.decorators"]
}
}
lib
中必须要有esnext.decorators
或者esnext
。
结束
装饰器metadata的特性介绍到此结束。
TypeScript的这一新特性实际上和JavaScript的新特性一致的,因此我们学完TypeScript的这一新特性后,JavaScript的这一新特性也就基本上都了解了。