Skip to content
标签
note
字数
2105 字
阅读时间
10 分钟

简介

装饰器(Decorator)用来增强 JavaScript 类(class)的功能,许多面向对象的语言都有这种语法,目前有一个 提案 将其引入了 ECMAScript。

装饰器是一种函数,写成 @ + 函数名,可以用来装饰四种类型的值。

  • 类的属性
  • 类的方法
  • 属性存取器(accessor)

下面的例子是装饰器放在类名和类方法名之前,大家可以感受一下写法。

javascript
@frozen class Foo {
  @configurable(false)
  @enumerable(true)
  method() {}

  @throttle(500)
  expensiveMethod() {}
}

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

装饰器 API

装饰器是一个函数,API 的类型描述如下(TypeScript 写法)。

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

装饰器函数有两个参数。运行时,JavaScript 引擎会提供这两个参数。

  • value:所要装饰的值,某些情况下可能是 undefined(装饰属性时)。
  • context:上下文信息对象。

装饰器函数的返回值,是一个新版本的装饰对象,但也可以不返回任何值(void)。

context 对象有很多属性,其中 kind 属性表示属于哪一种装饰,其他属性的含义如下。

  • kind:字符串,表示装饰类型,可能的取值有 classmethodgettersetterfieldaccessor
  • name:被装饰的值的名称: The name of the value, or in the case of private elements the description of it (e.g. the readable name).
  • access:对象,包含访问这个值的方法,即存值器和取值器。
  • static: 布尔值,该值是否为静态元素。
  • private:布尔值,该值是否为私有元素。
  • addInitializer:函数,允许用户增加初始化逻辑。

装饰器的执行步骤如下。

  1. 计算各个装饰器的值,按照从左到右,从上到下的顺序。
  2. 调用方法装饰器。
  3. 调用类装饰器。

装饰器实现原理

类装饰器

javascript
class C {}

C = logged(C, {
  kind: "class",
  name: "C",
}) ?? C;

new C(1);

属性装饰器

javascript
let initializeX = logged(undefined, {
  kind: "field",
  name: "x",
  static: false,
  private: false,
}) ?? (initialValue) => initialValue;

class C {
  x = initializeX.call(this, 1);
}

方法装饰器

改掉原型链上面 m() 方法。

javascript
class C {
  m(arg) {}
}

C.prototype.m = logged(C.prototype.m, {
  kind: "method",
  name: "m",
  static: false,
  private: false,
}) ?? C.prototype.m;

存取器装饰器

改掉了类的原型链

javascript
class C {
  set x(arg) {}
}

let { set } = Object.getOwnPropertyDescriptor(C.prototype, "x");
set = logged(set, {
  kind: "setter",
  name: "x",
  static: false,
  private: false,
}) ?? set;

Object.defineProperty(C.prototype, "x", { set });

其他

accesstoraddInitialize()

类型声明

类装饰器

typescript
type ClassDecorator = (value: Function, context: {
  kind: "class";
  name: string | undefined;
  addInitializer(initializer: () => void): void;
}) => Function | void;

属性装饰器

typescript
type ClassFieldDecorator = (value: undefined, context: {
  kind: "field";
  name: string | symbol;
  access: { get(): unknown, set(value: unknown): void };
  static: boolean;
  private: boolean;
}) => (initialValue: unknown) => unknown | void;

方法装饰器

typescript
type ClassMethodDecorator = (value: Function, context: {
  kind: "method";
  name: string | symbol;
  access: { get(): unknown };
  static: boolean;
  private: boolean;
  addInitializer(initializer: () => void): void;
}) => Function | void;

存取器装饰器

typescript
type ClassGetterDecorator = (value: Function, context: {
  kind: "getter";
  name: string | symbol;
  access: { get(): unknown };
  static: boolean;
  private: boolean;
  addInitializer(initializer: () => void): void;
}) => Function | void;

