🎪 [ECMAScript6] 31. Decorator
简介
装饰器(Decorator)用来增强 JavaScript 类(class)的功能,许多面向对象的语言都有这种语法,目前有一个提案将其引入了 ECMAScript。
装饰器是一种函数,写成@ + 函数名,可以用来装饰四种类型的值。
javascriptCopy
@frozen class Foo {
@configurable(false)
@enumerable(true)
method() {}
@throttle(500)
expensiveMethod() {}
}装饰器API
装饰器是一个函数,API 的类型描述如下(TypeScript 写法)。
typescriptCopy
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 引擎会提供这两个参数。
装饰器函数的返回值,是一个新版本的装饰对象,但也可以不返回任何值(void)。
context对象有很多属性,其中kind属性表示属于哪一种装饰,其他属性的含义如下。
装饰器的执行步骤如下。
类的装饰器
javascriptCopy
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
/*---- 可以多添加参数 ----*/
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
@testable(false)
class MyClass {}
MyClass.isTestable // false方法装饰器
方法装饰器可以返回一个新函数,取代原来的方法,也可以不返回值,表示依然使用原来的方法。如果返回其他类型的值,就会报错。下面是一个例子。
javascriptCopy
function replaceMethod() {
return function () {
return `How are you, ${this.name}?`;
}
}
class Person {
constructor(name) {
this.name = name;
}
@replaceMethod
hello() {
return `Hi ${this.name}!`;
}
}
const robin = new Person('Robin');
robin.hello(), 'How are you, Robin?'属性装饰器
装饰器函数readonly一共可以接受三个参数。
javascriptCopy
function readonly(target, name, descriptor){
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}
readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);下面是另一个例子,修改属性描述对象的enumerable属性,使得该属性不可遍历。
javascriptCopy
class Person {
@nonenumerable
get kidCount() { return this.children.length; }
}
function nonenumerable(target, name, descriptor) {
descriptor.enumerable = false;
return descriptor;
}
下面的@log装饰器,可以起到输出日志的作用。
javascriptCopy
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;
}
const math = new Math();
// passed parameters should get logged now
math.add(2, 4);装饰器不能用于函数
装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。
javascriptCopy
var counter = 0;
var add = function () {
counter++;
};
@add
function foo() {
}上面的代码,意图是执行后counter等于 1,但是实际上结果是counter等于 0。因为函数提升,使得实际执行的代码是下面这样。
javascriptCopy
var counter;
var add;
@add
function foo() {
}
counter = 0;
add = function () {
counter++;
};addInitalizer方法
除了属性装饰器,其他装饰器的上下文对象还包括一个addInitializer()方法,用来完成初始化操作。
它的运行时间如下。
使用装饰器实现自动发布事件
javascriptCopy
const postal = require("postal/lib/postal.lodash");
export default function publish(topic, channel) {
const channelName = channel || '/';
const msgChannel = postal.channel(channelName);
msgChannel.subscribe(topic, v => {
console.log('频道: ', channelName);
console.log('事件: ', topic);
console.log('数据: ', v);
});
return function(target, name, descriptor) {
const fn = descriptor.value;
descriptor.value = function() {
let value = fn.apply(this, arguments);
msgChannel.publish(topic, value);
};
};
}上面代码定义了一个名为publish的装饰器,它通过改写descriptor.value,使得原方法被调用时,会自动发出一个事件。它使用的事件“发布/订阅”库是Postal.js。
javascriptCopy
// index.js
import publish from './publish';
class FooComponent {
@publish('foo.some.message', 'component')
someMethod() {
return { my: 'data' };
}
@publish('foo.some.other')
anotherMethod() {
// ...
}
}
let foo = new FooComponent();
foo.someMethod();
foo.anotherMethod();