ES Decorators簡介

發表於2015-07-21

我跟你說,我最討厭“簡介”這種文章了,要不是語文是體育老師教的,早就換標題了!

Decorators是ECMAScript現在處於Stage 1的一個提案。當然ECMAScript會有很多新的特性,特地介紹這一個是因為它能夠在實際的程式設計中提供很大的幫助,甚至於改變不少功能的設計。

先說說怎麼回事

如果光從概念上來介紹的話,官方是這麼說的:

Decorators make it possible to annotate and modify classes and properties at design time.

我翻譯一下:

裝飾器讓你可以在設計時對類和類的屬性進行註解和修改。

什麼鬼,說人話!

所以我們還是用一段程式碼來看一下好了:

別去試上面的程式碼,瞎寫的,估計跑不起來就是了。這個程式碼的作用其實看函式的命名就能明白,我們要給Foo#getFooById方法加一個快取,快取使用第一個引數作為對應的鍵。

可以看出來,上面程式碼的重點在於:

  1. 有一個memoize函式。
  2. 在類的某個方法上加了@memoize;這樣一個標記。

而這個@memoize就是所謂的Decorator,我稱之為裝飾器。一個裝飾器有以下特點:

  1. 首先它是一個函式。
  2. 這個函式會接收3個引數,分別是targetkeydescriptor,具體的作用後面再說。
  3. 它可以修改descriptor做一些額外的邏輯。

看到了基本用法其實並不能說明什麼,我們有幾個核心的問題有待說明:

有幾種裝飾器

現階段官方說有2種裝飾器,但從實際使用上來看是有4種,分別是:

  • 放在class上的“類裝飾器”。
  • 放在屬性上的“屬性裝飾器”,這需要配合另一個Stage 0的類屬性語法提案,或者只能放在物件字面量上了。
  • 放在方法上的“方法裝飾器”。
  • 放在gettersetter上的“訪問器裝飾器”。

其中類裝飾器只能放在class上,而另外3種可以同時放在class和屬性或者物件字面量的屬性上,比如這樣也是可以的:

不過注意放在物件字面量時,裝飾器後面不能寫分號,這是個比較怪異的問題,後面還會說到更怪異的情況,我也在和提案的作者溝通這是為啥。

之所以這麼分,是因為不同情況下,裝飾器接收的3個引數代表的意義並不相同。

裝飾器的3個引數是什麼

裝飾器接收3個引數,分別是targetkeydescriptor,他們各自分別是什麼值,用一段程式碼就能很容易表達出來:

這是使用babel轉換的JavaScript的輸出,從這裡可以看到:

  1. key很明顯就是當前方法名,我們可以推斷出來用於屬性的時候就是屬性名
  2. descriptor顯然是一個PropertyDescriptor,就是我們用於defineProperty時的那個東西。
  3. target確實不是那麼容易看出來,所以我用了3行程式碼。首先這是一個物件,然後是一個有constructor屬性的物件,最後constructur指向的是Bar這個函式。所以我們也能推測出來這貨就是Bar.prototype沒跑了。

那如果裝飾器放在物件字面量上,而不是類上呢?這邊就不再給程式碼,直接放結論了:

  1. keydescriptor和放在類屬性/方法上一樣沒變,這當然也不應該變。
  2. targetObject物件,相信我你不會想用這個引數的。

當裝飾器放在屬性、方法、訪問器上時,都符合上面的原則,但放在類上的時候,有一些不同:

  1. keydescriptor不會提供,只有target引數。
  2. target會變成Bar這個方法,而不是其prototype

其實對於屬性、方法和訪問器,真正有用的就是descriptor,其它幾個無視問題也不大就是了。而對於類,由於target是唯一能用的,所以會需要它。

對於這一環節,我們需要特別注意一點,由於target是類的prototype,所以往它上面新增屬性是,要注意繼承時是會被繼承下去的,而子類上再加同樣屬性又會有覆蓋甚至物件、陣列同引用混在一起的問題。這和我們平時儘量不在prototype上放物件或者陣列的思路是一致的,要避免這一問題。

裝飾器在什麼時候執行

既然裝飾器本身是一個函式,那麼自然要有函式被執行的時候。

現階段,裝飾器只能放在一個類或者一個物件上,我們可以用程式碼看一下什麼時候執行:

輸出如下:

從輸出上,我們可以看到幾個規則:

  • 裝飾器是在宣告期就起效的,並不需要類進行例項化。類例項化並不會致使裝飾器多次執行,因此不會對例項化帶來額外的開銷。
  • 按編碼時的宣告順序執行,並不會將屬性、方法、訪問器進行重排序。

因為以上這2個規則,我們需要特別注意一點,在裝飾器執行時,你所能得到的環境是空的,在Bar.prototype或者Bar上的屬性是獲取不到的,也就是說整個target裡其實只有constructor這一個屬性。換句話說,裝飾器執行時所有的屬性和方法均未定義

descriptor裡有啥

我們都知道,PropertyDescriptor的基本內容如下:

  • configurable控制是不是能刪、能修改descriptor本身。
  • writable控制是不是能修改值。
  • enumerable控制是不是能列舉出屬性。
  • value控制對應的值,方法只是一個value是函式的屬性。
  • getset控制訪問咕嚕的讀和寫邏輯。

根據裝飾器放的位置不同,descriptor引數中就會有上面的這些屬性,其中前3個是必然存在的,隨後根據放在屬性、方法上還是放在訪問器上決定是value還是get/set

