邂逅AOP:说说JavaScript中的修饰器

标签: AOP 修饰器 JS
170人阅读 评论(0) 收藏 举报
分类:

  Hi,各位朋友,大家好,欢迎大家关注我的博客,我是Payne,我的博客地址是https://qinyuanpei.github.io。这个月基本上没怎么更新博客和公众号,所以今天想写一篇科普性质的文章,主题是JavaScript中的修饰器。 为什么使用了”邂逅”这样一个词汇呢?因为当你知道无法再邂逅爱情的时候,你只能去期待邂逅爱情以外的事物;当你意识到爱情不过是生命里的小插曲,你只能去努力弥补生命的完整性。在过往的博客中,我曾向大家介绍过譬如Spring.NET、Unity、AspectCore等AOP相关的框架,亦曾向大家介绍过譬如Python中的装饰器、.NET中的Attribute、Java中的注解等等。再我看来,这些都是非常相近的概念,所以今天这篇文章我们又双叒叕要说AOP啦!什么?你说JavaScript里居然AOP!这简直比任何特性都要开心好吗?而这就要从本文的主角——JavaScript中的修饰器说起。

什么是修饰器?

  JavaScript中的修饰器(Decorator),是ES7的一个提案。目前的浏览器版本均不支持这一特性,所以主流的技术方案是采用Babel进行转译,事实上前端的工具链有相当多的工具都是这样,当然这些都是我们以后的话题啦!修饰器的出现,主要解决了下面这两个问题:

  • 不同类间共享方法
  • 编译时期间对类及其方法进行修改

  这里第一点看起来意义并不显著啊,因为JavaScript里有了模块化以后,在不同间共享方法只需要将其按模块导出即可。当然,在模块化这个问题上,JavaScript社区发扬了一贯的混乱传统,CommonJS、AMD、CMD等等不同的规范层出不穷,幸运的是ES6中使用了import和export实现了模块功能,这是目前事实上的模块化标准。这里需要关注的第二点,在编译时期间对类及其方法进行修改,这可以对类及其方法进行修改,这就非常有趣了呀!再注意到这里的修饰器即Decorator,我们立刻想Python中的装饰器,想到装饰器模式,想到代理模式,所以相信到这里大家不难理解我所说的,我们又双叒叕要说AOP啦!

  那么说了这么多,JavaScript中的修饰器到底长什么样子呢?其实,它没有什么好神秘的,我们在Python和Java中都曾见过它,前者称为装饰器,后者称为注解,即在类或者方法的上面增加一个@符号,联想一下Spring中的Controller,我们大概知道它长下面这样:

/* 修饰类 */
@bar
class foo {}

/* 修饰方法 */
@bar
foo(){}

  OK,现在大家一定觉得,这TM简直就是抄袭了Python好吗?为了避免大家变成一个肤浅的人,我们一起来看看下面具体的例子:

修饰类

@setProp
class User {}

function setProp(target) {
    target.age = 30
}

console.log(User.age)

  这个例子展示的是,我们如何通过修饰器函数setProp()来为User对象赋值,为什么叫做修饰器函数呢?因为这就是个函数啊,而且JavaScript和Python一样都是支持函数式编程的编程语言,所以大家看到这个大可不必感到吃惊,因为大道至简殊途同归。好了,注意到SetProp()方法有一个参数target,因为该方法修饰User类,所以它的参数就是User类,显然它为User类扩展了一个属性age,并给它赋值为30。相信有朋友一定会奇怪这个age是哪里定义的,我只能说JavaScript是个神奇的语言,一切都是对象,一切都是函数。现在,当我们执行到最后一句时,会输出30,这是因为修饰器对类进行修改。

  现在我们尝试修改下这个方法,我们希望可以通过修饰器修改age属性的值,而不是让它成为一个固定数值30,这样就涉及到带参数的修饰器函数。修饰器函数本身会接收三个参数,第一个参数是被修饰的对象,因此为了增加一个新的参数,我们需要对原来的函数进行一层包装,你知道吗?此时我感到非常兴奋,因为这TM真的和Python一模一样啊。好了,遵从这个策略,我们修改原来的代码,并将其调整如下:

