新手快速入门JavaScript装饰者模式与AOP


Posted in Javascript onJune 24, 2019

什么是装饰者模式

当我们拍了一张照片准备发朋友圈时,许多小伙伴会选择给照片加上滤镜。同一张照片、不同的滤镜组合起来就会有不同的体验。这里实际上就应用了装饰者模式:是通过滤镜装饰了照片。在不改变对象(照片)的情况下动态的为其添加功能(滤镜)。

需要注意的是:由于 JavaScript 语言动态的特性,我们很容易就能改变某个对象(JavaScript 中函数是一等公民)。但是我们要尽量避免直接改写某个函数,这会导致代码的可维护性、可扩展性变差,甚至会污染其他业务。

什么是 AOP

想必大家对"餐前洗手、饭后漱口"都不陌生。这句标语其实就是 AOP 在生活中的例子:吃饭这个动作相当于切点,我们可以在这个切点前、后插入其它如洗手等动作。

AOP(Aspect-Oriented Programming):面向切面编程,是对 OOP 的补充。利用AOP可以对业务逻辑的各个部分进行隔离,也可以隔离业务无关的功能比如日志上报、异常处理等,从而使得业务逻辑各部分之间的耦合度降低,提高业务无关的功能的复用性,也就提高了开发的效率。

在 JavaScript 中,我们可以通过装饰者模式来实现 AOP,但是两者并不是一个维度的概念。 AOP 是一种编程范式,而装饰者是一种设计模式。

ES3 下装饰者的实现

了解了装饰者模式和 AOP 的概念之后,我们写一段能够兼容 ES3 的代码来实现装饰者模式:

// 原函数
var takePhoto =function(){
console.log('拍照片');
}
// 定义 aop 函数
var after=function( fn, afterfn ){ 
return function(){
let res = fn.apply( this, arguments ); 
afterfn.apply( this, arguments );
return res;
}
}
// 装饰函数
var addFilter=function(){
console.log('加滤镜');
}
// 用装饰函数装饰原函数
takePhoto=after(takePhoto,addFilter);
takePhoto();

这样我们就实现了抽离拍照与滤镜逻辑,如果以后需要自动上传功能,也可以通过aop函数after来添加。

ES5 下装饰者的实现

在 ES5 中引入了Object.defineProperty,我们可以更方便的给对象添加属性:

let takePhoto = 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('打开相机');
},
});
// 包装方法
let aop = function (fn) {
return function () {
fn.before()
fn()
fn.after()
}
}
takePhoto = aop(takePhoto)
takePhoto()

基于原型链和类的装饰者实现

我们知道,在 JavaScript 中,函数也好,类也好都有着自己的原型,通过原型链我们也能够很方便的动态扩展,以下是基于原型链的写法:

class Test {
takePhoto() {
console.log('拍照');
}
}
// after AOP
function after(target, action, fn) {
let old = target.prototype[action];
if (old) {
target.prototype[action] = function () {
let self = this;
fn.bind(self);
fn(handle);
}
}
}
// 用 AOP 函数修饰原函数
after(Test, 'takePhoto', () => {
console.log('添加滤镜');
});
let t = new Test();
t.takePhoto();

使用 ES7 修饰器实现装饰者

在 ES7 中引入了@decorator 修饰器的提案,参考阮一峰的文章。修饰器是一个函数,用来修改类的行为。目前Babel转码器已经支持。注意修饰器只能装饰类或者类属性、方法。三者的具体区别请参考 MDN Object.defineProperty ;而 TypeScript 的实现又有所不同:TypeScript Decorator。

接下来我们通过修饰器来实现对方法的装饰:

function after(target, key, desc) {
const { value } = desc;
desc.value = function (...args) {
let res = value.apply(this, args);
console.log('加滤镜')
return res;
}
return desc;
}
class Test{
@after
takePhoto(){
console.log('拍照')
}
}
let t = new Test()
t.takePhoto()

可以看到,使用修饰器的代码非常简洁明了。

场景:性能上报

装饰者模式可以应用在很多场景,典型的场景是记录某异步请求请求耗时的性能数据并上报:

function report(target, key, desc) {
const { value } = desc;
desc.value = async function (...args) {
let start = Date.now();
let res = await value.apply(this, args);
let millis = Date.now()-start;
// 上报代码
return res;
}
return desc;
}
class Test{
@report
getData(url){
// fetch 代码
}
}
let t = new Test()
t.getData()

这样使用@report修饰后的代码就会上报请求所消耗的时间。扩展或者修改report函数不会影响业务代码,反之亦然。

场景:异常处理

我们可以对原有代码进行简单的异常处理,而无需侵入式的修改:

function handleError(target, key, desc) {
const { value } = desc;
desc.value = async function (...args) {
let res;
try{
res = await value.apply(this, args);
}catch(err){
// 异常处理
logger.error(err)
}
return res;
}
return desc;
}
class Test{
@handleError
getData(url){
// fetch 代码
}
}
let t = new Test()
t.getData()

