使用ES7中的Decorator来简化代码
Opened this issue · 0 comments
之前偶然在浏览网上的代码中发现了很多类的开头有这么一个@符号,出于好奇心,查阅了一番网上的资料,发现这是 ES7 中的新特性,还处于不稳定的状态,但是它却能帮助我们写出很简洁的代码。所以,我就把我对它的理解写下来。
在面向对象编程中,我们经常使用封装,继承,多态的特性。当我们需要为类新添一个打印日志的功能的时候,并且这个打印日志的功能可能很多地方都需要用到,我们就会抽象一个日志类,让其他类继承于它。但是这种方式不够灵活,通过继承的方式可能会导致子类繁多,仅仅为了增加一个单一的功能,就会显得多余。在js
中,我们可以用包装对象的方式将打印日志的功能注入到类中,在不影响原有类功能的基础上,这也就是装饰器模式。这里的装饰器模式跟 es7 中的装饰器的概念是相同的,只是实现的方式不同而已。
装饰器模式
首先,先介绍下js
中的装饰器模式,它的实现方式是用包装对象的形式,类似于函数复合或高阶组件。先来看个例子:
function cat() {
console.log('my name is kitty');
}
function bell(wrapper) {
return function() {
const result = wrapper.apply(this, arguments);
console.log('ding ding ding');
return result;
};
}
const kittyCat = bell(cat);
kittyCat();
上面的代码中,我为这只猫加了一个铃铛,每当猫走路时,铃铛就会响。bell
方法通过返回函数的形式实现装饰器模式。关键代码在wrapper.apply(this, arguments)
上,不改变原函数的行为。执行结果如下:
上面代码用class
的方式也很容易实现。
class Cat {
constructor(name) {
console.log(`my name is ${name}`);
}
}
class Bell {
constructor(cat) {
console.log('ding ding ding');
}
}
const kittyCat = new Bell(new Cat('kitty'));
这就是js
实现的装饰器模式,当然,还有其他方法可以实现。
ES7 中的装饰器
ES7 中的装饰器能够让我们动态扩展类的功能、修改类的行为、参数等等。
使用@操作符
Decorator 装饰器是 ES7 中的提案,它目前还只是一个提案,在浏览器或者nodejs
中都暂时还不支持,需要借助babel
转换。
- 安装
babel
npm install -g babel-cli
npm install --save-dev babel-preset-env babel-plugin-transform-decorators-legacy
- 创建
.babelrc
文件,并添加:
{ "presets": ["env"], "plugins": ["transform-decorators-legacy"] }
这样准备工作就做好了。
实现
ES7 中的装饰器使用了 ES5 种的Object.defineProperty(target, name, descriptor)
这一新特性。如果还不了解Object.defineProperty
,请参考MDN 文档。
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。该方法具有三个参数:
- target: 目标对象
- name: 要定义或修改的属性的名称
- descriptor:将被定义或修改的属性描述符
这里最重要的是descriptor
这个参数,它是一个数据或访问器的属性描述对象。在对数据和访问器属性描述时,它们都具有 configurable
、enumerable
属性可用。而在数据描述时,value、writable 属性则是数据所特有的。get
、set
属性则是访问器属性描述所特有的。属性描述器中的属性决定了对象 prop
属性的一些特性。比如 enumerable
,它决定了目标对象是否可被枚举,能够在 for…in
循环中遍历到,或者出现在 Object.keys
法的返回值中;writable
则决定了目标对象的属性是否可以被更改。
对于descriptor
中的属性,它们可以被我们在 Decorator 中使用,或者修改的,以达到我们标注或者拦截的目的。
举个例子:
class Cat {
constructor(name) {
this.name = name;
this.meow();
}
meow() {
console.log(`my name is ${this.name}`);
}
}
const kittyCat = new Cat('kitty');
1. 对方法的修饰
我想让这只猫出生的时候伴随着一声叫声,要怎么办呢~
function meowDecorator(msg) {
return function(target, key, descriptor) {
const method = descriptor.value; // 保留原有函数调用
descriptor.value = function(...args) {
const ret = method.apply(this, args); // 调用原有函数声明
console.log(msg); // 打印出小猫的叫声~
return ret;
};
return descriptor; // 注意:最后要返回descriptor
};
}
class Cat {
constructor(name) {
this.name = name;
this.meow();
}
@meowDecorator('ao uuu~~~') // 给类的方法添加装饰器
meow() {
console.log(`my name is ${this.name}`);
}
}
const kittyCat = new Cat('kitty');
上面代码中,声明了一个 meowDecorator 的装饰器,并且返回了一个匿名函数,target
指向了Cat
类,key
指meow
方法,descriptor
指meow
的装饰符,类似下方的对象。
{ value: [Function: meow],
writable: true,
enumerable: false,
configurable: true,
}
最后的执行结果如下:
注意:在调用原有函数方法时,上下文没有指向target
原有的类,而是指向了调用该方法的this
,也就是Cat
的实例对象,这样才能正确访问this.name
实例属性。
2. 对类的修饰
上面主要是对类的方法的修饰,装饰器也可以对类进行修饰。只是参数稍有区别,只有target
参数指向当前类对象。举个例子,我们为这只 kitty 赋予飞行的能力~
+function flyDecorator(target) {
+ target.prototype.fly = function() { // 添加飞行能力
+ console.log('i can fly~');
+ }
+ return target;
+}
+
+@flyDecorator
class Cat {
constructor(name) {
this.name = name;
}
const kittyCat = new Cat('kitty');
+kittyCat.fly();
这里我添加了一个flyDecorator
的装饰器,为类的原型添加了fly
方法,这样猫就能飞起来了~
注意: 装饰器不能用于函数声明,因为函数声明会函数提升。
总结一下,ES7 中的装饰器使我们能够动态改变运行时类的行为,因此适用于 AOP(面向切面编程)的场景,譬如需要为类增加打印日志这些无需侵入类的行为的功能的时候,这样就不违反设计模式中的单一职责原则。AOP 为传统的 OOP(面向对象编程)提供了横向的扩展,使得这种编程模型更加立体,弥补了 OOP 纵向扩展的不足~功力未深,还望指正~
完整代码可以在github下的decorator文件夹查看~