javascript 装饰器
本文已参与「新人创作礼」活动,一起开启掘金创作之路。
装饰器介绍
- 开发中存在一些场景,需要给类或者类型的成员添加一下修改。装饰器就提供了一种为类和类的成员添加额外功能或修改的方法
- 根据使用位置的不同,我们可以把装饰器分为类装饰器、方法装饰器、访问器装饰器、属性装饰器、和参数装饰器,它们分别可以作用在类声明、方法、存取器、属性或者参数上。
- 使用形式为@expression
- 装饰器使用时会被当成函数调用,不同类型的装饰器参数有些许不同
使用装饰器
如果要对装饰器开启支持有两种方法
- 需要在tsconfig.json中开启
{
"conpilerOptions":{
"target":"ES5",
"experimentalDecorators":true
}
}
需要注意的是,如果target小于ES5,装饰器也能使用,但是在有些装饰器函数中,descriptor参数会不存在
- 可以在命令行中开启experimentalDecorators
tsc --target ES5 --experimentalDecorators
类装饰器
- 类装饰器用于类的声明之前
- 参数:被装饰类的构造函数
- 返回:可以返回一个新的构造函数替换类型声明
以下例子把 extendsAnimal装饰器用在了Animal上 其中装饰器函数的参数target就是[class Animal] 它扩展了Animal的功能,给出了一个默认的sex为male,age比参数多1岁
function extendsAnimal(target:typeof Animal){
console.log(target === Animal.prototype.constructor)
return class extends target{
sex = 'male'
age: number
constructor(name:string, age:number){
super(name, age)
this.age += 1
}
}
}
@extendsAnimal
class Animal{
constructor(public name:string, public age:number){}
}
interface Tom extends Animal{
sex:string
}
const tom = new Animal('tom',1) as Tom
console.log(tom.sex) // male
console.log(tom.age) //2
方法装饰器
- 方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。
- 参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
- 成员的属性描述符。(如果ts输出目标版本小于ES5,那么属性描述符为undefined)
- 返回:返回属性描述符descriptor或者没有返回
以下例子重写了action方法,使action可以打印年龄
function consoleAge(target: any, propertyKey:string, descriptor:PropertyDescriptor){
const value = descriptor.value;
if(typeof value === 'function'){
descriptor.value = function(){
console.log(`name:${this.name} age:${this.age} run!!`)
}
}
}
class Animal{
private _address:string = ''
constructor(public name:string, public age:number){}
@consoleAge
action(){
console.log('run');
}
}
interface Tom extends Animal{
sex:string
}
const tom = new Animal('tom',1) as Tom
tom.action() // name:tom age:1 run!!
访问器装饰器
- 访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。
- 参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
- 成员的属性描述符。
- 返回:返回属性描述符descriptor或者没有返回 以下例子通过访问器装饰器重写了get address方法,使get address可以打印名字
function formatAddress(target: any, propertyKey:string, descriptor:PropertyDescriptor){
descriptor.get = function(){
return `${this.name}住在${this._address}`
}
}
class Animal{
private _address:string = ''
constructor(public name:string, public age:number){}
@formatAddress
get address(){
return this._address;
}
set address(address:string){
this._address = address
}
}
interface Tom extends Animal{
sex:string
}
const tom = new Animal('tom',1) as Tom
tom.address = '猫窝'
console.log(tom.address) // tom住在猫窝
属性装饰器
- 属性装饰器声明在一个属性声明之前(紧靠着属性声明)。
- 参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
- 用法基本上同函数装饰器一样,只不过参数没有属性描述符
下边这个例子修改了Animal的静态属性,需要注意和上边例子的不同点是,这里装饰器修饰了静态属性,所以target变成了类本身
export function yellowColor(target:any,propertyKey:string ){
target[propertyKey] = 'yellow'
}
class Animal{
private _address:string = ''
@yellowColor static color:string = 'black'
}
interface Tom extends Animal{
sex:string
}
console.log(Animal.color); // yellow
参数装饰器
- 参数装饰器声明在一个参数声明之前(紧靠着参数声明)。
- 参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
- 参数在函数参数列表中的索引。
- 参数装饰器的返回值会被忽略。
下边这个例子组合使用函数装饰器和参数装饰器,利用闭包存储参数必填的索引,在函数执行的时候,检查索引对应的参数是否为空,并提示
// 利用闭包保存函数缺失的必填参数
export function validateFactory(){
const requiredMap:Record<string, number[]|undefined> = {}
function requiredParam(target:any, propertyKey:string, index:number){
const indexes = [...requiredMap[propertyKey] ||[], index]
// 保存必填参数
requiredMap[propertyKey] = indexes
}
function validateRequired(target:any, propertyKey:string, descriptor:PropertyDescriptor){
const method = descriptor.value
descriptor.value=function(){
Object.keys(requiredMap).forEach(methodName=>{
if(methodName === propertyKey){
requiredMap[methodName].forEach(index=>{
if(arguments[index]===undefined){
console.log(`${propertyKey} method miss required argument ${index}`)
}
})
}
})
return method.apply(this, arguments)
}
}
return {validateRequired, requiredParam}
}
// 获取真正的装饰器
const {validateRequired, requiredParam }= validateFactory()
class Animal{
constructor(public name:string, public age:number){}
@validateRequired
say(@requiredParam message:string, @requiredParam second?:string, @requiredParam third?:string){
console.log(`${this.name} say ${message}`)
}
@validateRequired
eat(@requiredParam message:string, @requiredParam second?:string){
console.log(`${this.name} eat ${message}`)
}
}
interface Tom extends Animal{
sex:string
}
const tom = new Animal('tom',1) as Tom
tom.say('hello' ,undefined,)
tom.eat('food')
// 打印如下,可以对必填字段提示
// say method miss required argument 2
// say method miss required argument 1
// tom say hello
// eat method miss required argument 1
// tom eat food
装饰器调用顺序
多个装饰器可以同时应用到一个声明上 可以书写在多行上
@log3
@log2
@log1
class Person{}
也可以书写在单行上
// 也可以这么写
@log3 @log2 @log1
class Animal{}
当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合log3和log2 log1时,复合的结果等同于log3(log2(log1(x)))。
在TypeScript里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:
- 由上至下依次对装饰器表达式求值。
- 求值的结果会被当作函数,由下至上依次调用。
// 装饰器可以用在一行或多行上
function log1(target) {
console.log('log1')
}
function log2(target) {
console.log('log2')
}
function log3(target) {
console.log('log3')
}
@log3
@log2
@log1
class Person{}
// 这个地方并没有new一个Person,也能打印
// 打印顺序
// log1
// log2
// log3
装饰器工厂和工厂的调用顺序
- 装饰器工厂就是一个简单的函数,它返回一个函数,返回的这个函数实际上就是需要调用的装饰器。
- 我们可以利用装饰器工厂传入额外参数。
- 先自上而下调用工厂函数创建装饰器,再自下而上调用装饰器
// 装饰器工厂1
function f() {
console.log("f(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("f(): called");
}
}
// 装饰器工厂2
function g() {
console.log("g(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("g(): called");
}
}
class C {
@f()
@g()
method() {
console.log('method called')
}
}
// 在new 之前装饰器工厂以及装饰器函数就会执行,顺序如下
// decoratorFirst
// decoratorFirst
// f(): evaluated
// g(): evaluated
// g(): called
// f(): called
const c = new C
c.method()
// 函数调用之后再执行 装饰完成的函数
// method called
不同的装饰器将按以下规定的顺序应用:
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
- 参数装饰器应用到构造函数。
- 类装饰器应用到类。
完整代码:github代码
参考资料:typescript装饰器
上一篇: 详解 Python 的装饰器
下一篇: 装饰器模式 C++
推荐阅读
-
[C 语言教程] [嵌入式程序设计] (I) 简介和先决条件 (II) 嵌入式程序设计基础 (III) 硬件基础 (IV) 硬件寄存器操作
-
系统架构设计器教程 第 19 章 19.4 Kappa 架构说明
-
网络服务器监控:Nginx 监控指标解释
-
使用计时器创建打开屏幕弹出窗口
-
微服务开源框架 TARS RPC 源代码初识 TARS C++ 服务器端
-
[X11 转发]解决远程服务器上无法显示可视图形用户界面的问题(Mac m1)
-
如何使用 @Builder 装饰器优化 HarmonyOS NEXT 中 UI 组件的重复使用?
-
Axure 重要组件 III - 中继器功能
-
[嵌入式设备] 蓝牙鼠标遥控器
-
卸载 RVM(Ruby 版本管理器)及其管理的所有 Ruby 版本