装饰器

148 阅读9分钟

image.png

写在前面:

后面跟随思路写的例子我们都可以通过:

www.typescriptlang.org/zh/play?#co…

在线进行运行,可以实时看到日志,控制台,包括直接编译成js

什么是装饰器:

装饰器模式

装饰器模式(Decorator Pattern)是一种结构型设计模式,旨在促进代码复用,可以用于修改现有的系统,希望在系统中为对象添加额外的功能,同时又不需要大量修改原有的代码。

JS中的装饰器

JS中的装饰器是ES7中的一个新语法,TS 中装饰器使用@expression这种模式,装饰器本质上就是函数。装饰器可以作用于: 1. 类声明 2. 方法 3. 访问器(getter/setter) 4. 属性 5. 方法参数,从而进行装饰, 它的写法与Java的注解(Annotation)类似,但是功能有比较大的区别。

特点

装饰器的特点是:在不改变对象自身结构的前提下,向对象添加新的功能。

使用注意

需要注意的是,若要启用实验性的装饰器特性,你必须在命令行或 tsconfig.json 里启用 experimentalDecorators 编译器选项:

命令行:

tsc -- target ES5 -- experimentalDecorators

json文件配置:

tsconfig. json:

{

  "compilerOptions": {

      "target" : "ES5",

      "experimentalDecorators" : true

   }

}

装饰器分类:

  • 类装饰器

  • 属性装饰器

  • 方法装饰器

  • 参数装饰器

  • 访问器装饰器

了解

先来快速认识一下这 5 种装饰器:


// 类装饰器

@classDecorator

class Person {

  // 属性装饰器

  @propertyDecorator

  name: string;

  // 方法装饰器

  @methodDecorator

  intro(

  // 方法参数装饰器

  @parameterDecorator words: string

  ) {}

  // 访问器装饰器

  @accessDecorator

  get Name() {}

}

// 此时的 Person 已经是被装饰器增强过的了

const p= new Person()

装饰器只在解释执行时应用一次,比如上面的例子中,在完成 Person 的声明后,就已经应用了装饰器,之后的所有实例化都是增强过的 Person。

如何使用:

我们就只分析类装饰器和方法装饰器,其他装饰器的逻辑大都大同小异

类装饰器

类装饰器可用于继承现有的类,或者为现有类添加属性和方法。其类型声明如下::

type ClassDecorator = < TFunction extends Function>( target: TFunction) => TFunction | void;

参数

  • target:类的构造函数

  • 返回值:如果类装饰器返回了一个非空的值,那么该值将用来替代原本的类

举个例子 ,为Person类添加run方法:

type Constructor = new (... args: any[]) => Object;

function addRun( target: Constructor) {

  target. prototype. run = function () {

      console. log( "我在狂奔");

   };

}

@addRun

class Person {}

new Person(). run(); // 我在狂奔

方法装饰器

方法装饰器可用于修改方法原本的实现。其类型声明如下:

declare type MethodDecorator = < T>( target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor< T>) => TypedPropertyDescriptor< T> | void;

参数:

  • target:修饰静态方法时,是类的构造方法;否则是类的原型(prototype)

  • propertyKey: 方法名

  • descriptor:方法的描述对象

  • 返回值:如果方法装饰器返回了一个非空的值,那么该值将用来替代方法原本的描述对象

举个例子,为Person类的run方法进行改变:

function addLog( target: any, key: string, descriptor: PropertyDescriptor) {

descriptor. value = function () {

  console. log( "我在休息");

    };

}

class Person {

@addLog

run() {

  console. log( "我在狂奔");

    }

}

new Person(). run();

此时会发现现在执行run方法打印的不是"我在狂奔"而是"我在休息"

装饰器工厂

那么我们要想在执行run方法打印"我在学习","我在吃饭"呢?

写多个装饰器显然过于繁琐麻烦。那么我们可以使用装饰器工厂函数。装饰器工厂本质也是函数,它会返回装饰器表达式供装饰器运行时调用。直接上例子:

