深刻 JavaScript 设计模式,今后有了优化代码的理论依据

1、设计模式综述

我想不少和我同样的朋友小时候都看过《天龙八部》,里面的女主角王语嫣是个武学博才,但本身却毫无实战。好比段誉和慕容复交手时,她连连口述指导:"段郎,二龙爪手,抢珠三式,小心你的腰肋,注意你的气户穴。潘月偷心,扶手相望......",虽然看着感受都是一些最基本的拳脚功夫,但有解说在旁边,到底仍是感受高大上了不少。没错,设计模式其实就和这些招数名差很少,不少模式都给人一种其实平时没少用,可就是不知道原来这是一个专业招术...。但咱们确实须要从系统层面深刻理解一下这些经常使用的模式,不只能够起到发散思惟的做用,同时也能够指导咱们解决问题的能力。若是以前不多接触过设计模式,那么这篇文章但愿能够助力你一下,感谢关注和点赞。css

1.1 模式定义

设计模式的定义:在面向对象软件设计过程当中针对特定问题的简洁而优雅的解决方案。html

说白了,设计模式就是一种理念,经过一些设计思惟来解决平时编写底层或业务代码时遇到的场景问题。好比早期业务中的一个封装类,同时带有一些封装方法。若是如今该类不能再知足所有业务场景,且不容许修改原方法,此时就须要装饰器或适配器模式来解决;又好比当设计一个场景,在调用一个固定对象时必定要先执行某些方法,好比验证登陆、验证身份ID等场景,此时就应该用到代理模式。这种例子有不少,能够先看一下设计模式的分类。前端

1.2 模式分类

设计模式,按标准划分,有3大类23种,而因为JavaScript的一些特性,如弱类型语言、无接口编程等特征,故其中只有一些模式是比较重要的。下面给出这23种设计模式名称。vue

类型 模式名称
建立型 工厂 单例 原型
组合型(结构型) 适配器 装饰器 代理 外观 桥接
行为型 观察者 命令 中介者 状态 策略 解释器 迭代器 访问者 模板方法 职责链 备忘录

是否是以为这些高逼格的词汇很霸气,下面就先从一些重要的模式开展了解和深刻。node

2、工厂模式

1.1 基本特征

工厂模式有三种形式:简单工厂模式(Simple Factory)、工厂方法模式(Factory Method)和抽象工厂模式(Abstract Factory)。在js中咱们最多见的当属简单工厂模式。工厂模式的设计思想即:npm

  • 将 new 操做单独封装,只对外提供相应接口;
  • 遇到new 时,就要考虑是否应该使用工厂模式;

1.2 核心做用

工厂模式的核心做用以下:编程

  • 主要用于隐藏建立实例的复杂度,只需对外提供一个接口;
  • 实现构造函数和建立者的分离,知足开放封闭的原则;

1.3 分类

  • 简单工厂模式:一个工厂对象建立一种产品对象实例。即用来建立同一类对象;
  • 工厂方法模式:创建抽象核心类,将建立实例的实际重心放在核心抽象大类的子类中;
  • 抽象工厂模式:对类的工厂抽象用来建立产品类簇,不负责建立某一类产品的实例。 因为在JS中基本不会使用抽象工厂模式,所以本文探究前两类模式。

1.4 实例演示

先经过一个简单例子最直观感觉什么是工厂:redux

// 定义产品
class Product {
    constructor(name) {
        this.name = name;
    }
    init() {
        console.log('初始化产品')
    }
}

// 定义工厂
class Factory {
    create(name) {
        return new Product(name); // 核心思想
    }
}

let c = new Factory(); 
let p = c.create('p1');
p.init();
复制代码

工厂模式最直观的地方在于,建立产品对象不是经过直接new 产品类实现,而是经过工厂方法实现。如今再用一个稍微有些好看的例子描述一下简单工厂:设计模式

//User类
class User {
  //构造器
  constructor(opt) {
    this.name = opt.name;
    this.viewPage = opt.viewPage;
  }

  static getInstance(role) {
    switch (role) {
      case 'superAdmin':
        return new User({ name: '超级管理员', viewPage: ['首页', '通信录', '发现页', '应用数据', '权限管理'] });
        break;
      case 'admin':
        return new User({ name: '管理员', viewPage: ['首页', '通信录'] });
        break;
      default:
        throw new Error('params error')
    }
  }
}

//调用
let superAdmin = User.getInstance('superAdmin');
let admin = User.getInstance('admin');
复制代码