@setProp(20)
class User {}

function setProp(value) {
    return function (target) {
        target.age = value
    }
}

console.log(User.age)

此种差别,大家可以非常明显地看出来,我们在使用修饰器函数setProp()的时候,现在允许传入一个参数20,此时的结果是非常地显而易见的,这段代码将如你所愿地输出20。

修饰方法

  既然修饰器可以修饰类,那么可不可以修饰方法呢?答案自然是可以的。因为当修饰器修饰类的时候,修饰器函数的参数是一个对象,即target,而当修饰器修饰方法的时候,修饰器函数的参数是一个函数。可函数难道就不是对象吗?.NET里的委托最终不是同样会生成一个类吗?Python中不是有函数对象这一概念吗?那么,我们继续看一个例子 :

class User {
    @readonly
    getName() {
        return 'Hello World'
    }
}

// readonly修饰函数,对方法进行只读操作
function readonly(target, name, descriptor) {
    descriptor.writable = false
    return descriptor
}

let u = new User()
// 尝试修改函数,在控制台会报错
u.getName = () => {
    return 'I will override'
}

在这个例子中,我们通过修饰器函数readonly()对getName()方法进行修饰,使其变成一个readonly的方法。我们提到修饰器函数有三个参数,target指被修饰的对象,name指被修饰器对象的名称,descriptor指被修饰对象的defineProperty。因为设置descriptor的writable属性为false以后,这个函数就无法被覆盖重写,所以代码中尝试重写该方法时就会报错;同理,如果我们对descriptor的value属性进行修改,则可以对该函数进行重写。

总结

  相信熟悉Python中的朋友,应该会知道在Python中内置了大量的装饰器,譬如@property可以让一个方法像属性一样被调用、@staticmethod可以让一个方法变成静态方法、@classmethod可以让一个方法变成类方法等。那么,作为Python的追随者,JavaSript中是否存在相类似的概念呢?答案还是肯定的啊!哈哈。具体大家可以参考这里:ES6 Decorator

AOP与修饰器

  熟悉我写作风格的朋友,应该可以猜到我接下来要做什么了。的确,作为一个在某些方面有强迫症的人,我一直在不遗余力地向大家推广AOP,因为我相信AOP真的可以帮大家去做很多事情。比如最简单的记录日志,或许在前端项目中大家更习惯用console.log()来记录日志,甚至是使用alert(),毕竟这些东西不会在界面上展示出来,所以写一写这些东西好像无可厚非。可当你有了AOP以后,为什么还要做如此出力不讨好的事情呢?我写这篇文章的一个重要原因,正是我看到在前端同事的代码中,使用修饰器做了一个简单的AOP,这非常符合我的品味。具体怎么样去做呢?我们一起来看这段代码:

class Bussiness {
    @log
    step1() {}

    @log
    step2() {}
}

function log(target,name,decriptor){
    var origin = descriptor.value;
    descriptor.value = function(){
      console.log('Calling function "${name}" with ', argumants);
      return origin.apply(null, arguments);
    };

    return descriptor;
}

  我们刚刚提到通过修改descriptor的value属性可以达到重写方法的目的,那么这里就是利用这种方式对原来的方法进行了修改,在调用原来的方法前调用console.log()写了一行日志。的确,就是这样一行平淡无奇的代码,将我们从泥潭中解救出来。试想看到一段日志记录和业务流程掺杂的代码,谁会有心情去解读代码背后真实的含义,更不必说将来有一天要去删除这些日志有多么艰难啦。AOP的基本思想是在代码执行前后插入代码片段,因为根据JavaScript中的原型继承,我们可以非常容易地为Function类型扩展出before和after两个函数:

Function.prototype.before = function(beforefunc){
  var self = this;
  var outerArgs = Array.prototype.slice.call(arguments,1);
  return function{
    var innerArgs = Array.prototype.slice.call(arguments);
    beforefunc.apply(this,innerArgs);
    self.apply(this,outerArgs)
  };
};

