Javascript中裝飾器的實現原理

_zon_發表於2018-03-23

基於Node的web伺服器開發中使用decorator對請求進行許可權校驗和資料格式的處理是一個看起來比較漂亮的寫法,這裡正好整理一下對javascript中的decorator的理解。

decorator的概念在其他語言中早有存在,在javascript中目前(2017/09/17)還處於stage 2階段,基本確定會進入正式的ECMA規範了。但是目前還不能直接使用,只能使用babel來進行語法的轉換。

官方的說法是:

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

大概的意思就是在執行時改變類或者類的屬性。

正文

首先看下裝飾器常見的兩種使用:

  1. 裝飾一個類的屬性。
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 ~
複製程式碼
  1. 裝飾一個類。
function isAnimal(target) {
    target.isAnimal = true;
  	return target;
}
@isAnimal
class Cat {
    ...
}
console.log(Cat.isAnimal);    // true
複製程式碼

裝飾一個類的屬性

ES6中的類實際上就是一個語法糖,本質上是建構函式,類的屬性的定義使用的是 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
});
複製程式碼

細心的小夥伴已經發現了

Object.defineProperty(obj, prop, descriptor)
複製程式碼

接收的引數和作用於類的屬性的時候裝飾器函式的接收的引數很像。

可以知道作用於類的屬性的時候的裝飾器函式接收的引數就是上述ES6中的類定義屬性時候使用Object.defineProperty時接收的引數,一模一樣...

本質上也就是說裝飾器在作用於類的屬性的時候,實際上是通過 Object.defineProperty 來對原有的descriptor進行封裝:

descriptor:

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

通過處理descriptor可以改變原有屬性。 被裝飾的屬性的定義在實際上執行的是以下的程式碼:

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);
複製程式碼

也就是說,上面的那個@readonly其實就是

descriptor = readonly(Cat.prototype, "say", descriptor) || descriptor;
複製程式碼

的語法糖,要注意的是,裝飾器執行的時間是在屬性定義的時候,也就是被裝飾的屬性在定義後就是已經被裝飾器處理過的不一樣的屬性了。

裝飾一個類

裝飾一個類的時候類本身本質上是一個函式,沒有descriptor,target是這個函式本身。

function isAnimal(target) {
    target.isAnimal = true;
  	return target;
}
@isAnimal
class Cat {
    ...
}
console.log(Cat.isAnimal);    // true
複製程式碼

也就是說,上面的@isAnimal其實就是做了下面這件事

Cat = isAnimal(function Cat() { ... });
複製程式碼

在瞭解了兩種情況下裝飾器本質上做了什麼之後,順帶可以看出,裝飾器函式執行的時間:

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() {}
};
複製程式碼

輸出結果:

before class
class method bar
class getter alice
class property bob
class Bar
after class
object method bar
複製程式碼

可以看出裝飾器在定義時就執行了,也就對應著官方的那句話:

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

在類和類的屬性定義的時候就對它們進行了"裝飾"。

以上大致的說了下javascript的裝飾器的原理和使用,但是還有一些細節有待進一步的深入。

TBD

參考資料

相關文章