Jansiel Notes

JavaScript装饰器(decorator)

简介

装饰器其实就是一个函数,用于描述类、函数、属性、参数,通过@函数名的方式进行调用,它可以放在类和属性的前面。例如:

1// 创建一个装饰器函数
2function log(target) {
3  console.log(\'我是log装饰器函数\')
4}
5// 装饰器调用
6@log
7class App {}

装饰器只是一种语法糖,只是调用方式不一样而已,转换后的代码其实本质上还是下面这样:

1function log(target) {
2  console.log(\'我是log装饰器函数\')
3}
4var _class = class App {};
5let App = log(_class) || _class;
6

装饰是前置执行,例如:类装饰器,会在类被使用前进行调用;函数装饰器会在函数被调用前执行。

环境搭建

因为装饰器decorator是ES7中的一个提案,目前处于stage-2阶段,所以不管是node还是浏览器,现在都没有直接支持这个语法,我们要想使用该语法,就必须要通过babel将它进行一个编译转换,所以我们需要搭建一个babel编译环境。

安装babel相关包

1npm i @babel/cli @babel/core @babel/plugin-proposal-decorators -D

在项目根目录下创建.babelrc

{
  \"plugins\": [
    [
      \"@babel/plugin-proposal-decorators\",
      {
        \"legacy\": true
      }
    ]
  ]
}

基础环境搭建好以后,接下来我们就可以尽情的使用装饰器了!
提示:如果你不想在本地搭建babel环境,也可以使用babel-repl在线转换工具实时将装饰器转换成ES5的语法

类装饰器

  • 类装饰器:顾名思义就是用来装饰整个类的,可以用来修改类的一些行为。
  • 参数:类装饰器只有一个参数target,就是被装饰类的本身。
  • 返回值:只能是一个类(被装饰的类target或者一个新的类),也可以是一个为false的值:undefined、null、false、0,或者不写返回值。

简单类装饰器

源码:

1// src/demo01.js
2// 类装饰器:只要target一个参数,而且target代表被装饰的类本身
3function withRouter(target) {
4  console.log(\'withRouter:\', target);
5  // 注意:返回值可以不写,但是不能随便写返回值,即使要写也只能是target,或者是一个类,如果返回是一个对象,会导致装饰后的类无法被new
6  // 类装饰器的返回值只能以下3种:target(类本身)、新的类、(null、undefined、false、0) 返回为false值,编译后的代码装饰器也会处理为target
7}
8@withRouter
9class App {}

编译 & 执行:

1// 使用babel编译,将代码编译输出到dist文件夹
2npx babel src/demo01.js -d dist   
3// 执行编译后的代码
4node dist/demo01.js
5// 执行输出
6withRouter: [class App]

编译后的代码:

1// dist/demo01.js
2var _class;
3// src/demo01.js
4// 类装饰器
5function withRouter(target) {
6  console.log(\'withRouter:\', target);
7}
8let App = withRouter(_class = class App {}) || _class;

上面babel编译后的代码读起来可能有点绕,为了方便理解我整理一下:

function withRouter(target) {
  console.log(\'withRouter:\', target);
}

var _class = class App {};
let App = withRouter(_class) || _class; // 如果withRouter装饰器有返回值,直接将装饰器的返回值给App

可以看到其实类装饰器就是一个函数,接收一个类作为参数,装饰器函数内部的target参数就是被装饰的类本身,我们可以在装饰器函数内部对这个类进行一些修改,比如:添加静态属性,给原型添加函数等等。
装饰器其实就是一种语法糖而已,本质上还是一个函数,只是通过@函数名这方式调用而已,跟函数名()调用方式没有任何区别。

带参数的类装饰器

带参数的装饰器,需要在外面再套一层接受参数的函数,像下面这样:

源码

 1// src/demo02.js
 2// 带参数的类装饰器
 3function withRouter(params) { // 接收参数的函数
 4  console.log(\'withRouter.params:\', params);
 5  return function(target) { // 装饰器函数
 6    // 给被装饰的类添加一个静态属性
 7    target.params = params;
 8    // 也可以给原型添加函数和属性,例如:target.prottotype.name = \'Jameswain\';
 9      console.log(\'withRouter.target:\', target);
10  }
11}
12@withRouter(\'Jameswain\')
13class App {
14}
15console.log(App)

编译 & 执行:

1// 编译
2npx babel src/demo02.js -d dist   
3// 执行
4node src/demo02.js
5// 输出结果:
6withRouter.params: Jameswain
7withRouter.target: [class App] { params: \'Jameswain\' }
8[class App] { params: \'Jameswain\' }

为了方便理解,我将babel编译后的代码进行了逻辑上简化整理,如果想看babel编译后的源码,自己执行命令编译一下即可:

 1// src/demo02.js
 2// 带参数的类装饰器
 3function withRouter(params) { // 接收参数的函数
 4  console.log(\'withRouter.params:\', params);
 5  return function (target) { // 装饰器函数
 6    // 给被装饰的类添加一个静态属性
 7    target.params = params;
 8    console.log(\'withRouter.target:\', target);
 9  };  
10}
11var _dec = withRouter(\'Jameswain\'); // 传入参数给装饰器,其实就是执行@withRouter(\'Jameswain\')
12var _class = class App {};
13let App = _dec(_class) || _class; // 调用装饰器函数,对类进行装饰
14console.log(App);

带参数的装饰器,其实就是函数柯里化。

类装饰器的执行顺序

类装饰器其实就是两种,一种是有参数,一种是无参数。当一个类被多个装饰器装饰时,并且有带参数的装饰,则执行顺序跟dom事件差不多:从上往下进入,从下往上返回;先捕获(从上往下执行参数层函数),后冒泡(从下往上执行装饰器层函数)