探索 ECMAScript 修饰符 (译)

Posted 边城客栈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了探索 ECMAScript 修饰符 (译)相关的知识,希望对你有一定的参考价值。

原文:Exploring ES7 Decorators

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

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

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

修饰符示例

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

@mydecorator
def myfunc():
   pass

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

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

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

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

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

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

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

使用 ES2016 修饰符

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

修饰属性

先来看一个基础类,Cat:

class Cat {
    meow() {
       return `${$this.name} says Meow!`;    } }

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

Object.defineProperty(Cat.prototype, 'meow', {
    value: specifiedFunction,
    enumerable: false,
    configurable: true,
    writable: true
});

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

function readonly(target, key, descriptor) {
    descriptor.writable = false;
   return descriptor; }

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

class Cat {
    @readonly
    meow() {
       return `${this.name} says Meow!`;    } }

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

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

let descriptor = {
    value: specifiedFunction,
    enumerable: false,
    configurable: true,
    writable: true
}

// 修饰符的签名与 `Object.defineProperty` 相同,
// 它会在实际与之相关的 `defineProperty` 实际发生前
// 进行处理
descriptor = readonly(Cat.prototype, 'meow', descriptor) || descriptor;
Object.defineProperty(Cat.prototype, 'meow', descriptor);

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

var garfield = new Cat();
garfied.meow = function() {
   console.log("I want lasagne!"); }

// Exception: Attempted to assign to readonly property

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

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

import { readonly } from 'core-decoratiors';

class
Meal {    @readonly    entree = 'steak'; }
var dinner = new Meal(); dinner.entree = 'salmon';

// Cannot assign to read only property 'entree' of [object Object]

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

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

import { deprecate } from 'core-decorators';

class
Person {    @deprecate    facepalm() {}    @deprecate('We stopped facepalming')    facepalmHard() {}    @deprecate('We stopeped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })    facepalmHarder() {} }

let captainPicard = new Person(); captainPicard.facepalm();
// DEPRECATION Person#facepalm: This function will be removed in future versions.

captainPicard.facepalmHard();
// DEPRECATION Person#facepalm: We stopped facepalming

captainPicard.facepalmHarder();
// DEPRECATION Person#facepalmHarder: We stopped facepalming
//
//    See http://knowyourmeme.com/memes/facepalm for more details.
//

修饰类

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

function superhero(target) {
    target.isSupperhero = true;
    target.power = 'flight';
}

@superhero
class MySuperHero() {}
console.log(MySuperHero.isSuperhero);   // true

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

function superhero(isSuperhero) {
   return function(target) {        target.isSuperhero = isSuperhero    } } @superhero(true)
class MySuperheroClass() {}
console.log(MySuperheroClass.isSuperhero)   // true

@superhero(false)class MySuperheroClass() {}
console.log(MySuperheroClass.isSuperhero)   // false

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

ES2016 修饰符和混入

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

function mixin(behaviour, sharedBehaviour = {}) {
   const instanceKeys = Reflect.ownKeys(behaviour);
   const sharedKeys = Reflect.ownKeys(sahredBehaviour);
   const typeTag = Symbol('isa');
   
   function _mixin(clazz) {
       for (let property of instanceKeys) {
           Object.defineProperty(clazz.prototype, property, { value: behaviour[property] });        }

       Object.defineProperty(clazz.prottype, typeTag, { value: true }));
       return clazz;    }

   for (let property of sharedKeys) {
       Object.defineProperty(_mixin, property, {            value: sharedBehaviour[property],            enumerable: sharedBehaviour.propertyIsEnumerable(property)        });    }

   Object.defineProperty(_mixin, Symbol.hasInstance, {        value: (i) => !!i[typeTag]    });
   return _mixin; }

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

class ComicBookCharacter {
    constructor(first, last) {
       this.firstName = first;
       this.lastName = last;    }
   realName() {
       return this.firstName + ' ' + this.lastName;    } };

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

const SuperPowers = mixin({
    addPower(name) {
       this.powers().push(name);
       return this;    },    powers() {
       return this._powers_pocessed || (this._powers_pocessed = []);    } });

const UtilityBelt = mixin({    addToBelt(name) {
       this.utilities().push(name);
       return this;    },    utilities() {
       return this._utility_items || (this._utility_items = []);    } });

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

@SuperPowers
@UtilityBelt
class ComicBookCharacter {    constructor(first, last) {
       this.firstName = first;
       this.lastName = last;    }    realName() {
       return this.firstName + ' ' + this.lastName;    } };

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

const batman = new ComicBookCharacter('Bruce', 'Wayne');
console.log(batman.realName());
// Bruce Wayne

batman    .addToBelt('batarang')    .addToBelt('cape');
console.log(batman.utilities());
// ['batarang', 'cape']

batman    .addPower('detective')    .addPower('voice sounds like Gollum has asthma');
console.log(batman.powers());
// ['detective', 'voice sounds like Gollum has asthma']

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

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

在 Babel 中允许修饰符

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

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

$ babel --optional es7.decorators

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

babel.transform("code", { optional: ["es7.decorators"] });

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

有趣的实验

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

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

class MyComponent {
    @read
    readSomeStuff() {
       console.log('read');

       // Throws a warning.
       document.querySelector('.button').style.top = '100px';    }    @write    writeSomeStuff() {
       console.log('write');
       
       // Throws a warning.
       document.querySelector('.button').focus();    } }

现在就尝试修饰符!

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

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

库作者正在讨论使用修饰符代替混入,而且可以确定肯定会有办法在 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 审阅本文并认真地提供反馈 ❤

P.S:如果你需要本文的代码段或者可访问的版本,可以看我原来的[gist](https://gist.github.com/addyosmani/a0ccf60eae4d8e5290a0)。

  • 修饰符 - https://medium.com/tag/decorators?source=post

  • JavaScript - https://medium.com/tag/javascript?source=post

  • 设计模式 - https://medium.com/tag/design-patterns?source=post

以上是关于探索 ECMAScript 修饰符 (译)的主要内容,如果未能解决你的问题,请参考以下文章

入口点不能用“异步”修饰符标记

微软泄露的Windows源代码可编译;ECMAScript 国际化API规范已完成...

10方法的定义和重载和递归

Java初识方法

Java方法

将 NSString 转换为 AXUIElementPostKeyboardEvent 的 keyCode+修饰符