當class properties趕上decorator

前言

本篇共3個章節。babel

前2個章節介紹class中兩種方式定義方法的不一樣、decorator如何做用於class的方法。app

最後1個章節經過一個demo介紹瞭如何實現一個兼容class普通方法和class屬性方法的裝飾器,以及如何保留裝飾器裝飾的箭頭函數式中this爲類實例的特性。函數

1、class中的函數

在React中的函數中固定this指向組件實例是一個常見的需求,一般有如下三種寫法:this

1.在constructor中使用bind指定this:spa

this.handlePress = this.handlePress.bind(this)
複製代碼

2.使用autobind的裝飾器:prototype

@autobind
  handlePress(){}
複製代碼

3.使用class properties與arrow functioncode

handlePress = () => {}
複製代碼

這裏有兩種爲類聲明方法的方式,第一種如一、2在類中直接聲明方法,第二種爲將方法聲明爲類的一個屬性(’=‘標識)。對象

咱們都知道class即function,讓咱們定義一個簡單的類,觀察babel編譯後的結果,看看這兩種方式聲明的方法有何不一樣。ip

class A {
    sayHello() {
    }
    sayWorld = function() {
    }
  }
複製代碼

編譯後原型鏈

var A = function () {
      function A() {
          _classCallCheck(this, A);
  
          this.sayWorld = function () {};
      }
  
      _createClass(A, [{
          key: "sayHello",
          value: function sayHello() {}
      }]);
  
      return A;
  }();
複製代碼

編譯後的代碼中sayHello和sayWorld是經過不一樣方式關聯到A上的。sayWorld的定義發生在構造函數執行期間,即類實例的建立時。而sayHello是經過_createClass方法關聯到A上的。

來看看_createClass作了什麼:

var _createClass = function () {
    function defineProperties(target, props) { 
      for (var i = 0; i < props.length; i++) { 
        // 建立一個數據屬性,並將其定義在target對象上
        var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; 
        descriptor.configurable = true; 
        if ("value" in descriptor) descriptor.writable = true; 
        Object.defineProperty(target, descriptor.key, descriptor); 
      } 
    } 
    return function (Constructor, protoProps, staticProps) { 
      if (protoProps) defineProperties(Constructor.prototype, protoProps); 
      if (staticProps) defineProperties(Constructor, staticProps); 
      return Constructor;
    }; 
  }();
複製代碼

_createClass中建立了一個以下的數據屬性,使用Object.defineProperty定義在A.prototype上。

{
  enumerable: false,
  configurable: true,
  writable: true,
  value: function sayHello() {}
}
複製代碼

可見sayHello方法是定義在A.prototype上的方法,會被衆多A的實例所共享;而sayWorld則是每一個A實例獨有的方法(每次建立實例都會新建)。

得出結論:

一、普通的類方法實際歸屬於class.prototype,該類的衆多實例將經過原型鏈共享該方法。

二、屬性方式定義的類方法歸屬於class的實例,同名方法在類的不一樣實例中並不相同。

 

讓咱們對A作一些修改,從新編譯。

class A {
    sayHello() {
      console.log('hello', this);
    }
    sayWorld = function() {
      console.log('world', this);
    }
    sayName = () => {
      console.log('name', this);
    }
  }
複製代碼

編譯後

var A = function () {
    function A() {
      var _this = this;
  
      _classCallCheck(this, A);
  
      this.sayWorld = function () {
        console.log('world', this);
      };
  
      this.sayName = function () {
        console.log('name', _this);
      };
    }
  
    _createClass(A, [{
      key: 'sayHello',
      value: function sayHello() {
        console.log('hello', this);
      }
    }]);
  
    return A;
  }();
複製代碼

咱們都知道箭頭函數中this的指向爲其聲明時當前做用域的this,因此sayName中的this在編譯過程當中被替換爲_this(構造函數執行時的this,即類實例自己),這就是前面固定方法this指向實例的第三種方法"使用class properties與arrow function"生效的緣由。

2、decorator

裝飾器(decorator)是一個函數,用於改造類與類的方法。篇幅緣由咱們這裏只介紹做用於類方法的裝飾器。一個簡單的函數裝飾器構造以下:

