Javascript 中的装饰器

Stella981
• 阅读 630

Javascript 中的装饰器

前言

在 ES6 中增加了对类对象的相关定义和操作(比如 class 和 extends ),这就使得我们在多个不同类之间共享或者扩展一些方法或者行为的时候,变得并不是那么优雅。这个时候,我们就需要一种更优雅的方法来帮助我们完成这些事情。

Python 中的装饰器

decorators 即 装饰器,这一特性的提出来源于 python 之类的语言,如果你熟悉 python 的话,对它一定不会陌生。那么我们先来看一下 python 里的装饰器是什么样子的吧:

A Python decorator is a function that takes another function, extending the behavior of the latter function without explicitly modifying it.

装饰器是在 python 2.4 里增加的功能,它的主要作用是给一个已有的方法或类扩展一些新的行为,而不是去直接修改它本身。

听起来有点儿懵😳,“show me the code !”

def decorator(f):
    print "my decorator"
    return f@decoratordef myfunc():
    print "my function"myfunc()# my decorator# my function

这里的 @decorator 就是我们说的装饰器。在上面的代码中,我们利用装饰器给我们的目标方法执行前打印出了一行文本,并且并没有对原方法做任何的修改。代码基本等同于

def decorator(f):
    def wrapper():
        print "my decorator"
        return f()
    return wrapperdef myfunc():
    print "my function"myfunc = decorator(myfuc)

通过代码我们也不难看出,装饰器 decorator 接收一个参数,也就是我们被装饰的目标方法,处理完扩展的内容以后再返回一个方法,供以后调用,同时也失去了对原方法对象的访问。当我们对某个应用了装饰以后,其实就改变了被装饰方法的入口引用,使其重新指向了装饰器返回的方法的入口点,从而来实现我们对原函数的扩展、修改等操作。

引入到 Javascript 中

那么我们了解到了装饰器在 python 中的表现以后,会不会觉得其实装饰器其实蛮简单的,就是一个 wrapper 嘛,对于 Javascript 这种语言来说,这种形态不是很常见吗,干嘛还要引入这么一个东西呢?

是的,在 ES6 之前,装饰器对于 JS 来说确实显得不太重要,你只是需要加几层 wrapper 包裹就好了(虽然也会显得不那么优雅)。但是正如文章开头所说,在 ES6 提出之后,你会发现,好像事情变得有些不同了。当我们需要在多个不同的类之间共享或者扩展一些方法或行为的时候,代码会变得错综复杂,极其不优雅,这也就是装饰器被提出的一个很重要的原因。

话说从装饰器被提出已经有一年多的时间了,同时期的很多其他新的特性已经随着 ES6 的推进而被大家广泛使用,而这货现在却还停留在 stage 2 的阶段,也很少被人提及和应用。那么,装饰器到底是在 Javascript 中是怎样表现的呢?我们下面来一起看一下吧!

Javascript 中的装饰器

先来看一下装饰器在代码中是长成什么样子吧

@decoratorclass Cat {}class Dog {
    @decorator
    run() {}}

嗯,代码中的 @decorator 就是 JS 中的装饰器,看起来基本和 python 中的样子一样,以 @ 作为标识符,可以作用于类,也可以作用于类的属性。那么接下来,我们就来看看它具体的表现及运行原理吧。

ES6 中的类

首先我们先来看一下关于 ES6 中的类吧

class Cat {
    say() {
        console.log("meow ~");
    }}

上面这段代码是 ES6 中定义一个类的写法,其实只是一个语法糖,而实际上当我们给一个类添加一个属性的时候,会调用到 Object.defineProperty 这个方法,它会接受三个参数:target 、name 和 descriptor ,所以上面的代码实际上在执行时是这样的:

function Cat() {}Object.defineProperty(Cat.prototype, "say", {
    value: function() { console.log("meow ~"); },
    enumerable: false,
    configurable: true,
    writable: true});

好了,有了上面这段代码以后,我们再来看看装饰器在 JS 中到底是怎么样工作的吧!

作用于类的装饰器

当一个装饰器作用于类的时候,大概是这个样子的:

function isAnimal(target) {
    target.isAnimal = true;
    return target;}@isAnimalclass Cat {
    ...}console.log(Cat.isAnimal);    // true

是不是很像之前我们在 python 中看到的装饰器?

(๑•̀ㅂ•́)و✧

所以这段代码实际上基本等同于:

Cat = isAnimal(function Cat() { ... });

那么我们再来看一下作用于类的单个属性方法的装饰器

作用于类属性的装饰器

比如有的时候,我们希望把我们的部分属性置成只读,以避免别人对其进行修改,如果使用装饰器的话,我们可以这样来做:

function readonly(target, name, descriptor) {
    discriptor.writable = false;
    return discriptor;}class Cat {
    @readonly
    say() {
        console.log("meow ~");
    }}var kitty = new Cat();kitty.say = function() {
    console.log("woof !");}kitty.say()    // meow ~

我们通过上面的代码把 say 方法设置成了只读,所以在我们后面再次对它赋值的时候就不会生效,调用的还是之前的方法。

在上面的代码中我们可以看到,我们在定义装饰器的时候,参数是有三个,targetnamedescriptor 。

诶?等一下,这里怎么这么眼熟?⊙_⊙

没错,就是我们上文提到过的关于类的定义那一块儿的 Object.defineProperty 的参数,所以其实装饰器在作用于属性的时候,实际上是通过 Object.defineProperty 来进行扩展和封装的。

所以在上面的这段代码中,装饰器实际的作用形式是这样的:

let descriptor = {
    value: function() {
        console.log("meow ~");
    },
    enumerable: false,
    configurable: true,
    writable: true};descriptor = readonly(Cat.prototype, "say", descriptor) || descriptor;Object.defineProperty(Cat.prototype, "say", descriptor);

嗯嗯,是不是这样看就清楚很多了呢?这里也是 JS 里装饰器作用于类和作用于类的属性的不同的地方。

我们可以看出,当装饰器作用于类本身的时候,我们操作的对象也是这个类本身,而当装饰器作用于类的某个具体的属性的时候,我们操作的对象既不是类本身,也不是类的属性,而是它的描述符(descriptor),而描述符里记录着我们对这个属性的全部信息,所以,我们可以对它自由的进行扩展和封装,最后达到的目的呢,就和之前说过的装饰器的作用是一样的。

当然,如果你喜欢的话,也可以直接在 target 上进行扩展和封装,比如

function fast(target, name, descriptor) {
    target.speed = 20;

    let run = descriptor.value;
    descriptor.value = function() {
        run();
        console.log(`speed ${this.speed}`);
    }

    return descriptor;}class Rabbit {
    @fast
    run() {
        console.log("running~");
    }}var bunny = new Rabbit();bunny.run();// running~// speed 20console.log(bunny.speed);   // 20

小结

OK,让我们再来看一下 JS 里对于装饰器的描述吧:

Decorators make it possible to annotate and modify classes and properties at design time.

A decorator is:

  • an expression

  • that evaluates to a function

  • that takes the target, name, and decorator descriptor as arguments

  • and optionally returns a decorator descriptor to install on the target object

装饰器允许你在类和方法定义的时候去注释或者修改它。装饰器是一个作用于函数的表达式,它接收三个参数 target、 name 和 descriptor , 然后可选性的返回被装饰之后的 descriptor 对象。

现在是不是对装饰器的作用及原理都清楚了呢?

最后一点就是,现在装饰器因为还在草案阶段,所以还没有被大部分环境支持,如果要用的话,需要使用 Babel 进行转码,需要用到 babel-plugin-transform-decorators-legacy 这个插件:

babel --plugins transform-decorators-legacy es6.js > es5.js

如果你感兴趣的话,也可以看一下转码以后的代码,我这里就不做详细介绍了,很有帮助哦~

如果本文描述的有错误的地方,欢迎留言~ ヾ(o◕∀◕)ノ

参考文献:

感谢您的阅读,本文由 凹凸实验室 版权所有。如若转载,请注明出处:凹凸实验室

Javascript 中的装饰器

本文分享自微信公众号 - 凹凸实验室(AOTULabs)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Karen110 Karen110
3年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Peter20 Peter20
3年前
mysql中like用法
like的通配符有两种%(百分号):代表零个、一个或者多个字符。\(下划线):代表一个数字或者字符。1\.name以"李"开头wherenamelike'李%'2\.name中包含"云",“云”可以在任何位置wherenamelike'%云%'3\.第二个和第三个字符是0的值wheresalarylike'\00%'4\
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这