从Typescript装饰器浅谈装饰者模式

1,719 阅读8分钟

前言

时至今日,Typescript流行已久。Typescript不仅仅为我们提供了静态类型检查,它还实现了许多面向对象语言中才有的一些特性,比如:接口、抽象类、装饰器等等。typescript编译器会将这些特性转译为js的实现方式。今天我们会简要的介绍一下typescript中的装饰器到底是什么,但不会过多的深入其中,随后,我们通过typescript的装饰器延伸到装饰者模式。看看装饰者模式到底是怎么回事?

装饰器

随着Typescript和ES6中Class的引入,现在存在这样的一个场景:需要支持注解(Annotation)以修改类或者类中的成员。装饰器提供了这样的一种方式来支持注解和元编程的语法来修改类和它的成员。在JavaScript中,装饰器目前仍然处于stage 2 proposal的阶段,并且在typescript中这也是一个实验性的feature,这意味着目前typescript实现的装饰器可能会随着标准的改变而发生改变。

我们可以通过以下的命令来编译带有装饰器的ts文件

tsc --target ES5 --experimentalDecorators

或者在tsconfig.json文件中编写:

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

装饰器是一种特殊的声明方式,它可以被附加到类、方法、getter/setter、属性或者参数上。

装饰器使用 @expression这样的语法,这里的expression其实就是一个函数,这个函数会在运行时被调用。

编写装饰器

现在我们开始编写一些装饰器。

类装饰器

function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

@sealed被执行时,它将密封此类的构造函数和原型。(参见:Object.seal

或者我们可以用于重载构造函数

function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
    return class extends constructor {
        newProperty = "new property";
        hello = "override";
    }
}

@classDecorator
class Greeter {
    property = "property";
    hello: string;
    constructor(m: string) {
        this.hello = m;
    }
}

console.log(new Greeter("world"));

类装饰器在类声明之前被声明。类装饰应用于类构造函数,可以用来监视、修改或替换类定义。

类装饰器表达式会在运行时被当作函数调用,类的构造函数是其唯一的参数。

如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

方法装饰器

function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting;
    }
}

这里的@enumerable(false)是一个装饰器工厂。 当装饰器 @enumerable(false)被调用时,它会修改属性描述符的enumerable属性。

方法装饰器被应用到方法的属性描述符上,可以用来监视、修改或者替换方法定义。

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

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

如果访问装饰器返回一个值,它会被用作方法的属性描述符。如下:

function override(f) {
    return function (target, propertyKey, descriptor: PropertyDescriptor) {
        return {
            value: f,
            configurable: false,
            enumerable: false,
            writable: false,
        };
    };
}

class Greeter {
    property = "property";
    constructor() {}

    @override(function () {
        console.log("another function");
    })
    hello() {
        console.log(this.property);
    }
}

const t = new Greeter();
t.hello();
// 输出:
another function

访问器装饰器

与方法装饰器类似

其他的装饰器

  • 属性装饰器
  • 参数装饰器

我们就不过多的深入了,今天的重点是讲解装饰者模式 Typescript装饰器

装饰器这种语法编译成js后是什么样子?

对于这个问题,我们查看编译后的js文件: 以下面这段ts代码为例:

function override(f) {
    return function (target, propertyKey, descriptor: PropertyDescriptor) {
        return {
            value: f,
            configurable: false,
            enumerable: false,
            writable: false,
        };
    };
}

class Greeter {
    constructor() {}

    @override(function () {
        console.log("another function");
    })
    hello() {
        console.log("hello world");
    }
}

const t = new Greeter();
t.hello();

编译为js后:

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 override(f) {
    return function (target, propertyKey, descriptor) {
        return {
            value: f,
            configurable: false,
            enumerable: false,
            writable: false,
        };
    };
}
var Greeter = /** @class */ (function () {
    function Greeter() {}
    Greeter.prototype.hello = function () {
        console.log("hello world");
    };
    __decorate(
        [
            override(function () {
                console.log("another function");
            }),
        ],
        Greeter.prototype,
        "hello",
        null
    );
    return Greeter;
})();
var t = new Greeter();
t.hello();

我们可以看到这里多了一个 _decorate函数。我们现在来逐行的解读这个函数:

var _decorate = this && this._decorate

一般来讲,ts会在全局编译一个typescript.js的文件,里面定义了一些全局的变量,就包含了这个 _decorate,所以这里会先判断当前全局环境内是否存在 _decorate 函数,如果存在就不继续往下执行了。 如果不存在,就重新生成一个_decorate函数。

