Quiet
  • 主页
  • 归档
  • 分类
  • 标签
  • 链接
  • 关于我

bajiu

  • 主页
  • 归档
  • 分类
  • 标签
  • 链接
  • 关于我
Quiet主题
  • JavaScript

typescript装饰器

bajiu
前端

2020-08-02 22:10:27

装饰器让程序员可以编写元信息以内省代码。装饰器的最佳使用场景是横切关注点——面向切面编程。

面向切面编程(AOP) 是一种编程范式,它允许我们分离横切关注点,藉此达到增加模块化程度的目标。它可以在不修改代码自身的前提下,给已有代码增加额外的行为(通知)。

@log // 类装饰器
class Person {
  constructor(private firstName: string, private lastName: string) {}

  @log // 方法装饰器
  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

什么是装饰器?它的目的和类型

装饰器是一种特殊的声明,可附加在类、方法、访问器、属性、参数声明上。
装饰器使用 @expression 的形式,其中 expression 必须能够演算为在运行时调用的函数,其中包括装饰声明信息。

它起到了以声明式方法将元信息添加至已有代码的作用。

装饰器类型及其执行优先级为

  1. 类装饰器——优先级 4 (对象实例化,静态)
  2. 方法装饰器——优先级 2 (对象实例化,静态)
  3. 访问器或属性装饰器——优先级 3 (对象实例化,静态)
  4. 参数装饰器——优先级 1 (对象实例化,静态)

注意,如果装饰器应用于类构造函数的参数,那么不同装饰器的优先级为:1. 参数装饰器,2. 方法装饰器,3. 访问器或参数装饰器,4. 构造器参数装饰器,5. 类装饰器。

// 这是一个装饰器工厂——有助于将用户参数传给装饰器声明
function f() {
  console.log("f(): evaluated");
  return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("f(): called");
  }
}

function g() {
  console.log("g(): evaluated");
  return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("g(): called");
  }
}

class C {
  @f()
  @g()
  method() {}
}

// f(): evaluated
// g(): evaluated
// g(): called
// f(): called

我们看到,上面的代码中,f 和 g 返回了另一个函数(装饰器函数)。f 和 g 称为装饰器工厂。

装饰器工厂 帮助用户传递可供装饰器利用的参数。

我们还可以看到,演算顺序为由顶向下,执行顺序为由底向上。

方法装饰器

方法装饰器函数有三个参数:

  • target —— 当前对象的原型,也就是说,假设 Employee 是对象,那么 target 就是 Employee.prototype
  • propertyKey —— 方法的名称
  • descriptor —— 方法的属性描述符,即 Object.getOwnPropertyDescriptor(Employee.prototype, propertyKey)
export function logMethod(
  target: Object,
  propertyName: string,
  propertyDescriptor: PropertyDescriptor): PropertyDescriptor {
  // target === Employee.prototype
  // propertyName === "greet"
  // propertyDesciptor === Object.getOwnPropertyDescriptor(Employee.prototype, "greet")
  const method = propertyDesciptor.value;

  propertyDesciptor.value = function (...args: any[]) {
    // 将 greet 的参数列表转换为字符串
    const params = args.map(a => JSON.stringify(a)).join();
    // 调用 greet() 并获取其返回值
    const result = method.apply(this, args);
    // 转换结尾为字符串
    const r = JSON.stringify(result);
    // 在终端显示函数调用细节
    console.log(`Call: ${propertyName}(${params}) => ${r}`);
    // 返回调用函数的结果
    return result;
  }
  return propertyDesciptor;
};

class Employee {
    constructor(private firstName: string, private lastName: string
    ) {}

    @logMethod
    greet(message: string): string {
        return `${this.firstName} ${this.lastName} says: ${message}`;
    }
}

const emp = new Employee('Mohan Ram', 'Ratnakumar');
emp.greet('hello');

属性装饰器

属性装饰器函数有两个参数:

  1. target —— 当前对象的原型,也就是说,假设 Employee 是对象,那么 target 就是 Employee.prototype
  2. propertyKey —— 属性的名称
