天天看點

JS Decorator —— 裝飾器(裝飾模式)

一、簡介

官方定義

随着TypeScript和ES6裡引入了類,在一些場景下我們需要額外的特性來支援标注或修改類及其成員。 裝飾器(Decorators)為我們在類的聲明及成員上通過元程式設計文法添加标注提供了一種方式。

裝飾器是一種特殊類型的聲明,它能夠被附加到​類聲明,方法,通路符,屬性或參數​上。

注意  Javascript裡的裝飾器目前處在 提案階段,在未來的版本中可能會發生改變。

屬于一種​設計模式​,許多面向對象的語言都有這項功能。

特點

  • ​解耦​
可以在不侵入到原有代碼内部的情況下而通過标注的方式修改類代碼行為。(将輔助性的功能和核心功能分開)
  • ​優雅複用​, 使用​

    ​@expression​

    ​ 寫法。

二、舉個栗子

運作環境

目前想要在 JS 中使用裝飾器,需要通過 babel 将裝飾器文法轉義成 ES5 文法執行

package.json

{
  "scripts": {
    "build": "babel src -d build"
  },
  "dependencies": {
    "babel-cli": "6.26.0",
    "babel-plugin-transform-decorators-legacy": "1.3.5",
    "babel-preset-env": "1.7.0",
  }
}      

.babelrc

{
  "presets": ["env"],
  "plugins": ["transform-decorators-legacy"]
}      

建立 build 檔案夾和 src 檔案夾。

在src檔案夾中建立js 檔案,編寫完執行 ​

​npm run build​

​ 後,就可以直接運作對應 build 檔案夾下的轉義好的 js 檔案。

栗子1:類裝飾器

​主要将類的構造函數傳遞給裝飾器為參數,可以給這個類新增屬性/方法,也可以傳回一個新的類替換原類。​

如下,我們有一個 YaSuo 的類。

class YaSuo {
  constructor() {
    this.name = '亞索'
  }
}      

接着給這個類增加一個類裝飾器:

@testDecorator
class YaSuo {
  constructor() {
    this.name = '亞索'
  }
}
// 類裝飾器
function testDecorator() {
  console.log(arguments);
}      

我們看一下轉義後的代碼:

'use strict';

var _class;

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

// 将類的構造函數傳遞給裝飾器
var YaSuo = testDecorator(_class = function YaSuo() {
  // 防止構造函數被當做普通函數執行
  _classCallCheck(this, YaSuo);

  this.name = '亞索';
}) || _class;

function testDecorator() {
  console.log(arguments); // >>> [Arguments] { '0': [Function: YaSuo] }
}      

現在需要給 YaSuo 類​新增一些其他的屬性和方法​:

@attackDecorator
class YaSuo {
  constructor() {
    this.name = '亞索'
  }
}
// 向類新增屬性/方法
function attackDecorator(target) {
  target.prototype.attack = 50; // 初始攻擊力
  target.prototype.speak = () => {
    console.log('面對疾風吧!');
  }
}
const yaSuo = new YaSuo();
console.log(yaSuo.attack); // >>> 50
yaSuo.speak(); // >>> 面對疾風吧!      

可以看到已經成功給類新增了屬性和方法(其實是給原型添加)。

接着為了讓裝飾器複用,增加參數傳遞,采用高階函數方式。

@attackDecorator(50)
class YaSuo {
  constructor() {
    this.name = '亞索'
  }
}
@attackDecorator(70)
class GaiLun {
  constructor() {
    this.name = '蓋倫'
  }
}
// 高階函數可傳參
function attackDecorator(attack) {
  return function (target) {
    target.prototype.attack = attack
  }
}

const yaSuo = new YaSuo()
console.log(yaSuo.attack); // >>> 50

const gaiLun = new GaiLun()
console.log(gaiLun.attack); // >>> 70      

類裝飾器還可以覆寫原類:

@nickNameDecorator('哈撒給')
class YaSuo {
  constructor() {
    this.name = '亞索'
    this.nickName = '疾風劍豪'
  }
}

// 覆寫原類,修改 nickName 屬性
function nickNameDecorator(nickName) {
  return function (target) {
    return class extends target {
      constructor() {
        super();
        this.nickName = nickName
      }
    }
    // return 1 // 任意覆寫,甚至可以為 1
  }
}

