前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【翻译】ECMAScript装饰器的简单指南

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

作者头像
腾讯IVWEB团队
发布2020-06-28 11:43:17
6520
发布2020-06-28 11:43:17
举报
ECMAScript装饰器的简单指南(翻译)

简要介绍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等语法编译器。使用js-plugin-starter插件来创建一个非常基本的项目,我在里面加了些东西来支持这片文章。


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

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

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

Alt text

现在,如果我们向下面的myPropOne属性写入新值,操作将会成功,我们将得到更改后的值。

代码语言:javascript
复制
myObj.myPropOne = 10;
console.log( myObj.myPropOne ); //==> 10

要获取属性的property descriptor,我们需要使用Object.getOwnPropertyDescriptor(obj,propName)方法。 这里的Own表示仅当属性属于对象obj而不属于原型链时才返回propName属性的property descriptor。

代码语言:javascript
复制
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

代码语言:javascript
复制
'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后会发生什么?

代码语言:javascript
复制
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。

代码语言:javascript
复制
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函数的语法。

代码语言:javascript
复制
var obj = Object.create( prototype, { property: descriptor, ... } )

这里的prototype是一个对象,它将成为obj的原型。 如果原型为null,那么obj将不会有任何原型。 当用var obj = {}定义一个空或非空对象时,默认情况下,obj .__ proto__指向Object.prototype,因此obj具有Object类的原型。

这与使用Object.create,用Object.prototype作为第一个参数(正在创建的对象的原型)类似。

代码语言:javascript
复制
'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时,我们会得到以下错误。

代码语言:javascript
复制
'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来返回用户的全名。

代码语言:javascript
复制
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方法。

代码语言:javascript
复制
User.prototype.getFullName = function() {
    return 'HACKED!';
}

于是,现在我们得到了以下结果。

代码语言:javascript
复制
HACKED!

为了避免公共访问覆盖我们的任何方法,我们需要修改位于User.prototype对象上的getFullName方法的property descriptor

代码语言:javascript
复制
Object.defineProperty( User.prototype, 'getFullName', {
    writable: false
} );

现在,如果任何用户尝试覆盖getFullName方法,将会得到以下错误。

Alt text

但是,如果我们在User类中有很多方法,那么手动执行这些操作就不会那么好。 这就是装饰者的由来。我们可以通过在下面的getFullName方法的顶部放置@readonly语法来实现同样的事情。

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
@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设置默认值。

代码语言:javascript
复制
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修饰器,它将改变类实例字段的默认值的大小写。

代码语言:javascript
复制
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

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

代码语言:javascript
复制
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类只不过是一个函数,用于添加原型方法并为字段定义一些初始值。

代码语言: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类,如下所示。

代码语言:javascript
复制
class User {
    constructor( firstName, lastName ) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

我们的User类目前没有任何方法。 如前所述,类装饰器必须返回一个构造函数。

代码语言:javascript
复制
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类)。 因此,类也是一个构造函数,这是合法的。

代码语言:javascript
复制
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, 祝好。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ECMAScript装饰器的简单指南(翻译)
  • Class Instance Field Decorator
  • Class Decorator
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档