装饰者模式介绍

装饰者模式(Decorator Pattern)也称为装饰器模式,在不改变对象自身的基础上,动态增加额外的职责。属于结构型模式的一种。

使用装饰者模式的优点:把对象核心职责和要装饰的功能分开了。非侵入式的行为修改。

举个例子来说,原本长相一般的女孩,借助美颜功能,也能拍出逆天的颜值。只要善于运用辅助的装饰功能,开启瘦脸,增大眼睛,来点磨皮后,咔嚓一拍,惊艳无比。

经过这一系列叠加的装饰,你还是你,长相不增不减,却能在镜头前增加了多重美。如果你愿意,还可以尝试不同的装饰风格,只要装饰功能做的好,你就能成为“百变星君”。

可以用代码表示,把每个功能抽象成一个类:

// 女孩子
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(); // 

从代码的表现来看,将一个对象嵌入到另一个对象中,相当于通过一个对象对另一个对象进行包装,形成一条包装链。调用后,随着包装的链条传递给每一个对象,让每个对象都有处理的机会。

这种方式在增加删除装饰功能上都有极大的灵活性,假如你有勇气展示真实的脸,去掉瘦脸的包装即可,这对其他功能毫无影响;假如要增加磨皮,再来个功能类,继续装饰下去,对其他功能也无影响,可以并存运行。

在JavaScript中增加小功能使用类,显的有点笨重,JavaScript的优点是灵活,可以使用对象来表示:

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中的装饰器

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。

看了编译后的源码,相信会对装饰器的理解更深刻。

以上就是JS装饰者模式和TypeScript装饰器的详细内容,更多关于JS的资料请关注Devmax其它相关文章!

JS装饰者模式和TypeScript装饰器的更多相关文章

  1. html5 拖拽及用 js 实现拖拽功能的示例代码

    这篇文章主要介绍了html5 拖拽及用 js 实现拖拽,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  2. amaze ui 的使用详细教程

    这篇文章主要介绍了amaze ui 的使用详细教程,本文通过多种方法给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  3. swift皮筋弹动发射飞机ios源码

    这是一个款采用swift实现的皮筋弹动发射飞机游戏源码,游戏源码比较详细,大家可以研究学习一下吧。

  4. Swift Selector

    看了喵神的Swifter100个必备的小tips总结下swift中不支持@selector关键字,将SEL重新定义为结构体了。

  5. Swift与Js通过WebView交互

    开发环境:Swfit2.3XCode8.2基础概念jscontext,jscontext是代表JS的执行环境,通过-evaluateScript:方法就可以执行一JS代码JSValue,JSValue封装了JS与ObjC中的对应的类型,以及调用JS的API等JSExport,JSExport是一个协议,遵守此协议,就可以定义我们自己的协议,在协议中声明的API都会在JS中暴露出来,才能调用Swif

  6. JSCore swift

    如果双方相互引用,会造成循环引用,而导致内存泄露。以上是Jscore的基本使用,比较简单

  7. Swift WKWebView的js调用swift

    最近项目需求,需要用到JavaScriptCore和WebKit,但是网上的资源有限,而且比较杂,都是一个博客复制另外一个博客,都没有去实际敲代码验证,下面给大家分享一下我的学习过程。

  8. Swift WKWebView的swift调用js

    不多说,直接上代码:在html里面要添加的的代码,显示swift传过去的参数:这样就实现了swift给js传参数和调用!

  9. 在 Swift 專案中使用 Javascript:編寫一個將 Markdown 轉為 HTML 的編輯器

    你有強烈的好奇心,希望在你的iOS專案中使用JavaScript。jscontext中的所有值都是JSValue對象,JSValue類用於表示任意類型的JavaScript值。因此,我們既需要寫Swift代碼也要寫JavaScript代碼。此外,我們還會在JavaScript中按照這個類的定義來創建一個對象并對其屬性進行賦值。從Swift中呼叫JavaScript就如介紹中所言,JavaScriptCore中最主要的角色就是jscontext類。一個jscontext對象是位於JavaScript環境和本

  10. swift - WKWebView JS 交互

    本文介绍WKWebView怎么与js交互,至于怎么用WKWebView这里就不介绍了HTML代码APP调JS代码结果JS给APP传参数首先注册你需要监听的js方法名2.继承WKScriptMessageHandler并重写userContentController方法,在该方法里接收JS传来的参数3.结果

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部