type ClassSetterDecorator = (value: Function, context: {
  kind: "setter";
  name: string | symbol;
  access: { set(value: unknown): void };
  static: boolean;
  private: boolean;
  addInitializer(initializer: () => void): void;
}) => Function | void;

注意

多个装饰器的执行顺序

如果同一个方法有多个装饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。

我们可以对同一属性应用多个装饰器,他们的顺序是:

  1. 先从外层到内层求值装饰器(如果是函数工厂的话)
  2. 应用装饰器时,是从内层到外层
typescript
function fn(str: string) {
  console.log("求值装饰器:", str);
  return function () {
    console.log("应用装饰器:", str);
  };
}

function decorator() {
  console.log("应用其他装饰器");
}

class T {
  @fn("外层")
  @decorator
  @fn("内层")
  method() {}
}

代码将会输出:

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

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

  1. 首先,根据书写先后,顺序执行实例成员(即 prototype)上的所有装饰器。对于同一方法来说,一定是先应用参数装饰器,再应用方法装饰器(参数装饰器 -> 方法 / 访问器 / 属性 装饰器)
  2. 执行静态成员上的所有装饰器,顺序与上一条一致(参数装饰器 -> 方法 / 访问器 / 属性 装饰器)
  3. 执行构造方法上的所有装饰器(参数装饰器 -> 类装饰器)
typescript
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) {}
}

代码将会输出:

bash
求值装饰器: 属性装饰器
应用装饰器: 属性装饰器
求值装饰器: 方法装饰器
求值装饰器: 方法参数装饰器
应用装饰器: 方法参数装饰器
应用装饰器: 方法装饰器
求值装饰器: 访问器装饰器
求值装饰器: 访问器参数装饰器
应用装饰器: 访问器参数装饰器
应用装饰器: 访问器装饰器

求值装饰器: 静态属性装饰器
应用装饰器: 静态属性装饰器
求值装饰器: 静态方法装饰器
求值装饰器: 静态方法参数装饰器
应用装饰器: 静态方法参数装饰器
应用装饰器: 静态方法装饰器
求值装饰器: 静态访问器装饰器
求值装饰器: 静态访问器参数装饰器
应用装饰器: 静态访问器参数装饰器
应用装饰器: 静态访问器装饰器

求值装饰器: 类装饰器
求值装饰器: 类参数装饰器
应用装饰器: 类参数装饰器
应用装饰器: 类装饰器

装饰器只能用于类和类的方法,不能用于函数

因为存在函数提升。

javascript
var counter = 0;

var add = function () {
  counter++;
};

@add
function foo() {
}

上面的代码,意图是执行后 counter 等于 1,但是实际上结果是 counter 等于 0。因为函数提升,使得实际执行的代码是下面这样。

javascript
var counter;
var add;

@add
function foo() {
}

counter = 0;

add = function () {
  counter++;
};

下面是另一个例子。

javascript
var readOnly = require("some-decorator");

@readOnly
function foo() {
}

上面代码也有问题,因为实际执行是下面这样。

javascript
var readOnly;

@readOnly
function foo() {
}

readOnly = require("some-decorator");

总之,由于存在函数提升,使得装饰器不能用于函数。类是不会提升的,所以就没有这方面的问题。

另一方面,如果一定要装饰函数,可以采用 高阶函数 的形式直接执行。

javascript
function doSomething(name) {
  console.log('Hello, ' + name);
}

function loggingDecorator(wrapped) {
  return function() {
    console.log('Starting');
    const result = wrapped.apply(this, arguments);
    console.log('Finished');
    return result;
  }
}

const wrapped = loggingDecorator(doSomething);

更多使用

技巧

Mixin,以及进阶版的 Traint

第三方库

GitHub - jayphelps/core-decorators

参考

装饰器 - ECMAScript 6入门

一起读透TS装饰器 - 掘金

贡献者

The avatar of contributor named as jiechen jiechen

页面历史

撰写