function logParameter(target: Object, propertyName: string) {
    // 属性值
    let _val = this[propertyName];

    // 属性读取访问器
    const getter = () => {
        console.log(`Get: ${propertyName} => ${_val}`);
        return _val;
    };

    // 属性写入访问器
    const setter = newVal => {
        console.log(`Set: ${propertyName} => ${newVal}`);
        _val = newVal;
    };

    // 删除属性
    if (delete this[propertyName]) {
        // 创建新属性及其读取访问器、写入访问器
        Object.defineProperty(target, propertyName, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class Employee {
    @logParameter
    name: string;
}

const emp = new Employee();
emp.name = 'Mohan Ram';
console.log(emp.name);
// Set: name => Mohan Ram
// Get: name => Mohan Ram
// Mohan Ram

上面的代码中,我们在装饰器中内省属性的可访问性。下面是编译后的代码。

var Employee = /** @class */ (function () {
    function Employee() {
    }
    __decorate([
        logParameter
    ], Employee.prototype, "name");
    return Employee;
}());
var emp = new Employee();
emp.name = 'Mohan Ram'; // Set: name => Mohan Ram
console.log(emp.name); // Get: name => Mohan Ram

参数装饰器

参数装饰器函数有三个参数:

  • target —— 当前对象的原型,也就是说,假设 Employee 是对象,那么 target 就是 Employee.prototype
  • propertyKey —— 参数的名称
  • index —— 参数数组中的位置
function logParameter(target: Object, propertyName: string, index: number) {
    // 为相应方法生成元数据键,以储存被装饰的参数的位置
    const metadataKey = `log_${propertyName}_parameters`;
    if (Array.isArray(target[metadataKey])) {
        target[metadataKey].push(index);
    }
    else {
        target[metadataKey] = [index];
    }
}

class Employee {
    greet(@logParameter message: string): string {
        return `hello ${message}`;
    }
}
const emp = new Employee();
emp.greet('hello');

访问器装饰器

访问器不过是类声明中属性的读取访问器和写入访问器。

访问器装饰器应用于访问器的属性描述符,可用于观测、修改、替换访问器的定义

function enumerable(value: boolean) {
    return function (
      target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log('decorator - sets the enumeration part of the accessor');
        descriptor.enumerable = value;
    };
}

class Employee {
    private _salary: number;
    private _name: string;

    @enumerable(false)
    get salary() { return `Rs. ${this._salary}`; }

    set salary(salary: any) { this._salary = +salary; }

    @enumerable(true)
    get name() {
        return `Sir/Madam, ${this._name}`;
    }

    set name(name: string) {
        this._name = name;
    }

}

const emp = new Employee();
emp.salary = 1000;
for (let prop in emp) {
    console.log(`enumerable property = ${prop}`);
}
// salary 属性不在清单上,因为我们将其设为假
// output:
// decorator - sets the enumeration part of the accessor
// decorator - sets the enumeration part of the accessor
// enumerable property = _salary
// enumerable property = name

上面的例子中,我们定义了两个访问器 name 和 salary,并通过装饰器设置是否将其列入清单,据此决定对象的行为。name 将列入清单,而 salary 不会。

类装饰器

类装饰器应用于类的构造器,可用于观测、修改、替换类定义。

export function logClass(target: Function) {
    // 保存一份原构造器的引用
    const original = target;

    // 生成类的实例的辅助函数
    function construct(constructor, args) {
        const c: any = function () {
            return constructor.apply(this, args);
        }
        c.prototype = constructor.prototype;
        return new c();
    }

    // 新构造器行为
    const f: any = function (...args) {
        console.log(`New: ${original['name']} is created`);
        return construct(original, args);
    }

    // 复制 prototype 属性,保持 intanceof 操作符可用
    f.prototype = original.prototype;

    // 返回新构造器(将覆盖原构造器)
    return f;
}

@logClass
class Employee {}

let emp = new Employee();
console.log('emp instanceof Employee');
console.log(emp instanceof Employee); // true

上面的装饰器声明了一个名为 original 的变量,将其值设为被装饰的类构造器。

接着声明了名为 construct 的辅助函数。该函数用于创建类的实例。

我们接下来创建了一个名为 f 的变量,该变量将用作新构造器。该函数调用原构造器,同时在控制台打印实例化的类名。这正是我们给原构造器加入额外行为的地方。

原构造器的原型复制到 f,以确保创建一个 Employee 新实例的时候,instanceof 操作符的效果符合预期。

新构造器一旦就绪,我们便返回它,以完成类构造器的实现。
新构造器就绪之后,每次创建实例时会在控制台打印类名。

结尾

  • 装饰器 不过是在设计时(design time)帮助内省代码,注解及修改类和属性的函数。
  • Yehuda Katz 提议在 ECMAScript 2016 标准中加入装饰器特性:tc39/proposal-decorators
  • 我们可以通过装饰器工厂将用户提供的参数传给装饰器。
  • 有 4 种装饰器:类装饰器、方法装饰器、属性/访问器装饰器、参数装饰器。
  • 元信息反射 API 有助于以标准方式在对象中加入元信息,以及在运行时获取设计类型信息。

上一篇

tsconfig.json 文件结构

下一篇

EventEmitter API详解(附源码)

©2024 By bajiu.