经过上例,咱们能够看到,每次建立新的对象实例时,只须要传入相应的参数,就能够获得指定的对象实例。最直观的例子是若是不用工厂模式,那代码中是否是就会多出好多个new,这样看着也不太舒服。数组

其实简单工厂模式已经能知足咱们前端大部分业务场景了,若是非要说其一个缺陷,那就是每次有新实例时,咱们须要重写这个User大类,总归感受和后面所述的装饰器模式有一些冲突。此时,工厂方法模式就出来了,其核心思想就是独立出一个大的User类,将建立实例对象的过程用其子类来实现:

class User {
  constructor(name = '', viewPage = []) {
    this.name = name;
    this.viewPage = viewPage;
  }
}

class UserFactory extends User {
  constructor(name, viewPage) {
    super(name, viewPage)
  }
  create(role) {
    switch (role) {
      case 'superAdmin': 
        return new UserFactory( '超级管理员', ['首页', '通信录', '发现页', '应用数据', '权限管理'] );
        break;
      case 'admin':
        return new UserFactory( '管理员', ['首页', '通信录'] );
        break;
      default:
        throw new Error('params error');
    }
  }
}
let userFactory = new UserFactory();
let superAdmin = userFactory.create('superAdmin');
let admin = userFactory.create('admin');
let user = userFactory.create('user');
复制代码

这样,虽然也得经过 new 一个实例,但至少咱们能够无需修改User类里面的东西,虽然说代码量上感受和简单模式差不了多少,但思想主体确实就是这样。

1.5 应用场景

(1) jQuery的选择器$(selector)

$('div')new $('div') 有何区别? 为何 $('div') 就能直接实现 new的效果,同时去除了 new $('div') 这种$('div') 去除了 new 书写繁杂的弊端,还能实现完美的链式操做代码简介,就是由于$内置的实现机制是工厂模式。其底层代码以下:

class jQuery {
    constructor(selector) {
        super(selector)
    }
    // ...
}

window.$ = function(selector) {
    return new jQuery(selector)
}
复制代码

(2) Vue 异步组件

Vue.component('async-example' , (resolve , reject) => {
    setTimeout(function() {
        resolve({
            template: `<div>I am async!</div>`
        })
    }, 1000)
})
复制代码

除了上述两个常见的实例场景,还有React.createElement() 也是工厂原理。因此,当咱们平时遇到要建立实例的时候,就能够想一想可否用工厂模式实现了。

3、单例模式

3.1 基本特征

单例模式,顾名思义即保证明例在全局的单一性,概述以下:

  • 系统中被惟一使用
  • 一个类只有一个实例(注意只能有一个实例,必须是强相等===)

在平常业务场景中,咱们常常会遇到须要单例模式的场景,好比最基本的弹窗,或是购物车等。由于不管是在单页面仍是多页面应用程序中,咱们都须要这些业务场景只会同时存在一个。而若是用单例模式,则会避免须要外部变量来断定是否存在的低端方法。

3.2 实例演示

举一个单例模式的例子:

class Modal {
    login() {
        console.log('login...');
    }
}
Modal.create = (function() {
    let instance
    return function() {
        if(!instance) {
           instance = new Modal();
        }
        return instance
    }
})()
let m1 = Modal.create();
let m2 = Modal.create();
console.log(m1 === m2) // true
复制代码

上述代码是一种简单版单例模式,经过js的当即执行函数和闭包函数,将初始实例肯定,以后即可经过断定instance是否存在,果存在则直接返回,反之则建立了再返回,即确保一个类只有一个实例对象。还有一种种“透明版”单例模式:

let Modal = (function(){
    let instance;
    return function(name) {
        if (instance) {
           return instance;
        }
        this.name = name;
        return instance = this;
    }
})();

Modal.prototype.getName = function() {
    return this.name
}

let question = new Modal('问题框');
let answer = new Modal('回答框');

console.log(question === answer); // true
console.log(question.getName());  // '问题框'
console.log(answer.getName());  // '问题框'
复制代码

因此,单例模式的实现实质即建立一个能够返回对象实例的引用和一个获取该实例的方法。保证建立对象的引用恒惟一。

3.3 应用场景

单例模式应用场景太多了 在Vue 中 咱们熟知的Vuex 和 redux 中的 store

3、适配器模式

3.1 定义及特征

适配器模式很好理解,在平常开发中其实不经意间就用到了。适配器模式(Adapter)是将一个类(对象)的接口(方法或属性)转化成适应当前场景的另外一个接口(方法或属性),适配器模式使得本来因为接口不兼容而不能一块儿工做的那些类(对象)能够一些工做。因此,适配器模式必须包含目标(Target)、源(Adaptee)和适配器(Adapter)三个角色。

