前言
在 ES6 中增加了對類物件的相關定義和操作(比如 class
和 extends
),這就使得我們在多個不同類之間共享或者擴充套件一些方法或者行為的時候,變得並不是那麼優雅。這個時候,我們就需要一種更優雅的方法來幫助我們完成這些事情。
Python 中的裝飾器
decorators 即 裝飾器,這一特性的提出來源於 python 之類的語言,如果你熟悉 python 的話,對它一定不會陌生。那麼我們先來看一下 python 裡的裝飾器是什麼樣子的吧:
A Python decorator is a function that takes another function, extending the behavior of the latter function without explicitly modifying it.
裝飾器是在 python 2.4 裡增加的功能,它的主要作用是給一個已有的方法或類擴充套件一些新的行為,而不是去直接修改它本身。
聽起來有點兒懵?,“show me the code !”
1 2 3 4 5 6 7 8 9 10 11 12 |
def decorator(f): print "my decorator" return f @decorator def myfunc(): print "my function" myfunc() # my decorator # my function |
這裡的 @decorator
就是我們說的裝飾器。在上面的程式碼中,我們利用裝飾器給我們的目標方法執行前列印出了一行文字,並且並沒有對原方法做任何的修改。程式碼基本等同於
1 2 3 4 5 6 7 8 9 10 |
def decorator(f): def wrapper(): print "my decorator" return f() return wrapper def myfunc(): print "my function" myfunc = decorator(myfuc) |
通過程式碼我們也不難看出,裝飾器 decorator 接收一個引數,也就是我們被裝飾的目標方法,處理完擴充套件的內容以後再返回一個方法,供以後呼叫,同時也失去了對原方法物件的訪問。當我們對某個應用了裝飾以後,其實就改變了被裝飾方法的入口引用,使其重新指向了裝飾器返回的方法的入口點,從而來實現我們對原函式的擴充套件、修改等操作。
引入到 Javascript 中
那麼我們瞭解到了裝飾器在 python 中的表現以後,會不會覺得其實裝飾器其實蠻簡單的,就是一個 wrapper 嘛,對於 Javascript 這種語言來說,這種形態不是很常見嗎,幹嘛還要引入這麼一個東西呢?
是的,在 ES6 之前,裝飾器對於 JS 來說確實顯得不太重要,你只是需要加幾層 wrapper 包裹就好了(雖然也會顯得不那麼優雅)。但是正如文章開頭所說,在 ES6 提出之後,你會發現,好像事情變得有些不同了。當我們需要在多個不同的類之間共享或者擴充套件一些方法或行為的時候,程式碼會變得錯綜複雜,極其不優雅,這也就是裝飾器被提出的一個很重要的原因。
話說從裝飾器被提出已經有一年多的時間了,同時期的很多其他新的特性已經隨著 ES6 的推進而被大家廣泛使用,而這貨現在卻還停留在 stage 2 的階段,也很少被人提及和應用。那麼,裝飾器到底是在 Javascript 中是怎樣表現的呢?我們下面來一起看一下吧!
Javascript 中的裝飾器
先來看一下裝飾器在程式碼中是長成什麼樣子吧
1 2 3 4 5 6 7 |
@decorator class Cat {} class Dog { @decorator run() {} } |
嗯,程式碼中的 @decorator
就是 JS 中的裝飾器,看起來基本和 python 中的樣子一樣,以 @
作為識別符號,可以作用於類,也可以作用於類的屬性。那麼接下來,我們就來看看它具體的表現及執行原理吧。
ES6 中的類
首先我們先來看一下關於 ES6 中的類吧
1 2 3 4 5 |
class Cat { say() { console.log("meow ~"); } } |
上面這段程式碼是 ES6 中定義一個類的寫法,其實只是一個語法糖,而實際上當我們給一個類新增一個屬性的時候,會呼叫到 Object.defineProperty
這個方法,它會接受三個引數:target
、name
和 descriptor
,所以上面的程式碼實際上在執行時是這樣的:
1 2 3 4 5 6 7 |
function Cat() {} Object.defineProperty(Cat.prototype, "say", { value: function() { console.log("meow ~"); }, enumerable: false, configurable: true, writable: true }); |
好了,有了上面這段程式碼以後,我們再來看看裝飾器在 JS 中到底是怎麼樣工作的吧!
作用於類的裝飾器
當一個裝飾器作用於類的時候,大概是這個樣子的:
1 2 3 4 5 6 7 8 9 10 11 |
function isAnimal(target) { target.isAnimal = true; return target; } @isAnimal class Cat { ... } console.log(Cat.isAnimal); // true |
是不是很像之前我們在 python 中看到的裝飾器?
(๑•̀ㅂ•́)و✧
所以這段程式碼實際上基本等同於:
1 |
Cat = isAnimal(function Cat() { ... }); |
那麼我們再來看一下作用於類的單個屬性方法的裝飾器
作用於類屬性的裝飾器
比如有的時候,我們希望把我們的部分屬性置成只讀,以避免別人對其進行修改,如果使用裝飾器的話,我們可以這樣來做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function readonly(target, name, descriptor) { discriptor.writable = false; return discriptor; } class Cat { @readonly say() { console.log("meow ~"); } } var kitty = new Cat(); kitty.say = function() { console.log("woof !"); } kitty.say() // meow ~ |
我們通過上面的程式碼把 say
方法設定成了只讀,所以在我們後面再次對它賦值的時候就不會生效,呼叫的還是之前的方法。
在上面的程式碼中我們可以看到,我們在定義裝飾器的時候,引數是有三個,target
、name
、descriptor
。
誒?等一下,這裡怎麼這麼眼熟?⊙_⊙
沒錯,就是我們上文提到過的關於類的定義那一塊兒的 Object.defineProperty
的引數,所以其實裝飾器在作用於屬性的時候,實際上是通過 Object.defineProperty
來進行擴充套件和封裝的。
所以在上面的這段程式碼中,裝飾器實際的作用形式是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 |
let descriptor = { value: function() { console.log("meow ~"); }, enumerable: false, configurable: true, writable: true }; descriptor = readonly(Cat.prototype, "say", descriptor) || descriptor; Object.defineProperty(Cat.prototype, "say", descriptor); |
嗯嗯,是不是這樣看就清楚很多了呢?這裡也是 JS 裡裝飾器作用於類和作用於類的屬性的不同的地方。
我們可以看出,當裝飾器作用於類本身的時候,我們操作的物件也是這個類本身,而當裝飾器作用於類的某個具體的屬性的時候,我們操作的物件既不是類本身,也不是類的屬性,而是它的描述符(descriptor),而描述符裡記錄著我們對這個屬性的全部資訊,所以,我們可以對它自由的進行擴充套件和封裝,最後達到的目的呢,就和之前說過的裝飾器的作用是一樣的。
當然,如果你喜歡的話,也可以直接在 target
上進行擴充套件和封裝,比如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
function fast(target, name, descriptor) { target.speed = 20; let run = descriptor.value; descriptor.value = function() { run(); console.log(`speed ${this.speed}`); } return descriptor; } class Rabbit { @fast run() { console.log("running~"); } } var bunny = new Rabbit(); bunny.run(); // running~ // speed 20 console.log(bunny.speed); // 20 |
小結
OK,讓我們再來看一下 JS 裡對於裝飾器的描述吧:
Decorators make it possible to annotate and modify classes and properties at design time.
A decorator is:
- an expression
- that evaluates to a function
- that takes the target, name, and decorator descriptor as arguments
- and optionally returns a decorator descriptor to install on the target object
裝飾器允許你在類和方法定義的時候去註釋或者修改它。裝飾器是一個作用於函式的表示式,它接收三個引數 target
、 name
和 descriptor
, 然後可選性的返回被裝飾之後的 descriptor
物件。
現在是不是對裝飾器的作用及原理都清楚了呢?
最後一點就是,現在裝飾器因為還在草案階段,所以還沒有被大部分環境支援,如果要用的話,需要使用 Babel 進行轉碼,需要用到 babel-plugin-transform-decorators-legacy
這個外掛:
1 |
babel --plugins transform-decorators-legacy es6.js > es5.js |
如果你感興趣的話,也可以看一下轉碼以後的程式碼,我這裡就不做詳細介紹了,很有幫助哦~
如果本文描述的有錯誤的地方,歡迎留言~ ヾ(o◕∀◕)ノ
參考文獻:
- https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841#.cmajiy3p1
- https://github.com/wycats/javascript-decorators
- http://www.artima.com/weblogs/viewpost.jsp?thread=240808
- http://taobaofed.org/blog/2015/11/16/es7-decorator/?utm_source=tuicool&utm_medium=referral
- https://github.com/jayphelps/core-decorators.js