xiaohai Blog

快速入门 JavaScript 装饰器

October 19, 2021

什么是装饰器

装饰器翻译自单词 decorators,在 JavaScript 中是一个能修改现有功能的函数。

这里的功能包含类或者类的方法。如果是修改类,称为类装饰器;修改类的方法,则是方法装饰器。

在 JavaScript 中使用装饰器

JavaScript 装饰器目前在 提案 阶段,可以通过 babel 或者 typescript 使用

在 babel 中使用

需要安装插件 @babel/plugin-proposal-decorators

yarn add @babel/plugin-proposal-decorators --dev

在配置文件 babel.config.json 中使用插件

{
    "plugins": ["@babel/plugin-syntax-decorators"]
}

在 TypeScript 中使用

TypeScript 中是实验性支持的特性,使用时需要开启 experimentalDecorators 编译选项。开启方式:

  • 命令行编译
tsc --target ES5 --experimentalDecorators
  • 在配置文件 tsconfig.json 进行设置
{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

装饰类

在装饰类时,需要在类的声明上方使用 @ 符号加上装饰器函数名。比如这里 annotation 函数就是一个类装饰器:

@annotation
class MyClass {}

function annotation(target) {
    target.annotated = true
}

以上代码使用 babel 会被编译为

var _class

let MyClass = annotation(_class = class MyClass {}) || _class

function annotation(target) {
    target.annotated = true
}

所以类装饰器的行为用代码表示就是

@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A

类装饰器是一个对类进行处理的函数,函数的第一个参数,是装饰的目标类:

function annotation(target) {
    // ...
}

如果需要在使用时传入额外的参数,需要在外层封装一个函数:

@isTestable(true)
class MyClass {}

function isTestable(value) {
    return function decorator(target) {
        target.isTestable = value
    }
}

装饰类的方法

装饰类的方法时,需要把装饰器放在方法上面:

class Cat {
    @readonly
    name() {}
}

function readonly(target, key, descriptor) {
    descriptor.enumerable = value
    return descriptor
}

类方法装饰器一共接收 3 个参数:

  • 第 1 个参数 target 是类的原型对象
  • 第 2 个参数 key 是装饰方法的名称
  • 第 3 个参数 descriptor 是该方法的描述对象

当需要传入参数时,同样需要在外层封装一个函数:

class C {
    @enumerable(false)
    method() {}
}

function enumerable(value) {
    return function(target, key, descriptor) {
        descriptor.enumerable = value
        return descriptor
    }
}

同时使用多个装饰器

多个装饰器可以一起使用:

function first() {
    console.log('first(): factory evaluated')
    return function(target, key, descriptor) {
        console.log('first(): called')
    }
}

function second() {
    console.log('second(): factory evaluated')
    return function(target, key, descriptor) {
        console.log('second(): called')
    }
}

class ExampleClass {
    @first()
    @second()
    method() {}
}

以上代码输出:

first(): factory evaluated
second(): factory evaluated
second(): called
first(): called

需要注意执行顺序,装饰器声明的执行顺序是从上往下,而调用时的执行顺序是从下往上。

使用场景

通过装饰器可以在不修改原有类的情况下扩展其功能,使用场景很广泛。以下举例说明:

  • log:通过封装 log 装饰器,可以方便地记录 log
class Math {
    @log
    add(a, b) {
        return a + b
    }
}

function log(target, name, descriptor) {
    var oldValue = descriptor.value

    descriptor.value = function() {
        console.log(`Calling ${name} with`, arguments)
        return oldValue.apply(this, arguments)
    }

    return descriptor
}
  • TypeORM 是一个使用 TypeScript 开发的 ORM,其中使用装饰器进行 model 声明:
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'

@Entity()
export class User {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

    @Column()
    age: number;
}
  • Nest 是一个使用 TypeScript 开发的 Node.js 框架,其中使用装饰器声明路由
import { Controller, Get } from '@nestjs/common'

@Controller('cats')
export class CatsController {
    @Get()
    findAll(): string {
        return 'This action returns all cats'
    }
}

参考