新手快速入门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 相关文章推荐
一个判断email合法性的函数[非正则]
Dec 09 Javascript
Extjs TriggerField在弹出窗口显示不出问题的解决方法
Jan 08 Javascript
javascript实现的像java、c#之类的sleep暂停的函数代码
Mar 04 Javascript
jquery text(),val(),html()方法区别总结
Nov 04 Javascript
原生js和jquery实现图片轮播特效
Apr 23 Javascript
JS判断是否在微信浏览器打开的简单实例(推荐)
Aug 24 Javascript
详解基于Node.js的微信JS-SDK后端接口实现代码
Jul 15 Javascript
原生JS实现动态加载js文件并在加载成功后执行回调函数的方法
Dec 30 Javascript
详解Node.js读写中文内容文件操作
Oct 10 Javascript
如何阻止小程序遮罩层下方图层滚动
Sep 05 Javascript
vue点击当前路由高亮小案例
Sep 26 Javascript
Vue.js桌面端自定义滚动条组件之美化滚动条VScroll
Dec 01 Vue.js
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生成静态页面详解
2006/11/19 PHP
ThinkPHP之foreach标签使用概述
2014/06/30 PHP
ThinkPHP多语言支持与多模板支持概述
2014/08/22 PHP
php web环境和命令行环境下查找php.ini的位置
2019/07/17 PHP
jQuery动态地获取系统时间实现代码
2013/05/24 Javascript
JS判定是否原生方法
2013/07/22 Javascript
jQuery中delegate和on的用法与区别详细解析
2014/01/26 Javascript
JQuery 使用attr方法实现下拉列表选中
2014/10/13 Javascript
javascript实现动态表头及表列的展现方法
2015/07/14 Javascript
javascript实现数组内值索引随机化及创建随机数组的方法
2015/08/10 Javascript
浅析JavaScript中的对象类型Object
2016/05/26 Javascript
详解前后端分离之VueJS前端
2017/05/24 Javascript
微信小程序实现简易table表格
2020/06/19 Javascript
vue项目中使用AES实现密码加密解密(ECB和CBC两种模式)
2019/08/12 Javascript
Vue3 的响应式和以前有什么区别,Proxy 无敌?
2020/05/20 Javascript
vue-router重写push方法,解决相同路径跳转报错问题
2020/08/07 Javascript
uni-app 自定义底部导航栏的实现
2020/12/11 Javascript
Python使用Scrapy爬取妹子图
2015/05/28 Python
python过滤字符串中不属于指定集合中字符的类实例
2015/06/30 Python
Python用zip函数同时遍历多个迭代器示例详解
2016/11/14 Python
Python创建xml文件示例
2017/03/22 Python
python+selenium识别验证码并登录的示例代码
2017/12/21 Python
浅析Python 读取图像文件的性能对比
2019/03/07 Python
python爬虫 批量下载zabbix文档代码实例
2019/08/21 Python
什么是python的函数体
2020/06/19 Python
基于python图书馆管理系统设计实例详解
2020/08/05 Python
HTML5 Canvas像素处理使用接口介绍
2012/12/02 HTML / CSS
PHP数据运算类型都有哪些
2013/11/05 面试题
介绍一下Java中的static关键字
2012/05/12 面试题
简历的自荐信
2013/12/19 职场文书
世界地球日活动总结
2015/02/09 职场文书
2016暑期社会实践心得体会范文
2016/01/14 职场文书
2016教师学习党章心得体会
2016/01/15 职场文书
Java方法重载和方法重写的区别到底在哪?
2021/06/11 Java/Android
Python Pandas常用函数方法总结
2021/06/15 Python
python如何查找列表中元素的位置
2022/05/30 Python