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事件差不多:从上往下进入,从下往上返回;先捕获(从上往下执行参数层函数),后冒泡(从下往上执行装饰器层函数)