ES7 decorator 深入探析

起因

一直享受着 Anuglar 和 Nest 的紅利,上來就是 @Component(...) 或者 @Controller(...),自己卻沒有實際的探究過背後的原理。於是今天想好好總結一下,沉澱沉澱。

前置條件(es5 原理)

之前看過紅寶書,第六章提到過,js 對象的屬性有幾個特性:

  1. [[configurable]] 是否可配置
  2. [[enumerable]] 是否可枚舉
  3. [[writeble]] 是否可修改值
  4. [[value]] 寫入的值是啥

四個配置項都爲 boolean 類型。
這四個配置聯合起來有一個名字,叫做對象屬性的描述符(descriptor)
其中,writeble 和 value 還有另外一個名字, settergetter 訪問器)。
上代碼:

const obj = { };
Object.defineProperty(obj,'a', {
value: 1,
writeble: false,
});
console.log(obj); // {a: 1}
console.log(obj.a) // 1
obj.a = 3; // 修改 a 屬性的值
console.log(obj.a) // 1

/**====================另一種寫法====================*/
const d = {};
Object.defineProperty(d , 'name' {
get: function() {return 1},
set: function(value) {return false}
});

console.log(d) // {} 注意!!!!這裏跟 writeble 和 value 不太一樣,這裏打印出來的對象,是沒有顯示 name 屬性的!!!但是訪問可以訪問出來
d.name; // 1
d.name = 3; // 嘗試修改 name 屬性 
d.name; // 1

我們發現,配置了可寫入項爲 false 時,我們就無法去修改對象屬性的值了,有點像凍結的意思。剛好,JS 有個 Object.freeze(), 來看一下

const c = {name: 1};
Object.freeze(c);
c.name = 3;
console.log(c) // {a: 1}

發現和我們自己去配置 writeble: false 效果相同。不信?來驗證一下:

Object.getOwnPropertyDescriptor(c);
// 返回: 
{
	 name: {
	 configurable: false
	 enumerable: true
	 value: 1
	 writable: false
	}
}

ES6 還要這麼寫嗎?

不用。直接用裝飾器 decorator來寫。

第一種,直接裝飾 class,

作用: 給類增加動態屬性,該動態屬性並不會被繼承,只能作爲 被裝飾類 的 靜態屬性。
注意: 給類添加靜態屬性的這種行爲,是在 編譯時 發生的!所以說:
裝飾器的本質就是編譯時運行的函數

function addFlag(object) {
object.flag = true;
}

@addFlag
class Foo(){}
Foo.flag // true


// 來個實例
const f1 = new Foo();
f1.flag // undefined

第二種,裝飾屬性

裝飾器會在 Object.defineProperty 之前執行,也就是攔截默認的訪問修飾符。
舉個例子:

// CSDN markdown 編輯器 爲什麼不支持 typescript 高亮?無語...
function nameEqual3(object, key, descriptor: PropertyDescriptor) {
    descriptor.value = 3;
    descriptor.writable = false;
}
class Person {

    @nameEqual3
    name() { }
}

const p = new Person();
console.log(p.name); // 3

可見其效果。
也支持傳參,如下代碼所示,請仔細閱讀註釋:

  // 裝飾器函數 (用閉包來封裝一下)
  function sign(id) {
    return function (target, name, descriptor) {
      /**
       *  這裏的 value 在我看來,更像是一個 getter, 所以可以直接被賦值成一個函數
       *  類似於:
       *  descriptor = {
       *     get: function(){ return this.value } 
       *  }
       */
      const oldValue = descriptor.value;
      /**
       * 這裏的 args 實際上就是裝飾器在運行時,掛載的函數的入參,下面的 log 日誌會證明
       */
      descriptor.value = function (...args) {
        console.log(`args =>`, args);
        console.log(`標記 ${id}`);
        return oldValue.apply(this, args);
      };

      return descriptor;
    }
  }

  class Person {
    @sign(1)
    method(a, b) {
      return a + b;
    }
  }

  // 實例化和調用
  const p1 = new Person();
  p1.method(2, 4);
  
  // 輸出:
   args => [3,4]
   標記 1

第三種,裝飾器的高級用法(鏈式調用, combine 以及 mixin)

1.鏈式(連續)

首先來看鏈式(連續)調用,這次多加一個裝飾器,並且繼續通過打印的方式來查看下調用的順序:


// 裝飾器函數 再 封裝一層
function mark(id) {
  // 真正的裝飾器函數以閉包形式返回
  return (obj, target, descriptor) => {
    // 不破壞原 getter 函數
    const old = descriptor.value;
    console.log(id);
    return descriptor.value = () => old.apply(this, id);
  }
}



class Person {

  @mark(1)
  @mark(2)
  method() { }
}


const p1 = new Person();

p1.method();

// 輸出:
2 
1