再說說類屬性的情況,由於類屬性本身是一個比裝飾器更不靠譜的Stage 0的提案,所以情況就會變成2個提案的相互作用了。

當裝飾器用於類屬性時,descriptor將變成一個叫“類屬性描述符”的東西,其區別在於沒有valuegetset,且多出一個initializer屬性,型別是函式,在類建構函式執行時,initializer返回的值作為屬性的值,也就是說一個foo屬性對應程式碼是類似這樣的:

所以我們也可以寫很簡單的裝飾器:

再說說怎麼用

在基本把概念說完後,其實我們並沒有說裝飾器怎麼用,雖然前面有一些程式碼,但並不能邏輯完善地說明問題。

descriptor的使用

對於屬性、方法、訪問器的裝飾器,真正的作用在於對descriptor這個屬性的修改。我們拿一些原始的例子來看,比如你要給一個物件宣告一個屬性:

但是我們現在又不高興了,我們希望這個屬性是隻讀的,OK這是個非常簡單的問題:

但是有時候,我們面對幾百幾千個屬性,真心不想一個一個寫writable: false,看著也不容易明白。或者這個descriptor根本是其他地方給我們的,我們只有defineProperty的權利,無法修改原來的東西,所以我們希望是這樣的:

通過函式式的程式設計進行函式轉換,既能讀程式碼時就看出來這是隻讀的,又能用在所有以前的descriptor上而不需要改以前的程式碼,將“定義”和“使用”分離了開來。

而裝飾器無非是將這件事放到了語法的層面上,我們有一個機會在類或者屬性、訪問器、方法定義的時候去修改它的descriptor,這種對“後設資料”的修改使得我們有很大的靈活性,包括但不侷限於:

  1. 通過descriptor.value的修改直接給改成不同的值,適用於方法的裝飾器。
  2. 通過descriptor.getdescriptor.set修改邏輯,適用於訪問器的裝飾器。
  3. 通過descriptor.initializer修改屬性值,適用於屬性的裝飾器。
  4. 修改configurablewritableenumerable控制屬性本身的特性,常見的就是修改為只讀。

裝飾器是最後的修改descriptor的機會,再往後如果configurable被設為false的話,就再也沒機會去改變這些後設資料了。

類裝飾器的使用

類裝飾器不大一樣,因為沒有descriptor給你,你唯一能獲得的是類本身,也就是一個函式。

但是有了類本身,我們可以做一件事,就是繼承:

實際的使用場景

上面的程式碼可能都很扯談,誰會有這種奇怪的需求,所以舉一些真正實用的程式碼來看看。

一個比較可能的場合是在製作一個檢視類的時候,我們可以:

  • 通過訪問器裝飾器來宣告類屬性與DOM元素之間的繫結關係。
  • 通過方法裝飾器指定方法處理某個DOM元素的某個事件。
  • 通過類裝飾器指定一個類為檢視處理類,且在DOMContentLoaded時執行。

參考程式碼如下,以一個簡單的登入表單為例:

這種程式設計方式我們經常稱之為“宣告式程式設計”,好處是更為直觀,且能夠通過裝飾器等手段複用邏輯。

這只是一個很簡單直觀的例子,我們用裝飾器可以做更多的事,有待在實際開發中慢慢發掘,同時DecorateThis專案給我們做了不少的示範,雖然我覺得這個庫提供的裝飾器並沒有什麼卯月……

題外話的概念和坑

到這邊基本把裝飾器的概念和使用都講了,我理解有不少FE一時不好接受這些(QWrap那邊的人倒應該能非常迅速地接受這種函式式的玩法),後面說一些題外話,主要是裝飾器與其它語言類似功能的比較,以及一些坑爹的坑。

和其它語言的比較

大部分開發者都會覺得裝飾器這個語法很眼熟,因為我們在Java中有Annotation這個東西,而在C#中也有Attribute這個東西。

所以我說為啥一個語言搞一個東西還要名字不一樣啊……我推薦PHP也來一個,就叫GreenHat好了……

不過有些同學可能會受此誤導,其實裝飾器和Java、C#裡的東西不一樣。

其區別在於Annotation和Attribute是一種後設資料的宣告,僅包含資訊(資料),而不包含任何邏輯,必須有外部的邏輯來讀取這些資訊產生分支才有作用,比如@override這個Annotation相對應的邏輯在編譯器是實現,而不是在對應的class中實現。

而裝飾器,和Python的相同功能同名(赤裸裸的抄襲),其核心是一段邏輯,起源於裝飾器設計模式,讓你有機會去改變一個方法、屬性、類的邏輯,StackOverflow上Python的回答能比較好地解釋這個區別。

幾個坑

在我看來,裝飾器現在有幾個坑得注意一下。

首先,語法上很奇怪,特別是在裝飾器後面的分號上。屬性、訪問器、方法的裝飾器後面是可以加分號的,並且個人推薦加上,不然你可能遇到這樣的問題:

上面的程式碼到底是@bar作為裝飾器的方法呢,還是@bar[1 + 2]()後跟著一個空的Block{}呢?

但是,放在類上的裝飾器,以及放在物件字面量的屬性、訪問器、方法上的裝飾器,是不能加分號的, 不然就是語法錯誤。我不明白為啥就不能加分號,以至於這個語法簡直精神分裂……

其次,如果你把裝飾器用在類的屬性上,建議一定加上分號,看看下面的程式碼:

想一想如果因為特性比較新,壓縮工具一個沒做好沒給補上分號壓成了一行,這是一個怎麼樣的程式碼……

總結

我不寫總結,就醬。

相關文章