3.2 应用场景

举个我工做中最生动简单的例子,你就知道原来适配器无处不在。前端经过接口请求来一组数据集,类型分别文章、回答和课程,其中文章类返回的日期类型是2019-08-15 09:00:00格式字符串,回答类是2019/08/15 09:00:00,课程类返回的是时间戳格式,且文章、回答的建立时间字段叫createAt,课程叫createTime(咱们真就是这样......)返回数据以下:

let result = [
      {
          id: 1
          type: 'Article',
          createAt: '2019-06-12 08:10:20',
          updateAt: '2019-08-15 09:00:00',
          ......
      },
      {
          id: 2
          type: 'Answer',
          createAt: '2019-04-11 08:11:23',
          updateAt: '2019/08/15 09:00:00',
          ......
      },
      {
          id: 3
          type: 'Course',
          createTime: 1554941483000,
          updateAt: 1565830800000,
          ......
      }
    ]
复制代码

如今咱们要呈现这些实体的格式到移动端。并显示一个统一的时间格式。而通常状况下在遇到时间类型时,咱们一般首先想到的就是先 new Date() 一下,再作相应的转换,可是很遗憾,在移动端IOS系统上,2019-08-15这种横杠分隔格式的时间是不被识别的,因此,咱们此时就须要作个数据适配器作兼容处理:

let endResult = result.map(item => adapter(item));
 
 let adapter = function(item) {
    switch(item.type) {
        case 'Article':
          [item.createAt, item.updateAt] = [
             new Date(item.createAt.replace(/-/g,'/')).getTime(),
             new Date(item.updateAt.replace(/-/g,'/')).getTime()
          ]
        break;
        case: 'Answer': 
          item.createAt = new Date(item.createAt.replace(/-/g,'/')).getTime();
        break;
        case: 'Course':
          item.createAt = item.createTime
        break;
    }
 }
复制代码

恩,没错,这个adapter 也能够叫作数据适配器,有了这个方法,全部实体数据类型的数据就均可适配了。

再看一个基于ES6类的适配器例子:

// 目标
class Target {
    typeGB() {
        throw new Error('This method must be overwritten!');
    }
}

// 源
class Adaptee {
    typeHKB() {
        console.log("香港(Hong Kong)标准配件"); // 港独都是sb
    }
}

// 适配器
class Adapter extends Target {
    constructor(adaptee) {
        super();
        this.adaptee = adaptee;
    }
    typeGB() {
        this.adaptee.typeHKB();
    }
}

let adaptee = new Adaptee();

let adapter = new Adapter(adaptee);
adapter.typeGB(); //香港(Hong Kong)标准配件
复制代码

上述实例就将 Adaptee 类的实例对象的 typeHKB() 适配了通用的 typeGB() 方法。另外我不想重申官方说过的话,我只想直白一些:港..独都是sb

4、装饰器模式

4.1 定义及特征

装饰器,顾名思义,就是在原来方法的基础上去装饰一些针对特别场景所适用的方法,即添加一些新功能。所以其特征主要有两点:

  • 为对象添加新功能;
  • 不改变其原有的结构和功能,即原有功能还继续会用,且场景不会改变。

直接上个例子:

class Circle {
    draw() {
        console.log('画一个圆形');
    }
}

class Decorator {
    constructor(circle) {
        this.circle = circle;
    }
    draw() {
        this.circle.draw();
        this.setRedBorder(circle);
    }
    setRedBorder(circle) {
        console.log('画一个红色边框');
    }
}

let circle = new Circle();
let decorator = new Decorator(circle);
decorator.draw(); //画一个圆形,画一个红色边框
复制代码

该例中,咱们写了一个Decorator装饰器类,它重写了实例对象的draw方法,给其方法新增了一个setRedBorder(),所以最后为其输出结果进行了装饰。

4.2 装饰器插件

ES7 中就存在了装饰器语法,须要安装相应的babel插件,一块儿看一下该插件如何用,首先安装一下插件,并作相关的语法配置:

npm i babel-plugin-transform-decorators-legacy 

//.babelrc
{
    "presets": ["es2015", "latest"],
    "plugins": ["transform-decorators-legacy"]
}
复制代码

给一个Demo类上添加一个装饰器 testDec,此时 Demo类就具备了 装饰器赋予的属性:

@testDec
class Demo {}

