新手快速入门JavaScript装饰者模式与AOP
什么是装饰者模式
当我们拍了一张照片准备发朋友圈时,许多小伙伴会选择给照片加上滤镜。同一张照片、不同的滤镜组合起来就会有不同的体验。这里实际上就应用了装饰者模式:是通过滤镜装饰了照片。在不改变对象(照片)的情况下动态的为其添加功能(滤镜)。
需要注意的是:由于JavaScript语言动态的特性,我们很容易就能改变某个对象(JavaScript中函数是一等公民)。但是我们要尽量避免直接改写某个函数,这会导致代码的可维护性、可扩展性变差,甚至会污染其他业务。
什么是AOP
想必大家对"餐前洗手、饭后漱口"都不陌生。这句标语其实就是AOP在生活中的例子:吃饭这个动作相当于切点,我们可以在这个切点前、后插入其它如洗手等动作。
AOP(Aspect-OrientedProgramming):面向切面编程,是对OOP的补充。利用AOP可以对业务逻辑的各个部分进行隔离,也可以隔离业务无关的功能比如日志上报、异常处理等,从而使得业务逻辑各部分之间的耦合度降低,提高业务无关的功能的复用性,也就提高了开发的效率。
在JavaScript中,我们可以通过装饰者模式来实现AOP,但是两者并不是一个维度的概念。AOP是一种编程范式,而装饰者是一种设计模式。
ES3下装饰者的实现
了解了装饰者模式和AOP的概念之后,我们写一段能够兼容ES3的代码来实现装饰者模式:
//原函数
vartakePhoto=function(){
console.log('拍照片');
}
//定义aop函数
varafter=function(fn,afterfn){
returnfunction(){
letres=fn.apply(this,arguments);
afterfn.apply(this,arguments);
returnres;
}
}
//装饰函数
varaddFilter=function(){
console.log('加滤镜');
}
//用装饰函数装饰原函数
takePhoto=after(takePhoto,addFilter);
takePhoto();
这样我们就实现了抽离拍照与滤镜逻辑,如果以后需要自动上传功能,也可以通过aop函数after来添加。
ES5下装饰者的实现
在ES5中引入了Object.defineProperty,我们可以更方便的给对象添加属性:
lettakePhoto=function(){
console.log('拍照片');
}
//给takePhoto添加属性after
Object.defineProperty(takePhoto,'after',{
writable:true,
value:function(){
console.log('加滤镜');
},
});
//给takePhoto添加属性before
Object.defineProperty(takePhoto,'before',{
writable:true,
value:function(){
console.log('打开相机');
},
});
//包装方法
letaop=function(fn){
returnfunction(){
fn.before()
fn()
fn.after()
}
}
takePhoto=aop(takePhoto)
takePhoto()
基于原型链和类的装饰者实现
我们知道,在JavaScript中,函数也好,类也好都有着自己的原型,通过原型链我们也能够很方便的动态扩展,以下是基于原型链的写法:
classTest{
takePhoto(){
console.log('拍照');
}
}
//afterAOP
functionafter(target,action,fn){
letold=target.prototype[action];
if(old){
target.prototype[action]=function(){
letself=this;
fn.bind(self);
fn(handle);
}
}
}
//用AOP函数修饰原函数
after(Test,'takePhoto',()=>{
console.log('添加滤镜');
});
lett=newTest();
t.takePhoto();
使用ES7修饰器实现装饰者
在ES7中引入了@decorator修饰器的提案,参考阮一峰的文章。修饰器是一个函数,用来修改类的行为。目前Babel转码器已经支持。注意修饰器只能装饰类或者类属性、方法。三者的具体区别请参考MDNObject.defineProperty;而TypeScript的实现又有所不同:TypeScriptDecorator。
接下来我们通过修饰器来实现对方法的装饰:
functionafter(target,key,desc){
const{value}=desc;
desc.value=function(...args){
letres=value.apply(this,args);
console.log('加滤镜')
returnres;
}
returndesc;
}
classTest{
@after
takePhoto(){
console.log('拍照')
}
}
lett=newTest()
t.takePhoto()
可以看到,使用修饰器的代码非常简洁明了。
场景:性能上报
装饰者模式可以应用在很多场景,典型的场景是记录某异步请求请求耗时的性能数据并上报:
functionreport(target,key,desc){
const{value}=desc;
desc.value=asyncfunction(...args){
letstart=Date.now();
letres=awaitvalue.apply(this,args);
letmillis=Date.now()-start;
//上报代码
returnres;
}
returndesc;
}
classTest{
@report
getData(url){
//fetch代码
}
}
lett=newTest()
t.getData()
这样使用@report修饰后的代码就会上报请求所消耗的时间。扩展或者修改report函数不会影响业务代码,反之亦然。
场景:异常处理
我们可以对原有代码进行简单的异常处理,而无需侵入式的修改:
functionhandleError(target,key,desc){
const{value}=desc;
desc.value=asyncfunction(...args){
letres;
try{
res=awaitvalue.apply(this,args);
}catch(err){
//异常处理
logger.error(err)
}
returnres;
}
returndesc;
}
classTest{
@handleError
getData(url){
//fetch代码
}
}
lett=newTest()
t.getData()
通过以上两个示例我们可以看到,修饰器的定义很简单,功能却非常强大。
小结
我们一步一步通过高阶函数、原型链、Object.defineProperty和@Decorator分别实现了装饰者模式。接下来在回顾一下:
- 装饰者模式非常适合给业务代码附加非业务相关功能(如日志上报),就如同给照片加滤镜;
- 装饰者模式非常适合无痛扩展别人的代码(你经常需要接手别人的项目吧)
有些朋友可能会觉得装饰者模式和vue的mixin机制很像,其实他们都是“开放-封闭原则”和“单一职责原则”的体现。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,