JavaScript裝飾器的實現(xiàn)原理詳解
最近在使用TS+Vue的開發(fā)模式,發(fā)現(xiàn)項目中大量使用了裝飾器,看得我手足無措,今天特意研究一下實現(xiàn)原理,方便自己理解這塊知識點。
裝飾器的常見作用
- 裝飾一個類的屬性
- 裝飾一個類
裝飾器只能針對類和類的屬性,不能直接作用于函數(shù),因為存在函數(shù)提升。
下面我們針對這兩種情況進行舉例闡述。
裝飾類的屬性
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 ~
ES6中的類實際上就是一個語法糖,本質(zhì)上是構(gòu)造函數(shù),類的屬性的定義使用的是 Object.defineProperty() 類的代碼可以改寫為下面代碼:
class Cat {
say() {
console.log("meow ~");
}
}
// 上面等價于下面代碼
function Cat() {}
Object.defineProperty(Cat.prototype, "say", {
value: function() { console.log("meow ~"); },
enumerable: false,
configurable: true,
writable: true
});在say方法前面加入@readonly后,整個代碼等價于下面代碼:
function readonly(target, name, descriptor) {
discriptor.writable = false;
return discriptor;
}
function Cat() {}
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);
裝飾類屬性的大致實現(xiàn)原理,就是上面的語法糖實現(xiàn)過程。
這里注意下裝飾器的用法,是在屬性前面加入@readonly,不是@readonly()。如果是后一種寫法的話,裝飾器函數(shù)需要返回一個函數(shù)來表示裝飾器。這種寫法主要是通過傳參來擴展裝飾器函數(shù)的功能,使裝飾器函數(shù)復(fù)用性更強。
如通過傳參決定是否設(shè)置只讀,代碼如下:
function readonly(flag) {
return function (target, name, descriptor) {
if (flag) {
discriptor.writable = false;
} else {
discriptor.writable = true;
}
return discriptor;
}
}
class Cat {
@readonly(true)
say() {
console.log("meow ~");
}
}
var kitty = new Cat();
kitty.say = function() {
console.log("woof !");
}
kitty.say()
裝飾類
裝飾一個類的時候類本身本質(zhì)上是一個函數(shù),沒有descriptor,target是這個函數(shù)本身。
function isAnimal(target) {
target.isAnimal = true;
return target;
}
@isAnimal
class Cat {
...
}
console.log(Cat.isAnimal); // true
也就是說,上面的@isAnimal其實就是做了下面這件事
Cat = isAnimal(function Cat() { ... });
注意
作用在方法上的 decorator 接收的第一個參數(shù)(target )是類的 prototype;如果把一個 decorator 作用到類上,則它的第一個參數(shù) target 是 類本身。
裝飾器執(zhí)行的時間是在屬性定義的時候,也就是被裝飾的屬性在定義后就是已經(jīng)被裝飾器處理過的不一樣的屬性了。
下面舉例看下裝飾器的執(zhí)行時間:
function log(message) {
return function() {
console.log(message);
}
}
console.log('before class');
@log('class Bar')
class Bar {
@log('class method bar');
bar() {}
@log('class getter alice');
get alice() {}
@log('class property bob');
bob = 1;
}
console.log('after class');
let bar = {
@log('object method bar')
bar() {}
};輸出結(jié)果:
before class
class method bar
class getter alice
class property bob
class Bar
after class
object method bar
可以看出裝飾器在類和類的屬性定義的時候就對它們進行了"裝飾"。
實例應(yīng)用
我們的示例場景是這樣的
- 首先創(chuàng)建一個普通的Man類,它的抵御值 2,攻擊力為 3,血量為 3;
- 然后我們讓其帶上鋼鐵俠的盔甲,這樣他的抵御力增加 100,變成 102;
- 讓其帶上光束手套,攻擊力增加 50,變成 53;
- 最后讓他增加“飛行”能力
1. 創(chuàng)建 Man 類:
class Man{
constructor(def = 2,atk = 3,hp = 3){
this.init(def,atk,hp);
}
init(def,atk,hp){
this.def = def; // 防御值
this.atk = atk; // 攻擊力
this.hp = hp; // 血量
}
toString(){
return `防御力:${this.def},攻擊力:${this.atk},血量:${this.hp}`;
}
}
var tony = new Man();
console.log(`當前狀態(tài) ===> ${tony}`);
// 輸出:當前狀態(tài) ===> 防御力:2,攻擊力:3,血量:3
2. 裝飾器為鋼鐵俠裝配盔甲:
function decorateArmour(target, key, descriptor) {
const method = descriptor.value;
let moreDef = 100;
let ret;
descriptor.value = (...args)=>{
args[0] += moreDef;
ret = method.apply(target, args);
return ret;
}
return descriptor;
}
class Man{
constructor(def = 2,atk = 3,hp = 3){
this.init(def,atk,hp);
}
@decorateArmour
init(def,atk,hp){
this.def = def; // 防御值
this.atk = atk; // 攻擊力
this.hp = hp; // 血量
}
toString(){
return `防御力:${this.def},攻擊力:${this.atk},血量:${this.hp}`;
}
}
var tony = new Man();
console.log(`當前狀態(tài) ===> ${tony}`);
// 輸出:當前狀態(tài) ===> 防御力:102,攻擊力:3,血量:3
3. 裝飾器增加光束手套
function decorateLight(target, key, descriptor) {
const method = descriptor.value;
let moreAtk = 50;
let ret;
descriptor.value = (...args)=>{
args[1] += moreAtk;
ret = method.apply(target, args);
return ret;
}
return descriptor;
}
class Man{
constructor(def = 2,atk = 3,hp = 3){
this.init(def,atk,hp);
}
@decorateArmour
@decorateLight
init(def,atk,hp){
this.def = def; // 防御值
this.atk = atk; // 攻擊力
this.hp = hp; // 血量
}
...
}
var tony = new Man();
console.log(`當前狀態(tài) ===> ${tony}`);
//輸出:當前狀態(tài) ===> 防御力:102,攻擊力:53,血量:3
在這里你就能看出裝飾模式的優(yōu)勢了,它可以對某個方法進行疊加使用,對原類的侵入性非常小,只是增加一行@decorateLight而已,可以方便地增刪;(同時還可以復(fù)用)
4. 增加飛行能力
function addFly(canFly){
return function(target){
target.prototype.canFly = canFly;
let extra = canFly ? '(技能加成:飛行能力)' : '';
let method = target.prototype.toString;
target.prototype.toString = (...args)=>{
return method.apply(target.prototype,args) + extra;
}
return target;
}
}
@addFly(true)
class Man{
constructor(def = 2,atk = 3,hp = 3){
this.init(def,atk,hp);
}
@decorateArmour
@decorateLight
init(def,atk,hp){
this.def = def; // 防御值
this.atk = atk; // 攻擊力
this.hp = hp; // 血量
}
...
}
...
console.log(`當前狀態(tài) ===> ${tony}`);
// 輸出:當前狀態(tài) ===> 防御力:102,攻擊力:53,血量:3(技能加成:飛行能力)到此這篇關(guān)于JavaScript裝飾器的實現(xiàn)原理詳解的文章就介紹到這了,更多相關(guān)JavaScript裝飾器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Typescript 實現(xiàn)函數(shù)重載的方式
函數(shù)重載簡單點說就是,同一個函數(shù)的不同實現(xiàn)方式,根據(jù)傳入的參數(shù)的類型或者長度,決定最終調(diào)用哪一個實現(xiàn),本文給大家介紹了Typescript 實現(xiàn)函數(shù)重載的方式,需要的朋友可以參考下2024-05-05

