JS 中的装饰器模式

更新日期: 2020-01-14阅读: 2.3k标签: 模式

背景

使用过 mobx + mobx-react 的同学对于 ES 的新特性装饰器肯定不陌生。我在第一次使用装饰器的时候,我就对它爱不释手,书写起来简单优雅,太适合我这种爱装 X 且懒的同学了。今天我就带着大家深入浅出这个优雅的语法特性:装饰器。


预备知识

全球统一为 ECMAScript 新特性、语法制定统一标准的组织委员会是 TC39;

对于单个的新特性,TC39 有专门的标准和阶段去跟进该特性,也就是我们常说的 stage-0 到 stage-4,其中的新特性的成熟完备性从低到高;

普及完一些必要的知识点后,我们继续进入到我们的主题:装饰器。


演变过程

装饰器的制定过程也不是一帆风顺的,而且就算是2020年初的现在,这个备受争议的语法特性官方标准还在讨论制定当中,目前仍处于 stage-2: 草稿状态

但目前市面上 babel、TypeScript 编译支持的装饰器语法主要包括两种方式,一个是 传统方式(legacy) 和目前标准方式

由于目前标准还不是很成熟,编译器的支持并不全面,所以市面上大部分的装饰器库,大都只是兼容 legacy 方式,如 Mobx,如下为 Mobx 官网中的一段话:

Note that the legacy mode is important (as is putting the decorators proposal first). Non-legacy mode is WIP.

下面我就从实际场景出发,来使用装饰器模式来实现我们常见的一些业务场景。

注意:由于新版标准可以说是在 legacy 的方式下改造出来的,legacy 更加灵活,标准方式则主张静态配置去扩展实现装饰器功能


实际场景

需求

我希望实现一个 validate 修饰器,用于定义成员变量的校验规则,使用如下

import {validate, check} from 'validate'

class Person {
   @validate(val => !['M', 'W'].includes(val) && '需要为 M 或者 W')
   gender = 'M'
}

const person = new Person();
person.gender = null;
check(person); // => [{ name: 'gender', error: '需要为 M 或者 W' }]

以上这种方式,相比于运行时 validate,如下

const check = (person) => {
   const errors = [];
   if (!['M', 'W'].includes(person.gender)) {
      errors.push({name: 'gender', error: '需要为 M 或者 W'});
   }
   return errors;
}

装饰器的方式能够更快捷的维护校验逻辑,更加具有表驱动程序的优势,只需要改配置即可。但是对于没有接触过装饰器模式模式的同学,深入改造装饰器内部的逻辑就有一定门坎了(但是不怕,这篇文章帮助大家降低门坎)。

实现

由于目前 Babel 编译对于新版标准支持不是很完全,对于标准的装饰器模式实现有一定程度的影响,所以本文主要介绍 legacy 方式的实现,相信对于大家后续实现标准的装饰器也是有帮助的!

思路整理

按照 api 的使用用例,我们可以知道,对于 person 实例是已经注入了 validate 校验逻辑的,然后在 check 方法中提取校验逻辑并执行即可。

@validate // 注入校验逻辑
    |
  check   // 提取校验逻辑并执行
    |
返回校验结果

首先我们在 babel 配置中需要如下配置:

"plugins": [
  [
    "@babel/proposal-decorators",
    {
      "legacy": true
    }
  ],
  ["@babel/proposal-class-properties", { "loose": true }]
]

对于我们需要实现的 @validate 装饰器结构如下:

// rule 为外界自定义校验逻辑
function validate(rule) {
  // target 为原型,也就是 Person.prototype
  // keyName 为修饰的成员名,如 `gender`
  // descriptor 为该成员的是修饰实体
  return (target, keyName, descriptor) => {
     // 注入 rule
     target['check'] = target['check'] || {};
     target['check'][keyName] = rule;
     return descriptor;
  }
}

根据上述逻辑,执行完 @validate 之后,在 Person.prototype 中会注入 'check' 属性,同时我们在 check方法中拿到该属性即可进行校验。

那么我们是不是完成了该方法呢?其实还远远不够:

首先,对于隐式注入的 check 属性需要足够隐藏,同时属性名 check 未免太容易被实例属性覆盖,从而不能通过原型链找到该属性

在类继承模式下,check 属性可能会丢失,甚至会污染校验规则

首先我们来看第一个问题:改造我们的代码

const getInjectPropName =
  typeof Symbol === 'function' ? name => Symbol.for(`[[${name}]]`) : name => `[[${name}]]`

const addHideProps = (target, name, value) => {
  Object.defineProperty(target, name, {
    enumerable: false,
    configurable: true,
    writable: true,
    value
  })
}

function validate(rule) {
  return (target, keyName, descriptor) => {
     const name = getInjectPropName('check');
     addHideProps(target, name, target[name] || {});
     target[name][keyName] = rule;
     return descriptor;
  }
}

相比于之前的代码实现,这样 Object.keys(Person.prototype) 不会包含 check 属性,同时也大大降低了属性命名冲突的问题。

对于第二个问题,类继承模式下的装饰器书写。如下例子:

class Person {
   @validate(val => !['M', 'W'].includes(val) && '需要为 M 或者 W')
   gender = 'M'

   @validate(a => !(a > 10) && '需要大于10')
   age = 12
} 

class Man extends Person {
   @validate(val => !['M'].includes(val) && '需要为 M')
   gender = 'M'
}

其中的原型链模型图如下

       person instance     +-------------------+
          +----------+     |  Person.prototype |
          |__proto___+------>------------------+
          |         |+     |   rules           |
          +----------+     +-------+--+-+------+
          |          |             ^  ^ ^
          |          |             |  | |
          |          |                | |
          +----------+             |  |
          | rules    +- -- -- -- --   | |
          +----------+                |
                                      | |
                                      | |
                       person instance+
                          +----------+  |
                          |__proto___|  |
man instance              |         |+
        +-----------+     +----------+  |
        |__proto__  |     |          |  |
        |           +---->+          |
        +-----------+     |          |  |
        |           |     +----------+
        |           |     | rules    |  |
        |           |     +---^------+
        |           |                   |
        |           |                   |
        +-----------+
        | rules     | - - - - - -- - - -+
        +-----------+

可以看到 man instance 和 person instance 共享同一份 rules,同时 Man 中的 validate 已经污染了共享的这份 rules,导致 person instance 校验逻辑

所以我们需要把原型模型修改为如下模式:

       person instance     +-------------------+
          +----------+     |  Person.prototype |
          |__proto___+------>------------------+
          |         |+     |   rules           |
          +----------+     +-------+-----------+
          |          |             ^
          |          |             |
          |          |
          +----------+             |
          | rules    +- -- -- -- --
          +----------+


                       person instance2
                         Man.prototype
                          +----------+
                          |__proto___|
man instance              |          |
        +-----------+     +----------+
        |__proto__  |     |          |
        |           +---->+          |
        +-----------+     |          |
        |           |     +----------+
        |           |     | rules    |
        |           |     +---+------+
        |           |         ^
        |           |         |
        +-----------+         |
        | rules     | - - - - +
        +-----------+

可以看到 man instance 和 person instance 都有一份 rules 在其原型链上,这样就不会有污染的问题,同时也不会丢失校验规则

修改我们的代码:

const getInjectPropName =
  typeof Symbol === 'function' ? name => Symbol.for(`[[${name}]]`) : name => `[[${name}]]`

const addHideProps = (target, name, value) => {
  Object.defineProperty(target, name, {
    enumerable: false,
    configurable: true,
    writable: true,
    value
  })
}

function validate(rule) {
  return (target, keyName, descriptor) => {
     const name = getInjectPropName('check');
     // 没有注入过 rules
     if (!target[name]) {
        addHideProps(target, name, {});
     } else {
        // 已经注入,但是是注入在 target.__proto__ 中
        // 也就是继承模式
        if (!target.hasOwnProperty(name)) {
           // 浅拷贝一份至 own
           addHideProps(target, name, {...target[name]})
        }
     }

     target[name][keyName] = rule;
     return descriptor;
  }
}

如上,才算是我们完备的代码!而且 mobx 也是有相同场景的考虑的。


总结

总结是把以上模式沉淀为 decorate-utils 方便我们自定义自己的修饰器

原文:https://segmentfault.com/a/1190000021641817

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

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 设计模式 - 简单工厂

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

点击更多...

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