Javascript装饰器原理

更新日期: 2021-01-04阅读: 1.2k标签: 模式

一个以@开头的描述性词语。英语的decorator动词是decorate,装饰的意思。其中词根dek(dec发音)原始印欧语系中意思是“接受”。即,原来的某个事物接受一些新东西(而变得更好)。从另外一个角度描述,装饰器主要是在被装饰对象的外部起作用,而非入侵其内部发生什么改变。装饰器模式同时也是一种开发模式,其地位虽然弱于MVC、IoC等,但不失为一种优秀的模式。

JavaScript的装饰器可能是借鉴自Python也或许是Java。较为明显的不同的是大部分语言的装饰器必须是一行行分开,而JS的装饰器可以在一行中。


装饰器存在的意义

会偷懒的程序员,才是优秀的程序员。

举个例子:我拿着员工卡进入公司总部大楼。因为每个员工所属的部门、级别不同,并不能进入大楼的任何房间。每个房间都有一扇门;那么,公司需要安排每个办公室里至少一个人关于验证来访者的工作:

  1. 先登记来访者
  2. 验证是否有权限进入,如果没有则要求其离开
  3. 记录其离开时间

还有一个选择方式,就是安装电子门锁,门锁只是将员工卡的信息传输给机房,由特定的程序验证。

前者暂且称之为笨模式,代码如下:

function A101(who){
  record(who,new Date(),'enter');
  if (!permission(who)) {
    record(who,new Date(),'no permission')
    return void;
  }
  // 继续执行
  doSomeWork();
  record(who,new Date(),'leave')
}

function A102(who){
record(who,new Date(),'enter');
  if (!permission(who)) {
    record(who,new Date(),'no permission')
    return void;
  }
  // 继续执行
  doSomeWork();
  record(who,new Date(),'leave')
}

// ... 

有经验的大家肯定第一时间想到了,把那些重复语句封装为一个方法,并统一调用。是的,这样可以解决大部分问题,但是还不够“优雅”。同时还有另外一个问题,如果“房间”特别多,又或者只有大楼奇数号房间要验证偶数不验证,那岂不是很“变态”?如果使用装饰器模式来做,代码会如下面这样的:

@verify(who)
class Building {
  @verify(who)
  A101(){/*...*/}
  @verify(who)
  A102(){/*...*/}
  //...
}

verify是验证的装饰器,而其本质就是一组函数


JavaScript装饰器

正如先前的那个例子,装饰器其实本身就是一个函数,它在执行被装饰的对象之前先被执行。

在JavaScript中,装饰器的类型有:

  • 存取方法(属性的get和set)
  • 字段
  • 方法
  • 参数

由于目前装饰器概念还处于提案阶段,不是一个正式可用的JS功能,所以想要使用这个功能,不得不借助翻译器工具,例如babel工具或者TypeScript编译JS代码转后才能被执行。我们需要先搭建运行环境,配置一些参数。(以下过程,假设已经正确安装了NodeJS开发环境以及包管理工具)

cd project && npm init
npm i -D @babel/cli @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/preset-env babel-plugin-parameter-decorator

创建一个.babelrc配置文件,如下:

{
  "presets": ["@babel/preset-env"],
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": true }],
    "babel-plugin-parameter-decorator"
  ]
}

利用下面的转换命令,我们可以得到ES5的转换程序:

npx babel source.js --out-file target.js


类装饰器

创建一个使用装饰器的JS程序decorate-class.js

@classDecorator
class Building {
  constructor() {
    this.name = "company";
  }
}

const building = new Building();

function classDecorator(target) {
  console.log("target", target);
}

以上是最最简单的装饰器程序,我们利用babel将其“翻译”为ES5的程序,然后再美化一下后得到如下程序

"use strict";

var _class;

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

var Building =
  classDecorator(
    (_class = function Building() {
      _classCallCheck(this, Building);

      this.name = "company";
    })
  ) || _class;

var building = new Building();

function classDecorator(target) {
  console.log("target", target);
}

第12行就是在类生成过程中,调用函数形态的装饰器,并将构造函数(类本身)送入其中。同样揭示了装饰器的第一个参数是类的构造函数的由来。


方法 (method)装饰器

稍微修改一下代码,依旧是尽量保持最简单:

class Building {
  constructor() {
    this.name = "company";
  }
  @methodDecorator
  openDoor() {
    console.log("The door being open");
  }
}

const building = new Building();

function methodDecorator(target, property, descriptor) {
  console.log("target", target);
  if (property) {
    console.log("property", property);
  }
  if (descriptor) {
    console.log("descriptor", descriptor);
  }
  console.log("=====end of decorator=========");
}

然后转换代码,可以发现,这次代码量突然增大了很多。排除掉_classCallCheck、_defineProperties和_createClass三个函数,关注_applyDecoratedDescriptor函数:

function _applyDecoratedDescriptor(
  target,
  property,
  decorators,
  descriptor,
  context
) {
  var desc = {};
  Object.keys(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) {
      return decorator(target, property, desc) || desc;
    }, desc);
  if (context && desc.initializer !== void 0) {
    desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
    desc.initializer = undefined;
  }
  if (desc.initializer === void 0) {
    Object.defineProperty(target, property, desc);
    desc = null;
  }
  return desc;
}

它在生成构造函数之后,执行了这个函数,特别注意,这个装饰器函数是以数组形式的参数传递的。然后到上述代码的17~22行,将装饰器逐个应用,其中对装饰器的调用就在第21行。它发送了3个参数,target指类本身。property指方法名(或者属性名),desc是可能被先前装饰器被处理过的descriptor,如果是第一次循环或只有一个装饰器,那么就是方法或属性本身的descriptor。


存取器(accessor)装饰

JS关于类的定义中,支持get和set关键字针对设置某个字段的读写操作逻辑,装饰器也同样支持这类方法的操作。

class Building {
  constructor() {
    this.name = "company";
  }
  @propertyDecorator
  get roomNumber() {
    return this._roomNumber;
  }

  _roomNumber = "";
  openDoor() {
    console.log("The door being open");
  }
}

有心的读者可能已经发现了,存取器装饰的代码与上面的方法装饰代码非常接近。关于属性 get和set方法,其本身也是一种方法的特殊形态。所以他们之间的代码就非常接近了。


属性装饰器

继续修改源代码:

class Building {
  constructor() {
    this.name = "company";
  }
  @propertyDecorator
  roomNumber = "";
}

const building = new Building();

function propertyDecorator(target, property, descriptor) {
  console.log("target", target);
  if (property) {
    console.log("property", property);
  }
  if (descriptor) {
    console.log("descriptor", descriptor);
  }
  console.log("=====end of decorator=========");
}

转换后的代码,还是与上述属性、存取器的代码非常接近。但除了_applyDecoratedDescriptor外,还多了一个_initializerDefineProperty函数。这个函数在生成构造函数时,将声明的各种字段绑定给对象。


参数装饰器

参数装饰器的使用位置较之前集中装饰器略有不同,它被使用在行内。

class Building {
  constructor() {
    this.name = "company";
  }
  openDoor(@parameterDecorator num, @parameterDecorator zoz) {
    console.log(`${num} door being open`);
  }
}

const building = new Building();

function parameterDecorator(target, property, key) {
  console.log("target", target);
  if (property) {
    console.log("property", property);
  }
  if (key) {
    console.log("key", key);
  }
  console.log("=====end of decorator=========");
}

转换后的代码区别就比较明显了,babel并没有对其生成一个特定的函数对其进行特有的操作,而只在创建完类(构造函数)以及相关属性、方法后直接调用了开发者自己编写的装饰器函数:

var Building = /*#__PURE__*/function () {
  function Building() {
    _classCallCheck(this, Building);

    this.name = "company";
  }

  _createClass(Building, [{
    key: "openDoor",
    value: function openDoor(num, zoz) {
      console.log("".concat(num, " door being open"));
    }
  }]);

  parameterDecorator(Building.prototype, "openDoor", 1);
  parameterDecorator(Building.prototype, "openDoor", 0);
  return Building;
}();


装饰器应用

使用参数——闭包

以上所有的案例,装饰器本身均没有使用任何参数。然实际应用中,经常会需要有特定的参数需求。我们再回到一开头的例子中verify(who),其中需要传入一个身份变量。哪又怎么做?我们少许改变一下类装饰器的代码:

const who = "Django";
@classDecorator(who)
class Building {
  constructor() {
    this.name = "company";
  }
}

转换后得到

// ...
var who = "Django";
var Building =
  ((_dec = classDecorator(who)),
  _dec(
    (_class = function Building() {
      _classCallCheck(this, Building);

      this.name = "company";
    })
  ) || _class);
// ...

请注意第4第5行,它先执行了装饰器,然后再用返回值将类(构造函数)送入。相对应的,我们就应该将构造函数写成下面这样:

function classDecorator(people) {
  console.log(`hi~ ${people}`);
  return function (target) {
    console.log("target", target);
  };
}

同样的,方法、存取器、属性和参数装饰器均是如此。


装饰器包裹方法

到此,我们已经可以将装饰器参数与目标对象结合起来,进行一些逻辑类的操作。那么再回到文章的开头的例子中:需求中要先验证来访者权限,然后记录,最后在来访者离开时再做一次记录。此时需要监管对象方法被调用的整个过程。

请大家留意那个方法装饰器的descriptor,我们可以利用这个对象来“重写”这个方法。

class Building {
  constructor() {
    this.name = "company";
  }

  @methodDecorator("Gate")
  openDoor(firstName, lastName) {
    return `The door will be open, when ${firstName} ${lastName} is walking into the ${this.name}.`;
  }
}

let building = new Building();
console.log(building.openDoor("django", "xiang"));

function methodDecorator(door) {
  return function (target, property, descriptor) {
    let fn = descriptor.value;
    descriptor.value = function (...args) {
      let [firstName, lastName] = args;
      console.log(`log: ${firstName}, who are comming.`);
      // verify(firstName,lastName)
      let result = Reflect.apply(fn, this, [firstName, lastName]);
      console.log(`log: ${result}`);
      console.log(`log: ${firstName}, who are leaving.`);
      return result;
    };
    return descriptor;
  };
}

代码第17行,将原方法暂存;18行定义一个新的方法,20~25行,记录、验证和记录离开的动作。

log: Django, who are comming.
log: The door will be open, when Django Xiang is walking in to the company.
log: Django, who are leaving.
The door will be open, when Django Xiang is walking in to the company


装饰顺序

通过阅读转换后的代码,我们知道装饰器工作的时刻是在类被实例化之前,在生成之中完成装饰函数的动作。那么,如果不同类型的多个装饰器同时作用,其过程是怎样的?我们将先前的案例全部整合到一起看看:

const who = "Django";
@classDecorator(who)
class Building {
  constructor() {
    this.name = "company";
  }

  @propertyDecorator
  roomNumber = "";

  @methodDecorator
  openDoor(@parameterDecorator num) {
    console.log(`${num} door being open`);
  }

  @accessorDecorator
  get roomNumber() {
    return this._roomNumber;
  }
}

const building = new Building();

function classDecorator(people) {
  console.log(`class decorator`);
  return function (target) {
    console.log("target", target);
  };
}

function methodDecorator(target, property, descriptor) {
  console.log("method decorator");
}

function accessorDecorator(target, property, descriptor) {
  console.log("accessor decorator");
}

function propertyDecorator(target, property, descriptor) {
  console.log("property decoator");
}

function parameterDecorator(target, property, key) {
  console.log("parameter decorator");
}
  1. class decorator
  2. parameter decorator
  3. property decoator
  4. method decorator
  5. accessor decorator

还可以通过阅读转换后的源代码得到执行顺序:

  1. 类装饰器(在最外层)
  2. 参数装饰器(在生成构造函数最里层)
  3. 按照出现的先后顺序的:属性、方法和存取器


总结

装饰器是一种优雅的开发模式,极大的方便了开发者编码过程,同时提升了代码的可读性。我们在使用装饰器开发时,还是非常有必要了解其运行机理。


链接: https://www.fly63.com/article/detial/10053

js设计模式之单例模式,javascript如何将一个对象设计成单例

单例模式是我们开发中一个非常典型的设计模式,js单例模式要保证全局只生成唯一实例,提供一个单一的访问入口,单例的对象不同于静态类,我们可以延迟单例对象的初始化,通常这种情况发生在我们需要等待加载创建单例的依赖。

前端设计模式:从js原始模式开始,去理解Js工厂模式和构造函数模式

工厂模式下的对象我们不能识别它的类型,由于typeof返回的都是object类型,不知道它是那个对象的实例。另外每次造人时都要创建一个独立的person的对象,会造成代码臃肿的情况。

JavaScript设计模式_js实现建造者模式

建造者模式:是将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象

html和xhtml,DOCTYPE和DTD,标准模式和兼容模式

主要涉及知识点: HTML与XHTML,HTML与XHTML的区别,DOCTYPE与DTD的概念,DTD的分类以及DOCTYPE的声明方式,标准模式(Standard Mode)和兼容模式(Quircks Mode),标准模式(Standard Mode)和兼容模式(Quircks Mode)的区别

前端四种设计模式_JS常见的4种模式

JavaScript中常见的四种设计模式:工厂模式、单例模式、沙箱模式、发布者订阅模式

javascript 策略模式_理解js中的策略模式

javascript 策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。 策略模式利用组合,委托等技术和思想,有效的避免很多if条件语句,策略模式提供了开放-封闭原则,使代码更容易理解和扩展, 策略模式中的代码可以复用。

javascript观察者模式_深入理解js中的观察者模式

javascript观察者模式又叫发布订阅模式,观察者模式的好处:js观察者模式支持简单的广播通信,自动通知所有已经订阅过的对象。存在一种动态关联,增加了灵活性。目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用。

Vue中如何使用方法、计算属性或观察者

熟悉 Vue 的都知道 方法methods、计算属性computed、观察者watcher 在 Vue 中有着非常重要的作用,有些时候我们实现一个功能的时候可以使用它们中任何一个都是可以的

我最喜欢的 JavaScript 设计模式

我觉得聊一下我爱用的 JavaScript 设计模式应该很有意思。我是一步一步才定下来的,经过一段时间从各种来源吸收和适应直到达到一个能提供我所需的灵活性的模式。让我给你看看概览,然后再来看它是怎么形成的

Flutter 设计模式 - 简单工厂

在围绕设计模式的话题中,工厂这个词频繁出现,从 简单工厂 模式到 工厂方法 模式,再到 抽象工厂 模式。工厂名称含义是制造产品的工业场所,应用在面向对象中,顺理成章地成为了比较典型的创建型模式

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!