JavaScript装饰器的实现原理详解

 更新时间:2022年10月13日 16:12:19   作者:青天诀  
最近在使用TS+Vue的开发模式,发现项目中大量使用了装饰器,看得我手足无措,今天特意研究一下实现原理,方便自己理解这块知识点,有需要的小伙伴也可以参考一下

最近在使用TS+Vue的开发模式,发现项目中大量使用了装饰器,看得我手足无措,今天特意研究一下实现原理,方便自己理解这块知识点。

装饰器的常见作用

  • 装饰一个类的属性
  • 装饰一个类

装饰器只能针对类和类的属性,不能直接作用于函数,因为存在函数提升。

下面我们针对这两种情况进行举例阐述。

装饰类的属性

function readonly(target, name, descriptor) {
    discriptor.writable = false;
    return discriptor;
}
class Cat {
    @readonly
    say() {
        console.log("meow ~");
    }
}
var kitty = new Cat();
kitty.say = function() {
    console.log("woof !");
}
kitty.say()    // meow ~

ES6中的类实际上就是一个语法糖,本质上是构造函数,类的属性的定义使用的是 Object.defineProperty() 类的代码可以改写为下面代码:

class Cat {
    say() {
        console.log("meow ~");
    }
}

// 上面等价于下面代码

function Cat() {}

Object.defineProperty(Cat.prototype, "say", {
    value: function() { console.log("meow ~"); },
    enumerable: false,
    configurable: true,
    writable: true
});

say方法前面加入@readonly后,整个代码等价于下面代码:

function readonly(target, name, descriptor) {
    discriptor.writable = false;
    return discriptor;
}

function Cat() {}

let descriptor = {
    value: function() { console.log("meow ~"); },
    enumerable: false,
    configurable: true,
    writable: true
};

descriptor = readonly(Cat.prototype, 'say', descriptor) || descriptor;

Object.defineProperty(Cat.prototype, "say", descriptor);

装饰类属性的大致实现原理,就是上面的语法糖实现过程。

这里注意下装饰器的用法,是在属性前面加入@readonly,不是@readonly()。如果是后一种写法的话,装饰器函数需要返回一个函数来表示装饰器。这种写法主要是通过传参来扩展装饰器函数的功能,使装饰器函数复用性更强。

如通过传参决定是否设置只读,代码如下:

function readonly(flag) {
    return function (target, name, descriptor) {
        if (flag) {
            discriptor.writable = false;
        } else {
            discriptor.writable = true;
        }
        return discriptor;
    }
}
class Cat {
    @readonly(true)
    say() {
        console.log("meow ~");
    }
}
var kitty = new Cat();
kitty.say = function() {
    console.log("woof !");
}
kitty.say()

装饰类

装饰一个类的时候类本身本质上是一个函数,没有descriptor,target是这个函数本身。

function isAnimal(target) {
    target.isAnimal = true;
  	return target;
}
@isAnimal
class Cat {
    ...
}
console.log(Cat.isAnimal);    // true

也就是说,上面的@isAnimal其实就是做了下面这件事

Cat = isAnimal(function Cat() { ... });

注意

作用在方法上的 decorator 接收的第一个参数(target )是类的 prototype;如果把一个 decorator 作用到类上,则它的第一个参数 target 是 类本身。

装饰器执行的时间是在属性定义的时候,也就是被装饰的属性在定义后就是已经被装饰器处理过的不一样的属性了。

下面举例看下装饰器的执行时间:

function log(message) {
    return function() {
        console.log(message);
    }
}
console.log('before class');
@log('class Bar')
class Bar {
    @log('class method bar');
    bar() {}
    @log('class getter alice');
    get alice() {}
    @log('class property bob');
    bob = 1;
}
console.log('after class');
let bar = {
    @log('object method bar')
    bar() {}
};

输出结果:

before class
class method bar
class getter alice
class property bob
class Bar
after class
object method bar

可以看出装饰器在类和类的属性定义的时候就对它们进行了"装饰"。

实例应用

我们的示例场景是这样的

  • 首先创建一个普通的Man类,它的抵御值 2,攻击力为 3,血量为 3;
  • 然后我们让其带上钢铁侠的盔甲,这样他的抵御力增加 100,变成 102;
  • 让其带上光束手套,攻击力增加 50,变成 53;
  • 最后让他增加“飞行”能力

1. 创建 Man 类:

class Man{
  constructor(def = 2,atk = 3,hp = 3){
    this.init(def,atk,hp);
  }

  init(def,atk,hp){
    this.def = def; // 防御值
    this.atk = atk;  // 攻击力
    this.hp = hp;  // 血量
  }
  toString(){
    return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}`;
  }
}

var tony = new Man();

console.log(`当前状态 ===> ${tony}`); 

// 输出:当前状态 ===> 防御力:2,攻击力:3,血量:3

2. 装饰器为钢铁侠装配盔甲:

function decorateArmour(target, key, descriptor) {
  const method = descriptor.value;
  let moreDef = 100;
  let ret;
  descriptor.value = (...args)=>{
    args[0] += moreDef;
    ret = method.apply(target, args);
    return ret;
  }
  return descriptor;
}

class Man{
  constructor(def = 2,atk = 3,hp = 3){
    this.init(def,atk,hp);
  }

  @decorateArmour
  init(def,atk,hp){
    this.def = def; // 防御值
    this.atk = atk;  // 攻击力
    this.hp = hp;  // 血量
  }
  toString(){
    return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}`;
  }
}

var tony = new Man();

console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:102,攻击力:3,血量:3

3. 装饰器增加光束手套

function decorateLight(target, key, descriptor) {
  const method = descriptor.value;
  let moreAtk = 50;
  let ret;
  descriptor.value = (...args)=>{
    args[1] += moreAtk;
    ret = method.apply(target, args);
    return ret;
  }
  return descriptor;
}

class Man{
  constructor(def = 2,atk = 3,hp = 3){
    this.init(def,atk,hp);
  }

  @decorateArmour
  @decorateLight
  init(def,atk,hp){
    this.def = def; // 防御值
    this.atk = atk;  // 攻击力
    this.hp = hp;  // 血量
  }
...
}
var tony = new Man();
console.log(`当前状态 ===> ${tony}`);
//输出:当前状态 ===> 防御力:102,攻击力:53,血量:3

在这里你就能看出装饰模式的优势了,它可以对某个方法进行叠加使用,对原类的侵入性非常小,只是增加一行@decorateLight而已,可以方便地增删;(同时还可以复用)

4. 增加飞行能力

function addFly(canFly){
  return function(target){
    target.prototype.canFly = canFly;
    let extra = canFly ? '(技能加成:飞行能力)' : '';
    let method = target.prototype.toString;
    target.prototype.toString = (...args)=>{
      return method.apply(target.prototype,args) + extra;
    }
    return target;
  }
}

@addFly(true)
class Man{
  constructor(def = 2,atk = 3,hp = 3){
    this.init(def,atk,hp);
  }

  @decorateArmour
  @decorateLight
  init(def,atk,hp){
    this.def = def; // 防御值
    this.atk = atk;  // 攻击力
    this.hp = hp;  // 血量
  }
  ...
}
...

console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:102,攻击力:53,血量:3(技能加成:飞行能力)

到此这篇关于JavaScript装饰器的实现原理详解的文章就介绍到这了,更多相关JavaScript装饰器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Fetch超时设置与终止请求详解

    Fetch超时设置与终止请求详解

    这篇文章主要给大家介绍了关于Fetch超时设置与终止请求的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Fetch具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-05-05
  • JavaScript模拟数组合并concat

    JavaScript模拟数组合并concat

    这篇文章主要介绍了JavaScript模拟数组合并concat的相关资料,需要的朋友可以参考下
    2016-03-03
  • 详解如何愉快的在微信小程序中使用SVG图标

    详解如何愉快的在微信小程序中使用SVG图标

    这篇文章主要介绍了详解如何愉快的在微信小程序中使用SVG图标,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-03-03
  • 详解webpack性能优化——DLL

    详解webpack性能优化——DLL

    本篇文章主要介绍了详解webpack性能优化——DLL,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • 基于replaceChild制作简单的吞噬特效

    基于replaceChild制作简单的吞噬特效

    本篇文章通过js和css结合实现简单的吞噬效果,代码简单易懂,需要的朋友可以参考下本文
    2015-09-09
  • JavaScript把局部变量变成全局变量的方法

    JavaScript把局部变量变成全局变量的方法

    这篇文章主要介绍了JavaScript把局部变量变成全局变量的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-05-05
  • 微信小程序select下拉框实现效果

    微信小程序select下拉框实现效果

    这篇文章主要介绍了微信小程序select下拉框实现效果,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-05-05
  • Typescript 实现函数重载的方式

    Typescript 实现函数重载的方式

    函数重载简单点说就是,同一个函数的不同实现方式,根据传入的参数的类型或者长度,决定最终调用哪一个实现,本文给大家介绍了Typescript 实现函数重载的方式,需要的朋友可以参考下
    2024-05-05
  • JavaScript常用事件介绍

    JavaScript常用事件介绍

    今天小编就为大家分享一篇关于JavaScript常用事件介绍,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-01-01
  • JS 中实现一个串型异步函数队列

    JS 中实现一个串型异步函数队列

    这篇文章主要介绍了JS 中实现一个串型异步函数队列,文章通过async/await 串型请求展开详情,具有一定的参考价值,需要的朋友可以参考一下
    2022-07-07

最新评论