为何标题是 ECMAScript 修饰器,而不是 JavaScript 修饰器?由于,ECMAScript 是编写像 JavaScript 这种脚本语言的标准,它不强制 JavaScript 支持全部规范内容,JavaScript 引擎(不一样浏览器使用不一样引擎)不必定支持 ECMAScript 引入的功能,或者支持行为不一致。javascript
能够将 ECMAScript 理解为咱们说的语言,好比英语。那 JavaScript 就是一种方言,相似英国英语。方言自己就是一种语言,但它是基于语言衍生出来的。因此,ECMAScript 是烹饪/编写 JavaScript 的烹饪书,是否遵循其中全部成分/规则彻底取决于厨师/开发者。前端
理论上来讲,JavaScript 使用者应该遵循语言规范中全部规则(开发者或许会疯掉吧),但实际上新版 JavaScript 引擎很晚才会实现这些规则,开发者要确保一切正常后(才会切换)。TC39 也就是 ECMA 国际技术委员会第 39 号 负责维护 ECMAScript 语言规范。该团队的成员大多来自于 ECMA 国际、浏览器厂商和对 Web 感兴趣的公司。java
因为 ECMAScript 是开放标准,任何人均可以提出新的想法或功能并对其进行处理。所以,新功能的提议将经历 4 个主要阶段,TC39 将参与此过程,直到该功能准备好发布。node
+-------+-----------+----------------------------------------+
| stage | name | mission |
+-------+-----------+----------------------------------------+
| 0 | strawman | Present a new feature (proposal) |
| | | to TC39 committee. Generally presented |
| | | by TC39 member or TC39 contributor. |
+-------+-----------+----------------------------------------+
| 1 | proposal | Define use cases for the proposal, |
| | | dependencies, challenges, demos, |
| | | polyfills etc. A champion |
| | | (TC39 member) will be |
| | | responsible for this proposal. |
+-------+-----------+----------------------------------------+
| 2 | draft | This is the initial version of |
| | | the feature that will be |
| | | eventually added. Hence description |
| | | and syntax of feature should |
| | | be presented. A transpiler such as |
| | | Babel should support and |
| | | demonstrate implementation. |
+-------+-----------+----------------------------------------+
| 3 | candidate | Proposal is almost ready and some |
| | | changes can be made in response to |
| | | critical issues raised by adopters |
| | | and TC39 committee. |
+-------+-----------+----------------------------------------+
| 4 | finished | The proposal is ready to be |
| | | included in the standard. |
+-------+-----------+----------------------------------------+
复制代码
如今(2018 年 6 月),修饰器提案正处于第二阶段,咱们可使用 babel-plugin-transform-decorators-legacy
这个 Babel 插件来转换它。在第二阶段,因为功能的语法会发生变化,所以不建议在生产环境中使用它。不管如何,修饰器都很优美,也有助于更快地完成任务。android
从如今开始,咱们要开始研究实验性的 JavaScript 了,所以你的 node.js 版本可能不支持这个新特性。因此咱们须要使用 Babel 或 TypeScript 转换器。可使用我准备的 js-plugin-starter 插件来设置项目,其中包括了这篇文章中用到的插件。ios
要理解修饰器,首先须要了解 JavaScript 对象属性的属性描述符。 属性描述符是对象属性的一组规则,例如属性是可写仍是可枚举。当咱们建立一个简单的对象并向其添加一些属性时,每一个属性都有默认的属性描述符。git
var myObj = {
myPropOne: 1,
myPropTwo: 2
};
复制代码
myObj
是一个简单的 JavaScript 对象,在控制台中以下所示:github
如今,若是咱们像下面那样将新值写入 myPropOne
属性,操做能够成功,咱们能够得到更改后的值。typescript
myObj.myPropOne = 10;
console.log( myObj.myPropOne ); //==> 10
复制代码
为了获取属性的属性描述符,咱们须要使用 Object.getOwnPropertyDescriptor(obj, propName)
方法。这里 Own 的意思是只有 propName
属性是 obj
对象自有属性而不是在原型链上查找的属性时,才会返回 propName
的属性描述符。编程
let descriptor = Object.getOwnPropertyDescriptor(
myObj,
'myPropOne'
);
console.log( descriptor );
复制代码
Object.getOwnPropertyDescriptor
方法返回一个对象,该对象包含描述属性权限和当前状态的键。 value
表示属性的当前值,writable
表示用户是否能够为属性赋值,enumerable
表示该属性是否会出如今 for in
循环或 for of
循环或 Object.keys
等遍历方法中。configurable
表示用户是否有权更改属性描述符并更改 writable
和 enumerable
。属性描述符还有 get
和 set
键,它们是获取值或设置值的中间件函数,但这两个是可选的。
要在对象上建立新属性或使用自定义描述符修改现有属性,咱们使用 Object.defineProperty
方法。让咱们修改 myPropOne
这个现有属性,writable
设置为 false
,这会禁止向 myObj.myPropOne
写入值。
'use strict';
var myObj = {
myPropOne: 1,
myPropTwo: 2
};
// 修改属性描述符
Object.defineProperty( myObj, 'myPropOne', {
writable: false
} );
// 打印属性描述符
let descriptor = Object.getOwnPropertyDescriptor(
myObj, 'myPropOne'
);
console.log( descriptor );
// 设置新值
myObj.myPropOne = 2;
复制代码
从上面的报错中能够看出,myPropOne
属性是不可写入的。所以若是用户尝试给它赋予新值,就会抛出错误。
若是使用
Object.defineProperty
来修改现有属性的描述符,那原始描述符会被新的修改覆盖。Object.defineProperty
方法会返回修改后的myObj
对象。
让咱们看看若是将 enumerable
描述符键设置为 false
会发生什么。
var myObj = {
myPropOne: 1,
myPropTwo: 2
};
// 修改描述符
Object.defineProperty( myObj, 'myPropOne', {
enumerable: false
} );
// 打印描述符
let descriptor = Object.getOwnPropertyDescriptor(
myObj, 'myPropOne'
);
console.log( descriptor );
// 打印遍历对象
console.log(
Object.keys( myObj )
);
复制代码
从上面的结果能够看出,咱们在 Object.keys
枚举中看不到对象的 myPropOne
属性。
使用 Object.defineProperty
在对象上定义新属性并传递空 {}
描述符时,默认描述符以下所示:
如今,让咱们使用自定义描述符定义一个新属性,其中 configurable
键设置为 false
。咱们将 writable
保持为false
、enumerable
为 true
,并将 value
设置为 3
。
var myObj = {
myPropOne: 1,
myPropTwo: 2
};
// 设置新属性描述符
Object.defineProperty( myObj, 'myPropThree', {
value: 3,
writable: false,
configurable: false,
enumerable: true
} );
// 打印属性描述符
let descriptor = Object.getOwnPropertyDescriptor(
myObj, 'myPropThree'
);
console.log( descriptor );
// 修改属性描述符
Object.defineProperty( myObj, 'myPropThree', {
writable: true
} );
复制代码
经过将 configurable
设置为 false
,咱们失去了更改 myPropThree
属性描述符的能力。若是不但愿用户操做对象的行为,这将很是有用。
get(getter)和 set(setter)也能够在属性描述符中设置。可是当你定义一个 getter 时,也会带来一些牺牲。你根本不能在描述符上有初始值或 value
,由于 getter 将返回该属性的值。你也不能在描述符上使用 writable
,由于你的写操做是经过 setter 完成的,能够防止写入。看看 MDN 文档关于 getter 和 setter,或阅读这篇文章,这里不须要太多解释。
可使用带有两个参数的
Object.defineProperties
方法一次建立/更新多个属性描述符。第一个参数是目标对象,在其中添加/修改属性,第二个参数是一个对象,其中key
为属性名,value
是它的属性描述符。此函数返回目标对象。
你是否尝试过使用 Object.create
方法来建立对象?这是建立没有原型或自定义原型对象最简单方法。它也是使用自定义属性描述符从头开始建立对象的更简单方法之一。
Object.create
方法具备如下语法:
var obj = Object.create( prototype, { property: descriptor, ... } )
复制代码
这里 prototype
是一个对象,它将成为 obj
的原型。若是 prototype
是 null
,那么 obj
将没有任何原型。使用 var obj = {}
语法定义空或非空对象时,默认状况下,obj.__proto__
指向 Object.prototype
,所以 obj
具备 Object
类的原型。
这相似于用 Object.prototype
做为第一个参数(正在建立对象的原型)使用 Object.create
方法 。
'use strict';
var o = Object.create( Object.prototype, {
a: { value: 1, writable: false },
b: { value: 2, writable: true }
} );
console.log( o.__proto__ );
console.log(
'o.hasOwnProperty( "a" ) => ',
o.hasOwnProperty( "a" )
);
复制代码
但当咱们把 prototype 参数设置为 null
时,会出现下面的错误:
'use strict';
var o = Object.create( null, {
a: { value: 1, writable: false },
b: { value: 2, writable: true }
} );
console.log( o.__proto__ );
console.log(
'o.hasOwnProperty( "a" ) => ',
o.hasOwnProperty( "a" )
);
复制代码
如今咱们已经了解了如何定义/配置对象的新属性/现有属性,让咱们把注意力转移到修饰器以及为何讨论属性描述符上。
修饰器是一个 JavaScript 函数(建议是纯函数),它用于修改类属性/方法或类自己。当你在类属性、方法或类自己顶部添加 @decoratorFunction
语法后,decoratorFunction
方法会以一些参数被调用,而后就可使用这些参数来修改类或类属性了。
让咱们建立一个简单的 readonly
修饰器函数。但在此以前,先建立一个包含 getFullName
方法简单的 User
类,这个方法经过组合 firstName
和 lastName
返回用户的全名。
class User {
constructor( firstname, lastName ) {
this.firstname = firstname;
this.lastName = lastName;
}
getFullName() {
return this.firstname + ' ' + this.lastName;
}
}
// 建立实例
let user = new User( 'John', 'Doe' );
console.log( user.getFullName() );
复制代码
运行上面的代码,控制台中会打印出 John Doe
。但这样有一个问题:任何人均可以修改 getFullName
方法。
User.prototype.getFullName = function() {
return 'HACKED!';
}
复制代码
通过上面的修改,就会获得如下输出:
HACKED!
复制代码
为了限制修改咱们任何方法的权限,须要修改 getFullName
方法的属性描述符,这个属性属于 User.prototype
对象。
Object.defineProperty( User.prototype, 'getFullName', {
writable: false
} );
复制代码
如今,若是还有用户尝试覆盖 getFullName
方法,他/她就会获得下面的错误。
但若是 User
类有不少方法,上面这种手动修改就不太好了。这就是修饰器的用武之地了。经过在 getFullName
方法上添加 @readonly
也能够实现一样功能,以下:
function readonly( target, property, descriptor ) {
descriptor.writable = false;
return descriptor;
}
class User {
constructor( firstname, lastName ) {
this.firstname = firstname;
this.lastName = lastName;
}
@readonly
getFullName() {
return this.firstname + ' ' + this.lastName;
}
}
User.prototype.getFullName = function() {
return 'HACKED!';
}
复制代码
看一下 readonly
函数。它接收三个参数。property
是属性/方法的名字,target
是这些属性/方法属于的对象(就和 User.prototype
同样),descriptor
是这个属性的描述符。在修饰器函数中,咱们必须返回 descriptor
对象。这个修改后的 descriptor
会替换该属性原来的属性描述符。
修饰器写法还有另外一种版本,相似 @decoratorWrapperFunction( ...customArgs )
这样。但这样写,decoratorWrapperFunction
函数应该返回一个 decoratorFunction
修饰器函数,它的使用和上面的例子相同。
function log( logMessage ) {
// 返回修饰器函数
return function ( target, property, descriptor ) {
// 保存属性原始值,它是一个方法(函数)
let originalMethod = descriptor.value;
// 修改方法实现
descriptor.value = function( ...args ) {
console.log( '[LOG]', logMessage );
// 这里,调用原始方法
// `this` 指向调用实例
return originalMethod.call( this, ...args );
};
return descriptor;
}
}
class User {
constructor( firstname, lastName ) {
this.firstname = firstname;
this.lastName = lastName;
}
@log('calling getFullName method on User class')
getFullName() {
return this.firstname + ' ' + this.lastName;
}
}
var user = new User( 'John', 'Doe' );
console.log( user.getFullName() );
复制代码
修饰器不区分静态和非静态方法。下面的代码一样能够工做,惟一不一样是你如何访问这些方法。这个结论也适用于咱们下面要讨论的类实例字段修饰器。
@log('calling getVersion static method of User class')
static getVersion() {
return 'v1.0.0';
}
console.log( User.getVersion() );
复制代码
目前为止,咱们已经看到经过 @decorator
或 @decorator(..args)
语法来修改类方法的属性描述符,但如何修改 **公有/私有属性(类实例字段)**呢?
与 typescript
或 java
不一样,JavaScript 类没有类实例字段或者说没有类属性。这是由于任何在 class
里面、constructor
外面定义的都属于类的原型。但也有一个新的提案,它提议使用 public
和 private
访问修饰符来启用类实例字段,目前处于第 3 阶段,也能够经过 babel transformer plugin 这个插件来使用它。
定义一个简单的 User
类,但这一次,不须要在构造函数中设置 firstName
和 lastName
的默认值。
class User {
firstName = 'default_first_name';
lastName = 'default_last_name';
constructor( firstName, lastName ) {
if( firstName ) this.firstName = firstName;
if( lastName ) this.lastName = lastName;
}
getFullName() {
return this.firstName + ' ' + this.lastName;
}
}
var defaultUser = new User();
console.log( '[defaultUser] ==> ', defaultUser );
console.log( '[defaultUser.getFullName] ==> ', defaultUser.getFullName() );
var user = new User( 'John', 'Doe' );
console.log( '[user] ==> ', user );
console.log( '[user.getFullName] ==> ', user.getFullName() );
复制代码
如今,若是查看 User
类的原型,你不会看到 firstName
和 lastName
这两个属性。
类实例字段很是有用,仍是面向对象编程(OOP)的重要组成部分。咱们提出相应的提案很好,但故事远未结束。
与类方法处于类的原型上不一样,类实例字段处于对象/实例上。因为类实例字段既不是类的一部分也不是它原型的一部分,所以操做它的描述符有点困难。Babel 为类实例字段的属性描述符提供了 initializer
方法来替代 value
。为何要用 initializer
方法来替代 value
呢?这个问题有些争议,由于修饰器提案还处于第二阶段,尚未发布最终草案来讲明这个问题,但你能够经过查看 Stack Overflow 上这个答案 来了解背景故事。
也就是说,让咱们修改以前示例并建立简单的 @upperCase
修饰器函数,它会改变类实例字段默认值的大小写。
function upperCase( target, name, descriptor ) {
let initValue = descriptor.initializer();
descriptor.initializer = function(){
return initValue.toUpperCase();
}
return descriptor;
}
class User {
@upperCase
firstName = 'default_first_name';
lastName = 'default_last_name';
constructor( firstName, lastName ) {
if( firstName ) this.firstName = firstName;
if( lastName ) this.lastName = lastName;
}
getFullName() {
return this.firstName + ' ' + this.lastName;
}
}
console.log( new User() );
复制代码
咱们也可使用带参数的修饰器函数,让它更有定制性。
function toCase( CASE = 'lower' ) {
return function ( target, name, descriptor ) {
let initValue = descriptor.initializer();
descriptor.initializer = function(){
return ( CASE == 'lower' ) ?
initValue.toLowerCase() : initValue.toUpperCase();
}
return descriptor;
}
}
class User {
@toCase( 'upper' )
firstName = 'default_first_name';
lastName = 'default_last_name';
constructor( firstName, lastName ) {
if( firstName ) this.firstName = firstName;
if( lastName ) this.lastName = lastName;
}
getFullName() {
return this.firstName + ' ' + this.lastName;
}
}
console.log( new User() );
复制代码
descriptor.initializer
方法由 Babel 内部实现对象属性描述符的 value
的建立。它会返回分配给类实例字段的初始值。在修饰器函数内部,咱们须要返回另外一个 initializer
方法,它会返回最终值。
类实例字段提案具备高度实验性,在到达第 4 阶段前,它的语法颇有可能会改变。所以,将类实例字段与修饰器一块儿使用还不是一个好习惯。
如今咱们已经熟悉了修饰器能作什么。它能够改变属性、类方法行为和类实例字段,使咱们能灵活地经过简单的语法来实现这些。
类修饰器和咱们以前看到的修饰器有些不一样。以前,咱们使用属性修饰器来修改属性或方法的实现,但类修饰器函数中,咱们须要返回一个构造函数。
咱们先来理解下什么是构造函数。在下面,一个 JavaScript 类只不过是一个函数,这个函数添加了原型方法、定义了一些初始值。
function User( firstName, lastName ) {
this.firstName = firstName;
this.lastName = lastName;
}
User.prototype.getFullName = function() {
return this.firstName + ' ' + this.lastName;
}
let user = new User( 'John', 'Doe' );
console.log( user );
console.log( user.__proto__ );
console.log( user.getFullName() );
复制代码
这篇文章 对理解 JavaScript 中的
this
颇有帮助。
所以,当咱们调用 new User
时,就会使用传递的参数调用 User
这个函数,返回结果是一个对象。因此,User
就是一个构造函数。顺便说一句,JavaScript 中每一个函数都是一个构造函数,由于若是你查看 function.prototype
,你会发现 constructor
属性。只要咱们使用 new
关键字调用函数,都会获得一个对象。
若是从构造函数返回一个有效的 JavaScript 对象,那么就会使用这个对象,而不用
this
赋值建立新对象了。这将打破原型链,由于修改后的对象将不具备构造函数的任何原型方法。
考虑到这一点,让咱们看看类修饰器能够作什么。类修饰器必须位于类的顶部,就像以前咱们在方法名或字段名上看到的修饰器同样。这个修饰器也是一个函数,但它应该返回构造函数或类。
假设我有一个简单的 User
类以下:
class User {
constructor( firstName, lastName ) {
this.firstName = firstName;
this.lastName = lastName;
}
}
复制代码
这里的 User
类不包含任何方法。正如上面所说,类修饰器应该返回一个构造函数。
function withLoginStatus( UserRef ) {
return function( firstName, lastName ) {
this.firstName = firstName;
this.lastName = lastName;
this.loggedIn = false;
}
}
@withLoginStatus
class User {
constructor( firstName, lastName ) {
this.firstName = firstName;
this.lastName = lastName;
}
}
let user = new User( 'John', 'Doe' );
console.log( user );
复制代码
类修饰器函数会接收目标类 UserRef
,在上面的示例中是 User
(修饰器的做用目标)而且必须返回构造函数。这打开了使用修饰器无限可能性的大门。所以,类修饰器比方法/属性修饰器更受欢迎。
可是上面的例子太基础了,当咱们的 User
类有大量的属性和原型方法时,咱们不想建立一个新的构造函数。好消息是,咱们在修饰器函数中能够引用类,即 UserRef
。能够从构造函数返回新类,该类将扩展 User
类(UserRef
指向的类)。由于,类也是构造函数,因此下面的代码也是合法的。
function withLoginStatus( UserRef ) {
return class extends UserRef {
constructor( ...args ) {
super( ...args );
this.isLoggedIn = false;
}
setLoggedIn() {
this.isLoggedIn = true;
}
}
}
@withLoginStatus
class User {
constructor( firstName, lastName ) {
this.firstName = firstName;
this.lastName = lastName;
}
}
let user = new User( 'John', 'Doe' );
console.log( 'Before ===> ', user );
// 设置为已登陆
user.setLoggedIn();
console.log( 'After ===> ', user );
复制代码
你能够将多个修饰器放在一块儿,执行顺序和它们外观顺序一致。
修饰器是更快地达到目的的奇特方式。在它们正式加入 ECMAScript 规范以前,咱们先期待一下吧。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。