简要介绍JavaScript中的“装饰器”的提案的一些基础示例以及ECMAScript相关的内容
javascript
为何用ECMAScript装饰器代替标题中的JavaScript装饰器? 由于ECMAScript是用于编写脚本语言(如JavaScript)的标准,因此它不强制JavaScript支持全部规范,但JavaScript引擎(由不一样浏览器使用)可能支持或不支持由ECMAScript引入的功能,或者支持一些不一样的行为。java
将ECMAScript视为您所说的某种语言,例如英语。 那么JavaScript就像英式英语同样。 方言自己就是一种语言,可是它是基于它所源自的语言的原则而应运而生。 所以,ECMAScript是烹饪/书写JavaScript的“烹饪书”,由主厨/开发人员决定遵循或不遵照全部配料/规则。node
一般而言,JavaScript采用者遵循用语言编写的全部规范(否则开发人员将会被逼疯),并在新版本的JavaScript引擎出现后,而且直到确保一切正常,才会发布它。 ECMA International的TC39或技术委员会39负责维护ECMAScript语言规范。 通常来讲,该团队的成员是由ECMA International、浏览器供应商和对网络感兴趣的公司而组成。git
因为ECMAScript是开放标准,任何人均可以提出新的想法或功能,并对其进行推进实行。 所以,一个新功能的提案会经历4个主要阶段,而且TC39会参与这个过程,直到该功能准备好施行。github
阶段 | 名称 | 任务 |
---|---|---|
0 | strawman | 提出新功能(建议) 到TC39委员会。 通常由TC39成员或TC39撰稿人提供。 |
1 | proposal | 定义提案,依赖,挑战,示例,polyfills等使用用例。某个拥护者(TC39成员)将负责此提案。 |
2 | draft | 这是最终版本的草稿版本。 所以须要提供该功能的描述和语法。另外 例如Babel这样的语法编译器须要进行支持。 |
3 | candidate | 提案已经准备就绪,能够针对采用者和TC39委员会提出的关键问题作出一些修订。 |
4 | finished | 提案已经准备被归入规范中 |
直到如今(2018年6月),装饰器处于第二阶段,咱们作了一个Babel插件babel-plugin-transform-decorators-legacy
来转化装饰器功能。在第二阶段,功能的语法可能会改变,所以不建议在如今的生产项目中使用这个功能。不管如何,我以为装饰器在快速达成目标上都是优雅的和有效的。typescript
从如今开始,咱们试验实验性质的JavaScript, 所以你的node.js的版本可能不支持这些功能。因此,咱们会须要Babel或者TypeScript等语法编译器。使用js-plugin-starter插件来建立一个很是基本的项目,我在里面加了些东西来支持这片文章。编程
为了理解装饰器,咱们须要首先理解什么是JavaScript对象属性的property descriptor。 property descriptor是一个对象属性的一组规则,例如属性是可写的仍是可枚举的。 当咱们建立一个简单的对象并添加一些属性时,每一个属性都有默认的property descriptor。浏览器
var myObj = {
myPropOne: 1,
myPropTwo: 2
};
复制代码
myObj
是以下控制台所示的一个简单JavaScript对象。 babel
如今,若是咱们向下面的myPropOne属性写入新值,操做将会成功,咱们将获得更改后的值。网络
myObj.myPropOne = 10;
console.log( myObj.myPropOne ); //==> 10
复制代码
要获取属性的property descriptor,咱们须要使用Object.getOwnPropertyDescriptor(obj,propName)
方法。 这里的Own
表示仅当属性属于对象obj
而不属于原型链时才返回propName
属性的property descriptor。
let descriptor = Object.getOwnPropertyDescriptor(
myObj,
'myPropOne'
);
console.log( descriptor );
复制代码
Object.getOwnPropertyDescriptor
方法返回一个具备描述属性权限和当前状态的键的对象。 value
是属性的当前值,writable
是用户是否能够为属性赋予新值,enumerable
是该属性是否会在如for in
循环或for of
循环或Object.keys
等枚举中显示。configurable
的是用户是否具备更改property descriptor
的权限,并对writable
和enumerable
进行更改。 property descriptor
也有get
和set
中间件函数来返回值或更新值的键,但这些是可选的。
要在对象上建立新属性或使用自定义descriptor
更新现有属性,咱们使用Object.defineProperty
。 让咱们修改一个现有属性myPropOne
,其中的writable
属性设置为false
,这会禁止写入myObj.myPropOne
。
'use strict';
var myObj = {
myPropOne: 1,
myPropTwo: 2
};
// modify property descriptor
Object.defineProperty( myObj, 'myPropOne', {
writable: false
} );
// print property descriptor
let descriptor = Object.getOwnPropertyDescriptor(
myObj, 'myPropOne'
);
console.log( descriptor );
// set new value
myObj.myPropOne = 2;
复制代码
从上面的错误能够看出,咱们的属性myPropOne是不可写的,所以若是用户试图为其分配新值,它将抛出错误。
若是Object.defineProperty
正在更新现有property descriptor
,则原始的descriptor
将被新的修改覆盖。 更改以后,Object.defineProperty
返回原始对象myObj
。
下面再看一下若是enumerable
被设置成false后会发生什么?
var myObj = {
myPropOne: 1,
myPropTwo: 2
};
// modify property descriptor
Object.defineProperty( myObj, 'myPropOne', {
enumerable: false
} );
// print property descriptor
let descriptor = Object.getOwnPropertyDescriptor(
myObj, 'myPropOne'
);
console.log( descriptor );
// print keys
console.log(
Object.keys( myObj )
);
复制代码
正如你看到的那样,在Object.keys
的枚举中,咱们看不见myPropOne
这个属性了。
当你用Object.defineProperty
定义一个对象的新属性的时候,传递一个空的{}descriptor
,默认的descriptor
会看起来向下面的那样。
如今,让咱们定义一个带有自定义descriptor
的新属性,其中configurable
设为false
,writable
保持为false
,enumerable
为true
,并将valu
设为3。
var myObj = {
myPropOne: 1,
myPropTwo: 2
};
// modify property descriptor
Object.defineProperty( myObj, 'myPropThree', {
value: 3,
writable: false,
configurable: false,
enumerable: true
} );
// print property descriptor
let descriptor = Object.getOwnPropertyDescriptor(
myObj, 'myPropThree'
);
console.log( descriptor );
// change property descriptor
Object.defineProperty( myObj, 'myPropThree', {
writable: true
} );
复制代码
经过将configurable
设置为false,咱们失去了更改属性myPropThree
的descriptor
的能力。 若是不但愿用户操纵对象的默认行为,这很是有用。
get(getter)和set(setter)属性也能够在property descriptor
中设置。 可是当你定义一个getter时,它会带来一些损失。 descriptor
上不能有初始值或值键,由于getter会返回该属性的值。 您也不能在descriptor
上使用writable
属性,由于您的写入是经过setter完成的,您能够在那里阻止写入。 能够看看相关getter和setter的MDN文档,或阅读此文,这里很少做赘诉。
您可使用带有两个参数的Object.defineProperties
一次建立和/或更新多个属性。 第一个参数是属性被添加/修改的目标对象,第二个参数是属性名做为key
,值为property descriptor
的对象。 该函数返回第一个目标对象。
你有没有尝试过Object.create
函数来建立对象? 这是建立没有或自定义原型的对象的最简单方法。 它也是使用自定义property descriptor
从头开始建立对象的更简单的方法之一。 如下是Object.create
函数的语法。
var obj = Object.create( prototype, { property: descriptor, ... } )
复制代码
这里的prototype
是一个对象,它将成为obj
的原型。 若是原型为null
,那么obj
将不会有任何原型。 当用var obj = {}
定义一个空或非空对象时,默认状况下,obj .__ proto__
指向Object.prototype
,所以obj
具备Object
类的原型。
这与使用Object.create
,用Object.prototype
做为第一个参数(正在建立的对象的原型)相似。
'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" )
);
复制代码
可是当咱们将原型设置为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" )
);
复制代码
###Class Method Decorator 如今咱们了解了如何定义和配置对象的新属性或现有属性,让咱们将注意力转移到装饰器上,以及为何咱们讨论了property descriptor
。
Decorator
是一个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;
}
}
// create instance
let user = new User( 'John', 'Doe' );
console.log( user.getFullName() );
复制代码
上面的代码打印John Doe
到控制台。 可是存在巨大的问题,任何人均可以修改getFullName
方法。
User.prototype.getFullName = function() {
return 'HACKED!';
}
复制代码
因而,如今咱们获得了如下结果。
HACKED!
复制代码
为了不公共访问覆盖咱们的任何方法,咱们须要修改位于User.prototype
对象上的getFullName
方法的property descriptor
。
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
是属于目标对象的属性/方法的名称(与User.prototype
相同),descriptor
是该属性的property descriptor
。 从装饰器功能中,咱们必须不惜代价返回descriptor
。 这里的descriptor
将替换该属性的现有property descriptor
。
还有另外一个版本的装饰器语法,就像@decoratorWrapperFunction(... customArgs)
同样。 可是在这个语法中,decoratorWrapperFunction
应该返回一个与以前示例中使用的相同的decoratorFunction
。
function log( logMessage ) {
// return decorator function
return function ( target, property, descriptor ) {
// save original value, which is method (function)
let originalMethod = descriptor.value;
// replace method implementation
descriptor.value = function( ...args ) {
console.log( '[LOG]', logMessage );
// here, call original method
// `this` points to the instance
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() );
复制代码
装饰者不区分静态和非静态方法。 下面的代码能执行得很好,惟一会改变的是你如何访问该方法。 这一样适用于咱们将在下面看到的Instance Field Decorators
。
@log('calling getVersion static method of User class')
static getVersion() {
return 'v1.0.0';
}
console.log( User.getVersion() );
复制代码
到目前为止,咱们已经看到使用@decorator
或@decorator(.. args)
语法更改方法的property descriptor
,可是公共/私有属性(类实例字段)呢? 与typescript
或java
不一样,JavaScript类没有如咱们所知道的类实例字段类属性。 这是由于在类中和构造函数外定义的任何东西都应该属于类原型。 可是有一个新的方案使用公共和私人访问修饰符来启用类实例字段,如今已经进入阶段3,而且咱们有对应的babel转换器插件。 让咱们定义一个简单的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)的很是有用和重要的部分。 咱们有这样的提案是很好的,但“革命还还没有成功”啊各位。
与位于类原型的类方法不一样,类实例字段位于对象/实例上。 因为类实例字段既不是类的一部分也不是它的原型,所以操做它的descriptor
并不简单。 Babel给咱们的是类实例字段的property descriptor
上的初始化函数,而不是值键。 为何初始化函数而不是值,这个主题是争论的,由于装饰器处于第2阶段,没有发布最终草案来概述这个,但你能够按照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内部使用来建立对象属性的property descriptor
的值。 该函数返回分配给类实例字段的初始值。 在装饰器内部,咱们须要返回另外一个返回最终值的初始化函数。
类实例字段提案具备高度的实验性,而且直到它进入第4阶段以前颇有可能它的语法可能会发生变化。 所以,将类实例字段与装饰器一块儿使用并非一个好习惯。
如今咱们熟悉装饰者能够作什么。 它们能够改变类方法和类实例字段的属性和行为,使咱们能够灵活地使用更简单的语法动态实现这些内容。
类装饰器与咱们以前看到的装饰器略有不一样。 以前,咱们使用property descriptor
来修改属性或方法的行为,但在类装饰器的状况下,咱们须要返回一个构造函数。
让咱们来了解一下构造函数是什么。 在下面,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来理解这一点。
因此当咱们调用new User
时,User
函数是经过咱们传递的参数来调用的,结果咱们获得了一个对象。 所以,User
是一个构造函数。 顺便说一句,JavaScript中的每一个函数都是构造函数,由于若是你检查function.prototype
,你将得到构造函数属性。 只要咱们在函数中使用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
,而且必须返回一个构造函数。 这为装饰者打开了无限可能的大门。 所以类装饰器比方法/属性装饰器更受欢迎。
上面的例子比较基础,当咱们的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 );
// set logged in
user.setLoggedIn();
console.log( 'After ===> ', user );
复制代码
你能够经过将一个装饰器放到另外一个上面,链式地使用多个装饰器。执行顺序与他们出现的位置顺序一致。
装饰者是更快达成目标的巧妙方式。 不久的未来它们便会被添加到ECMAScript规范中。
翻译自A minimal guide to ECMAScript Decorators, 祝好。
《IVWEB 技术周刊》 震撼上线了,关注公众号:IVWEB社区,每周定时推送优质文章。