博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【翻译】ECMAScript装饰器的简单指南
阅读量:6134 次
发布时间:2019-06-21

本文共 13020 字,大约阅读时间需要 43 分钟。

简要介绍JavaScript中的“装饰器”的提案的一些基础示例以及ECMAScript相关的内容

为什么用ECMAScript装饰器代替标题中的JavaScript装饰器? 因为ECMAScript是用于编写脚本语言(如JavaScript)的标准,所以它不强制JavaScript支持所有规范,但JavaScript引擎(由不同浏览器使用)可能支持或不支持由ECMAScript引入的功能,或者支持一些不同的行为。

将ECMAScript视为您所说的某种语言,例如英语。 那么JavaScript就像英式英语一样。 方言本身就是一种语言,但是它是基于它所源自的语言的原则而应运而生。 因此,ECMAScript是烹饪/书写JavaScript的“烹饪书”,由主厨/开发人员决定遵循或不遵守所有配料/规则。

通常而言,JavaScript采用者遵循用语言编写的所有规范(不然开发人员将会被逼疯),并在新版本的JavaScript引擎出现后,并且直到确保一切正常,才会发布它。 ECMA International的TC39或技术委员会39负责维护ECMAScript语言规范。 一般来说,该团队的成员是由ECMA International、浏览器供应商和对网络感兴趣的公司而组成。

由于ECMAScript是开放标准,任何人都可以提出新的想法或功能,并对其进行推动实行。 因此,一个新功能的提案会经历4个主要阶段,并且TC39会参与这个过程,直到该功能准备好施行。

阶段 名称 任务
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来转化装饰器功能。在第二阶段,功能的语法可能会改变,因此不建议在现在的生产项目中使用这个功能。无论如何,我觉得装饰器在快速达成目标上都是优雅的和有效的。

从现在开始,我们试验实验性质的JavaScript, 因此你的node.js的版本可能不支持这些功能。所以,我们会需要Babel或者TypeScript等语法编译器。使用插件来创建一个非常基本的项目,我在里面加了些东西来支持这片文章。


为了理解装饰器,我们需要首先理解什么是JavaScript对象属性的property descriptor。 property descriptor是一个对象属性的一组规则,例如属性是可写的还是可枚举的。 当我们创建一个简单的对象并添加一些属性时,每个属性都有默认的property descriptor。

var myObj = {    myPropOne: 1,    myPropTwo: 2};复制代码

myObj是如下控制台所示的一个简单JavaScript对象。

现在,如果我们向下面的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的权限,并对writableenumerable进行更改。 property descriptor也有getset中间件函数来返回值或更新值的键,但这些是可选的。

要在对象上创建新属性或使用自定义descriptor更新现有属性,我们使用Object.defineProperty。 让我们修改一个现有属性myPropOne,其中的writable属性设置为false,这会禁止写入myObj.myPropOne

'use strict';var myObj = {    myPropOne: 1,    myPropTwo: 2};// modify property descriptorObject.defineProperty( myObj, 'myPropOne', {    writable: false} );// print property descriptorlet descriptor = Object.getOwnPropertyDescriptor(    myObj, 'myPropOne');console.log( descriptor );// set new valuemyObj.myPropOne = 2;复制代码

从上面的错误可以看出,我们的属性myPropOne是不可写的,因此如果用户试图为其分配新值,它将抛出错误。

如果Object.defineProperty正在更新现有property descriptor,则原始的descriptor将被新的修改覆盖。 更改之后,Object.defineProperty返回原始对象myObj

下面再看一下如果enumerable被设置成false后会发生什么?

var myObj = {    myPropOne: 1,    myPropTwo: 2};// modify property descriptorObject.defineProperty( myObj, 'myPropOne', {    enumerable: false} );// print property descriptorlet descriptor = Object.getOwnPropertyDescriptor(    myObj, 'myPropOne');console.log( descriptor );// print keysconsole.log(    Object.keys( myObj ));复制代码

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

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

现在,让我们定义一个带有自定义descriptor的新属性,其中configurable设为falsewritable保持为falseenumerabletrue,并将valu设为3。

var myObj = {    myPropOne: 1,    myPropTwo: 2};// modify property descriptorObject.defineProperty( myObj, 'myPropThree', {    value: 3,    writable: false,    configurable: false,    enumerable: true} );// print property descriptorlet descriptor = Object.getOwnPropertyDescriptor(    myObj, 'myPropThree');console.log( descriptor );// change property descriptorObject.defineProperty( myObj, 'myPropThree', {    writable: true} );复制代码

通过将configurable设置为false,我们失去了更改属性myPropThreedescriptor的能力。 如果不希望用户操纵对象的默认行为,这非常有用。

get(getter)和set(setter)属性也可以在property descriptor中设置。 但是当你定义一个getter时,它会带来一些损失。 descriptor上不能有初始值或值键,因为getter会返回该属性的值。 您也不能在descriptor上使用writable属性,因为您的写入是通过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类,该方法通过组合firstNamelastName来返回用户的全名。

class User {    constructor( firstname, lastName ) {        this.firstname = firstname;        this.lastName = lastName;    }    getFullName() {        return this.firstname + ' ' + this.lastName;    }}// create instancelet 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() );复制代码

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

现在,如果检查User类的原型,将无法看到firstNamelastName属性。

类实例字段是面向对象编程(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阶段之前很有可能它的语法可能会发生变化。 因此,将类实例字段与装饰器一起使用并不是一个好习惯。

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

这里有一篇很棒的文章,用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;    }}@withLoginStatusclass 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;        }    }}@withLoginStatusclass User {    constructor( firstName, lastName ) {        this.firstName = firstName;        this.lastName = lastName;    }}let user = new User( 'John', 'Doe' );console.log( 'Before ===> ', user );// set logged inuser.setLoggedIn();console.log( 'After ===> ', user );复制代码

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

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

翻译自, 祝好。


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

  • 周刊文章集合:
  • 团队开源项目:

转载地址:http://lteua.baihongyu.com/

你可能感兴趣的文章
详解 CSS 绝对定位
查看>>
AOP
查看>>
我的友情链接
查看>>
NGUI Label Color Code
查看>>
.NET Core微服务之基于Polly+AspectCore实现熔断与降级机制
查看>>
vue组件开发练习--焦点图切换
查看>>
浅谈OSI七层模型
查看>>
Webpack 2 中一些常见的优化措施
查看>>
移动端响应式
查看>>
python实现牛顿法求解求解最小值(包括拟牛顿法)【最优化课程笔记】
查看>>
js中var、let、const的区别
查看>>
腾讯云加入LoRa联盟成为发起成员,加速推动物联网到智联网的进化
查看>>
从Python2到Python3:超百万行代码迁移实践
查看>>
Windows Server已可安装Docker,Azure开始支持Mesosphere
查看>>
简洁优雅地实现夜间模式
查看>>
react学习总结
查看>>
微软正式发布PowerShell Core 6.0
查看>>
Amazon发布新的会话管理器
查看>>
InfoQ趋势报告:DevOps 和云计算
查看>>
舍弃Python,为什么知乎选用Go重构推荐系统?
查看>>