探索 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;
}
};
这可能是世界上最无聊的角色,但是我们可以定义一些混入来提供 SuperPowers
和 UtilityBelt
。现在用 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 修饰符 (译)的主要内容,如果未能解决你的问题,请参考以下文章