裝飾器與後設資料反射(1)方法裝飾器

sept08發表於2019-02-01

讓我來深入地瞭解一下TypeScript對於裝飾器模式的實現,以及反射與依賴注入等相關特性。

Typescript原始碼中,可以看到裝飾器能用來修飾class,property,method,parameter

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

接下來深入地瞭解一下每種裝飾器:

方法裝飾器

首先來根據上面的標識,實現一個名為log的方法裝飾器。使用裝飾器的方法很簡單:在裝飾器名前加@字元,寫在想要裝飾的方法上,類似寫註釋的方式:

class C {
    @log
    foo(n: number) {
        return n * 2;
    }
}

裝飾器實際上是一個函式,入參為所裝飾的方法,返回值為裝飾後的方法。在使用之前需要提前實現這個裝飾器函式,如下:

function log(target: Function, key: string, descriptor: any) {
    // target === C.prototype
    // key === "foo"
    // descriptor === Object.getOwnPropertyDescriptor(C.prototype, "foo")
    // 儲存對原方法的引用,避免重寫
    var originalMethod = descriptor.value; 

    descriptor.value =  function (...args: any[]) {
        // 將“foo”函式的引數列表轉化為字串
        var a = args.map(a => JSON.stringify(a)).join();
        // 呼叫 foo() 並獲取它的返回值
        var result = originalMethod.apply(this, args);
        // 將返回的結果轉成字串
        var r = JSON.stringify(result);
        // 列印日誌
        console.log(`Call: ${key}(${a}) => ${r}`);
        // 返回撥用 foo 的結果
        return result;
    }

    // 返回已編輯的描述符
    return descriptor;
}

該裝飾器函式包含三個引數:

  • target:所要修飾的方法。
  • key:被修飾方法的名字。
  • descriptor:屬性描述符,如果為給定可以通過呼叫Object.getOwnPropertyDescriptor()來獲取。

我們觀察到,類C中使用的裝飾器函式log並沒有顯式的引數傳遞,不免好奇它所需要的引數是如何傳遞的?以及該函式是如何被呼叫的?

TypeScript最終還是會被編譯為JavaScript執行,為了搞清上面的問題,我們來看一下TypeScript編譯器將類C的定義最終生成的JavaScript程式碼:

var C = (function () {
    function C() {
    }
    C.prototype.foo = function (n) {
        return n * 2;
    };
    Object.defineProperty(C.prototype, "foo",
        __decorate([
            log
        ], C.prototype, "foo", Object.getOwnPropertyDescriptor(C.prototype, "foo")));
    return C;
})();

而為新增裝飾器所生成的JavaScript程式碼如下:

var C = (function () {
    function C() {
    }
    C.prototype.foo = function (n) {
        return n * 2;
    };
    return C;
})();

對比兩者發現使用裝飾的不同,只是在類定義中,多瞭如下程式碼:

Object.defineProperty(
  __decorate(
    [log],                                              // 裝飾器
    C.prototype,                                        // target:C的原型
    "foo",                                              // key:裝飾器修飾的方法名
    Object.getOwnPropertyDescriptor(C.prototype, "foo") // descriptor
  );
);

通過查詢MDN文件,可以知悉defineProperty的作用:

Object.defineProperty()方法可直接在一個物件上定義一個新的屬性,或者修改物件上一個已有的屬性,然後返回這個物件。

TypeScript編譯器通過defineProperty方法重寫了所修飾的方法foo,新方法的實現是由函式__decorate返回的,那麼問題來了:__decorate函式在哪宣告的呢?

掘地三尺不難找到,來一起把玩一下:

var __decorate = this.__decorate || function (decorators, target, key, desc) {
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
    return Reflect.decorate(decorators, target, key, desc);
  }
  switch (arguments.length) {
    case 2: 
      return decorators.reduceRight(function(o, d) { 
        return (d && d(o)) || o; 
      }, target);
    case 3: 
      return decorators.reduceRight(function(o, d) { 
        return (d && d(target, key)), void 0; 
      }, void 0);
    case 4: 
      return decorators.reduceRight(function(o, d) { 
        return (d && d(target, key, o)) || o; 
      }, desc);
  }
};

第一行使用了或操作符(||),以確保如果函式__decorate已被建立,他將不會被重寫。

if (typeof Reflect === "object" && typeof Reflect.decorate === "function")

第二行是一個條件語句,使用了JavaScript的一個新特性:後設資料反射。這個主題後續再展開講述,下面我們先聚焦觀察下該新特性的相容方案:

switch (arguments.length) { 
  case 2: 
    return decorators.reduceRight(function(o, d) { 
      return (d && d(o)) || o; 
    }, target);
  case 3: 
    return decorators.reduceRight(function(o, d) { 
      return (d && d(target, key)), void 0; 
    }, void 0);
  case 4: 
    return decorators.reduceRight(function(o, d) { 
      return (d && d(target, key, o)) || o; 
    }, desc);
}

此處__decorate函式接受了4個引數,所以case 4將被執行。平心而論這塊程式碼有點生澀,沒關係掰開揉碎了看。

reduceRight方法接受一個函式作為累加器和陣列的每個值(從右到左)將其減少為單個值。

為了方便理解,上面的程式碼重寫如下:

[log].reduceRight(function(log, desc) { 
  if(log) {
    return log(C.prototype, "foo", desc);
  }
  else {
    return desc;
  }
}, Object.getOwnPropertyDescriptor(C.prototype, "foo"));

可以看到當這段程式碼執行的時候,裝飾器函式log被呼叫,並且引數C.prototype"foo"previousValue也被傳入,如此之前的問題現在可以解答了。
經過裝飾過的foo方法,它依然按照原來的方式執行,只是額外執行了附件的裝飾器函式log的功能。

const c = new C();
const r = c.foo(23); //  "Call: foo(23) => 46"
console.log(r);    // 46

相關文章