【翻译】ECMAScript装饰器的简单指南

简要介绍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

Alt text

如今,若是咱们向下面的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 );
复制代码

Alt text

Object.getOwnPropertyDescriptor方法返回一个具备描述属性权限和当前状态的键的对象。 value是属性的当前值,writable是用户是否能够为属性赋予新值,enumerable是该属性是否会在如for in循环或for of循环或Object.keys等枚举中显示。configurable的是用户是否具备更改property descriptor的权限,并对writableenumerable进行更改。 property descriptor也有getset中间件函数来返回值或更新值的键,但这些是可选的。

要在对象上建立新属性或使用自定义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;
复制代码

Alt text

从上面的错误能够看出,咱们的属性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 )
);
复制代码

Alt text

正如你看到的那样,在Object.keys的枚举中,咱们看不见myPropOne这个属性了。

当你用Object.defineProperty定义一个对象的新属性的时候,传递一个空的{}descriptor,默认的descriptor会看起来向下面的那样。

Alt text

如今,让咱们定义一个带有自定义descriptor的新属性,其中configurable设为falsewritable保持为falseenumerabletrue,并将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
} );
复制代码

Alt text

经过将configurable设置为false,咱们失去了更改属性myPropThreedescriptor的能力。 若是不但愿用户操纵对象的默认行为,这很是有用。

get(getter)和set(setter)属性也能够在property descriptor中设置。 可是当你定义一个getter时,它会带来一些损失。 descriptor上不能有初始值或值键,由于getter会返回该属性的值。 您也不能在descriptor上使用writable属性,由于您的写入是经过setter完成的,您能够在那里阻止写入。 能够看看相关gettersetter的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" ) 
);
复制代码

Alt text

可是当咱们将原型设置为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" ) 
);
复制代码

Alt text


###Class Method Decorator 如今咱们了解了如何定义和配置对象的新属性或现有属性,让咱们将注意力转移到装饰器上,以及为何咱们讨论了property descriptor

Decorator是一个JavaScript函数(推荐的纯函数),用于修改类属性/方法或类自己。 当您在类属性,方法或类自己的顶部添加@decoratorFunction语法时,decoratorFunction由一些参数来调用,咱们可使用它们修改类或类的属性。 让咱们建立一个简单的readonly装饰器功能。 但在此以前,让咱们使用getFullName方法建立简单的User类,该方法经过组合firstNamelastName来返回用户的全名。

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方法,将会获得如下错误。

Alt text

可是,若是咱们在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() );
复制代码

Alt text

装饰者不区分静态和非静态方法。 下面的代码能执行得很好,惟一会改变的是你如何访问该方法。 这一样适用于咱们将在下面看到的Instance Field Decorators

@log('calling getVersion static method of User class')
static getVersion() {
    return 'v1.0.0';
}
console.log( User.getVersion() );
复制代码

Class Instance Field Decorator

到目前为止,咱们已经看到使用@decorator@decorator(.. args)语法更改方法的property descriptor,可是公共/私有属性(类实例字段)呢? 与typescriptjava不一样,JavaScript类没有如咱们所知道的类实例字段类属性。 这是由于在类中和构造函数外定义的任何东西都应该属于类原型。 可是有一个新的方案使用公共和私人访问修饰符来启用类实例字段,如今已经进入阶段3,而且咱们有对应的babel转换器插件。 让咱们定义一个简单的User类,可是此次咱们不须要为构造函数中的firstNamelastName设置默认值。

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() );
复制代码

Alt text

如今,若是检查User类的原型,将没法看到firstNamelastName属性。

Alt text

类实例字段是面向对象编程(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() );
复制代码

Alt text

咱们也可使用装饰器函数和参数来使其更具可定制性。

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阶段以前颇有可能它的语法可能会发生变化。 所以,将类实例字段与装饰器一块儿使用并非一个好习惯。

Class Decorator

如今咱们熟悉装饰者能够作什么。 它们能够改变类方法和类实例字段的属性和行为,使咱们能够灵活地使用更简单的语法动态实现这些内容。

类装饰器与咱们以前看到的装饰器略有不一样。 以前,咱们使用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() );
复制代码

Alt text

这里有一篇很棒的文章,用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 );
复制代码

Alt text

类装饰器函数将接收目标类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 );
复制代码

Alt text

你能够经过将一个装饰器放到另外一个上面,链式地使用多个装饰器。执行顺序与他们出现的位置顺序一致。

装饰者是更快达成目标的巧妙方式。 不久的未来它们便会被添加到ECMAScript规范中。

翻译自A minimal guide to ECMAScript Decorators, 祝好。


《IVWEB 技术周刊》 震撼上线了,关注公众号:IVWEB社区,每周定时推送优质文章。

相关文章
相关标签/搜索