const yaSuo = new YaSuo()
console.log(yaSuo.nickName); // >>> 哈撒給
console.log(yaSuo.name); // >>> 亞索      

栗子2:方法裝飾器

​與裝飾類不同,對類方法的裝飾本質是操作其描述符​

可以把此時的裝飾器了解成是 Object.defineProperty(obj, prop, descriptor) 的文法糖。

同樣有這麼一個 YaSuo 類,他有一個 ​

​init​

​​ 方法并且使用了 ​

​methodDecorator​

​ 裝飾器,看一下轉義後的代碼:

"use strict";
// 主要是将類中的方法拷貝到構造函數的原型上去
// 該函數也是一個自執行的函數,其傳回值是一個函數
var _createClass = function () {
    // 把props數組上每一個對象,通過Object.defineProperty方法,都定義到目标對象target上去
    function defineProperties(target, props) {
        for (var i = 0; i < props.length; i++) {
            //這裡要確定props[i]是一個對象,并且有key和value兩個鍵
            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) {
        // 如果傳入了原型屬性數組,就把屬性全部定義到Constructor的原型上去
        if (protoProps) defineProperties(Constructor.prototype, protoProps);
        // 如果傳入了靜态屬性數組,就把屬性全部定義到Constructor對象自身上去
        if (staticProps) defineProperties(Constructor, staticProps);
        return Constructor;
    };
}();

var _desc, _value, _class;

// 上面說過,防止構造函數被當做普通函數執行
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

// 此處target為類的原型對象,即方法Class.prototype
// ps:裝飾器的本意是要裝飾類的執行個體,但此時執行個體還未生成,是以隻能裝飾類的原型
// property 為要裝飾的方法(屬性名)
// decorators 為目前方法所有的裝飾器數組
// descriptor 為目前方法的描述對象
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;
  }
  // 周遊所有裝飾器,并調用(傳參分别為 原型對象, 方法名, 方法描述對象)
  desc = decorators.slice().reverse().reduce(function (desc, decorator) {
    // 裝飾器内可修改 desc
    return decorator(target, property, desc) || desc;
  }, desc);

  // void 0 === undefined
  // initializer 代表這是一個屬性而不是一個方法
  if (context && desc.initializer !== void 0) {
    desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
    desc.initializer = undefined;
  }
  // 最後調用 Object.definePropertype,将這個方法/屬性加到原型上
  if (desc.initializer === void 0) {
    Object['define' + 'Property'](target, property, desc);
    desc = null;
  }

  return desc;
}

var YaSuo = (_class = function () {
  function YaSuo() {
    var attack = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 50;

    _classCallCheck(this, YaSuo);

    this.init(attack);
  }

  _createClass(YaSuo, [{
    key: "init",
    value: function init(attack) {
      this.attack = attack;
    }
  }]);

  return YaSuo;
}(), (_applyDecoratedDescriptor(_class.prototype, "init", [methodDecorator], Object.getOwnPropertyDescriptor(_class.prototype, "init"), _class.prototype)), _class);


function methodDecorator() {
  console.log(arguments);
}      

接下來我們繼續裝飾亞索!如果亞索穿了皮膚,那麼攻擊力加10點:

class YaSuo {
  constructor(attack = 50, hasDress = false) {
    this.init(attack, hasDress)
  }

  @dressDecorator
  init(attack, hasDress) {
    this.attack = attack
    this.hasDress = hasDress
  }
}

function dressDecorator(target, name, descriptor) {
  const method = descriptor.value;
  descriptor.value = (attack, hasDress) => {
    let realAttack = attack; // 擷取初始攻擊力
    if (hasDress) {
      realAttack += 10
    }
    return method.apply(target, [realAttack, hasDress]); // 調用原方法,傳入新參數
  }
  return descriptor;
}

const yaSuo = new YaSuo(50, true)
console.log(yaSuo.attack); // >>> 60      

屬性裝飾器即可以修改原屬性值,原理和方法裝飾器基本一緻,不再贅述。

碼字不易,覺得有幫助的小夥伴點個贊支援下~

掃描上方二維碼關注我的訂閱号~

繼續閱讀