爲何標題是 ECMAScript 修飾器,而不是 JavaScript 修飾器?由於,ECMAScript 是編寫像 JavaScript 這種腳本語言的標準,它不強制 JavaScript 支持全部規範內容,JavaScript 引擎(不一樣瀏覽器使用不一樣引擎)不必定支持 ECMAScript 引入的功能,或者支持行爲不一致。javascript
能夠將 ECMAScript 理解爲咱們說的語言,好比英語。那 JavaScript 就是一種方言,相似英國英語。方言自己就是一種語言,但它是基於語言衍生出來的。因此,ECMAScript 是烹飪/編寫 JavaScript 的烹飪書,是否遵循其中全部成分/規則徹底取決於廚師/開發者。前端
理論上來講,JavaScript 使用者應該遵循語言規範中全部規則(開發者或許會瘋掉吧),但實際上新版 JavaScript 引擎很晚纔會實現這些規則,開發者要確保一切正常後(纔會切換)。TC39 也就是 ECMA 國際技術委員會第 39 號 負責維護 ECMAScript 語言規範。該團隊的成員大多來自於 ECMA 國際、瀏覽器廠商和對 Web 感興趣的公司。java
因爲 ECMAScript 是開放標準,任何人均可以提出新的想法或功能並對其進行處理。所以,新功能的提議將經歷 4 個主要階段,TC39 將參與此過程,直到該功能準備好發佈。node
+-------+-----------+----------------------------------------+
| stage | name | mission |
+-------+-----------+----------------------------------------+
| 0 | strawman | Present a new feature (proposal) |
| | | to TC39 committee. Generally presented |
| | | by TC39 member or TC39 contributor. |
+-------+-----------+----------------------------------------+
| 1 | proposal | Define use cases for the proposal, |
| | | dependencies, challenges, demos, |
| | | polyfills etc. A champion |
| | | (TC39 member) will be |
| | | responsible for this proposal. |
+-------+-----------+----------------------------------------+
| 2 | draft | This is the initial version of |
| | | the feature that will be |
| | | eventually added. Hence description |
| | | and syntax of feature should |
| | | be presented. A transpiler such as |
| | | Babel should support and |
| | | demonstrate implementation. |
+-------+-----------+----------------------------------------+
| 3 | candidate | Proposal is almost ready and some |
| | | changes can be made in response to |
| | | critical issues raised by adopters |
| | | and TC39 committee. |
+-------+-----------+----------------------------------------+
| 4 | finished | The proposal is ready to be |
| | | included in the standard. |
+-------+-----------+----------------------------------------+
複製代碼
如今(2018 年 6 月),修飾器提案正處於第二階段,咱們可使用 babel-plugin-transform-decorators-legacy
這個 Babel 插件來轉換它。在第二階段,因爲功能的語法會發生變化,所以不建議在生產環境中使用它。不管如何,修飾器都很優美,也有助於更快地完成任務。android
從如今開始,咱們要開始研究實驗性的 JavaScript 了,所以你的 node.js 版本可能不支持這個新特性。因此咱們須要使用 Babel 或 TypeScript 轉換器。可使用我準備的 js-plugin-starter 插件來設置項目,其中包括了這篇文章中用到的插件。ios
要理解修飾器,首先須要瞭解 JavaScript 對象屬性的屬性描述符。 屬性描述符是對象屬性的一組規則,例如屬性是可寫仍是可枚舉。當咱們建立一個簡單的對象並向其添加一些屬性時,每一個屬性都有默認的屬性描述符。git
var myObj = {
myPropOne: 1,
myPropTwo: 2
};
複製代碼
myObj
是一個簡單的 JavaScript 對象,在控制檯中以下所示:github
如今,若是咱們像下面那樣將新值寫入 myPropOne
屬性,操做能夠成功,咱們能夠得到更改後的值。typescript
myObj.myPropOne = 10;
console.log( myObj.myPropOne ); //==> 10
複製代碼
爲了獲取屬性的屬性描述符,咱們須要使用 Object.getOwnPropertyDescriptor(obj, propName)
方法。這裏 Own 的意思是隻有 propName
屬性是 obj
對象自有屬性而不是在原型鏈上查找的屬性時,纔會返回 propName
的屬性描述符。編程
let descriptor = Object.getOwnPropertyDescriptor(
myObj,
'myPropOne'
);
console.log( descriptor );
複製代碼
Object.getOwnPropertyDescriptor
方法返回一個對象,該對象包含描述屬性權限和當前狀態的鍵。 value
表示屬性的當前值,writable
表示用戶是否能夠爲屬性賦值,enumerable
表示該屬性是否會出如今 for in
循環或 for of
循環或 Object.keys
等遍歷方法中。configurable
表示用戶是否有權更改屬性描述符並更改 writable
和 enumerable
。屬性描述符還有 get
和 set
鍵,它們是獲取值或設置值的中間件函數,但這兩個是可選的。
要在對象上建立新屬性或使用自定義描述符修改現有屬性,咱們使用 Object.defineProperty
方法。讓咱們修改 myPropOne
這個現有屬性,writable
設置爲 false
,這會禁止向 myObj.myPropOne
寫入值。
'use strict';
var myObj = {
myPropOne: 1,
myPropTwo: 2
};
// 修改屬性描述符
Object.defineProperty( myObj, 'myPropOne', {
writable: false
} );
// 打印屬性描述符
let descriptor = Object.getOwnPropertyDescriptor(
myObj, 'myPropOne'
);
console.log( descriptor );
// 設置新值
myObj.myPropOne = 2;
複製代碼
從上面的報錯中能夠看出,myPropOne
屬性是不可寫入的。所以若是用戶嘗試給它賦予新值,就會拋出錯誤。
若是使用
Object.defineProperty
來修改現有屬性的描述符,那原始描述符會被新的修改覆蓋。Object.defineProperty
方法會返回修改後的myObj
對象。
讓咱們看看若是將 enumerable
描述符鍵設置爲 false
會發生什麼。
var myObj = {
myPropOne: 1,
myPropTwo: 2
};
// 修改描述符
Object.defineProperty( myObj, 'myPropOne', {
enumerable: false
} );
// 打印描述符
let descriptor = Object.getOwnPropertyDescriptor(
myObj, 'myPropOne'
);
console.log( descriptor );
// 打印遍歷對象
console.log(
Object.keys( myObj )
);
複製代碼
從上面的結果能夠看出,咱們在 Object.keys
枚舉中看不到對象的 myPropOne
屬性。
使用 Object.defineProperty
在對象上定義新屬性並傳遞空 {}
描述符時,默認描述符以下所示:
如今,讓咱們使用自定義描述符定義一個新屬性,其中 configurable
鍵設置爲 false
。咱們將 writable
保持爲false
、enumerable
爲 true
,並將 value
設置爲 3
。
var myObj = {
myPropOne: 1,
myPropTwo: 2
};
// 設置新屬性描述符
Object.defineProperty( myObj, 'myPropThree', {
value: 3,
writable: false,
configurable: false,
enumerable: true
} );
// 打印屬性描述符
let descriptor = Object.getOwnPropertyDescriptor(
myObj, 'myPropThree'
);
console.log( descriptor );
// 修改屬性描述符
Object.defineProperty( myObj, 'myPropThree', {
writable: true
} );
複製代碼
經過將 configurable
設置爲 false
,咱們失去了更改 myPropThree
屬性描述符的能力。若是不但願用戶操做對象的行爲,這將很是有用。
get(getter)和 set(setter)也能夠在屬性描述符中設置。可是當你定義一個 getter 時,也會帶來一些犧牲。你根本不能在描述符上有初始值或 value
,由於 getter 將返回該屬性的值。你也不能在描述符上使用 writable
,由於你的寫操做是經過 setter 完成的,能夠防止寫入。看看 MDN 文檔關於 getter 和 setter,或閱讀這篇文章,這裏不須要太多解釋。
可使用帶有兩個參數的
Object.defineProperties
方法一次建立/更新多個屬性描述符。第一個參數是目標對象,在其中添加/修改屬性,第二個參數是一個對象,其中key
爲屬性名,value
是它的屬性描述符。此函數返回目標對象。
你是否嘗試過使用 Object.create
方法來建立對象?這是建立沒有原型或自定義原型對象最簡單方法。它也是使用自定義屬性描述符從頭開始建立對象的更簡單方法之一。
Object.create
方法具備如下語法:
var obj = Object.create( prototype, { property: descriptor, ... } )
複製代碼
這裏 prototype
是一個對象,它將成爲 obj
的原型。若是 prototype
是 null
,那麼 obj
將沒有任何原型。使用 var obj = {}
語法定義空或非空對象時,默認狀況下,obj.__proto__
指向 Object.prototype
,所以 obj
具備 Object
類的原型。
這相似於用 Object.prototype
做爲第一個參數(正在建立對象的原型)使用 Object.create
方法 。
'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" )
);
複製代碼
但當咱們把 prototype 參數設置爲 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" )
);
複製代碼
如今咱們已經瞭解瞭如何定義/配置對象的新屬性/現有屬性,讓咱們把注意力轉移到修飾器以及爲何討論屬性描述符上。
修飾器是一個 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;
}
}
// 建立實例
let user = new User( 'John', 'Doe' );
console.log( user.getFullName() );
複製代碼
運行上面的代碼,控制檯中會打印出 John Doe
。但這樣有一個問題:任何人均可以修改 getFullName
方法。
User.prototype.getFullName = function() {
return 'HACKED!';
}
複製代碼
通過上面的修改,就會獲得如下輸出:
HACKED!
複製代碼
爲了限制修改咱們任何方法的權限,須要修改 getFullName
方法的屬性描述符,這個屬性屬於 User.prototype
對象。
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
是屬性/方法的名字,target
是這些屬性/方法屬於的對象(就和 User.prototype
同樣),descriptor
是這個屬性的描述符。在修飾器函數中,咱們必須返回 descriptor
對象。這個修改後的 descriptor
會替換該屬性原來的屬性描述符。
修飾器寫法還有另外一種版本,相似 @decoratorWrapperFunction( ...customArgs )
這樣。但這樣寫,decoratorWrapperFunction
函數應該返回一個 decoratorFunction
修飾器函數,它的使用和上面的例子相同。
function log( logMessage ) {
// 返回修飾器函數
return function ( target, property, descriptor ) {
// 保存屬性原始值,它是一個方法(函數)
let originalMethod = descriptor.value;
// 修改方法實現
descriptor.value = function( ...args ) {
console.log( '[LOG]', logMessage );
// 這裏,調用原始方法
// `this` 指向調用實例
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() );
複製代碼
修飾器不區分靜態和非靜態方法。下面的代碼一樣能夠工做,惟一不一樣是你如何訪問這些方法。這個結論也適用於咱們下面要討論的類實例字段修飾器。
@log('calling getVersion static method of User class')
static getVersion() {
return 'v1.0.0';
}
console.log( User.getVersion() );
複製代碼
目前爲止,咱們已經看到經過 @decorator
或 @decorator(..args)
語法來修改類方法的屬性描述符,但如何修改 **公有/私有屬性(類實例字段)**呢?
與 typescript
或 java
不一樣,JavaScript 類沒有類實例字段或者說沒有類屬性。這是由於任何在 class
裏面、constructor
外面定義的都屬於類的原型。但也有一個新的提案,它提議使用 public
和 private
訪問修飾符來啓用類實例字段,目前處於第 3 階段,也能夠經過 babel transformer plugin 這個插件來使用它。
定義一個簡單的 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)的重要組成部分。咱們提出相應的提案很好,但故事遠未結束。
與類方法處於類的原型上不一樣,類實例字段處於對象/實例上。因爲類實例字段既不是類的一部分也不是它原型的一部分,所以操做它的描述符有點困難。Babel 爲類實例字段的屬性描述符提供了 initializer
方法來替代 value
。爲何要用 initializer
方法來替代 value
呢?這個問題有些爭議,由於修飾器提案還處於第二階段,尚未發佈最終草案來講明這個問題,但你能夠經過查看 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 內部實現對象屬性描述符的 value
的建立。它會返回分配給類實例字段的初始值。在修飾器函數內部,咱們須要返回另外一個 initializer
方法,它會返回最終值。
類實例字段提案具備高度實驗性,在到達第 4 階段前,它的語法頗有可能會改變。所以,將類實例字段與修飾器一塊兒使用還不是一個好習慣。
如今咱們已經熟悉了修飾器能作什麼。它能夠改變屬性、類方法行爲和類實例字段,使咱們能靈活地經過簡單的語法來實現這些。
類修飾器和咱們以前看到的修飾器有些不一樣。以前,咱們使用屬性修飾器來修改屬性或方法的實現,但類修飾器函數中,咱們須要返回一個構造函數。
咱們先來理解下什麼是構造函數。在下面,一個 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 中的
this
頗有幫助。
所以,當咱們調用 new User
時,就會使用傳遞的參數調用 User
這個函數,返回結果是一個對象。因此,User
就是一個構造函數。順便說一句,JavaScript 中每一個函數都是一個構造函數,由於若是你查看 function.prototype
,你會發現 constructor
屬性。只要咱們使用 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
類有大量的屬性和原型方法時,咱們不想建立一個新的構造函數。好消息是,咱們在修飾器函數中能夠引用類,即 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 );
// 設置爲已登陸
user.setLoggedIn();
console.log( 'After ===> ', user );
複製代碼
你能夠將多個修飾器放在一塊兒,執行順序和它們外觀順序一致。
修飾器是更快地達到目的的奇特方式。在它們正式加入 ECMAScript 規範以前,咱們先期待一下吧。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。