初识ES6-装饰器

avatar
web前端开发 @CVTE

参考资料

1、阮一峰大大的ECMAScript 入门

2、JavaScript Decorators: What They Are and When to Use Them

3、TS装饰器

4、JavaScript prototype

5、一起聊聊JavaScript装饰器(decorator)

前言

只要对于java等面向对象的语言有了解的同学肯定知道装饰器(Decorators)这个东西; 在JavaScript中主要是用来增强类的功能,只是目前它还只是处于提案阶段,当前浏览器或者node都是不支持类装饰器这个特性的,必须要借助babel去转译;

环境搭建

不知道有没有大佬可以看出来下面的代码逻辑是否成立呢?上代码!!

// 下面的逻辑成立吗?
console.log("收藏"==="学会"); // true?
console.log("读完"==="精通"); // true?

当然我先给个结论,我认为是成立的,毕竟我的才(菜)华 和 收藏文章的数量是不容许我撒谎的。 要想验证那就先搭建个环境去验证一下试试吧。

首先我这边借助的是imooc-es-cli脚手架去生成项目的。

// 我这边是mac举例,win的话可以自己去查一下
// 第一步
sudo npm i -g imooc-es-cli
// 安装完毕后进行第二步
imooc-es-cli init

image.png

目录结构如下:

image.png

// 第三步
npm i
// 到这一步已经可以运行了
npm run start
// 先写个有装饰器的demo试一下
function liar() {
  console.log('收藏===学会', "收藏"==="学会");
  console.log('读完===学会', "读完"==="学会");
}


@liar()
class MyClass {
}

new MyClass();

image.png 可以看出来它是不支持这个语法的,这个时候我们就需要babel来帮忙了。在这里我们要借助@babel/plugin-proposal-decorators去转译; 安装后在webpack去配置即可 image.png

再来运行: image.png 成功咯,至此我们的环境已经搭建完毕了,也可以看到我们之前的结论不成立,那接下来就好好动手学习吧,手动阴险.jpg。

注意:为了后面代码举例可以顺利运行我们还需要再添加一个@babel/plugin-proposal-class-properties 最终我们的webpack配置如下:

image.png

装饰器是什么?

定义:装饰器只是将一段代码包装成另一段代码的一种方式,字面意思就是“装饰”它。

翻译一下就是,装饰器实现了某个特定功能,然后可以通过它给其他不同功能的模块添加这个特定功能。

注意:此处以及后续文章中的“模块”指的就是 “类或者类的方法”

代码举例:英雄联盟中的装备中有一个“长剑”,它的作用就是增加英雄 10 点攻击力,在这里我们可以把每个英雄都看作是一个独立的模块;当英雄装备它时,相当于我们通过”长剑”这个装饰器去修饰这个模块,使得只要通过“长剑”去修饰的英雄都具有 +10 攻击力的属性。

代码实现:

// 长剑(装饰器)
function longSword(target) {
  // 攻击力 +10
  target.prototype.attack = 10;
}

@longSword
class GaiLunClass {
  /**
   * 盖伦自己的各种属性
   * ...
   */
}

const gaiLun = new GaiLunClass();
console.log('盖伦的额外攻击属性:', gaiLun.attack);

image.png

这样我们的大宝剑就拥有了“攻击+10”的属性了;同理,其余的装备效果我们也可以用这种方法去实现。

注:装饰器是一项实验性特性,在未来的版本中可能会发生改变。

本质

本质上它是具有某种功能的函数;

例如我们上面实现的@longSword,它实现功能就是为被修饰的模块提供 “攻击力 + 10”的属性。

写法

使用@作为标识符

@ + 装饰器名称 的方式。

使用

它可以放在类或者类方法的定义前面,并且可以为一个模块添加多个装饰器,在执行时会按照顺序去执行;

// 长剑
function longSword(target) {
  target.prototype.attack = 10;
}
// 布甲
function clothArmour(target) {
  target.prototype.defense = 15;
}
// 日志打印
function log(target, name, descriptor) {
  const oldValue = descriptor.value;

  descriptor.value = function() {
    console.log(`调用方法: ${name} \n`, `方法参数:${JSON.stringify(arguments)}`);
    return oldValue.apply(this, arguments);
  };

  return descriptor;
}

@longSword
@clothArmour
class GaiLunClass {
  constructor(){
    this.attack = 60;
  }

  @log
  getAllSword(attack) {
    return this.attack + attack;
  }
  /**
   * 盖伦自己的各种属性
   * ...
   */
}

const gaiLun = new GaiLunClass();
gaiLun.getAllSword(10);
console.log('盖伦的额外攻击属性:', gaiLun.attack);
console.log('盖伦的额外防御属性:', gaiLun.defense);

执行结果:

image.png

JS装饰器分类

1、类装饰器

类装饰器在类声明之前被声明(紧靠着类声明)。

代码举例:

/**
 * @param {*} target 类本身
 */
function longSword(target) {
  target.prototype.attack = 10;
}

@longSword
class GaiLunClass {
  /**
   * 盖伦自己的各种属性
   * ...
   */
}

转译后的代码如下:

image.png 上面代码中的@longSword其实就是一个装饰器,本质也就是实现一个对类进行处理的函数;参数target也就是GaiLunClass类,它为GaiLunClass类添加了实例属性attack

  • 为类添加静态属性和实例属性的方法。
target.prototype.attack = 10; // 添加的是实例属性
target.attack = 10; // 这样添加的是静态属性,通过 GaiLunClass.attack 直接可以调用

tips:

在js中,prototype对象是实现面向对象的一个重要机制。每个函数就是一个对象,函数对象都有一个子对象prototype对象,类是以函数的形式来定义的。prototype表示该函数的原型,也表示一个类的成员的集合。在通过new创建一个类的实例对象的时候,prototype对象的成员都成为实例化对象的成员。
1、该对象被类所引用,只有函数对象才可引用;
2、在new实例化后,其成员被实例化,实例对象方可调用。

- 可接受参数的装饰器-装饰器工厂

装饰器工厂是由函数包装后的装饰器组成,通过参数是可以达到直接改变装饰器行为的效果。

代码举例:

function weak(num) {
  return function longSword(target) {
    target.prototype.attack = 10 * num;
  };
}

@weak(0.5)
class GaiLunClass { }

const gaiLun = new GaiLunClass();
console.log('长剑50%虚弱后的攻击力:', gaiLun.attack);

执行结果:

image.png

通过上面代码我们实现了对longSword“虚弱”状态的适配,同理我们可以通过参数去控制它的其他各种状态,例如暴击等,很方便的通过参数就可以去改变装饰器行为。

通过可接受参数的装饰器也可以实现将属性或方法添加到类中。

下面是将方法通过装饰器添加到类中的代码实现(添加属性同理不做举例)

function addAttribute(method) {
  return function longSword(target) {
    target.prototype.method = method;
  };
}

@addAttribute(()=>{
  console.log('我是新方法中的输出');
  return '添加了个新方法'
})
class GaiLunClass { }

const gaiLun = new GaiLunClass();
console.log(gaiLun.method());

输出结果:

image.png

重载构造函数


代码举例:

function loadConstructor(constructor) {
  return class extends constructor {
    attack = 100;
    defense = 50;
    attackSpeed = 0.8;
    des = '战斗力无敌';
  }
}

@loadConstructor
class GaiLunClass {
  constructor(){
    this.attack = 10;
    this.defense = 10;
    this.attackSpeed = 0.1;
    this.des = '战斗力为10的渣渣!';
  }
}
const gaiLun = new GaiLunClass();
console.log(gaiLun);

输出:

image.png 可以看到我们成功通过装饰器@loadConstructor重载了GaiLunClass的构造函数

2、方法装饰器

方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的属性描述符上,可以用来监视,修改或者替换方法定义。

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符

