超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

ES6 - 装饰器:Decorator

装饰器是 ES7 的一个新提案,本质上是 Object.defineProperty(obj, prop, descriptor) 的语法糖,用于动态的修改类的行为,相比较集成的方式而言,这种方式无疑更加的灵活。

装饰器的使用

装饰器用于类和类的成员上,但是不能用于函数,因为存在着函数提升。

作用于类的装饰器

装饰器作用于类上的时候,它的第一个参数 target 则是被装饰的类的本身,签名如下:

function(target)

const sayHello = target => {
target.sayHello = () => console.log("hello");
}

@sayHello
class Person {};

Person.sayHello(); // hello

作用于类成员的装饰器

当装饰器作用于类的属性的时候接收3个参数:要装饰的目标对象的原型、要装饰的属性名和该属性的描述对象,签名如下:

function(target, name, descriptor)

其参数与 Object.defineProperty 一致,如下定义了一个 readonly 的装饰器:

// 把 descriptor 的 writable 字段设为 false
function readonly(target, key, descriptor) {
descriptor.writable = false
return descriptor
}

class Person {
@readonly
name = "person"
}

let p = new Person();
p.name = "person2"
// Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Person>'

装饰器增强

如果装饰器需要接受参数,可以对其进行一层包装,在装饰的时候为其传入参数:

const sayHello = hello => target => {
target.sayHello = () => console.log(hello);
}

@sayHello('hi')
class Person {};

Person.sayHello(); // hi

也可以同时应用多个装饰器:

const before = target => {
console.log("before");
}
const sayHello = hello => target => {
target.sayHello = () => console.log(hello);
}

@before
@sayHello
class Person {
name = "person"
};

Person.sayHello();
// before
// hello

装饰器的原理

对于类而言,装饰器的行为实际上是把类作为参数执行装饰器函数,这里可以看一下的转换:

// 转换前
@F("color")
@G
class Foo {
}

// 转换后
var Foo = (function () {
class Foo {
}

Foo = F("color")(Foo = G(Foo) || Foo) || Foo;
return Foo;
})();

对于类的成员方法或者属性,装饰器的行为则是修改目标的 descriptor ,并返回一个新的 descriptor 然后把这个新的 descriptor 应用到目标方法或者属性上:

// 转换前
class Foo {
@F("color")
@G
bar() { }
}

// 转换后
var Foo = (function () {
class Foo {
bar() { }
}

var _temp;
_temp = F("color")(Foo.prototype, "bar",
_temp = G(Foo.prototype, "bar",
_temp = Object.getOwnPropertyDescriptor(Foo.prototype, "bar")) || _temp) || _temp;
if (_temp) Object.defineProperty(Foo.prototype, "bar", _temp);
return Foo;
})();

装饰器练习

下面根据装饰器的特性,写几个常用的装饰器作为练习:

Mixin

Mixin 是对象集成的一种替代,在一个对象中混入另一个对象的方法:

const mixins = (...objs) => {
return function (target) {
Object.assign(target.prototype, ...objs);
};
}

class Obj {
test() {
console.log("call test")
}
}
objA = {
print() {
console.log("call print")
}
}

objB = {
log() {
console.log("call log")
}
}

@mixins(objA, objB)
class Test extends Obj {}

new Test().print()
new Test().log()
new Test().log()

// call test
// call print
// call log

标记一个接口 deprecated

在开发的时候经常会接口更新,有些接口为了保持兼容而保留,但是希望这个接口再被调用的时候能够发出警告信息,提醒升级,实现如下:

const deprecated = (warnText=`The interface is deprecated`) => (target, key, descriptor) => {
if (typeof descriptor.value !== 'function') {
throw new Error('The @deprecated decorator can only be used on function.');
}

return {
...descriptor,
value: function() {
console.warn(warnText);
return descriptor.value.apply(this, arguments);
}
}
}

class Test {
@deprecated()
calculate() {
return;
}
}

new Test().calculate()
// The interface is deprecated

统计方法执行时间

对于复杂的操作可能会需要记录操作执行的时间:

const runtime = (target, key, descriptor) => {
if (typeof descriptor.value !== 'function') {
throw new Error('The @runtime decorator can only be used on function.');
}

label = `The function ${key} running used`

return {
...descriptor,
value: function() {
console.time(label);
let result = descriptor.value.apply(this, arguments);
console.timeEnd(label);
return result;
}
}
}

class Test {
@runtime
calculate() {
for(var i = 0; i < 10000000; i ++) {}
}
}

new Test().calculate()
// The function calculate running used: 8.162841796875ms

参考

Decorators in ES7
ECMAScript 6 入门
wycats/javascript-decorators