簡要介紹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社區,每週定時推送優質文章。