装饰器是什么?
装饰器(Decorator)是ES7中的一个提案,可能在将来会成为规范。
许多面向对象的编程语言中都具有该项功能,如Java、Python等。
装饰器本身是一种与类相关的语法,主要用来注释或修改类和类方法。许多面向对象的语言都支持这项功能。
装饰器的作用?
Decorator 如其名“装饰器”,可以对一些 对象
进行装饰然后返回一个被包装过的对象
,可以被装饰的对象
包括:类、类方法和访问器。
通过一系列的例子来感受下装饰器的作用。
装饰类
@speakName
class Hero {}
function speakName(target, name, descriptor){
target.prototype.name = 'hero';
}
const hero =new Hero();
console.log(hero.name); // hero
speak
作为装饰器修饰了 Hero
类,在类的原型上添加了 name
属性。当实例化Hero
类时,实例上拥有了name
属性。
如果需要将speak
装饰器做得更加灵活,可以给speak
增加参数的方式来实现。
@speakName('Julius')
class Hero {}
function speakName(name) {
return function(target, name, descriptor) {
target.prototype.name = name;
};
}
const hero = new Hero();
console.log(hero.name); // Julius
可以看到speak
装饰器接受参数,并将参数作为添加name
属性的值。
由此,可以知晓装饰器是支持传参与不传参的。
装饰类属性(类方法)
class Hero { name = "Julius";
@readOnly
speak() {
console.log(`My name is ${this.name}`);
}
}
function readOnly(target, name, descriptor) {
descriptor.writable = false;
}
const hero = new Hero();
hero.speak();
// My name is Julius hero.speak = ()=>{};
// Error: Cannot assign to read only property 'speak' of object
这里给 Hero
类的 speak
类方法添加了 readOnly
装饰器,如果想修改实例化后的 speak
类方法就会报错。
需要注意的是,装饰器目前仅支持装饰类属性中的类方法,无法装饰类中的属性变量,如上例中的 name
属性。
参数详解
可以发现,上述例子中的每个装饰器都具备三个参数,即 target
, name
, descriptor
。
首先,依据上述两个例子,分别看下target
, name
, descriptor
的内容。
@speakName("Julius")
class Hero {}
function speakName(kingname) {
return function(target, name, descriptor) {
console.log(target); // ƒ Hero()
console.log(name); // undefined
console.log(descriptor); // undefined
target.prototype.name = kingname;
};
}
装饰器在装饰类时,target
目标是类本身,也就是例子中的 Hero
类,而此时name
和 descriptor
均为 undefined
。
class Hero {
name = "Julius";
@readonly
speak() {
console.log(`My name is ${this.name}`);
}
}
function readonly(target, name, descriptor) {
console.log(target); // {} __proto__: Object: Object
console.log(name); // speak
console.log(descriptor);
/*
* configurable: true
* enumerable: false
* value: ƒ speak()
* writable: true
*/
descriptor.writable = false;
}
const hero = new Hero();
装饰器在装饰类方法时,target
目标是类实例,name
是类方法的名称,descriptor
则是类方法的描述对象,可看到 value
是类方法本身,其他3个值和 Object.defineProperty
的属性一样,负责控制值的行为,该例中的 writable
就是其中之一。
装饰器的实现原理
我们看一下装饰类属性的例子通过Babel编译为ES5后的源码。
// 装饰器方法
function _decorate(decorators, factory, superClass, mixins) {
// ... 省略具体实现
}
var Hero = _decorate(null, function (_initialize) {
var Hero = function Hero() {
_classCallCheck(this, Hero);
_initialize(this);
};
return {
F: Hero,
d: [
{ kind: "field",
key: "name",
value: function value() {return "Julius";}
},
{ kind: "method",
decorators: [readOnly],
key: "speak",
value: function speak() {console.log("My name is ".concat(this.name));}
}
]
};
});
function readOnly(target, name, descriptor) {
descriptor.writable = false;
}
var hero = new Hero();
hero.speak();
尝试使用脑图来解读下这段源码:
就上图,着重解释一下factory参数内容:
-
_decorate
装饰器方法被执行,factory参数传入了一个匿名函数。 - 匿名函数体中,Hero 类被编译成了 function,并返回对象
{F:类方法,d:[{类原有属性}]}
。 - 类原有属性中
decorators
字段为装饰方法(readOnly
函数)的数组。
decorators
数组中的函数最终将映射到Object.defineProperty操作对象的属性。此处readOnly(target, name, descriptor){}
修改的是属性的可写入性。
同时由于decorators
是数组,即可支持多个装饰方法作用于同一个对象属性,下面举例来看下。
class Hero {
name = "Julius";
@enumerable(false)
@readonly
speak() {
console.log(`My name is ${this.name}`);
}
}
function readonly(target, name, descriptor) {
descriptor.writable = false;
}
function enumerable(isEnumerable) {
return function(target, key, descriptor) {
descriptor.enumerable = isEnumerable;
};
}
const hero = new Hero();
hero.speak();
for (var o in hero) {
console.log(o);
}
// My name is Julius
// name
// 由于descriptor.enumerable=false,故speak属性无法被遍历
装饰器的使用场景
通过装饰器的使用,可以让日常代码变得更加优雅与抽象。下面以React使用场景为例,做下简单介绍。
下面的代码以“方法调用时需打印日志”的场景为例,采用装饰器的方式完成该功能。
import React from "react";
import ReactDOM from "react-dom";
class App extends React.Component {
constructor(...props) {
super(...props);
this.state = {
count: 1
};
}
@log()
onClick(count) {
this.setState({
count: Number(count) + 1
});
}
render() {
return (
<div>
<button onClick={() => this.onClick(this.state.count)}>
乘以{this.state.count}
</button>
<ul>
<li>{1 * this.state.count}</li>
<li>{2 * this.state.count}</li>
<li>{3 * this.state.count}</li>
</ul>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
function log() {
return function decorator(target, name, descriptor) {
if (typeof descriptor === "undefined") {
return;
}
const value = descriptor.value; // onClick方法函数
function overWriteValue(...args) {
console.log(`log:执行方法${name};参数为${args}`);
return value.bind(this)(args);
}
return {
...descriptor,
value: overWriteValue // 拦截并重写onClick方法
};
};
}
// 执行后,可以在日志中看到下面的内容
// log:执行方法onClick;参数为1
// log:执行方法onClick;参数为2
// log:执行方法onClick;参数为3
// log:执行方法onClick;参数为4
// log:执行方法onClick;参数为5
react-descriptor - StackBlitzstackblitz.com
小结
本文介绍了装饰器的原理、作用及使用场景,通过它能帮助我们在编码过程中实现更多实用且方便的功能。
希望在装饰器提案定案后,能有更好的使用体验,也能有更多的抽象实践。