天天看点

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      

属性装饰器即可以修改原属性值,原理和方法装饰器基本一致,不再赘述。

码字不易,觉得有帮助的小伙伴点个赞支持下~

扫描上方二维码关注我的订阅号~

继续阅读