TypeSript 5.2 有哪些新特性 – 装饰器metadata

TypeSript 5.2 有哪些新特性 - 装饰器metadata

装饰器metadata的英文是`Decorator Metadata`,装饰器大家用得应该很少,ECMAScript关于装饰器的提案也才是 Stage 3 阶段,期间经历了的较大的语法变更,现在终于快要成为正式标准了。而在装饰器提案的基础上,ECMAScript又有了新的扩展装饰器功能的提案:`装饰器metadata`。该提案也已经进入了 Stage 3,达到了稳定阶段。TypeScript在5.2版本中正式实现了该提案,下面咱们就来对它进行讲解。

装饰器是什么

详细的装饰器功能的讲解我们会另开一篇文章。这里我们只会简单讲解JavaScript的新语法-装饰器,目的是为了让大家更轻松地了解这篇文章的主角装饰器metadata

在此说明,我们这里讲解的是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的函数。

上面这个例子看不出它能干啥,我们下面再举一个例子:

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,那我们用伪代码解释是这样:

// f是装饰器返回的函数
// newM是类实例真正调用的函数
newM = f(initialM) {
	// initialM是类成员函数
	initialM();
}

成员函数m被装饰器中的函数包起来了,函数嵌套函数,就像装修一样,外面包一层装饰,所以叫装饰器。

这对于架构师来说又是一个大利好。
例如,架构师想把所有的登录判断逻辑抽象出来,而不用每次都让开发人员自己判断,那就可以这样:

// 装饰器由架构师统一封装
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举例:

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对象就可以了。
举例如下:

function needLogin(value, {metadata}) {
}

class C {
  @needLogin
  m(arg) {
  }
}
// C类的metadata
console.log(C[Symbol.metadata]);

特点3

所有作用于同一个类的装饰器中的metadata都是同一个对象。
举例如下:

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]);

这里的ab是相等的,因为同一个类中的metadata对象是同一个。

特点4

不同的类有不同的metadata对象,不能共享。所以类和metadata是一一对应关系,注意是和类而不是类的实例。
举例如下:

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


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
如下所示:

function needLogin(value, context: DecoratorContext) {
	const metadata = context.metadata;
}

class C {
  @needLogin
  m(arg) {
  }
}

不过TypeScript针对所装饰的类元素类型的不同,又分成了几个更细分的类型:
字段装饰器的Context:ClassFieldDecoratorContext
方法装饰器的Context:ClassMethodDecoratorContext
Setter装饰器的Context:ClassSetterDecoratorContext
等等。。。
它们都大同小异,都是下面的类型形式:

{
    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配置文件也需要进行相应的设置:

{
    "compilerOptions": {
        "lib": ["esnext.decorators"]
    }
}

lib中必须要有esnext.decorators或者esnext

结束

装饰器metadata的特性介绍到此结束。

TypeScript的这一新特性实际上和JavaScript的新特性一致的,因此我们学完TypeScript的这一新特性后,JavaScript的这一新特性也就基本上都了解了。

原文链接:https://juejin.cn/post/7328319117601652770 作者:吉灵云

(0)
上一篇 2024年1月28日 上午10:16
下一篇 2024年1月28日 上午10:26

相关推荐

发表回复

登录后才能评论