function testDec(target) {
   target.isDec = true;
}

alert(Demo.isDec) // true
复制代码

经过上例能够得出下述代码结论:

@decorator 
class A {}

// 等同于

class A {}
A = decorator(A) || A;
复制代码

4.3 实力场景

装饰器的实例场景有不少,咱们主要拿mixin和属性装饰学习一下。

(1) mixin 示例

function mixins(...list) {
   return function(target) {
      Object.assign(target.prototype, ...list)
   }
}

const Foo = {
    foo() {
        alert('foo');
    }
}

@mixins(Foo)
class MyClass { }

let obj = new MyClass();
obj.foo();
复制代码

上例中,Foo做为target的实参,MyClass做为 list的实参,最终实现将Foo的全部原型方法(foo)装饰到 MyClass类上,成为了MyClass的方法。最终代码的运行结果是执行了foo()

(2) 属性装饰器

固定语法:

function readonly(target, name, descriptor) {
    // descriptor 属性描述对象(Object.defineProperty 中会用到)
    /*
      {
          value: specifiedFunction,
          enumerable: false,
          configurable: true
          writable: true 是否可改
      }
    */
}
复制代码

设置类属性只读:

function readonly(target , name , descriptor) {
  descriptor.writable = false;
}

class Person {
    constructor() {
        this.first = '周';
        this.last = '杰伦';
    }

    @readonly
    name() {
        return `${this.first}${this.last}`
    }
}

const p = new Person();
console.log(p.name());  // 打印成功 ,‘周杰伦’

// 试图修改name:
p.name = function() {
    return true;
}
// Uncaught TypeError:Cannot assign to read only property 'name' of object '#<Person>'
复制代码

可见,再给属性添加了只读的装饰后,代码试图修改属性的命令将会报错。

5、代理模式

5.1 定义及特征

代理模式的定义以下:

为一个对象提供一个代用品或占位符,以便控制对它的访问。

通俗来讲,代理模式要突出“代理”的含义,该模式场景须要三类角色,分别为使用者、目标对象和代理者,使用者的目的是直接访问目标对象,但却不能直接访问,而是要先经过代理者。所以该模式很是像明星代理人的场景。其特征为:

  • 使用者无权访问目标对象;
  • 中间加代理,经过代理作受权和控制。

代理模式确实很方便,一般若是面临一些很大开销的操做,就能够并采用虚拟代理的方式延迟到须要它的时候再去建立,好比懒加载操做。或者一些前置条件较多的操做,好比目标操做实现的前提必须是已登陆,且Id符合必定特征,此时也能够将这些前置判断写到代理器中。举个加载图片的例子:

class ReadImg {
    constructor(fileName) {
       this.fileName = fileName;
       this.loadFromDisk();
    }

    display() {
        console.log('display...' + this.fileName);
    }

    loadFromDisk() {
        console.log('loading...' + this.fileName);
    }
}

class ProxyImg {
    constructor(fileName) {
       this.readImg = new ReadImg(fileName)
    }

    display() {
        this.readImg.display();
    }
}

let proxyImg = new ProxyImg('1.png');
proxyImg.display();
复制代码

5.2 实际应用

(1) HTML元素事件代理:

HTML元素代理事件,又名网页代理事件,举例以下:

<body>
    <div id="div1">
        <a href="#">a1</a>
        <a href="#">a2</a>
        <a href="#">a3</a>
        <a href="#">a4</a>
        <a href="#">a5</a>
    </div>

    <script>
       var div1 = document.getElementById('div1');
       div1.addEventListener('click', (e) => {
          var target = e.target;
          if(target.nodeName === 'A') {
             alert(target.innerHTML);
          }
       })
    </script>
</body>
复制代码

该例中,咱们并未直接在元素上定义点击事件,而是经过监听元素点击事件,并经过定位元素节点名称来代理到<a>标签的点击,最终利用捕获事件来实现相应的点击效果。

(2) $.proxy

$.proxyjQuery 提供给咱们的一个代理方法,还以上述 html 元素为例,写一个点击事件:

// html如上例
$('#div1').click(function() {
   setTimeout(function() {
      $(this).css('background-color', 'yellow')
   },1000)
})
复制代码

上述div的点击最终不会实现背景色变化,由于setTimeout的因素,致使内部函数中的this指向的是window而非相应的div。一般咱们的作法是在setTimeout方法前获取当前this 指向,代码以下:

$('#div1').click(function() {
   let _this = this;
   setTimeout(function() {
      $(_this).css('background-color', 'yellow')
   },1000)
})
复制代码

而若是不用上面的方法,咱们就能够用$.proxy代理目标元素来实现:

$('#div1').click(function() {
    var fn = $.proxy(function() {
        $(this).css('background-color', 'yellow')
    }, this);
    
    setTimeout(fn , 1000)
})
复制代码

(3) ES6 proxy

ES6的 Proxy 相信你们都不会陌生,Vue 3.0 的双向绑定原理就是依赖 ES6 的 Proxy 来实现,给一个简单的例子:

let star = {
    name: '菜徐坤',
    song: '~鸡你太美~'
    age: 40,
    phone: 13089898989
}

let agent = new Proxy(star , {
    get(target , key) {
        if(key == 'phone') {
            // 返回经济人本身的电话
            return 15667096303
        }
        if(key == 'price') {
           return 20000000000
        }
        return target[key]
    },
    set(target , key , val) {
       if(key === 'customPrice') {
          if(val < 100000000) {
              throw new Error('价格过低')
          }
          else {
              target[key] = value;
              return true
          }
       }
    }
})

// agent 对象会根据相应的代理规则,执行相应的操做:
agent.phone // 15667096303  
agent.price // 20000000000 
复制代码

不用多解释了,真不明白他咋火的。。。。。。

7、观察者模式

7.1 定义及特征

观察者模式有多重要?这么说吧,若是上帝告诉你,这辈子你只能学习一种模式,你该绝不犹豫选择观察者模式。观察者模式,也叫订阅-发布模式,熟悉Vue的朋友必定不会陌生,该模式定义了一种1对N的关系(注意:不必定是一对多,因此更准确地描述应该是1对N),使观察者们同时监听某一个对象相应的状态变换,一旦变化则通知到全部观察者,从而触发观察者相应的事件。所以,观察者模式中的角色有两类:观察者(发布者)和被观察者(订阅者)。

咱们可直接看一下观察者模式的UML类图:

image

类图解析:

  • 每个观察者(Observer)都有一个update 方法,而且观察者的状态就是等待被触发;
  • 每个主题(subject)均可以经过attach方法接纳N个观察者所观察,即观察者们存储在主题的observers数组里,;
  • 主题有初始化状态(init)、获取状态(getState)和设置状态(setState)三个通用型方法;
  • 当主题的状态发生变化时,经过特定的notifyAllObervers方法通知全部观察者。

这下就很明白了,针对如上描述再来个小例子:

// 建立一个主题,保存状态,状态变化以后触发全部观察者对象
class Subject {
    constructor() {
        this.state = 0;
        this.observers = []
    }

    getState() {
        return this.state
    }

    setState(state) {
       this.state = state;
       this.notifyAllObservers()
    }

    notifyAllObservers() {
        this.observers.forEach(observer => {
            observer.update()
        })
    }

    attach(observer) {
       this.observers.push(observer)
    }
}

// 观察者
class Observer {
    constructor(name , subject) {
       this.name = name;
       this.subject = subject;
       this.subject.attach(this);
    }
    update() {
        console.log(`${this.name} update, state: ${this.subject.getState()}`)
    }
}

let s = new Subject();
let o1 = new Observer('o1' , s);
let o2 = new Observer('o2' , s);
let o3 = new Observer('o3' , s);

s.setState(1)
s.setState(2)
s.setState(3)

/*
o1 update, state: 1
 o2 update, state: 1
o3 update, state: 1
o1 update, state: 2
o2 update, state: 2
o3 update, state: 2
o2 update, state: 3
o3 update, state: 3
*/
复制代码

经过最终结果不能看到,主题每次改变状态后都会触发全部观察者状态更新,主题触发了3次状态,观察者必定update了9次。

7.2 实例场景

其实咱们在平时不经意间就使用了不少观察者模式的例子,好比Promise等、Node.js中的 EventEmitter事件监听器、Vue 的 Watch生命周期钩子等等,这些都是观察者模式,好比在Vue组件生命周期Watch,为甚在Watch里设定了数据监听,一旦数据改变了就触发相应事件了?还有Promise,为何异步操做获得结果后就会进入到then或者catch里呢?这些都依赖于观察者模式。这里我引用一篇很不错的文章《vue的双向绑定原理及实现》

好了,这篇文章的内容就先告一段落,咱们已经把23中设计模式中的核心重点都过了一遍,剩下的一些非重点,我会尽快整理出来,欢迎你们关注和点赞。

感谢千阳老师的校验。

参考文章

相关文章
相关标签/搜索