通过以上两个示例我们可以看到,修饰器的定义很简单,功能却非常强大。

小结

我们一步一步通过高阶函数、原型链、Object.defineProperty和@Decorator分别实现了装饰者模式。接下来在回顾一下:

  • 装饰者模式非常适合给业务代码附加非业务相关功能(如日志上报),就如同给照片加滤镜;
  • 装饰者模式非常适合无痛扩展别人的代码(你经常需要接手别人的项目吧)

有些朋友可能会觉得装饰者模式和 vue 的 mixin 机制很像,其实他们都是“开放-封闭原则”和“单一职责原则”的体现。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,

Javascript 相关文章推荐
JavaScript 实现鼠标拖动元素实例代码
Feb 24 Javascript
Jquery1.9.1源码分析系列(十五)动画处理之外篇
Dec 04 Javascript
js添加千分位的实现代码(超简单)
Aug 01 Javascript
javascript中call,apply,bind函数用法示例
Dec 19 Javascript
vue指令以及dom操作详解
Mar 04 Javascript
基于vue2.0实现的级联选择器
Jun 09 Javascript
Bootstrap Tree View简单而优雅的树结构组件实例解析
Jun 15 Javascript
node+express+ejs使用模版引擎做的一个示例demo
Sep 18 Javascript
angular.js和vue.js中实现函数去抖示例(debounce)
Jan 18 Javascript
vue 项目中使用Loading组件的示例代码
Aug 31 Javascript
JS学习笔记之数组去重实现方法小结
May 29 Javascript
如何在JS文件中获取Vue组件
Sep 16 Javascript
Electron + vue 打包桌面操作流程详解
Jun 24 #Javascript
JS字符串常用操作方法实例小结
Jun 24 #Javascript
新手入门带你学习JavaScript引擎运行原理
Jun 24 #Javascript
vue+elementUI 复杂表单的验证、数据提交方案问题
Jun 24 #Javascript
新手如何快速理解js异步编程
Jun 24 #Javascript
简单了解小程序+node梳理登陆流程
Jun 24 #Javascript
JS数组扁平化(flat)方法总结详解
Jun 24 #Javascript
You might like
纯php打造的tab选项卡效果代码(不用js)
2010/12/29 PHP
linux iconv方法的使用
2011/10/01 PHP
使用php批量删除数据库下所有前缀为prefix_的表
2014/06/09 PHP
PHP代码实现表单数据验证类
2015/07/28 PHP
针对多用户实现头像上传功能PHP代码 适用于登陆页面制作
2016/08/17 PHP
PHP mysqli事务操作常用方法分析
2017/07/22 PHP
dropdownlist之间的互相联动实现(显示与隐藏)
2009/11/24 Javascript
初识JQuery 实例一(first)
2011/03/16 Javascript
ajax中get和post的说明及使用与区别
2012/12/23 Javascript
Jquery判断$("#id")获取的对象是否存在的方法
2013/09/25 Javascript
jquery实现弹出层遮罩效果的简单实例
2014/03/03 Javascript
JavaScript中的6种运算符总结
2014/10/16 Javascript
jquery简单实现图片切换效果的方法
2015/05/12 Javascript
js面向对象编程总结
2017/02/16 Javascript
基于jQuery实现可编辑的表格
2019/12/11 jQuery
jQuery实现动态加载瀑布流
2020/09/01 jQuery
[10:18]2018DOTA2国际邀请赛寻真——找回自信的TNCPredator
2018/08/13 DOTA
python使用pyhook监控键盘并实现切换歌曲的功能
2014/07/18 Python
几个提升Python运行效率的方法之间的对比
2015/04/03 Python
详解Python的Django框架中的模版相关知识
2015/07/15 Python
Python按行读取文件的简单实现方法
2016/06/22 Python
python函数式编程学习之yield表达式形式详解
2018/03/25 Python
基于pycharm导入模块显示不存在的解决方法
2018/10/13 Python
python的几种矩阵相乘的公式详解
2019/07/10 Python
用Anaconda安装本地python包的方法及路径问题(图文)
2019/07/16 Python
python绘制封闭多边形教程
2020/02/18 Python
浅谈selenium如何应对网页内容需要鼠标滚动加载的问题
2020/03/14 Python
使用Nibabel库对nii格式图像的读写操作
2020/07/01 Python
DJI大疆无人机官方商城:全球领先的无人飞行器研发和生产商
2016/12/21 全球购物
校长岗位职责
2013/11/26 职场文书
后勤采购员岗位职责
2013/12/19 职场文书
浙大毕业生自荐信
2014/01/26 职场文书
银行求职信范文怎么写
2015/03/20 职场文书
2015年七年级班主任工作总结
2015/05/21 职场文书
webpack的移动端适配方案小结
2021/07/25 Javascript
Redis如何使用乐观锁(CAS)保证数据一致性
2022/03/25 Redis