Skip to content

装饰器stage2

✍️ w 🕒 2024-06-21 15:59:59(21 hours ago) 🔗 A.前端知识整理

在JS中,装饰器是一个函数。(装饰器是要参与运行的)ts 和 js 装饰器本质是一样因此这里采用ts 作为说明案例

装饰器可以修饰:

  • 成员(属性+方法+访问器装饰器)
  • 参数
装饰器名称装饰器描述装饰器的参数说明
类装饰器(Class Decorators)应用于类构造函数,可以用于修改类的定义。constructor: Function
方法装饰器(Method Decorators)应用于方法,可以用于修改方法的行为。target: Object, propertyKey: string, descriptor: PropertyDescriptor
访问器装饰器(Accessor Decorators)应用于类的访问器属性(getter 或 setter)。target: Object, propertyKey: string, descriptor: PropertyDescriptor
属性装饰器(Property Decorators)应用于类的属性。target: Object, propertyKey: string
参数装饰器(Parameter Decorators)应用于方法参数。target: Object, propertyKey: string, parameterIndex: number

所有案例的tsconfig 配置

json
{
  "compilerOptions": {
    "target": "esnext",
    "experimentalDecorators": true,
    "moduleResolution": "NodeNext",
    "module": "NodeNext",
    "outDir": "dist"
  }
}

类装饰器

类装饰器本质是一个函数,该函数接收一个参数,表示类本身(构造函数本身),允许你修改类的行为或添加新的行为,而无需直接修改类的源代码。通过使用类装饰器,使用装饰器 @ 得到一个函数

通过下面案例可以发现装饰器函数有一个参数,这个参数 target 因此知道target 是谁就可以确定给所加的元数据针对。 下面代码运行结果可以发现其实就是构造函数

ts
let o: any;

function classDecoration(target: any) {
  o = target;
}

@classDecoration
class A {}

const a = new A();
console.log(o === A); // true
console.log(o === a.constructor); // true
console.log(o === Reflect.getPrototypeOf(a).constructor); // true

知道了target 其实就是构造函数那么ts 类型这里就不能是 any 应该是表示类本身(构造函数本身),在TS中,构造函数的表示

  • Function
  • new (...args:any[]) => any
ts
// 定义为Function
function classDecoration(target: Function) { 
  console.log("classDecoration");
  console.log(target)
}
@classDecoration
class A { }
ts
// 定义为构造函数
function classDecoration(target: new (...args: any[]) => any) {
  console.log("classDecoration");
  console.log(target)
}
@classDecoration
class A { }
  • 进一步可以通过泛型约束 来进一步完善 ts 写装饰器
ts
// 泛型类型别名
type constructor<T = any> = new (...args: any[]) => T;
function classDecoration(target: constructor) {
  console.log("classDecoration");
  console.log(target)
}
@classDecoration
class A { }

具体约束

ts
type constructor<T = any> = new (...args: any[]) => T;
type User = {
  id: number
  name: string
  info(): void
}

function classDecoration<T extends constructor<User>>(target: T) {
  console.log("classDecoration");
  console.log(target)
}
@classDecoration
class A { 
  constructor(public id: number, public name: string) { }
  info(){}
}

运行时机

类装饰器的运行时机是在定义这个类的时候,就会运行。而不是必须要等到你 new 对象的时候才会执行,可以看一下 ts 编译后js 的文件

js
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function classDecoration(target) {
    console.log("classDecoration");
}
let A = class A {
};
// 其实通过编译之后的代码,就可以看到,直接运行了__decorate函数
A = __decorate([
    classDecoration
], A);

使用案例

类装饰器可以具有的返回值:

  • void:仅运行函数
  • 返回一个新的类:会将新的类替换掉装饰目标

属性和方面的绑定

如果我们想通过传入一些参数这里 就需要使用 工厂模式的效果进行,只要最后返回一个装饰器函数即可

ts
type constructor<T = any> = new (...args: any[]) => T;
function classDecorator<T extends constructor>(str: string) {
  console.log("普通方法的参数:" + str);
  return function (target: T) { 
    console.log("类装饰器" + str)  
  }
}

@classDecorator("hello")
class A { 
}

因为 target 是一个构造函数,因此你可以绑定静态属性或者静态方法(也就是类属性类方法),也可以通过 prototype上进行 装饰器给实例添加属性和方法,注意属性绑定在原型链上会污染的不建议将属性绑定在原型链上

ts
type constructor<T = any> = new (...args: any[]) => T;

function classDecorator<T extends constructor>(obj: Record<string, any>) {
  return function (target: T) {
    Reflect.ownKeys(obj).forEach((key) => {
      // 绑在类属性和类方法上
      Reflect.set(target, key, Reflect.get(obj, "key"));

      // 绑在原型链上
      Reflect.set(target.prototype, key, Reflect.get(obj, "key"));
    });
  };
}

@classDecorator({
  age: "222",
  getName() {
    return this.age;
  },
})
class A {}

console.log(A);
console.log(new A());

  • 混入多个对象
ts
type constructor<T = any> = new (...args: any[]) => T;

function classDecorator<T extends constructor>(...list: Record<string, any>[]) {
  return function (target: T) {
    // 利用装饰混入多个属性案例
    Object.assign(target.prototype, ...list);
  };
}

@classDecorator({
  getName() {
    return this.age;
  },
})
class A {}

console.log(A);
console.log(new A());

实现继承

如果装饰返回的是一个class 则变相继承,也就是装饰器返回的类替换了被装饰的类,并且在ts 这些不会进行类型推导会导致说返回的新的类,并不知道有新加的内容

案例中很明显发现 对象实例构造函数不是 A 而是 [class (anonymous) extends A]

ts
type constructor<T = any> = new (...args: any[]) => T;

function classDecorator<T extends constructor>(target: T) {
  return class extends target {
    public newProperty = "new property";
    public hello = "override";
    info() {
      console.log("this is info");
    }
  };
}
@classDecorator
class A {
  public hello = "hello world";
}
const objA = new A();

console.log(objA.constructor); // [class (anonymous) extends A]
console.log(objA.hello); // override
console.log((objA as any).newProperty); //   new property 很明显,没有类型使用any
(objA as any).info(); // this is info
ts
function replaceConstructor<T extends { new(...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
        constructor(...args: any[]) {
            super(...args);
            console.log("Instance created");
        }
    };
}

@replaceConstructor
class User {
    constructor(public name: string) {}
}

const user = new User("Alice");

没有返回继承

如果在构造函数的时候没有去继承 target 而是直接去返回class 则装饰器的 class 会覆盖被装饰器修饰的class 本身

ts
type constructor<T = any> = new (...args: any[]) => T;

function classDecorator<T extends constructor>(target: T) {
  return class {
    public hello = "override";
  };
}
@classDecorator
class A {
  public hello = "hello world";
}
const objA = new A();

console.log(objA.constructor); // [class (anonymous)]

console.log(objA.hello); // override
  • 要注意的是多个装饰器修饰后使用工厂函数传参他们执行顺序从下到上,但是 被修饰装饰器是从下到上

成员装饰器(属性和方法装饰器)

  • 属性

属性装饰器也是一个函数,该函数需要两个参数:

  1. 如果是静态属性,则为类本身;如果是实例属性,则为类的原型;
  2. 固定为一个字符串,表示属性名
  • 方法(获取当前实例this 参考日志的案例中this指向)

方法装饰器也是一个函数,该函数需要三个参数:

  1. 如果是静态方法,则为类本身;如果是实例方法,则为类的原型;
  2. 固定为一个字符串,表示方法名
  3. 属性描述对象 ( configurable, enumerable , value , writable , get , set )
ts
function params(target: any, key: string) {
  console.log(target, key);
}

function fun(target: any, key: string, des: PropertyDescriptor) {
  console.log(target, key, des);
}

class A {
  @params
  static age: string;

  @params
  name: string;

  @fun
  static getAge() {
    return A.age;
  }

  @fun
  getName() {
    return this.name;
  }
}

// 说明一下 {} 其实就是原型对象 既 A.prototype
// {} name
// {} getName {
//   value: [Function: getName],
//   writable: true,
//   enumerable: false,
//   configurable: true
// }
// [class A] age
// [class A] getAge {
//   value: [Function: getAge],
//   writable: true,
//   enumerable: false,
//   configurable: true
// }

使用案例

这说明一下 js 属性装饰器其实有第三个参数 属性描述对象 但是不能去修改他的value,在ts 中是没有的

给方法在执行前后做拦截

ts
function enumerable() {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    console.log(target, key, descriptor)
    descriptor.enumerable = true;
  }
}

// 被废弃的方法
function noUse() { 
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    descriptor.value = function () { 
      console.log("被废弃的方法");
    }
  }
}

function interceptor(str: string) { 
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const temp = descriptor.value;
    descriptor.value = function (...args: any[]) { 
      console.log("前置拦截---" + str);
      temp.call(this, args);
      console.log("后置拦截---" + str);
    }
  }
}

class A {
  prop1: string;
  prop2: string;
  @enumerable()
  method1() { }
  
  @enumerable()
  @noUse()  
  method2() {
    console.log("正常执行......")
  }

  @enumerable()
  @interceptor("interceptor")
  method3(str: string) {
    console.log("正在执行 method3:" + str)
  }
}

const objA = new A();

for(let prop in objA){
  console.log(prop)
}
// 执行被废弃的方法
objA.method2();

// 拦截
objA.method3("hello world");

做一些数据缓存

ts
function cache(target: any, propKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  const cacheMap = new Map<string, any>();
  descriptor.value = function (...args: any[]) {
    const key = JSON.stringify(args);
    if (cacheMap.has(key)) {
      return cacheMap.get(key);
    }
    console.log(`==阶乘: ${args}`);
    const result = original.apply(this, args);
    cacheMap.set(key, result);
    return result;
  };
}

class MathOperations {
  @cache
  factorial(n: number): number {
    if (n <= 1) return 1;
    return n * this.factorial(n - 1);
  }
}

给方法加日志

ts
/**
 *
 * @param target 如果是类的静态方法,target就是类本身; 如果是实例对象,则target是实例
 * @param propKey 装饰的成员名称
 * @param descriptor 成员的属性喵舒服
 */

function log(target, propKey, descriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propKey} with arguments: ${args}`);
    const result = originalMethod.apply(this, args);
    console.log(`Result: ${result}`);
    return result;
  };
}

class Calculator {
  @log
  add(a: number, b: number): number {
    return a + b;
  }
}
const c1 = new Calculator();
console.log(c1.add(1, 6));

// Calling add with arguments: 1,6
// Result: 7
// 7

权限配置

ts
let users = {
  "001": { roles: ["admin"] },
  "002": { roles: ["member"] },
  "003": { roles: ["admin"] },
};

function authorize(
  target: any,
  propKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    let user = users[args[0]];
    if (user && user.roles.includes("admin")) {
      original.apply(this, args);
    } else {
      throw new Error("User is not authorized");
    }
  };
}

class AdminPanel {
  @authorize
  deleteUser(userId: string) {
    console.log(`user ${userId} is deleted`);
  }
}
const adminPanel = new AdminPanel();
adminPanel.deleteUser("002"); // throw error

设置描述符

可以直接返回一个描述符对象

ts
function authorize(
  target: any,
  propKey: string,
  descriptor: PropertyDescriptor
) {
  // 直接放回一个描述符的对象
  return {
    value(params) {
      console.log(params, 122);
      return "我被装饰了";
    },
    // 如果不配置下面三个项走他们的默认值
    writable: false,
    configurable: true,
    enumerable: true,
  };
}

class AdminPanel {
  @authorize
  deleteUser(userId: string) {}
}
js
function isEnumerable(params) {
    return (target, name, descriptor)=>{
        descriptor.enumerable = params
        console.log(descriptor)
        return descriptor
    }
}

class Person{
    @isEnumerable(true)
    speak(params){
        return '我瞎说'
    }
}

const person = new Person()

for(let i in person){
    console.log(i) // speak
}

访问器属性装饰器

访问器饰器也是一个函数,该函数需要三个参数:

  1. 类的原型;
  2. 固定为一个字符串,表示方法名
  3. 属性描述对象 ( configurable, enumerable , writable , get , set )

可以获取到this,其实只要给get 或 set 任意加上装饰器就可以进行拦截

使用案例

基础案例

ts
function d(str: string) {
  return function d<T>(
    target: any,
    key: string,
    descriptor: TypedPropertyDescriptor<T>
  ) {
    console.log(target, key);
    const temp = descriptor.set!;
    descriptor.set = function (value: T) {
      console.log("前置", str);
      temp.call(this, value);
      console.log("后置", str);
    };
  };
}

class User {
  public id: number;
  public name: string;
  private _age: number;

  @d("hello")
  set age(v: number) {
    console.log("set", v);
    this._age = v;
  }
}

const u = new User();
u.age = 10;

// {} age
// 前置 hello
// set 10
// 后置 hello

日志案例拦截判断get 还是set

ts
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalGet = descriptor.get;
    const originalSet = descriptor.set;
    if (originalGet) {
        descriptor.get = function() {
            const result = originalGet.apply(this);
            console.log(`Getting value of ${propertyKey}: ${result}`);
            return result;
        };
    }
    if (originalSet) {
        descriptor.set = function(value: any) {
            console.log(`Setting value of ${propertyKey} to: ${value}`);
            originalSet.apply(this, [value]);
        };
    }
    return descriptor;
}
class User {
    private _name: string;
    constructor(name: string) {
        this._name = name;
    }
    @log
    get name() {
        return this._name;
    }
    set name(value: string) {
        this._name = value;
    }
}
const user = new User("Alice");
console.log(user.name); // Getting value of name: Alice
user.name = "Bob"; // Setting value of name to: Bob
console.log(user.name); // Getting value of name: Bob

设置权限

ts
function adminOnly(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalGet = descriptor.get;
  descriptor.get = function () {
    const user = { role: "user" }; // 示例用户对象
    if (user.role !== "admin") {
      throw new Error("Access denied");
    }
    return originalGet.apply(this);
  };
  return descriptor;
}
class SecureData {
  private _secret: string = "top secret";
  @adminOnly
  get secret() {
    return this._secret;
  }
}
const data = new SecureData();
try {
  console.log(data.secret); // 抛出错误: Access denied
} catch (error) {
  console.log(error.message);
}

方法参数装饰器

方法参数几乎和属性装饰器一致,只是多了一个属性

  1. 如果是静态属性,为类本身;如果是实例属性,为类的原型

  2. 字符串,表示方法名

  3. 表示参数顺序

使用案例

ts
function paramDecorator() {
  return function (target: any, key: string, index: number) {
    console.log(target, key, index);
  };
}

class A {
  method1(@paramDecorator() id: number, @paramDecorator() name: string) {
    console.log("---", id, name);
  }
}

const objA = new A();
objA.method1(1, "hello");

// {} method1 1
// {} method1 0
// --- 1 hello

技巧

装饰器来看无论哪一种装饰器都不会获取实例对象,这是因为 装饰器在创建过程就生效。而不是在 new 创建实例时候才生效

需要确保装饰器在每个类实例创建时为实例属性赋值这通常是通过在构造函数中设置这些属性来完成的,但是由于装饰器不能直接访问类的构造函数,我们可以使用一点策略来解决

可以先挂载然后在创建实例时候去执行

ts
function d(value: string) {
 return function (target: any, key: string) {
   if (!target.__initProperties) {
     target.__initProperties = function () {
       for (let prop in target.__props) {
         this[prop] = target.__props[prop];
       }
     };
     target.__props = {};
   }
   target.__props[key] = value;
 };
}

class A {
 @d("hello")
 prop1: string;

 @d("world")
 prop2: string;

 constructor() {
   if (typeof this["__initProperties"] === "function") {
     this["__initProperties"]();
   }
 }
}

const a = new A();
console.log(a.prop1); // Output: "hello"
console.log(a.prop2); // Output: "world"
  • 改变类实例在构造函数时候声明
ts
function defaultValues(defaults: { [key: string]: any }) {
    return function <T extends { new(...args: any[]): {} }>(constructor: T) {
      return class extends constructor {
        constructor(...args: any[]) {
          super(...args);
          Object.keys(defaults).forEach(key => {
            if (this[key] === undefined) {
              this[key] = defaults[key];
            }
          });
        }
      }
    }
  }
  
  @defaultValues({
    theme: "dark",
  })
  class Settings {
    theme: string;
  }
  
  const s1 = new Settings();
  console.log(s1.theme);  // 输出应该是 "dark"

这里有个小冲突 "target": "esnext" 如果你配置的是 "target": "es2015" 你完全可以使用 get set 进行拦截也就是这样是生效的,但是这样的代码在 "target": "esnext" 不生效的,但这种在原型链上的绑定属于污染 也不建议这么写

ts
//属性访问控制
//还可以实现用属性装饰器来进行访问控制或者设置初始设置

function defaultValue(value:any){
    return function(target:any,propertyKey:string){
        let val = value;
        const getter = function(){
            return val;
        }
        const setter = function(newValue){
            val = newValue
        }
        //在类的原型上定义了一个属性
        Object.defineProperty(target,propertyKey,{
            enumerable:true,
            configurable:true,
            get:getter,
            set:setter
        });
    }
}

class Settings{
    @defaultValue('dark')
    theme:string
}

const settings = new Settings();
console.log(settings.theme)

执行顺序

  1. 属性装饰器(Property Decorators)和方法装饰器(Method Decorators)以及访问器装饰器(Accessor Decorators)
  • 按照它们在类中出现的顺序,从上到下依次执行。
  1. 参数装饰器(Parameter Decorators)
  • 在执行方法装饰器之前执行,按照参数的位置从左到右依次执行。
  1. 类装饰器(Class Decorators)
  • 最后执行。
ts
function classDecorator() {
    return function (constructor: Function) {
        console.log('Class decorator');
    };
}

function methodDecorator() {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log('Method decorator');
    };
}

function accessorDecorator() {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log('Accessor decorator');
    };
}

function propertyDecorator() {
    return function (target: any, propertyKey: string) {
        console.log('Property decorator');
    };
}

function parameterDecorator() {
    return function (target: any, propertyKey: string, parameterIndex: number) {
        console.log('Parameter decorator');
    };
}

@classDecorator()
class Example {
    @propertyDecorator()
    prop: string;

    @accessorDecorator()
    get myProp() {
        return this.prop;
    }

    @methodDecorator()
    method(@parameterDecorator() param: any) {
        console.log('Method execution');
    }
}

// Property decorator
// Accessor decorator
// Parameter decorator
// Method decorator
// Class decorator

参考

网道 / WangDoc.com

Released under the MIT License.