Function.prototype.after = function(afterfunc){
  var self = this;
  var outerArgs = Array.prototype.slice.call(arguments,1);
  return function{
    var innerArgs = Array.prototype.slice.call(arguments);
    self.apply(this,outerArgs)
    afterfunc.apply(this,innerArgs);
  };
};

  想象一下,现在我们在重写descriptor的value属性的时候,可以同时指定它的before()方法和after()方法,所以最初的这段代码可以继续被改写为:

var func = function(){
    console.log('Calling function "${name}" with ', argumants);
    return origin.apply(null, arguments);
};

func.before(function(){
  console.log('Start calling function ${name}');
})();

func.after(function(){
  console.log('End calling function ${name}');
})();

  所以,所有让你觉得会增加风险的东西,都是源于你内心的恐惧,因为你不愿意去尝试改变,这是真正的复用,如果Ctrl + C和Ctrl + V可以被称为复用的话,我觉得每一个人都可以说自己是网红啦!这并不是一个笑话,还有什么比写一个@log更简单的吗?同样,我们可以使用修饰器去统计代码运行的时间,而不是在所有地方用两个Date()对象去相减。遵从简洁,从心开始:

function time(){
  return function log(target,name,decriptor){
    var origin = descriptor.value;
    descriptor.value = function(){
      let beginTime = new Date();
      let result = origin.apply(null, arguments);
      let endTime = new Date();
      let time = endTime.getTime() - beginTime.getTime();
      console.log("Calling function '${name}' used '${time}' ms"); 
      return result;
    };

    return descriptor;
  };
}

@time
foo()

  再比如,我们的业务中要求:用户在访问相关资源或者是执行相关操作时,需要确保用户的状态是登录着的,因此,我们不可避免地在代码中,使用if语句去判断用户是否登录,试想如果所有的业务代码都这样写,两个模块间就存在了直接耦合,当然我们可以说这是最简单的做法,因为它照顾了大部分人的思维和情绪,可你看Angular/Redux/TypeScript等项目中无一不遍布着修饰器的身影,当一种框架逐渐流行并成为一种趋势的时候,好像大家立刻就忘记了一件事情:原本我们都是非常排斥这些奇技淫巧的,可因为框架的流行你就默认接受了这种设定。那么,这个逻辑如何使用修饰器来编写会怎么样呢?

class User {
    @checkLogin
    getUserInfo() {
        console.log('获取已登录用户的用户信息')
    }

    @checkLogin
    sendMsg() {
        console.log('发送消息')
    }
}

// 检查用户是否登录,如果没有登录,就跳转到登录页面
function checkLogin(target, name, descriptor) {
    let method = descriptor.value
    descriptor.value = function (...args) {
        //假想的校验方法,假设这里可以获取到用户名/密码
        if (validate(args)) {
            method.apply(this, args)
        } else {
            console.log('没有登录,即将跳转到登录页面...')
        }
    }
}
let u = new User()
u.getUserInfo()
u.sendMsg()

  显然,现在我们可以避免模块间的直接耦合,无需在每个业务方法中重复去写if语句,更重要的是通过JavaScript中的模块化规范,我们可以把checkLogin这个方法,扩展到更多的业务类及其方法中去,而唯一的代价就是在方法上增加@checkLogin修饰,你说,有这样优雅的策略,你为什么就不愿意去使用呢?在ASP.NET中我们通过Authorize特性就可以为API和页面授权,现在看来这是不是有点异曲同工之妙呢?你现在还觉得这样麻烦吗?

