TypeScript 装饰器

前言

        Decorator是一种语法结构,用来在定义时修改类(class)的行为。

        因为 类声明后立刻就会执行修饰器,所以如果没有提前声明,就会报错。

语法特征

  1. 第一个字符(或者说前缀)是@,后面是一个表达式。
  2. @后面的表达式,必须是一个函数(或者执行后可以得到一个函数)
  3. 这个函数接受所修饰对象的一些相关值作为参数
  4. 这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象

     例子:有一个函数decorateFn()当作装饰器使用,那就写作:@decorateFn,然后放在某个类的前面

@decorateFn class A {
  // ...
}

        下面的代码把函数simpleDecorator()用作装饰器,附加在类A之上,后者在代码解析时就会打印一行日志。

function simpleDecorator() {
  console.log('hi');
}
@simpleDecorator
class A {} // "hi"

        但是会报错,因为没有用到装饰器的参数 

TypeScript 装饰器_第1张图片

         正确的写法:

function simpleDecorator(
  target:any,
) {
  console.log(target);
  return target;
}
@simpleDecorator
class A {} // "class A {}"

合法的装饰器

        装饰器有多种形式,基本上只要在@符号后面添加表达式都是可以的。

@myFunc
@myFuncFactory(arg1, arg2)
@libraryModule.prop
@someObj.method(123)
@(wrap(dict['prop']))

        只要最后得到一个函数就行

装饰器的好处

        相比使用子类改变父类,装饰器更加简洁优雅

        缺点是不那么直观,功能也受到一些限制

        所以,装饰器一般只用来为类添加某种特定行为。

@frozen class Foo {
  @configurable(false)
  @enumerable(true)
  method() {}
  @throttle(500)
  expensiveMethod() {}
}

        上面示例中,一共有四个装饰器,一个用在类本身(@frozen),另外三个用在类的方法(@configurable、@enumerable、@throttle)。它们不仅增加了代码的可读性,清晰地表达了意图,而且提供一种方便的手段,增加或修改类的功能。

装饰器的结构

        先看源码,装饰器函数的类型定义如下:

type Decorator = (
  value: DecoratedValue,
  context: {
    kind: string;
    name: string | symbol;
    addInitializer?(initializer: () => void): void;
    static?: boolean;
    private?: boolean;
    access: {
      get?(): unknown;
      set?(value: unknown): void;
    };
  }
) => void | ReplacementValue;

        Decorator是装饰器的类型定义。它是一个函数,使用时会接收到value和context两个参数。

  • value:所装饰的对象。
  • context:上下文对象

        而这个上下文对象context,其属性根据所装饰对象的不同而不同,但是kind和name这两个属性必须有。

        不必纠结这个context对象怎么出现的。先把它当做一个用户定义的对象变量。

        上下文对象context的键值:

        kind:值为字符串,表示所装饰对象的类型,可能取以下的值

  •         ‘class’  类装饰器
  •         ‘method’ 方法装饰器
  •         ‘getter’ getter取值器装饰器
  •         ‘setter’ setter存值器装饰器
  •         ‘field’  属性装饰器
  •         ‘accessor’ accessor 装饰器

        这表示一共有六种类型的装饰器。

      name:字符串或者 Symbol 值,所装饰对象的名字,比如类名、属性名等

      addInitializer():函数,用来添加类的初始化逻辑。以前,这些逻辑通常放在构造函数里面,对方法进行初始化,现在改成以函数形式传入addInitializer()方法。注意,addInitializer()没有返回值

       先随便看看,后面展开说说。

装饰器写法

普通装饰器(无法传参)和装饰器工厂(可以传参)

普通装饰器:

TypeScript 装饰器_第2张图片

装饰器工厂:

TypeScript 装饰器_第3张图片

可以看到装饰器工厂是传入一个自定义参数,并返回一个回调函数 。这个回调函数才是真正的装饰器。这种通过高阶函数返回装饰器的做法,称为“工厂模式”,即可以像工厂那样生产出一个模子的装饰器。具体例子请看后面的方法装饰器。

类装饰器

        类装饰器一般用来对类进行操作,可以不返回任何值

function Greeter(value, context) {
  if (context.kind === 'class') {
    value.prototype.greet = function () {
      console.log('你好');
    };
  }
}
@Greeter
class User {}
let u = new User();
u.greet(); // "你好"

          类装饰器@Greeter在类User的原型对象上,添加了一个greet()方法,实例就可以直接使用该方法。

        再看一个实际应用:

class Person {
    constructor () {}
    play () {}
    study () {}
    sleep () {}
}
class Student {
    constructor () {}
    play () {}
    study () {}
    sleep () {}
}
class Teacher {
    constructor () {}
    play () {}
    study () {}
    sleep () {}
}

        有三个类,他们都有三个相同的方法play study sleep

        于是我们可以用extend来实现:

class People {
    constructor () {}
    play () {}
    study () {}
    sleep () {}
}
// 每次定义其他类的时候进行继承
class Student extends People {}
class Person extends People {}
class Teacher extends People {}

        也可以用装饰器来实现:

function fn(params: any) {
    params.prototype.play = function () {}
    params.prototype.study = function () {}
    params.prototype.sleep = function () {}
}

@fn
class Student {}

@fn
class Person {}

@fn
class Teacher {}

        如果我们要更改一下:

        Student类只有play和study  Person只有sleep 老师只有sleep和study

        那么:

function addPlay(params: any) {
    params.prototype.play = function () {}
}
function addStudy(params: any) {
    params.prototype.study = function () {}
}
function addSleep(params: any) {
    params.prototype.sleep = function () {}
}


@addStudy
@addPlay
class Student {}


@addSleep
class Person {}


@addSleep
@addStudy
class Teacher {}

        类装饰器可以返回一个函数,替代当前类的构造方法。  

function countInstances(value:any, context:any) {
  let instanceCount = 0;
  const wrapper = function (...args:any[]) {
    instanceCount++;
    const instance = new value(...args);
    instance.count = instanceCount;
    return instance;
  } as unknown as typeof MyClass;
  wrapper.prototype = value.prototype; // A
  return wrapper;
}
@countInstances
class MyClass {}
const inst1 = new MyClass();
inst1 instanceof MyClass // true
inst1.count // 1

        上面示例中,类装饰器@countInstances返回一个函数,替换了类MyClass的构造方法。新的构造方法实现了实例的计数,每新建一个实例,计数器就会加一,并且对实例添加count属性,表示当前实例的编号。

        注意,上例为了确保新构造方法继承定义在MyClass的原型之上的成员,特别加入A行,确保两者的原型对象是一致的。否则,新的构造函数wrapper的原型对象,与MyClass不同,通不过instanceof运算符。

function countInstances(value:any, context:any) {
  let instanceCount = 0;
  return class extends value {
    constructor(...args:any[]) {
      super(...args);
      instanceCount++;
      this.count = instanceCount;
    }
  };
}
@countInstances
class MyClass {}
const inst1 = new MyClass();
inst1 instanceof MyClass // true
inst1.count // 1

        上面的例子。类装饰器返回一个新的类,替代原来所装饰的类。

方法装饰器

        方法装饰器会改写类的原始方法,实质等同于下面的操作。

function trace(decoratedMethod) {
  // ...
}
class C {
  @trace
  toString() {
    return 'C';
  }
}
// `@trace` 等同于
// C.prototype.toString = trace(C.prototype.toString);

        @trace是方法toString()的装饰器,它的效果等同于最后一行对toString()的改写。

        如果方法装饰器返回一个新的函数,就会替代所装饰的原始函数。

function replaceMethod() {
  return function () {
    return `How are you, ${this.name}?`;
  }
}
class Person {
  constructor(name) {
    this.name = name;
  }
  @replaceMethod
  hello() {
    return `Hi ${this.name}!`;
  }
}
const robin = new Person('Robin');
robin.hello() // 'How are you, Robin?'

       具体例子:利用方法装饰器,可以将类的方法变成延迟执行。

function delay(milliseconds: number = 0) {
  return function (value, context) {
    if (context.kind === "method") {
      return function (...args: any[]) {
        setTimeout(() => {
          value.apply(this, args);
        }, milliseconds);
      };
    }
  };
}
class Logger {
  @delay(1000)
  log(msg: string) {
    console.log(`${msg}`);
  }
}
let logger = new Logger();
logger.log("Hello World");

     方法装饰器@delay(1000)将方法log()的执行推迟了1秒(1000毫秒)。这里真正的方法装饰器,是delay()执行后返回的函数,delay()的作用是接收参数,用来设置推迟执行的时间。

属性装饰器

        属性装饰器声明在一个属性声明之前(紧靠着属性声明)。 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。

        源码里关于属性装饰器的描述是这样的:

type ClassFieldDecorator = (
  value: undefined,
  context: {
    kind: 'field';
    name: string | symbol;
    ...
  }
) => (initialValue: unknown) => unknown | void;

        装饰器的第一个参数value的类型是undefined,这意味着这个参数实际上没用的,装饰器不能从value获取所装饰属性的值。另外,第二个参数context对象的kind属性的值为字符串field。

        属性装饰器要么不返回值,要么返回一个函数,该函数会自动执行,用来对所装饰属性进行初始化。该函数的参数是所装饰属性的初始值,该函数的返回值是该属性的最终值。

function logged(value:undefined, context:any) {
  const { kind, name } = context;
  if (kind === 'field') {
    return function (originValue:string) {
      console.log(originValue); // green
      return 'red';
    };
  }
}
class Color {
  @logged name = 'green';
}
const color = new Color();
console.log(color.name); // red

getter 装饰器,setter 装饰器

        getter 装饰器的上下文对象context的access属性,只包含get()方法;setter 装饰器的access属性,只包含set()方法。

        这两个装饰器要么不返回值,要么返回一个函数,取代原来的取值器或存值器。

class C {
  @lazy
  get value() {
    console.log('正在计算……');
    return '开销大的计算结果';
  }
}
function lazy(
  value:any,
  {kind, name}:any
) {
  if (kind === 'getter') {
    return function (this:any) {
      const result = value.call(this);
      Object.defineProperty(
        this, name,
        {
          value: result,
          writable: false,
        }
      );
      return result;
    };
  }
  return;
}
const inst = new C();
inst.value
// 正在计算……
// '开销大的计算结果'
inst.value
// '开销大的计算结果'

        上面示例中,第一次读取inst.value,会进行计算,然后装饰器@lazy将结果存入只读属性value,后面再读取这个属性,就不会进行计算了。 

装饰器的执行顺序

        装饰器的执行分为两个阶段。

        (1)评估(evaluation):计算@符号后面的表达式的值,得到的应该是函数。

        (2)应用(application):将评估装饰器后得到的函数,应用于所装饰对象。

        也就是说,装饰器的执行顺序是,先评估所有装饰器表达式的值,再将其应用于当前类。

        应用装饰器时,顺序依次为方法装饰器和属性装饰器,然后是类装饰器。

        请看下面的例子。

function d(str:string) {
  console.log(`评估 @d(): ${str}`);
  return (
    value:any, context:any
  ) => console.log(`应用 @d(): ${str}`);
}
function log(str:string) {
  console.log(str);
  return str;
}
@d('类装饰器')
class T {
  @d('静态属性装饰器')
  static staticField = log('静态属性值');
  @d('原型方法')
  [log('计算方法名')]() {}
  @d('实例属性')
  instanceField = log('实例属性值');
}

        上面示例中,类T有四种装饰器:类装饰器、静态属性装饰器、方法装饰器、属性装饰器。

        它的运行结果如下。

// "评估 @d(): 类装饰器"
// "评估 @d(): 静态属性装饰器"
// "评估 @d(): 原型方法"
// "计算方法名"
// "评估 @d(): 实例属性"
// "应用 @d(): 原型方法"
// "应用 @d(): 静态属性装饰器"
// "应用 @d(): 实例属性"
// "应用 @d(): 类装饰器"
// "静态属性值"

        可以看到,类载入的时候,代码按照以下顺序执行。

      (1)装饰器评估:这一步计算装饰器的值,首先是类装饰器,然后是类内部的装饰器,按照它们出现的顺序。

        注意,如果属性名或方法名是计算值(本例是“计算方法名”),则它们在对应的装饰器评估之后,也会进行自身的评估。

      (2)装饰器应用:实际执行装饰器函数,将它们与对应的方法和属性进行结合。

        原型方法的装饰器首先应用,然后是静态属性和静态方法装饰器,接下来是实例属性装饰器,最后是类装饰器。

        注意,“实例属性值”在类初始化的阶段并不执行,直到类实例化时才会执行。

        如果一个方法或属性有多个装饰器,则内层的装饰器先执行,外层的装饰器后执行。

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  @bound
  @log
  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}


        上面示例中,greet()有两个装饰器,内层的@log先执行,外层的@bound针对得到的结果再执行。

你可能感兴趣的:(Typescript,typescript,javascript,前端)