本文主要为三方面的内容:javascript
学习的目的是对装饰者模式模式有进一步的理解,并运用在本身的项目中;对TypeScript装饰器的理解,更好的使用装饰器,例如在 nodejs web 框架中、 vue-property-decorator 中,或者是自定义装饰器,能熟练运用并掌握其基本的实现原理。html
装饰者模式(Decorator Pattern)也称为装饰器模式,在不改变对象自身的基础上,动态增长额外的职责。属于结构型模式的一种。前端
使用装饰者模式的优势:把对象核心职责和要装饰的功能分开了。非侵入式的行为修改。vue
举个例子来讲,本来长相通常的女孩,借助美颜功能,也能拍出逆天的颜值。只要善于运用辅助的装饰功能,开启瘦脸,增大眼睛,来点磨皮后,咔嚓一拍,惊艳无比。java
通过这一系列叠加的装饰,你仍是你,长相不增不减,却能在镜头前增长了多重美。若是你愿意,还能够尝试不一样的装饰风格,只要装饰功能作的好,你就能成为“百变星君”。node
能够用代码表示,把每一个功能抽象成一个类:git
// 女孩子 class Girl { faceValue() { console.log('我本来的脸') } } class ThinFace { constructor(girl) { this.girl = girl; } faceValue() { this.girl.faceValue(); console.log('开启瘦脸') } } class IncreasingEyes { constructor(girl) { this.girl = girl; } faceValue() { this.girl.faceValue(); console.log('增大眼睛') } } let girl = new Girl(); girl = new ThinFace(girl); girl = new IncreasingEyes(girl); // 闪瞎你的眼 girl.faceValue(); //
从代码的表现来看,将一个对象嵌入到另外一个对象中,至关于经过一个对象对另外一个对象进行包装,造成一条包装链。调用后,随着包装的链条传递给每个对象,让每一个对象都有处理的机会。github
这种方式在增长删除装饰功能上都有极大的灵活性,假如你有勇气展现真实的脸,去掉瘦脸的包装便可,这对其余功能毫无影响;假如要增长磨皮,再来个功能类,继续装饰下去,对其余功能也无影响,能够并存运行。web
在 javascript 中增长小功能使用类,显的有点笨重,JavaScript 的优势是灵活,可使用对象来表示:express
let girl = { faceValue() { console.log('我本来的脸') } } function thinFace() { console.log('开启瘦脸') } function IncreasingEyes() { console.log('增大眼睛') } girl.faceValue = function(){ const originalFaveValue = girl.faceValue; // 原来的功能 return function() { originalFaveValue.call(girl); thinFace.call(girl); } }() girl.faceValue = function(){ const originalFaveValue = girl.faceValue; // 原来的功能 return function() { originalFaveValue.call(girl); IncreasingEyes.call(girl); } }() girl.faceValue();
在不改变原来代码的基础上,经过先保留原来函数,从新改写,在重写的代码中调用原来保留的函数。
用一张图来表示装饰者模式的原理:
从图中能够看出来,经过一层层的包装,增长了原先对象的功能。
TypeScript 中的装饰器使用 @expression 这种形式,expression 求值后为一个函数,它在运行时被调用,被装饰的声明信息会被作为参数传入。
Javascript规范里的装饰器目前处在 建议征集的第二阶段,也就意味着不能在原生代码中直接使用,浏览器暂不支持。
能够经过 babel 或 TypeScript 工具在编译阶段,把装饰器语法转换成浏览器可执行的代码。(最后会有编译后的源码分析)
如下主要讨论 TypeScript 中装饰器的使用。
TypeScript 中的装饰器能够被附加到类声明、方法、 访问符(getter/setter)、属性和参数上。
开启对装饰器的支持,命令行 编译文件时:
tsc --target ES5 --experimentalDecorators test.ts
配置文件 tsconfig.json
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true } }
装饰器实际上就是一个函数,在使用时前面加上 @ 符号,写在要装饰的声明以前,多个装饰器同时做用在一个声明时,能够写一行或换行写:
// 换行写 @test1 @test2 declaration //写一行 @test1 @test2 ... declaration
定义 face.ts 文件:
function thinFace() { console.log('开启瘦脸') } @thinFace class Girl { }
编译成 js 代码,在运行时,会直接调用 thinFace 函数。这个装饰器做用在类上,称之为类装饰器。
若是须要附加多个功能,能够组合多个装饰器一块儿使用:
function thinFace() { console.log('开启瘦脸') } function IncreasingEyes() { console.log('增大眼睛') } @thinFace @IncreasingEyes class Girl { }
多个装饰器组合在一块儿,在运行时,要注意,调用顺序是 从下至上 依次调用,正好和书写的顺序相反。例子中给出的运行结果是:
'增大眼睛' '开启瘦脸'
若是你要在一个装饰器中给类添加属性,在其余的装饰器中使用,那就要写在最后一个装饰器中,由于最后写的装饰器最早调用。
有时须要给装饰器传递一些参数,这要借助于装饰器工厂函数。装饰器工厂函数实际上就是一个高阶函数,在调用后返回一个函数,返回的函数做为装饰器函数。
function thinFace(value: string){ console.log('1-瘦脸工厂方法') return function(){ console.log(`4-我是瘦脸的装饰器,要瘦脸${value}`) } } function IncreasingEyes(value: string) { console.log('2-增大眼睛工厂方法') return function(){ console.log(`3-我是增大眼睛的装饰器,要${value}`) } } @thinFace('50%') @IncreasingEyes('增大一倍') class Girl { }
@ 符号后为调用工厂函数,依次从上到下执行,目的是求得装饰器函数。装饰器函数的运行顺序依然是从下到上依次执行。
运行的结果为:
1-瘦脸工厂方法 2-增大眼睛工厂方法 3-我是增大眼睛的装饰器,要增大一倍 4-我是瘦脸的装饰器,要瘦脸50%
总结一下:
做用在类声明上的装饰器,能够给咱们改变类的机会。在执行装饰器函数时,会把类构造函数传递给装饰器函数。
function classDecorator(value: string){ return function(constructor){ console.log('接收一个构造函数') } } function thinFace(constructor){ constructor.prototype.thinFaceFeature = function() { console.log('瘦脸功能') } } @thinFace @classDecorator('类装饰器') class Girl {} let g = new Girl(); g.thinFaceFeature(); // '瘦脸功能'
上面的例子中,拿到传递构造函数后,就能够给构造函数原型上增长新的方法,甚至也能够继承别的类。
做用在类的方法上,有静态方法和原型方法。做用在静态方法上,装饰器函数接收的是类构造函数;做用在原型方法上,装饰器函数接收的是原型对象。
这里拿做用在原型方法上举例。
function methodDecorator(value: string, Girl){ return function(prototype, key, descriptor){ console.log('接收原型对象,装饰的属性名,属性描述符', Girl.prototype === prototype) } } function thinFace(prototype, key, descriptor){ // 保留原来的方法逻辑 let originalMethod = descriptor.value; // 改写,增长逻辑,并执行原有逻辑 descriptor.value = function(){ originalMethod.call(this); // 注意修改this的指向 console.log('开启瘦脸模式') } } class Girl { @thinFace @methodDecorator('方式装饰器', Girl) faceValue(){ console.log('我是本来的面目') } } let g = new Girl(); g.faceValue();
从代码中能够看出,装饰器函数接收三个参数,原型对象、方法名、描述对象。对描述对象陌生的,能够参考 这里;
要加强功能,能够先保留原来的函数,改写描述对象的 value 为另外一函数。
当使用 g.faceValue() 访问方法时,访问的就是描述对象 value 对应的值。
在改写的函数中增长逻辑,并执行原来保留的原函数。注意原函数要用 call 或 apply 将 this 指向原型对象。
做用在类中定义的属性上,这些属性不是原型上的属性,而是经过类实例化获得的实例对象上的属性。
装饰器一样会接受两个参数,原型对象,和属性名。而没有属性描述对象,为何呢?这与TypeScript是如何初始化属性装饰器的有关。 目前没有办法在定义一个原型对象的成员时描述一个实例属性。
function propertyDecorator(value: string, Girl){ return function(prototype, key){ console.log('接收原型对象,装饰的属性名,属性描述符', Girl.prototype === prototype) } } function thinFace(prototype, key){ console.log(prototype, key) } class Girl { @thinFace @propertyDecorator('属性装饰器', Girl) public age: number = 18; } let g = new Girl(); console.log(g.age); // 18
下面组合多个装饰器写在一块儿,出了上面提到的三种,还有 访问符装饰器、参数装饰器。这些装饰器在一块儿时,会有执行顺序。
function classDecorator(value: string){ console.log(value) return function(){} } function propertyDecorator(value: string) { console.log(value) return function(){ console.log('propertyDecorator') } } function methodDecorator(value: string) { console.log(value) return function(){ console.log('methodDecorator') } } function paramDecorator(value: string) { console.log(value) return function(){ console.log('paramDecorator') } } function AccessDecorator(value: string) { console.log(value) return function(){ console.log('AccessDecorator') } } function thinFace(){ console.log('瘦脸') } function IncreasingEyes() { console.log('增大眼睛') } @thinFace @classDecorator('类装饰器') class Girl { @propertyDecorator('属性装饰器') age: number = 18; @AccessDecorator('访问符装饰器') get city(){} @methodDecorator('方法装饰器') @IncreasingEyes faceValue(){ console.log('本来的脸') } getAge(@paramDecorator('参数装饰器') name: string){} }
运行了这段编译后的代码,会发现这些访问器的顺序是,属性装饰器 -> 访问符装饰器 -> 方法装饰器 -> 参数装饰器 -> 类装饰器。
更详细的用法能够参考官网文档:https://www.tslang.cn/docs/handbook/decorators.html#decorator-factories
装饰器在浏览器中不支持,没办法直接使用,须要通过工具编译成浏览器可执行的代码。
分析一下经过工具编译后的代码。
生成 face.js 文件:
tsc --target ES5 --experimentalDecorators face.ts
打开 face.js 文件,会看到一段被压缩后的代码,能够格式化一下。
先看这段代码:
__decorate([ propertyDecorator('属性装饰器') ], Girl.prototype, "age", void 0); __decorate([ AccessDecorator('访问符装饰器') ], Girl.prototype, "city", null); __decorate([ methodDecorator('方法装饰器'), IncreasingEyes ], Girl.prototype, "faceValue", null); __decorate([ __param(0, paramDecorator('参数装饰器')) ], Girl.prototype, "getAge", null); Girl = __decorate([ thinFace, classDecorator('类装饰器') ], Girl);
__decorate 的做用就是执行装饰器函数,从这段代码中可以看出不少信息,印证上面获得的结论。
经过__decorate调用顺序,能够看出来,多个类型的装饰器一块儿使用时,顺序是,属性装饰器 -> 访问符装饰器 -> 方法装饰器 -> 参数装饰器 -> 类装饰器。
调用了 __decorate 函数,根据使用的装饰器类型不一样,传入的参数也不相同。
第一个参数传入的都同样,为数组,这样确保和咱们书写的顺序一致,每一项是求值后的装饰器函数,若是写的是 @propertyDecorator() 则一上来就执行,获得装饰器函数,这跟上面分析的一致。
类装饰器会把类做为第二个参数,其余的装饰器,把原型对象做为第二个参数,属性名做为第三个,第四个是 null 或 void 0。void 0的值为undefined,也就等于没传参数
要记住传给 __decorate 函数参数的个数和值,在深刻到 __decorate 源码中, 会根据这些值来决定执行装饰器函数时,传入参数的多少。
好,来看 __decorate 函数实现:
// 已存在此函数,直接使用,不然本身定义 var __decorate = (this && this.__decorate) || // 接收四个参数: //decorators存放装饰器函数的数组、target原型对象|类, //key属性名、desc描述(undefined或null) function(decorators, target, key, desc) { var c = arguments.length, // 拿到参数的个数 r = c < 3 // 参数小于三个,说明是类装饰器,直接拿到类 ? target : desc === null // 第四个参数为 null,则须要描述对象;属性装饰器传入是 void 0,没有描述对象。 ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; // 若是提供了Reflect.decorate方法,直接调用;不然本身实现 if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else // 装饰器函数执行顺序和书写的顺序相反,从下至上 执行 for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) // 拿到装饰器函数 r = (c < 3 // 参数小于3个,说明是类装饰器,执行装饰器函数,直接传入类 ? d(r) : c > 3 // 参数大于三个,是方法装饰器、访问符装饰器、参数装饰器,则执行传入描述对象 ? d(target, key, r) : d(target, key) // 为属性装饰器,不传入描述对象 ) || r; // 给被装饰的属性,设置获得的描述对象,主要是针对,方法、属性来讲的 /*** * r 的值分两种状况, * 一种是经过上面的 Object.getOwnPropertyDescriptor 获得的值 * 另外一种,是装饰器函数执行后的返回值,做为描述对象。 * 通常不给装饰器函数返回值。 */ return c > 3 && r && Object.defineProperty(target, key, r),r; };
上面的参数装饰器,调用了一个函数为 __params,
var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } };
目的是,要给装饰器函数传入参数的位置 paramIndex。
看了编译后的源码,相信会对装饰器的理解更深入。
以上若有误差欢迎指正学习,谢谢。~~~~
github博客地址:https://github.com/WYseven/blog,欢迎star。
若是对你有帮助,请关注【前端技能解锁】: