Javascript 中的裝飾器
在 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 里增加的功能,它的主要作用是給一個已有的方法或類擴展一些新的行為,而不是去直接修改它本身。
聽起來有點兒懵:flushed:,“show me the code !”
defdecorator(f):
print"my decorator"
returnf
@decorator
defmyfunc():
print"my function"
myfunc()
# my decorator
# my function
這里的 @decorator 就是我們說的裝飾器。在上面的代碼中,我們利用裝飾器給我們的目標方法執行前打印出了一行文本,并且并沒有對原方法做任何的修改。代碼基本等同于
defdecorator(f):
defwrapper():
print"my decorator"
returnf()
returnwrapper
defmyfunc():
print"my function"
myfunc = decorator(myfuc)
通過代碼我們也不難看出,裝飾器 decorator 接收一個參數,也就是我們被裝飾的目標方法,處理完擴展的內容以后再返回一個方法,供以后調用,同時也失去了對原方法對象的訪問。當我們對某個應用了裝飾以后,其實就改變了被裝飾方法的入口引用,使其重新指向了裝飾器返回的方法的入口點,從而來實現我們對原函數的擴展、修改等操作。
引入到 Javascript 中
那么我們了解到了裝飾器在 python 中的表現以后,會不會覺得其實裝飾器其實蠻簡單的,就是一個 wrapper 嘛,對于 Javascript 這種語言來說,這種形態不是很常見嗎,干嘛還要引入這么一個東西呢?
是的,在 ES6 之前,裝飾器對于 JS 來說確實顯得不太重要,你只是需要加幾層 wrapper 包裹就好了(雖然也會顯得不那么優雅)。但是正如文章開頭所說,在 ES6 提出之后,你會發現,好像事情變得有些不同了。當我們需要在多個不同的類之間共享或者擴展一些方法或行為的時候,代碼會變得錯綜復雜,極其不優雅,這也就是裝飾器被提出的一個很重要的原因。
話說從裝飾器被提出已經有一年多的時間了,同時期的很多其他新的特性已經隨著 ES6 的推進而被大家廣泛使用,而這貨現在卻還停留在 stage 2 的階段,也很少被人提及和應用。那么,裝飾器到底是在 Javascript 中是怎樣表現的呢?我們下面來一起看一下吧!
Javascript 中的裝飾器
先來看一下裝飾器在代碼中是長成什么樣子吧
@decorator
classCat{}
classDog{
@decorator
run() {}
}
嗯,代碼中的 @decorator 就是 JS 中的裝飾器,看起來基本和 python 中的樣子一樣,以 @ 作為標識符,可以作用于類,也可以作用于類的屬性。那么接下來,我們就來看看它具體的表現及運行原理吧。
ES6 中的類
首先我們先來看一下關于 ES6 中的類吧
classCat{
say() {
console.log("meow ~");
}
}
上面這段代碼是 ES6 中定義一個類的寫法,其實只是一個語法糖,而實際上當我們給一個類添加一個屬性的時候,會調用到 Object.defineProperty 這個方法,它會接受三個參數: target 、 name 和 descriptor ,所以上面的代碼實際上在執行時是這樣的:
functionCat(){}
Object.defineProperty(Cat.prototype,"say", {
value:function(){console.log("meow ~"); },
enumerable:false,
configurable:true,
writable:true
});
好了,有了上面這段代碼以后,我們再來看看裝飾器在 JS 中到底是怎么樣工作的吧!
作用于類的裝飾器
當一個裝飾器作用于類的時候,大概是這個樣子的:
functionisAnimal(target){
target.isAnimal = true;
returntarget;
}
@isAnimal
classCat{
...
}
console.log(Cat.isAnimal);// true
是不是很像之前我們在 python 中看到的裝飾器?
(??????)??
所以這段代碼實際上基本等同于:
Cat = isAnimal(functionCat(){ ... });
那么我們再來看一下作用于類的單個屬性方法的裝飾器
作用于類屬性的裝飾器
比如有的時候,我們希望把我們的部分屬性置成只讀,以避免別人對其進行修改,如果使用裝飾器的話,我們可以這樣來做:
functionreadonly(target, name, descriptor){
discriptor.writable = false;
returndiscriptor;
}
classCat{
@readonly
say() {
console.log("meow ~");
}
}
varkitty =newCat();
kitty.say = function(){
console.log("woof !");
}
kitty.say() // meow ~
我們通過上面的代碼把 say 方法設置成了只讀,所以在我們后面再次對它賦值的時候就不會生效,調用的還是之前的方法。
在上面的代碼中我們可以看到,我們在定義裝飾器的時候,參數是有三個, target 、 name 、 descriptor 。
誒?等一下,這里怎么這么眼熟?⊙_⊙
沒錯,就是我們上文提到過的關于類的定義那一塊兒的 Object.defineProperty 的參數,所以其實裝飾器在作用于屬性的時候,實際上是通過 Object.defineProperty 來進行擴展和封裝的。
所以在上面的這段代碼中,裝飾器實際的作用形式是這樣的:
letdescriptor = {
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 上進行擴展和封裝,比如
functionfast(target, name, descriptor){
target.speed = 20;
letrun = descriptor.value;
descriptor.value = function(){
run();
console.log(`speed${this.speed}`);
}
returndescriptor;
}
classRabbit{
@fast
run() {
console.log("running~");
}
}
varbunny =newRabbit();
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 這個插件:
babel --plugins transform-decorators-legacy es6.js > es5.js
來自:https://aotu.io/notes/2016/10/24/decorator/