// 我们继续往下看
function (decorators, target, key, desc) {
        var c = arguments.length,
        	// r 就是我们最后需要返回的对象
        	// 这里根据参数的个数不同,r变量的值也不同,如果参数个数小于3,则r === target, 如果大于等于3, 则返回属性描述符
            r =
                c < 3
                    ? target
                    : desc === null
                    ? (desc = Object.getOwnPropertyDescriptor(target, key))
                    : desc,
            d;
        // 这里的Reflect.decorate是为以后装饰器成为ES标准的一部分做准备,一旦标准中确定Reflect.decorate成为事实标准,则可以使用native代码而不是使用polyfill
        if (
            typeof Reflect === "object" &&
            typeof Reflect.decorate === "function"
        )
            r = Reflect.decorate(decorators, target, key, desc);
        else
        // 这里就是所谓的polyfill代码了。
        // 从后往前遍历所有的装饰器
            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;
		// 如果参数数量大于3,则需要定义属性描述符,然后返回r
        return c > 3 && r && Object.defineProperty(target, key, r), r;
    };

通过调用_decorate函数来达到装饰的效果。


var Greeter = /** @class */ (function () {
    function Greeter() {}
    Greeter.prototype.hello = function () {
        console.log("hello world");
    };
    __decorate(
        [
            override(function () {
                console.log("another function");
            }),
        ],
        Greeter.prototype,
        "hello",
        null
    );
    return Greeter;
})();

装饰器与装饰者模式的关系

装饰器

以Typescript中的方法装饰器为例,方法装饰器可以用来监视、修改或者替换方法的定义。

装饰者模式

给对象动态的增加指责的方式成为装饰者模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态的添加职责。

例如,我们设计一个飞机大战的游戏。随着飞机的升级,飞机可以发射更加强大的子弹。

一开始飞机只能发射普通子弹,代码如下:

var Plane = function () {};

Plane.prototype.fire = function () {
	console.log('普通子弹');
}

飞机升级后,能够发射更强大的子弹。我们可以选择在fire方法中增加if...else...来判断是否需要增加新的子弹,但是这样就侵入了我们原来的代码。我们此时可以采用装饰者模式。

飞机升级后可以发射导弹,我们可以设计一个装饰器来包裹原来的fire方法。

var MissileDecorator = function (plane) {
	this.plane = plane;
}

MissileDecorator.prototype.fire = function () {
	this.plane.fire();
    console.log('发射导弹');
}

这样我们可以不修改原始的fire方法,也达到了我们的目的。

跟装饰器相比,装饰器更加的强大,装饰器不仅能够增加职责,还可以修改、替换方法的定义。在Typescript的方法装饰器中,我们可以直接改写对象某个属性的属性描述符。而装饰者模式中,我们更倾向于不去改动原函数的原始代码,而是通过给原始函数包裹一层外部的函数,在这个外部的函数中调用这个原始函数,在添加一些我们需要的新的逻辑。这就像俄罗斯套娃一般,一层套着一层。从功能上而言,装饰者能很好的描述这个模式,但是从函数的结构上来看的话,包装器的说话更加的贴切,所以装饰者也是包装器。

应用场景

其实我们经常用到装饰者模式,其实很多高阶函数就是装饰者模式的典型应用场景。比如:防抖、节流,防抖函数throttle就是一个装饰器,我们把一个函数传入其中,返回出来的函数就是一个具有防抖功能的函数了。“防抖”这个功能就像是一个挂件一般,随取随用。

我们还可以利用装饰器做一些其他的事情,比如:插件式的表单验证功能、数据统计上报等等。

总结

本文大概的介绍了Typescript中的装饰器和装饰者模式。Typescript中的装饰器是一个还在实验中的功能,它的js实现可能随着标准的变化而发生变化。Reflect.decorate这个方法正是期望以后引擎能够实现的方法名,一旦标准中明确了装饰器的实现标准,这个方法就可以使用。目前,我们只能通过在js中编写polyfill的代码来进行模拟。

装饰者模式更多的是强调不改变对象自身的基础上,动态的增加职责的方式。从实现上来讲,就是在原对象的基础上,再“套”上一层,就像俄罗斯套娃一样,所以我们也将装饰者模式叫做包装者模式。

使用装饰者模式的优点在于:我们可以无侵入的修改原有的代码并增加一些其他的功能,这样我们可以让一些功能的有无变得更加的灵活,达到即插即用的效果,配合上策略模式,更是让程序更加的灵活、可配置性更强。

好了,今天的内容差不多就是这么多了。笔者水平有限,如文章有失偏颇,欢迎各位在评论区指正。

参考资料

Typescript装饰者

《Javascript设计模式与开发实践》——曾探