代码举例:

function enumerable(value) {
  return function (target, propertyKey, descriptor) {
    console.log('target:', target);
    console.log('propertyKey:', propertyKey);
    console.log('descriptor:', descriptor);
    console.log('----------我是分割线---------');

    descriptor.enumerable = value;
  };
}

class GaiLunClass {
  constructor(initAttribute) {
    this.baseAttribute = initAttribute;
  }

  // getSpecialAttribute设置为可枚举属性(默认就是可枚举,此处是为了和不可枚举做比较)
  @enumerable(true)
  getSpecialAttribute() {}

  // getSpecialAttribute设置为不可枚举属性
  @enumerable(false)
  getPrivateAttribute() {}
}
const gaiLun = new GaiLunClass('基础属性');

for (let item in gaiLun) {
  console.log(`盖伦的可见属性为:${item}`);
}

输出:

image.png 通过控制台的输出我们可以看到装饰器的三个参数分别对应之前我们上面所提到的1、2、3点;

基于它们我们也验证了经过装饰器(@enumerable(true))修饰后的方法(getPrivateAttribute())是不能枚举出来的。

通过方法装饰器替换原有方法

方法装饰器可以返回一个新函数,取代原来的方法,也可以不返回值,表示依然使用原来的方法。下面是代码举例:

function replaceFun() {
  return function () {
  // 返回一个新的方法去替换原有方法
    return {
      value: function () {
        return '我是新方法,被调用了';
      },
    };
  };
}

class GaiLunClass {
  @replaceFun()
  getBaseFun() {
  // 原有方法的返回值
    return '我是旧有方法'
  }
}

const gaiLun = new GaiLunClass();

console.log(gaiLun.getBaseFun());

输出:

image.png

可以看到原本的方法被替换为了新函数,取代了原来的getPrivateAttribute()方法。

存取器装饰器(访问器装饰器)

还有一个特殊的方法装饰器-存取器装饰器(访问器装饰器)它装饰的就是我们的setget方法,使用方法其实和方法装饰器是一样的,代码如下:

function replaceFun() {
  return function () {
    // 返回一个新的方法去替换原有方法
    return {
      value: function () {
        return '我是新方法,被调用了';
      },
    };
  };
}

class GaiLunClass {
  @replaceFun()
  get getAttr() {
    return 'get原方法返回值';
  }
  set setAttr(param) {}
}

const gaiLun = new GaiLunClass();
console.log(gaiLun.getAttr()); // 我是新方法,被调用了

3、属性装饰器

它的声明在属性声明之前; 它的参数有两个

// 第一个参数表示类的构造函数或者类的原型对象
// 第二个参数表示属性名

用户可以选择让装饰器返回一个初始化函数,当该属性被赋值时,这个初始化函数会自动运行,它会收到属性的初始值,然后返回一个新的初始值。属性装饰器也可以不返回任何值。

下面举例说一下通过返回值修改属性值的情况, 如下返回了一个对象,并且在对象中修改了它的值,所以最终这个值也被修改了(和上面的方法装饰器类似);

代码举例:

function loadConstructor(target, name) {
  console.log('target', target);
  console.log('name', name);
  
  // 返回一个新的值去修改
  return {value: '大宝剑'}; 
}

class GaiLunClass {
  @loadConstructor
  name = '盖伦'; // 初始化赋值
}

const gaiLun = new GaiLunClass();

console.log(gaiLun.name);

输出:

image.png

小结

1、JavaScript装饰器是个实验性的特性并且可能在以后的版本发生改变。

2、在学习时也发现了一个值得我们注意的问题,也就是js装饰器不能用于函数(原因是存在函数提升);这个问题阮一峰大大有写过一篇文章,大家可以直接移步去看。

3、目前不能在浏览器或Node环境中使用类装饰器,因为它们需要转译器支持。

以上内容便是本次分享,更多详细的分析可以去看阮一峰大大的文章,若有问题希望大家不吝赐教,谢谢🙏。