迭代器(Iterator)生成器(Generator) 以及 数组推导式,随着这些特性的出现,JavaScript 和 Python 变得越来越像,这让我激动不已。今天我们要谈到的是另一个符合 Python 惯用理念 (Pythonic) 的提议:ECMAScript — 修饰符 (Decorator),该提议来自 Yehuda Katz。

(译者注:ES 的修饰符(Decorator) 在 Python 中也称为修饰符,在 Java 中称为注解(Annotation),在 C# 中称为特性(Attribute)。由于“修饰符”和“特性”在编程语言中有具有其它意义,所以常会有人以“注解”来称呼这一语言特性)

更新 07-29:修饰符已经进入 TC39。他们的最新工作进展可以在提案库中找到。现在还加入了几个新的示例。

修饰符示例

修饰符到底是个什么东西呢?在 Python 中修饰符为调用高阶函数提供了非常简单的语法。Python 的修饰符是一个函数,它接收另一个函数并对其行为进行扩展,但不会显示的修改这个函数。Python 中最简单的修饰符看起来像这样:

最上面的东西 (@mydecorator) 就是修饰符,请注意,它看起来和 ES2016 (ES7) 中的修饰符没什么不同! :)。

@ 是一个解析符号,它表示我们使用了一个名为 mydecorator 修饰符,它是一个同名函数的引用。我们的修饰符需要一个参数(即被修饰的函数),它返回与添加了功能的函数相同的函数。

在你想通过透明的包装添加额外功能的时候,修饰符非常有用。这些功能包括存储、访问控制和认证、仪器仪表和定时功能、日志、速率限制,等等。

ES5 和 ES2015 (又名 ES6) 中的修饰符

ES5 中实现命令式的修饰符(作为纯函数)是相当琐碎的。ES2015(之前叫ES6)中,类已经支持继承,如果我们需要在多个类之间共享一个单一功能,就需要更好解决办法,某种更好的的分配方法。

Yehuda 的修饰符在设计时致力于允许注解和修改 JavaScript类、属性和对象字面量,同时还要保持语法本身的表述。

我们通过实际操作来看看 ES2016 的修饰符!

操作 ES2016 修饰符

记住从 Python 学到的东西。ES2016 修饰符是一个返回函数的表达式,它需要目标、名称和属性描述符作为参数。使用的时候需要前缀 @ 字符并将其放在修饰对象之前。可以为类或属性定义修饰符。

修饰属性

先来看一个基础类,Cat:

定义这个类的结果会在 Cat.prototype 中定义 meow 函数,大致如下:

设想一下,我们希望标记属性或方法名称为不可更改。修饰符放在定义属性的语法之前。我们可以这样定义 @readonly 修饰符:

然后像下面这样为 meow 属性添加这个修饰符:

修饰符是一个会在执行后返回一个函数的表达式。因此 @readonly 和 @something(parameter)都是正确的。

现在,在将描述符安装到 Cat.prototype 之前,引擎会先执行修饰符:

实际的结果是 meow 现在是只读的。我们可以这样来验证一下:

非常棒,不是吗?稍后再来看看修饰类(而不只是修饰属性),但是我要先花点时间说下相关库。尽管它们还是新,但是已经再现包含 2016 个修饰符的各种库,包括来自 Jay Phelps 的 https://github.com/jayphelps/core-decorators.js

与我们在上面让属性只读的尝试,这个库包含了自己实现的 @readonly,只需要导入即可:

这个库还包含其它一些修饰符工具,比如 @deprecate,有时候某个 API 需要提示方法可能会改变时就可以用这个修饰符:

调用 console.warn() 输出一个反对信息。允许使用自定义的消息代替默认消息。你还可以提供一下带 url 的选项,以便进一步阅读以了解相关信息。

修饰类

接下来我们看看怎样修饰类。这种情况下,根据提议规范,修饰符接受构造函数作为目标。假设有 MySuperHero 类,我们可以为其定义一个简单的 @superhero 修饰符:

这还可以进一步扩展,提供参数,把修饰符函数定义成工厂方法:

ES2016 修饰符作用于属性描述符和类。它们会自动获取传入的属性名称和目标对象,这些很快就会讲到。通过描述符可以做一些可以改变属性的事情,比如把 getter 的行为变得复杂一些,比如在第一次访问属性的时候自动为当前实例绑定方法。

ES2016 修饰符和混入

我非常喜欢阅读 Reg Braithwaite 最近写的 ES2016 修饰符和混入功能性混入。Reg 提出一个的辅助工具,它可以将行为混入任何目目标(类的原型或独立对象),然后继续描述类的特定版本。mixin 函数会将实例行为混入类原型,就像这样:

非常好。现在我们可以定义一些混入并尝试使用它们来修饰某个类。假设我们有一个简单的 ComicBookCharacter 类:

这可能是世界上最无聊的角色,但是我们可以定义一些混入来提供 SuperPowers 和 UtilityBelt。现在用 Reg 的 mixin 辅助函数来做这个事情:

在这之后我们可以使用 @ 语法,后面紧接着混入函数的名称,用来修饰 ComicBookCharacter,为其注入我们所期望的功能。请注意到我们在类前面添加了多个修饰符语句:

接下来用我们定义好的东西制作一个 Batman 角色。

类的这些修饰符相对紧凑,我自己会用它们来代替函数调用或者高阶组件的辅助。

注意:@WebReflection 有一些替代品,以 mixin 的形式在本节中使用,你可以在这里的评论中看到。

在 Babel 中允许修饰符

修饰符(在写作本文的时候)仍然只是个提议,还没有被批准。不过感谢 Babel 在实验模式下支持翻译这个语法,所以本文中提到的多数示例都可以直接在 Babel中进行尝试。

如果使用 Babel CLI,你可以这样使用修饰符:


$ babel --optional es7.decorators

或者,你也可以使用转换器来打开对修饰符的支持:

甚至还有一个在线的 Babel REPL,可以勾选“试验特性(Experimental)” 复选框来进行尝试![译者注:目前在 REPL 中没发现“试验特性”复选框,悲剧……]

有趣的实验

我有幸坐在 Paul Lewis 的旁边,他一直在试验 通过修饰符来调度读写 DOM 的代码。这个想法借鉴了 Wilson Page 的 FastDOM,但是提供的 API 接口更精简。Paul 的读/写修饰符还可以在你调用 @write 修饰的方法或属性(或者在 @read 时修改 DOM) 触发重新布局时,在控制台发发警告。

下面是 Paul 实验中的一个示例,它试图改变 @read 中的 DOM,因此会导致控制台输出一个警告:

现在就尝试修饰符!

在短时间内,ES2016 修饰符可用于声明修饰和注解、类型检查,以及挑战修饰 ES2015 类。长远来看,它们可能被证明对静态分析非常有用(静态分析是指可以让工具在编辑期进行类型检查或自动完成)。

他们与典型 OOP 中的修饰符没什么不同,这个模式允许使用行为来修饰对象,不管是静态的还是动态的都不会对同一个类的对象间产生碰撞。我认为它们是种巧妙的附着。用于类属性的修饰符的语义仍然在不断变化,但是可以随时关注 Yehuda 在项目中的更新。

库作者正在讨论使用修饰符代替混入,而且可以确定肯定会有办法在 React 中使用高阶组件。

我个人很高兴看到关于它们的各种实验出现,同时也希望你使用 Babel 来进行一些尝试,找到修饰符的新用途。你甚至可以像 Paul 一样分享你的作品 :)

相关阅读和参考

感谢 Jay PhelpsSebastian McKenziePaul Lewis 和 Surma 审阅本文并认真地提供反馈 ❤

P.S:很不幸,Medium 还不支持语法高亮。如果你需要本文的代码段或者可访问的版本,可以看我原来的[gist]。(https://gist.github.com/addyosmani/a0ccf60eae4d8e5290a0)