简介
装饰器(Decorator)用来增强 JavaScript 类(class)的功能,许多面向对象的语言都有这种语法,目前有一个 提案 将其引入了 ECMAScript。
装饰器是一种函数,写成 @ + 函数名,可以用来装饰四种类型的值。
- 类
- 类的属性
- 类的方法
- 属性存取器(accessor)
下面的例子是装饰器放在类名和类方法名之前,大家可以感受一下写法。
@frozen class Foo {
@configurable(false)
@enumerable(true)
method() {}
@throttle(500)
expensiveMethod() {}
}上面代码一共使用了四个装饰器,一个用在类本身(@frozen),另外三个用在类方法(@configurable()、@enumerable()、@throttle())。它们不仅增加了代码的可读性,清晰地表达了意图,而且提供一种方便的手段,增加或修改类的功能
装饰器 API
装饰器是一个函数,API 的类型描述如下(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:字符串,表示装饰类型,可能的取值有class、method、getter、setter、field、accessor。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:函数,允许用户增加初始化逻辑。
装饰器的执行步骤如下。
- 计算各个装饰器的值,按照从左到右,从上到下的顺序。
- 调用方法装饰器。
- 调用类装饰器。
装饰器实现原理
类装饰器
class C {}
C = logged(C, {
kind: "class",
name: "C",
}) ?? C;
new C(1);属性装饰器
let initializeX = logged(undefined, {
kind: "field",
name: "x",
static: false,
private: false,
}) ?? (initialValue) => initialValue;
class C {
x = initializeX.call(this, 1);
}方法装饰器
改掉原型链上面 m() 方法。
class C {
m(arg) {}
}
C.prototype.m = logged(C.prototype.m, {
kind: "method",
name: "m",
static: false,
private: false,
}) ?? C.prototype.m;存取器装饰器
改掉了类的原型链
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 });其他
accesstor、addInitialize()
类型声明
类装饰器
type ClassDecorator = (value: Function, context: {
kind: "class";
name: string | undefined;
addInitializer(initializer: () => void): void;
}) => Function | void;属性装饰器
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;方法装饰器
type ClassMethodDecorator = (value: Function, context: {
kind: "method";
name: string | symbol;
access: { get(): unknown };
static: boolean;
private: boolean;
addInitializer(initializer: () => void): void;
}) => Function | void;存取器装饰器
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;注意
多个装饰器的执行顺序
如果同一个方法有多个装饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。
我们可以对同一属性应用多个装饰器,他们的顺序是:
- 先从外层到内层求值装饰器(如果是函数工厂的话)
- 应用装饰器时,是从内层到外层
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) {}
}代码将会输出:
求值装饰器: 属性装饰器
应用装饰器: 属性装饰器
求值装饰器: 方法装饰器
求值装饰器: 方法参数装饰器
应用装饰器: 方法参数装饰器
应用装饰器: 方法装饰器
求值装饰器: 访问器装饰器
求值装饰器: 访问器参数装饰器
应用装饰器: 访问器参数装饰器
应用装饰器: 访问器装饰器
求值装饰器: 静态属性装饰器
应用装饰器: 静态属性装饰器
求值装饰器: 静态方法装饰器
求值装饰器: 静态方法参数装饰器
应用装饰器: 静态方法参数装饰器
应用装饰器: 静态方法装饰器
求值装饰器: 静态访问器装饰器
求值装饰器: 静态访问器参数装饰器
应用装饰器: 静态访问器参数装饰器
应用装饰器: 静态访问器装饰器
求值装饰器: 类装饰器
求值装饰器: 类参数装饰器
应用装饰器: 类参数装饰器
应用装饰器: 类装饰器装饰器只能用于类和类的方法,不能用于函数
因为存在函数提升。
var counter = 0;
var add = function () {
counter++;
};
@add
function foo() {
}上面的代码,意图是执行后 counter 等于 1,但是实际上结果是 counter 等于 0。因为函数提升,使得实际执行的代码是下面这样。
var counter;
var add;
@add
function foo() {
}
counter = 0;
add = function () {
counter++;
};下面是另一个例子。
var readOnly = require("some-decorator");
@readOnly
function foo() {
}上面代码也有问题,因为实际执行是下面这样。
var readOnly;
@readOnly
function foo() {
}
readOnly = require("some-decorator");总之,由于存在函数提升,使得装饰器不能用于函数。类是不会提升的,所以就没有这方面的问题。
另一方面,如果一定要装饰函数,可以采用 高阶函数 的形式直接执行。
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