function addLog( doing: string) {

return function ( target: any, key: string, descriptor: PropertyDescriptor) {

  descriptor. value = function () {

    console. log( `我在 ${ doing } `);

  };

};

}

class Person {

@addLog( "吃饭")

@addLog( "休息")

intro() {

  console. log( "我在狂奔");

}

}

new Person(). intro();

装饰器执行顺序

先从外层到内层求值装饰器(如果是函数工厂的话)

应用装饰器时,是从内层到外层


function fn( str: string) {

console. log( "求值装饰器:", str);

return function () {

  console. log( "应用装饰器:", str);

};

}

function decorator() {

console. log( "应用其他装饰器");

}

class T {

@fn( "外层")

@decorator

@fn( "内层")

method() {}

}

代码将会输出:

求值装饰器: 外层
求值装饰器: 内层
应用装饰器: 内层
应用其他装饰器
应用装饰器: 外层

对于不同的类型的装饰器的顺序也有明确的规定:

首先,根据书写先后,顺序执行实例成员(即prototype)上的所有装饰器。对于同一方法来说,一定是先应用参数装饰器,再应用方法装饰器(参数装饰器 -> 方法 / 访问器 / 属性 装饰器)

执行静态成员上的所有装饰器,顺序与上一条一致(参数装饰器 -> 方法 / 访问器 / 属性 装饰器)

执行构造方法上的所有装饰器(参数装饰器 -> 类装饰器)

大家可以执行下面代码,可以把各种类型的装饰器的顺序梳理清晰:

function fn( str: string) {

console. log( "求值装饰器:", str);

return function () {

  console. log( "应用装饰器:", str);

};

}

@fn( "类装饰器")

class T {

constructor(@fn( "类参数装饰器") foo: any) {}

@fn( "静态属性装饰器")

static a: any;

@fn( "属性装饰器")

b: any;

@fn( "方法装饰器")

methodA(@fn( "方法参数装饰器") foo: any) {}

@fn( "静态方法装饰器")

static methodB(@fn( "静态方法参数装饰器") foo: any) {}

@fn( "访问器装饰器")

set C(@fn( "访问器参数装饰器") foo: any) {}

@fn( "静态访问器装饰器")

static set D(@fn( "静态访问器参数装饰器") foo: any) {}

}

源码分析:

方法装饰器声明:在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。 方法装饰器不能用在声明文件( .d.ts),重载或者任何外部上下文(比如declare的类)中。

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • 成员的名字。
  • 成员的属性描述符。

为了方便学习,先写一个简单的类Person,再给这个Person类写一个方法装饰器。

function methodDeractor( msg: string): Function{

return function( target: any, name: string, descriptor: PropertyDescriptor){

    descriptor. value = () => console. log( msg)

}

}

class Person{

@methodDeractor( "hello world")

sayHello(){

    console. log( "what world")

}

}

let a = new Person();

a. sayHello(); //hello world

上面的例子是通过属性描述符来修改方法的定义的。

methodDeractor内部return的函数才是ts所关心的装饰器,这个装饰器接收三个参数:

  • target 目标方法所属类(函数)的原型

  • name 目标方法的名字

  • descriptor 目标方法的属性描述符

我们来看一下编译过后的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 methodDeractor( msg) {

    return function ( target, name, descriptor) {

        descriptor. value = function () { return console. log( msg); };

    };

}

var Person = /** @class */ ( function () {

    function Person() {

    }

    Person. prototype. sayHello = function () {

        console. log( "hello");

    };

    __decorate([

        methodDeractor( "hello world")

    ], Person. prototype, "sayHello", null);

    return Person;

}());

var a = new Person();

a. sayHello();

我们的关注点是__decorate的定义,这就是装饰器的定义。我们稍稍把它变的可读一些(格式化)

在此我们先来提下Descriptor

31EAC3F3-2FED-4BEF-B9A3-BBA537EAC1C0-2190-000000E5604DBFC5.PNG

对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

好了,我们直接上源码:

//装饰器,类的原型对象,方法名,属性描述符