本文小结

  这篇文章从一个前端项目中的日志拦截器(InterceptLog)为引子,引出了ES7提案中的一个特性:修饰器。修饰器的出现,解决了两个问题:第一、不同类间共享方法;第二、在编译时期间对类及其方法进行修改。虽然目前修饰器不能直接在浏览器中使用,可是通过Babel这样的转译工具,我们已经可以在项目中提前感受这一特性,这里表扬下前端组的同事们。JavaScript中的修饰器同Python中的修饰器类似,可以修饰类及其方法。JavaScript中的修饰器不建议修饰函数,因为存在一个函数提升的问题,如果一定要修饰函数,按照高阶函数的概念直接包装函数即可。通过修饰器可以简化我们的代码,在本文中我们例举了日志记录、运行时间记录、登录检查三个AOP相关的实例,希望大家可以从这篇文章中有所收获。

  最后,请允许博主爆一个料,因为要写一个简单的修饰器,需要安装若干Babel甚至是Webpack插件,我这篇文章中的代码,截止到写这篇文章时都没能在实际环境中运行,这不能怪我啊,因为前端的工具链实在是太长太多啦,这当然不能和直接内置装饰器的Python相比啊,这真的不是吐槽诶,我需要一个开箱即用的特性就这么难吗?人生苦短,我用Python!(逃

参考文章

查看评论

python装饰器与AOP编程,个人理解

Python装饰器是一个很出名的设计模式,它主要的功能就是不改变函数已有功能的情况下对函数起到一个锦上添花的作业,使函数的功能更加丰富,在插入日志,性能测试,缓存机制和权限验证都是比较好的左右。不同的...
  • qq_35064031
  • qq_35064031
  • 2018-03-06 20:25:49
  • 25

ECMAScript6(18):Decorator修饰器

相关文章ECMAScript6 (1):块级作用域 ECMAScript6 (2):解构赋值 ECMAScript6 (3):数值类型扩展 ECMAScript6 (4):字符串类型扩展 EC...
  • Faremax
  • Faremax
  • 2017-06-20 10:13:37
  • 2388

AOP和IOC的概念以及在spring中是如何应用的

aop 面向切面编程  就是拦截器 意思就是 举个例子:写好的程序 比如某系统的登录模块 现在已经写好了所有业务了 不想修改任何业务代码但是却想增加一个使用安卓客户端验证的特殊功能。这时候使用ao...
  • u013305082
  • u013305082
  • 2014-10-16 19:34:29
  • 1539

说说Spring中AOP和IOC的概念以及在spring中是如何应用的

今天楼主继续分享一道经典Java面试题:   说说Spring中AOP和IOC的概念以及在spring中是如何应用的?   AOP:面向切面编程   IOC:依赖倒置或者叫做控制反转   关于...
  • qq_32575047
  • qq_32575047
  • 2018-01-14 13:29:19
  • 221

Python 中的修饰器理解

文章先由stackoverflow上面的一个问题引起吧,如果使用如下的代码: @makebold @makeitalic def say(): return "Hello" 打印...
  • tb6013245
  • tb6013245
  • 2015-04-12 15:38:08
  • 10683

python中的修饰器@

'@'符号用作函数修饰符是python2.4新增加的功能,修饰符必须出现在函数定义前一行,不允许和函数定义在同一行。 也就是说 @A def f(): 是非法的。 只可以在模块或类定义层内对函数进行...
  • gzhouc
  • gzhouc
  • 2016-06-30 22:48:55
  • 1036

JavaScript的装饰器:它们是什么及如何使用

装饰器的流行应该感谢在Angular 2+中使用,在Angular中,装饰器因TypeScript能使用。但是在JavaScript中,还处于提议阶段。本文将介绍装饰器是什么,及装饰器如何让代码更加简...
  • owen1190
  • owen1190
  • 2017-06-13 20:37:53
  • 1133

python修饰器的作用

最近在看中的修饰符的作用,以前一直不是很理解修饰符的作用,今天 看了以后感觉挺好的,终于懂@的含义了,有一些初级用法,也有一些高级用法,转了一个我认为解释的很清楚的,供大家欣赏。 P...
  • xx5595480
  • xx5595480
  • 2017-05-18 22:08:46
  • 1793

Spring中的AOP理解

上篇谈到了IOC,这篇和下篇主要理一理AOP。
  • wangyajin333
  • wangyajin333
  • 2015-05-28 16:39:11
  • 1132

Java工程师的一些面试总结(4)

既然不愿意被风雨淋湿,何不未雨绸缪?既然不愿意屈服于坎坷崎岖,何不磨练自己,让自己适应困难,战胜困难;既然不能号令风雨,让日月臣服,为何不趁早学会适应,为自己撑起一方璀璨星空。好多朋友反馈说面试没有答...
  • wochunyang
  • wochunyang
  • 2017-08-02 09:56:40
  • 297
    个人资料
    专栏达人 持之以恒
    等级:
    访问量: 157万+
    积分: 1万+
    排名: 806
    公众号推广
    最新评论