【翻譯】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社區,每週定時推送優質文章。

相關文章
相關標籤/搜索