var __decorate = ( this && this. __decorate) || function ( decorators, target, key, desc) {

var c = arguments. length,

    //参数个数<3为目标方法,>3为属性描述符

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

            // console.log(d(target, key));

            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;

};

__decorate这个函数接收4个参数

  • decorators 其类型为一个数组,表示应用到目标方法的所有装饰器
  • target 其类型为一个对象,是该方法所属类的原型对象
  • key 其类型为字符串,是该方法的名称
  • desc 其类型也为一个对象,是目标方法的属性描述符

在初始,我们定义了三个变量,c,r,d

  • c为参数的个数,后文通过判断参数的个数来进行传参
  • r为目标方法的属性描述符或该方法所属类的原型对象
  • d为具体的装饰器

下面看一下c,r,d的赋值:

  • c: var c = arguments. length //通过arguments.length直接获取到参数个数

  • r: r = c < 3 ? target : desc === null ? desc = Object. getOwnPropertyDescriptor( target, key) : desc 通过划分3这个参数个数来进行分别传值,小于3就是<=2,通常只有装饰器数组和该方法所属类的原型对象被传递进来,此时的r为原型对象,大于3也就是有4个参数,r通过Object.getOwnPropertyDescriptor被赋值为该方法的属性描述符。

  • d: d没有被明确的赋初始值,在后文,会通过遍历装饰器数组对其进行赋值,现在认为d是一个具体的装饰器。

再接着看下一段代码:

//检测新特性

if ( typeof Reflect === "object" && typeof Reflect. decorate === "function"){

r = Reflect. decorate( decorators, target, key, desc);

}

这里是检测是否已经支持新特性了,该新特性是能够支持JS元数据反射的API,如果没有该特性的话直接进入else代码块

  //遍历装饰器个数

for( var i = decorators. length - 1; i >= 0; i--){

if ( d = decorators[ i]){

    // console.log(d(target, key));

    r = ( c < 3 ? d( r) : c > 3 ? d( target, key, r) : d( target, key)) || r;

    }

}

这里做的事情很简单,就是遍历传进来的装饰器个数,然后把遍历出的每个装饰器赋值给d,然后通过传递进来的target,key,r参数对其进行传参执行,此时也是通过判断参数个数来进行的传参,不同个数的参数进行不同的传参。

最后一句,返回目标方法的属性描述符,也就是r

return c > 3 && r && Object. defineProperty( target, key, r), r;

理解完了这段代码,我们再结合起来,综合来看一下,下面两段是:


function methodDeractor( msg) {

return function ( target, name, descriptor) {

    descriptor. value = function () { return console. log( msg); };

    };

}

var Person = /** @class */ ( function () {

    function Person() {

    }

    Person. prototype. sayHello = function () {

        console. log( "hello");

    };

    __decorate([

        methodDeractor( "hello world")

    ], Person. prototype, "sayHello", null);

    return Person;

}());

var a = new Person();

a. sayHello();

开头的methodDeractor是我们一开始就声明的装饰器,这里没有做任何的变动,原样的写了下来。

下面是我们声明的Person类,里面除了给Person附加sayHello方法以外,还做了一件事情,那就是执行装饰器,这里调用了上面声明的__decorate函数,传入四个参数,装饰器列表,该类的原型,目标方法的名称,还有就是目标方法的属性描述符,该属性描述符在此被传递为null,而真正的传值在__decorate内部,也就是r的值。

多个装饰器

多个装饰器和单个装饰器并没有太大的差别,仅仅是decorators扩大了,也就是装饰器数组扩大了,在内部还是得遍历装饰器数组一个一个执行。装饰器会使我们的代码变得优雅,同一个装饰器可以用于不同的目标,因此提高了复用度。

总结

因此,可以说,装饰器是在Person类构造的时候就已经传值了,这也就意味着,装饰器不等Person类new出实例,直接执行,恰恰可以体现装饰器的作用,比如在类的构造阶段为类添加各种元数据进行装饰或者改变目标方法的定义等等。