咦?明明 @mark(1)@mark(2) 之前調用的啊,爲什麼 2 比 1 先執行了呢?
讓我們打開 如下地址,跟着我一起分析:
Type Script - Play ground
來看右邊編譯後的 javascript 代碼,只看 var decorator 被編譯成了啥,下面的不用看,跟源碼差不多。請仔細閱讀註釋

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
// 判斷函數真正的入參,如果小於 3 個,r = target 否則 繼續判斷 ,在該 對象 的屬性(被裝飾的屬性)上原本的 descriptor 是不是 null ? 如果是,則 desc 等於 當前對象被裝飾屬性的 descriptor ,否則 r = 當前對象被裝飾屬性的 descriptor
// 這裏的 d 用於緩存 下面遍歷時 的 狀態
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    // 這裏的 Reflect 是 window 下的 全局對象,我們也知道, Reflect 對象根本沒有 decorate 方法,所以, turthy 的分支並不會執行,而是走 falsy 分支.
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    /**********關鍵步驟************/
    // 這裏遍歷的是入參的裝飾器數組,並且,從右倒敘遍歷(起始下標爲 decorators.length - 1)
    // d 是每次遍歷的 裝飾器返回的 descriptor, 通過 判斷入參個數,來決定 r 的類型,以及是否通過 d(r) 來裝飾某個對象。如果 入參 < 3 個,即 r 爲 一個對象,執行 d(r) ; 否則如果 入參 > 3 個,即運行時傳入了第四個參數 desc(descriptor) , 此時的 r 其實就是 desc ,d(target, key, r) 意思是:用 入參的 desc 裝飾對象 target 的 key 屬性;否則 c < 4 , 此時的 r  爲 object 對象,d(target, key);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};

/*************************下面這些先不用看***********************/

// 裝飾器函數 再 封裝一層
function mark(id) {
    var _this = this;
    // 真正的裝飾器函數以閉包形式返回
    return function (obj, target, descriptor) {
        // 不破壞原 getter 函數
        var old = descriptor.value;
        console.log(id);
        return descriptor.value = function () { return old.apply(_this, id); };
    };
}
var Person = /** @class */ (function () {
    function Person() {
    }
    Person.prototype.method = function () { };
    __decorate([
        mark(1),
        mark(2)
    ], Person.prototype, "method", null);
    return Person;
}());
var p1 = new Person();
p1.method();

上面囉裏囉唆的註釋是啥意思呢?
翻譯成人話: 裝飾器的執行順序是個 棧, 後進先出。像極了… 愛情?不,像極了 洋蔥模型

2. combine (合併)

合併指的是裝飾器裝飾某個類的屬性的時候,同時應用多個裝飾器的模式。(要跟下面的 @mixin)區分


function eatApple(count) {
  return (obj,target,descriptor) => {
    const old = descriptor.value;
    console.log(`吃了 ${count} 個 蘋果`);
    return old.apply(this);
  }
}


function runMeter(long) {
  return (obj,target,descriptor) => {
    const old = descriptor.value;
    console.log(`跑了 ${long} 米`);
    return old.apply(this);
  }
}


function combine(...descriptors) {
  // 想點辦法,讓入參的每個函數立馬執行!要把自己得到的對象分配給兩個小弟
  return (obj, target, descriptor) => descriptors.forEach(d => d.apply(this, [obj, target, descriptor]));
}


class Person {

  @combine(eatApple(1), runMeter(9))
  method() { }
}


const p1 = new Person();

p1.method();

// 輸出:
吃了一個蘋果
跑了 9

可見,在 @combine() 中傳入的參數順序,竟然跟最終的順序 是一樣的,咦?不是洋蔥嗎?這壓根不是棧啊!
腦子裏回想一下剛纔解析源碼的過程,我再次望向了這次的源碼:

var Person = /** @class */ (function () {
    function Person() {
    }
    Person.prototype.method = function () { };
    __decorate([
        combine(eatApple(1), runMeter(9))
    ], Person.prototype, "method", null);
    return Person;

顯而易見,這兩個函數,直接是作爲結果被傳進去的,相當於棧裏面只有 mixin 一個函數,無所謂是棧或者隊列了,反正兩個函數都在我內部執行,我讓他怎麼執行就怎麼執行,爲所欲爲。所以這裏的輸出結果是同步的,完全就是因爲棧裏只有一個 member。
不信驗證一下:


function eatApple(count) {
  return (obj,target,descriptor) => {
    const old = descriptor.value;
    console.log(`吃了 ${count} 個 蘋果`);
    return old.apply(this);
  }
}


function runMeter(long) {
  return (obj,target,descriptor) => {
    const old = descriptor.value;
    console.log(`跑了 ${long} 米`);
    return old.apply(this);
  }
}


function combine(...descriptors) {
  // 想點辦法,讓入參的每個函數立馬執行!要把自己得到的對象分配給兩個小弟
  return (obj, target, descriptor) => descriptors.forEach(d => d.apply(this, [obj, target, descriptor]));
}


class Person {

  @combine(eatApple(1), runMeter(9))
  @combine(eatApple(5),runMeter(100))
  method() { }
}


const p1 = new Person();

p1.method();

// 輸出:
吃了 5 個 蘋果
跑了 100 米
吃了 1 個 蘋果
跑了 9
3. mixin (混合)

mixin 意爲在一個對象之中混入另外一個對象的方法。

function mixins(...list) {
  return function (target) {
  // Object.assign 可用於對象,即 編譯後的 es3 runtime 指向 class.prototype
    Object.assign(target.prototype, ...list);
  };
}
const Foo = {
  foo() { console.log('foo') }
};

@mixins(Foo)
class MyClass {}

let obj = new MyClass();
obj.foo() // "foo"
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章