迭代器、生成器、数组操作,JavaScript和Python变得越来越像。今天我们来探讨ECMAScript的装饰器 —— 又一个Python化的JavaScript特性。
装饰器模式
装饰器到底是什么东西?在Python中,装饰器是调用高阶函数的一种非常简单的语法。 一个Python 的装饰器,实际上就是以另一个函数为参数的函数, 它以非常简洁的语法来增强参数函数的行为。 最简单的Python装饰器看起来像这样:
1 |
|
第一行的@mydecorator
就是一个装饰器。@
用来告诉Python解析器,我们将调用一个名为mydecorator
的函数来增强函数myfunc
,这将得到一个新的函数 —— 它可以增强myfunc
的功能。
ES5和ES2015中的装饰器
在ES5中,实现指令式的装饰器(作为纯函数)非常琐碎。 在ES2015中,由于类支持继承, 我们需要更好的 方法来实现在多个类之间共用同一功能。
Yehuda的装饰器实现是通过使用注解并在设计时修改JavaScript类、属性和对象来实现,他保持了装饰器的 声明式风格。
现在,让我们看一下ES2016中的装饰器的实战!
ES2016装饰器实战
请记住我们从Python中学到的东西。一个ES2016装饰器就是一个返回函数的表达式,它的参数为target、name
和descriptor。 使用@
符号来为类或属性应用一个装饰器。
装饰一个属性
让我们看一个非常简单的类,Cat:
1 | class Cat { |
执行这段语句,就会在原型对象Cat.prototype
上添加一个meow
方法,类似于:
1 | Object.defineProperty(Cat.prototype,'meow',{ |
假设我们希望将一个属性或方法标记为只读
的。我们可以定义一个@readonly
装饰器:
1 | function readonly(target,name,descriptor){ |
然后,我们可以在meow
方法上应用这个装饰器:
1 | class Cat { |
一个装饰器不过就是一个返回函数的表达式。因此,装饰器可以带参数,比如@readonly
和
@something(parameter)
都是合法的。
现在,在把描述子安装到Cat.prototype
上面之前,引擎将首先调用装饰器:
1 | let descriptor = { |
上面的代码执行后,meow
方法就变成只读了。我们可以验证一下:
1 | var garfield = new Cat(); |
执行到第2行时将抛出异常:Attempted to assign to readonly property
太棒了,对吗?
我们接下来将对类进行装饰。不过插一句,尽管JavaScript的装饰器概念还很新, ES2016的装饰器库已经开始出现了,比如Jay Phelps开发的core-decorators。
类似于我们上面的只读属性的尝试, Jay Phelps的库中已经包含了@readonly
的实现,只需要引入就可以直接使用:
1 | import { readonly } from 'core-decorators'; |
类似的,运行上面的代码,将在试图对dinner.entree
进行改写时抛出异常:Cannot assign to read only property ‘entree’ of [object Object]。
Jay Phelps的库中还包含了一些其他常用的装饰器,例如@deprecate
,用来对可能变化的API提供警示信息:
1 | import { deprecate } from 'core-decorators'; |
装饰一个类
接下来让我们看一下如何对类进行装饰。在这个案例中,一个装饰器将使用被装饰类的构造
函数作为参数。 对于一个假想的MySuperHero
类,我们可以定义一个简单的装饰器@superhero
:
1 | function superhero(target){ |
我们可以更进一步, 把装饰器函数定义为一个可接受参数的工厂函数:
1 | function superhero(isSuperHero){ |
ES2016装饰器可以作用于属性描述子和类。它们可以自动地获得传入的属性名和目标对象,
我们很快介绍这一点。 由于可以获取到属性描述子,这使得一个装饰器可以做很多事情,例如
可以将一个属性改变为使用getter
,这对于实现自动的数据绑定相当有用(mobx就是这么干的)。
ES2016装饰器和Mixins
我认真阅读了Reg Braithwaite最近发表的关于ES2016装饰器用作mixin的文章。Reg提出了一种方法
可以将功能混入
任何目标(类原型或独立对象),并且描述了一个类特定的版本。函数式mixin可以
将实例行为混入一个类的原型,看起来大致如下:
1 | function mixin(behaviour, sharedBehaviour = {}) { |
特别好。我们现在可以定义一些mixin,然后使用它们来装饰类。现在假设有一个类
ComicBookCharacter
:
1 | class ComicBookCharacter{ |
也许这时世界上最乏味的角色, 但是我们可以定义一些mixins来给这个角色
赋予超能SuperPowers
和工具带UtilityBelt
。让我们使用Reg的mixin辅助函数来实现:
1 | const SuperPowers = mixin({ |
有了这些做基础,我们可以使用@
语法来赋予 ComicBookCharacter
能力。注意
我们是如何为类应用多个装饰器的:
1 | @SuperPowers |
现在,让我们使用这个类来创建一个蝙蝠侠角色:
1 | const batman = new ComicBookCharacter('Bruce','Wayne'); |
这些用于类的装饰器相对紧凑,可以用它们来代替高阶函数调用。
开启Babel的装饰器功能
装饰器目前还停留在建议阶段,并未被最终批准。但是多种编译器都已经 支持这一特性,例如Babel、traceur和tsc。
如果在命令行中使用Babel,可以如下方式打开装饰器功能:
1 | $ babel --optional es7.decorators |
或者,也使用转换器打开这个功能:
1 | babel.transform("code",{optional:["es7.decorators"]}) |
有趣的实验
坐在我旁边的Paul Lewis尝试使用装饰器来重新调度读写DOM的代码。他借鉴了Wilson Page的FastDOM实现思路,但是提供了非常简单的API。
下面是Paul的一个实验示例,试图在@read
中改变DOM将在控制台输出警告:
1 | class MyComponent{ |
开始尝试装饰器!
短期来看,ES2016装饰器对于声明式装饰、注解、类型检查等都很有用。从 长远的角度,它们将对静态语言分析(编译时类型检查、自动完成等)非常有用。
ES2016的装饰器和经典OOP中的装饰器没有特别的差异。在传统OOP中,装饰器模式 允许为一个对象赋以额外的行为能力,这既可以是静态的装饰,也可以是动态的装饰—— 在这种情况下该类的其他实例不受影响。ES2016的装饰器语法还在变迁当中,可以留意 Yehuda的repo更新。
库作者们现在在讨论在什么地方可以使用装饰器代替mixins,显然,这些装饰器也可以用于 React中的高阶组件。
我个人对于围绕装饰器的实验和应用相当兴奋,也希望你们使用Babel来试一下,找到 并实现可以复用的装饰器,没准你也可以和Paul一样,分享你的成果!
进一步的阅读和参考
- https://github.com/wycats/javascript-decorators
- https://github.com/jayphelps/core-decorators.js
- http://blog.developsuperpowers.com/eli5-ecmascript-7-decorators/
- http://elmasse.github.io/js/decorators-bindings-es7.html
- http://raganwald.com/2015/06/26/decorators-in-es7.html
- Jay’s function expression ES2016 Decorators example
感谢Jay Phelps, Sebastian McKenzie, Paul Lewis 和 Surma 帮助审核这篇文章并提供了详细的反馈意见。
原文:Exploring EcmaScript Decorators by Addy Osmani