function decoratorA(target, name, descriptor) {
  // 未作任何修改
}
複製代碼
  • target爲class.prototype。

  • name即方法名稱。

  • descriptor有兩種,數據屬性和訪問器屬性。兩種屬性包含了6種特性,enumerable和configurable爲共有的2種特性,writable和value爲數據屬性獨有,而getter和setter爲訪問器屬性獨有。

看一個簡單的例子:

function decoratorA() {}
  function decoratorB() {}
  class A {
    @decoratorA
    @decoratorB
    sayHello() {
    }
  }
複製代碼

編譯後

function decoratorA() {}
  function decoratorB() {}
  var A = (_class = function () {
    function A() {
      _classCallCheck(this, A);
    }
  
    _createClass(A, [{
      key: "sayHello",
      value: function sayHello() {}
    }]);
  
    return A;
  }(), (_applyDecoratedDescriptor(_class.prototype, "sayHello", [decoratorA, decoratorB], Object.getOwnPropertyDescriptor(_class.prototype, "sayHello"), _class.prototype)), _class);
複製代碼

與以前同樣sayHello定義爲A.prototype的屬性,然後執行_applyDecoratedDescriptor應用裝飾器decoratorA和decoratorB。

來看看_applyDecoratedDescriptor作了什麼:

function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
    var desc = {};
    Object['ke' + 'ys'](descriptor).forEach(function (key) {
      desc[key] = descriptor[key];
    });
    desc.enumerable = !!desc.enumerable;
    desc.configurable = !!desc.configurable;
  
    if ('value' in desc || desc.initializer) {
      desc.writable = true;
    }
    // 以上爲初始化一個數據屬性(initializer不屬於上文提到的6種屬性特性,第三節詳述其做用)
    
    // 本例中此處desc爲{ enumerable: false, configurable: true, writable: true, value: function sayHello() {} }
  
    // 此處的reverse代表裝飾器將按照距離sayHello由近及遠的順序執行,即先應用decoratorB再應用decoratorA
    desc = decorators.slice().reverse().reduce(function (desc, decorator) {
      // 裝飾器執行,可在裝飾器內部按需修改desc
      return decorator(target, property, desc) || desc;
    }, desc);
    // 本例中無initializer不執行此段代碼
    if (context && desc.initializer !== void 0) {
      desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
      desc.initializer = undefined;
    }
  
    if (desc.initializer === void 0) {
      // 將裝飾器處理後的desc定義到target即A.prototype上
      Object['define' + 'Property'](target, property, desc);
      desc = null;
    }
    // 返回null
    return desc;
  }
複製代碼

經過上述代碼分析咱們認識到:

一、裝飾器的執行發生在類建立後,此時並沒有實例

二、依照距離函數由近及遠執行

三、經過修改被裝飾方法的屬性特性,能夠實現咱們所需的功能(例如autobind-decorator實現綁定this)。

3、當class properties遇到decorator

decorator是es7歸入規範的js特性,而class properties目前是stage3階段(截止2018.11.23)的提案,尚未正式歸入ECMAScript。

一個屬性方法的特色是其建立在實例生成階段(構造函數中),而裝飾器的執行是在類建立後(實例生成前),這裏就發生了一個概念上的小衝突,裝飾器執行時屬性方法彷佛還沒建立。那裝飾器是如何裝飾一個屬性方法的呢,讓咱們到代碼中找出答案。

function decoratorA() {}
  class A {
    @decoratorA
    sayName = () => {
      console.log(this);
    }
  }
複製代碼

編譯後

function _initDefineProp(target, property, descriptor, context) {
    if (!descriptor) return;
    Object.defineProperty(target, property, {
      enumerable: descriptor.enumerable,
      configurable: descriptor.configurable,
      writable: descriptor.writable,
      value: descriptor.initializer ? descriptor.initializer.call(context) : void 0
    });
  }

  function decoratorA() {}
  var A = (_class = function A() {
    _classCallCheck(this, A);
  
    _initDefineProp(this, "sayName", _descriptor, this);
  }, (_descriptor = _applyDecoratedDescriptor(_class.prototype, "sayName", [decoratorA], {
    enumerable: true,
    initializer: function initializer() {
      var _this = this;
  
      return function () {
        console.log(_this);
      };
    }
  })), _class);
複製代碼

簡單的描述:

一、經過initializer來記錄並標識類的屬性方法

二、_applyDecoratedDescriptor建立返回了一個屬性的描述對象_descriptor

三、在構造函數中經過_initDefineProp將_descriptor定義到實例this上(屬性方法依然歸屬於實例,而不是class.prototype)

從_initDefineProp逆推,有2個關鍵點須要注意:

一、_applyDecoratedDescriptor需返回一個包含initializer的descriptor,以確保屬性的value是經過initializer調用初始化

二、裝飾器在處理descriptor時,返回的descriptor需包含initializer,而不是數據屬性或訪問器屬性格式的descriptor.

實現一個兼容普通類函數和類屬性函數的裝飾器(保留箭頭函數的this綁定)

需求:檢查登陸狀態的裝飾器,當裝飾器修飾的方法調用時,檢查登陸狀態。若已登陸則執行該方法,若未登陸,則執行一個指定方法提示需登陸。

// 登陸狀態
  let logined = true;
  function checkLoginStatus() {
    return new Promise((resolve) => {
      resolve(logined);
      // 每次返回登陸狀態後對登陸狀態取反
      logined = !logined;
    });
  }
  // 提示須要登陸
  function notice(target, tag) {
    console.log(tag, this === target, 'Need Login!');
  }
  // 檢查登陸狀態的裝飾器
  function checkLogin(notLoginCallback) {
    return function decorator(target, name, descriptor) {
      // 方法爲類屬性方法
      if (descriptor.initializer) {
        const replaceInitializer = function replaceInitializer() {
          const that = this;
          // 此處傳入了指向類實例的this
          const fn = descriptor.initializer.call(that);
          return function replaceFn(...args) {
            checkLoginStatus().then((login) => {
              if (login) {
                return fn.call(this, ...args);
              }
              return notLoginCallback.call(this, ...args);
            });
          };
        };
        return {
          enumerable: true,
          configurable: true,
          writable: true,
          initializer: replaceInitializer,
        };
      }
      // 普通的類方法
      const originFn = descriptor.value;
      const replaceFn = function replaceFn(...args) {
        const that = this;
        checkLoginStatus().then((login) => {
          if (login) {
            return originFn.call(that, ...args);
          }
          return notLoginCallback.call(that, ...args);
        });
      };
      return {
        enumerable: true,
        configurable: true,
        writable: true,
        value: replaceFn,
      };
    }
  }
  
  class A {
    constructor() {
      this.printA2 = this.printA2.bind(this);
    }
    printA1(target, tag) {
      console.log(tag, this === target);
    }
    @checkLogin(notice)
    printA2(target, tag) {
      console.log(tag, this === target);
    }
    printB1 = function(target, tag) {
      console.log(tag, this === target);
    }
    @checkLogin(notice)
    printB2 = function(target, tag) {
      console.log(tag, this === target);
    }
    printC1 = (target, tag) => {
      console.log(tag, this === target);
    }
    @checkLogin(notice)
    printC2 = (target, tag) => {
      console.log(tag, this === target);
    }
  }
  
  const a = new A();
  a.printA1(a, 1);        // 1 true
  (0, a.printA1)(a, 2);   // 2 false
  a.printA2(a, 3);        // 3 true 
  (0, a.printA2)(a, 4);   // 4 true 'Need Login!'
  a.printB1(a, 5);        // 5 true
  (0, a.printB1)(a, 6);   // 6 false
  a.printB2(a, 7);        // 7 true
  (0, a.printB2)(a, 8);   // 8 false 'Need Login!'
  a.printC1(a, 9);        // 9 true
  (0, a.printC1)(a, 10);  // 10 true
  a.printC2(a, 11);       // 11 true
  (0, a.printC2)(a, 12);  // 12 true 'Need Login!'
複製代碼

結果:

一、應用了checkLogin裝飾器的普通類方法printA2可使用bind綁定this指向a。

二、箭頭函數this均保持指向了實例a。

三、應用了checkLogin裝飾器的方法連續兩次調用輸出的登陸狀態相反,符合預期的裝飾器效果。

 

若是讀到這裏,但願你能有所收穫~

相關文章
相關標籤/搜索