设计模式js版

# 简介

设计模式如果应用到项目中,可以实现代码的复用和解耦,提高代码质量

# 设计原则

最常见(7种)原则

  • ​ 单一职责原则(Single-Responsibility Principle)
  • ​ 开闭原则(Open-Closed Principle)
  • ​ 里氏代换原则(Liskov Substitution Principle)
  • ​ 依赖倒转原则(依赖抽象)(Dependence Inversion Principle)
  • ​ 接口隔离原则(Interface Segregation Principle)
  • ​ 合成复用原则(Composition/Aggregate Reuse Principle)
  • ​ 迪米特法则(最少知道原则(LeastKnowledge Principle, LKP)

设计模式原则

  • S – Single Responsibility Principle 单一职责原则
    • 一个程序只做好一件事
    • 如果功能过于复杂就拆分开,每个部分保持独立
  • O – OpenClosed Principle 开放/封闭原则
    • 对扩展开放,对修改封闭
    • 增加需求时,扩展新代码,而非修改已有代码
  • L – Liskov Substitution Principle 里氏替换原则
    • 子类能覆盖父类
    • 父类能出现的地方子类就能出现
  • I – Interface Segregation Principle 接口隔离原则
    • 保持接口的单一独立
    • 类似单一职责原则,这里更关注接口
  • D – Dependency Inversion Principle 依赖倒转原则
    • 面向接口编程,依赖于抽象而不依赖于具体
    • 使用方只关注接口而不关注具体类的实现

SO体现较多,举个栗子:(比如Promise)

  • 单一职责原则:每个then中的逻辑只做好一件事
  • 开放封闭原则(对扩展开放,对修改封闭):如果新增需求,扩展then

# 模式类型(共24种)

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

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

# 创建型模式

创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。 创建型模式在创建什么(What),由谁创建(Who),何时创建(When)等方面都为软件设计者提供了尽可能大的灵活性。创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。

# 类型(共6种)

  • 简单工厂模式(Simple Factory Pattern)
  • (多个)工厂方法模式(Factory Method Pattern)
  • 抽象工厂模式(Abstract Factory)
  • 单例模式(Singleton)
  • 建造者模式(Builder)
  • 原型模式(Prototype)

# 结构型模式

# 结构型模式分类

  • 类结构型模式
    • 关心类的组合
    • 由多个类可以组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系。
  • 对象结构型模式
    • 关心类与对象的组合
    • 通过关联关系使得在一个类中定义另一个类的实例对象,然后通过该对象调用其方法。

总结:根据“合成复用原则”,在系统中尽量使用关联关系来替代继承关系,因此大部分结构型模式都是对象结构型模式。

# 类型(共7种)

  • 适配器模式(Adapter)
  • 装饰器模式(Decorator)
  • 代理模式(Proxy)
  • 外观模式(Facade)
  • 桥接模式(Bridge)
  • 组合模式(Composite)
  • 享元模式(Flyweight)

# 行为型模式

行为型模式(Behavioral Pattern)是对在不同的对象之间划分责任和算法的抽象化。 行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。 通过行为型模式,可以更加清晰地划分类与对象的职责,并研究系统在运行时实例对象之间的交互。 在系统运行时,对象并不是孤立的,它们可以通过相互通信与协作完成某些复杂功能,一个对象在运行时也将影响到其他对象的运行。

# 行为型模式分类

  • 类行为型模式
    • 关心类的组合
    • 由多个类可以组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系。
    • 使用继承关系在几个类之间分配行为,类行为型模式主要通过多态等方式来分配父类与子类的职责。
  • 对象行为型模式
    • 关心类与对象的组合
    • 通过关联关系使得在一个类中定义另一个类的实例对象,然后通过该对象调用其方法。
    • 对象的行为型模式则使用对象的聚合关联关系来分配行为,对象行为型模式主要是通过对象关联等方式来
    • 分配两个或多个类的职责。
    • 根据“合成复用原则”,系统中要尽量使用关联关系来取代继承关系,因此大部分行为型设计模式都属于对象行为型设计模式。

# 类型(共11种)

  • 策略模式(Strategy)
  • 模板方法模式(TemplateMethod)
  • 观察者模式(Observer)
  • 迭代器模式(Iterator)
  • 责任链模式(Chain of Responsibility)
  • 命令模式(Command)
  • 备忘录模式(Memento)
  • 状态模式(State)
  • 访问者模式(Visitor)
  • 中介者模式(Mediator)
  • 解释器模式(Interpreter)

# 常用设计模式

tips:常用的**【工原简单,装适观代】** 谐音:公(工)园(原)简单,装饰(适)现代;

# 简单工厂模式

# 定义

又叫静态工厂方法,就是创建对象,并赋予属性和方法

# 场景

  • 如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择
  • 将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式;
  • 需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性

# 优点

  • 创建对象的过程可能很复杂,但我们只需要关心创建结果。
  • 构造函数和创建者分离, 符合“开闭原则”
  • 一个调用者想创建一个对象,只要知道其名称就可以了。
  • 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。

# 缺点

  • 添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度
  • 考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度

# 什么时候不用

当被应用到错误的问题类型上时,这一模式会给应用程序引入大量不必要的复杂性.除非为创建对象提供一个接口是我们编写的库或者框架的一个设计上目标,否则我会建议使用明确的构造器,以避免不必要的开销。

由于对象的创建过程被高效的抽象在一个接口后面的事实,这也会给依赖于这个过程可能会有多复杂的单元测试带来问题。

# 应用

抽取类相同的属性和方法封装到对象上

let UserFactory = function (role) {
  function User(opt) {
    this.name = opt.name;
    this.viewPage = opt.viewPage;
  }
  switch (role) {
    case 'superAdmin':
      return new User(superAdmin);
      break;
    case 'admin':
      return new User(admin);
      break;
    case 'user':
      return new User(user);
      break;
    default:
      throw new Error('参数错误, 可选参数:superAdmin、admin、user')
  }
}
let superAdmin = UserFactory('superAdmin');
let admin = UserFactory('admin') 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//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');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

简单工厂; 这样,虽然也得通过 new 一个实例,但至少我们可以无需修改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');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

通过一个类获取不同类的实例;

class Cat {}
class Dog {}
function Factory(type, args) {
    switch (type){
        case 'cat':
            return new Cat(args);
            break;
        default:
            return new Dog(args);
            break;
    }
}
const cat = new Factory('cat', {name: 'cat'});
const dog = new Factory('dog', {name: 'dog'});
console.log(cat, dog)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Man {
  constructor(name) {
    this.name = name
  }
  alertName() {
    alert(this.name)
  }
}
class Factory {
  static create(name) {
    return new Man(name)
  }
}
Factory.create('samy').alertName()
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 工厂方法模式

# 定义

对产品类的抽象使其创建业务主要负责用于创建多类产品的实例

作用:工厂起到的作用就是隐藏了创建实例的复杂度,只需要提供一个接口,简单清晰。

# 应用

  • jQuery的选择器$(selector)
  • Vue 异步组件

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

创建实例

var Factory=function(type,content){
  if(this instanceof Factory){
    var s=new this[type](content);
    return s;
  }else{
    return new Factory(type,content);
  }
}
Factory.prototype={//工厂原型中设置创建类型数据对象的属性
  Java:function(content){
    console.log('Java值为',content);
  },
  Python:function(content){
    console.log('Python值为',content);
  },
}
Factory('Python','我是Python');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

曾经我们熟悉的JQuery的$()就是一个工厂函数,它根据传入参数的不同创建元素或者去寻找上下文中的元素,创建成相应的jQuery对象;

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

class jQuery {
    constructor(selector) {
        super(selector)
    }
    add() {}
  // 此处省略若干API
}
window.$ = function(selector) {
    return new jQuery(selector)
}
1
2
3
4
5
6
7
8
9
10

在 Vue 源码中,你也可以看到工厂模式的使用,比如创建异步组件

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
    // 逻辑处理...
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  return vnode
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。例如:

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // 向 `resolve` 回调传递组件定义
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})
1
2
3
4
5
6
7
8

# 原型模式

# 定义

原型模式会创建新的对象,而不是创建未初始化的对象,它会返回使用从原型或样本对象复制的值进行初始化的对象。原型模式也称为属性模式

原型模式(prototype)是指用原型实例指向创建对象的种类,并且通过拷贝这些原型创建新的对象。

# 应用

实现继承

function Animal (name) {
  this.name = name || 'Animal';// 属性
  this.sleep = function(){  // 实例方法
    console.log(this.name + '正在睡觉!');
  }
}
Animal.prototype.eat = function(food) {// 原型方法
  console.log(this.name + '正在吃:' + food);
};
function Cat(){ }
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

var cat = new Cat();
console.log(cat.name);//cat
console.log(cat.eat('fish'));//cat正在吃:fish  undefined
console.log(cat.sleep());//cat正在睡觉! undefined
console.log(cat instanceof Animal); //true 
console.log(cat instanceof Cat); //true  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

原型模式,就是创建一个共享的原型,通过拷贝这个原型来创建新的类,用于创建重复的对象,带来性能上的提升。

class Person {
  constructor(name) {
    this.name = name
  }
  getName() {
    return this.name
  }
}
class Student extends Person {
  constructor(name) {
    super(name)
  }
  sayHello() {
    console.log(`Hello, My name is ${this.name}`)
  }
}
let student = new Student("xiaoming")
student.sayHello()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 单例模式

# 定义

确保一个类仅有一个实例,并提供一个访问它的全局访问点

# 场景

比如线程池、全局缓存等。我们所熟知的浏览器的window对象就是一个单例,在JavaScript开发中,对于这种只需要一个的对象,我们的实现往往使用单例。

# 优点

  • 划分命名空间,减少全局变量
  • 增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护
  • 且只会实例化一次。简化了代码的调试和维护

# 缺点

  • 由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合 从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一个单元一起测试。

# 应用

  • 定义命名空间和实现分支型方法
  • 登录框
  • vuex 和 redux中的store

登录框

 class LoginForm {
    constructor() {
        this.state = 'hide'
    }
    show() {
        if (this.state === 'show') {
            alert('已经显示')
            return
        }
        this.state = 'show'
        console.log('登录框显示成功')
    }
    hide() {
        if (this.state === 'hide') {
            alert('已经隐藏')
            return
        }
        this.state = 'hide'
        console.log('登录框隐藏成功')
    }
 }
 LoginForm.getInstance = (function () {
     let instance
     return function () {
        if (!instance) {
            instance = new LoginForm()
        }
        return instance
     }
 })()

let obj1 = LoginForm.getInstance()
obj1.show()

let obj2 = LoginForm.getInstance()
obj2.hide()

console.log(obj1 === obj2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

实现单例模式 (不透明的)

function Singleton(name) {
  this.name = name;
  this.instance = null;
}
Singleton.prototype.getName = function() {
  console.log(this.name)
};
Singleton.getInstance = function(name) {
  if(!this.instance) {
      this.instance = new Singleton(name);
  }
  return this.instance
};
var a = Singleton.getInstance('a');
var b = Singleton.getInstance('b');
console.log(a === b);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

也可以使用闭包来实现:【要点】

function Singleton(name) {
    this.name = name;
}
Singleton.prototype.getName = function() {
    console.log(this.name)
};
Singleton.getInstance = (function() {
    var instance = null;
    return function(name) {
        if(!this.instance) {
            this.instance = new Singleton(name);
        }
        return this.instance
    }       
})();
var a = Singleton.getInstance('a');
var b = Singleton.getInstance('b');
console.log(a === b);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

class方式:【要点】

class Singleton {
    constructor() {}
}
Singleton.getInstance = (function() {
    let instance
    return function() {
        if (!instance) {
            instance = new Singleton()
        }
        return instance
    }
})()
let s1 = Singleton.getInstance()
let s2 = Singleton.getInstance()
console.log(s1 === s2) // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

实现单例模式 (透明的)

function CreateSingleton (name) {
  this.name = name;
  this.getName();
};
CreateSingleton.prototype.getName = function() {
  console.log(this.name)
};
var Singleton = (function(){
  var instance;
  return function (name) {
      if(!instance) {
          instance = new CreateSingleton(name);
      }
      return instance;
  }
})();
var a = new Singleton('a');
var b = new Singleton('b');
console.log(a===b);//true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//简单版单例模式
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

//“透明版”单例模式
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());  // '问题框'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

在 Vuex 源码中的使用,虽然它的实现方式不大一样,通过一个外部变量来控制只安装一次 Vuex

let Vue // bind on install
export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    // ...
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}
1
2
3
4
5
6
7
8
9

# 惰性单例模式

# 应用实例

实现一个单例的遮罩

在开发中并不会单独使用遮罩层,遮罩层和弹出窗是经常结合在一起使用,前面提到过登陆弹窗使用单例模式实现也是最适合的。当然我们还有好的实现方式,那就是将上面单例中代码变化的部分和不变的部分,分离开来。

在下面的实现中将单例模式的惰性实现部分提取出来,实现了惰性实现代码的复用

var singleton = function(fn) {
  var instance;
  return function() {
      return instance || (instance = fn.apply(this, arguments));
  }
};
var createMask = function(){// 创建遮罩层
  var mask = document.createElement('div');
  mask.style.position = 'fixed';
  mask.style.top = '0';
  mask.style.right = '0';
  mask.style.bottom = '0';
  mask.style.left = '0';
  mask.style.opacity = 'o.75';
  mask.style.backgroundColor = '#000';
  mask.style.display = 'none';
  mask.style.zIndex = '98';
  document.body.appendChild(mask);
  mask.onclick = function(){
      this.style.display = 'none';
  }
  return mask;
};
var createLogin = function() {// 创建登陆窗口
  var login = document.createElement('div');
  login.style.position = 'fixed';
  login.style.top = '50%';
  login.style.left = '50%';
  login.style.zIndex = '100';
  login.style.display = 'none';
  login.style.padding = '50px 80px';
  login.style.backgroundColor = '#fff';
  login.style.border = '1px solid #ccc';
  login.style.borderRadius = '6px';
  login.innerHTML = 'login it';
  document.body.appendChild(login);
  return login;
};

<button id="btn">click it</button>
document.getElementById('btn').onclick = function() {
  var oMask = singleton(createMask)();
  oMask.style.display = 'block';
  var oLogin = singleton(createLogin)();
  oLogin.style.display = 'block';
  var w = parseInt(oLogin.clientWidth);
  var h = parseInt(oLogin.clientHeight);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 观察者模式

# 定义

事件订阅及发布; 观察者对象和被观察者对象 之间的订阅和触发事件; 由观察者和观察者组成。通过观察者调用被观察者的实例。发布-订阅模式也叫做观察者模式。通过一对一或者一对多的依赖关系,当对象发生改变时,订阅方都会收到通知。

# 优点

  • 支持简单的广播通信,自动通知所有已经订阅过的对象
  • 目标对象与观察者之间的抽象耦合关系能单独扩展以及重用
  • 增加了灵活性
  • 观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。

# 缺点

过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解

# 应用

  • vue 响应式;
  • DOM事件;

**“Vue 双向绑定实现”;**在 Vue 中,如何实现响应式也是使用了该模式。对于需要实现响应式的对象来说,在 get 的时候会进行依赖收集,当改变了对象的属性时,就会触发派发更新。

简单的观察者模式: (仿 Vue 实现)

class Dep {// 观察者
    constructor() {
        this.subs = []
    }
    addSub(sub) {
        this.subs.push(sub)
    }
    depend() {
        if (Dep.target) { 
            Dep.target.addDep(this);
        }
    }
    notify() {
        this.subs.forEach(sub => sub.update())
    }
}

class Watcher {// 被观察者
    constructor(vm, expOrFn) {
        this.vm = vm;
        this.getter = expOrFn;
        this.value;
    }
    get() {
        Dep.target = this;
        var vm = this.vm;
        var value = this.getter.call(vm, vm);
        return value;
    }
    evaluate() {
        this.value = this.get();
    }
    addDep(dep) {
        dep.addSub(this);
    }
    update() {
        console.log('更新, value:', this.value)
    }
}
// 观察者实例
var dep = new Dep();
//  被观察者实例
var watcher = new Watcher({x: 1}, (val) => val);
watcher.evaluate();//通过 `watcher.evaluate()` 将自身实例赋值给 `Dep.target`

// 观察者监听被观察对象
dep.depend()//调用 `dep.depend()` 将dep实例的 watcher 实例 push 到 dep.subs中
dep.notify()//通过数据劫持,在调用被劫持的对象的set方法时,调用 dep.subs 中所有的 `watcher.update()`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 发布/订阅者模式

是观察者模式的变体,比观察者模式多了一个调度中心

  • 发布者发布信息到调度中心;
  • 调度中心和订阅者直接完成订阅和触发事件事件;

使用场景案例:“DOM 的 addEventListener 事件”;

点击一个按钮触发了点击事件就是使用了该模式

<ul id="ul"></ul>
<script>
    let ul = document.querySelector('#ul')
    ul.addEventListener('click', (event) => {
        console.log(event.target);
    })
</script>

document.body.addEventListener('click', function() {
    console.log('hello world!');
});
document.body.click()
1
2
3
4
5
6
7
8
9
10
11
12

一个简单的发布/订阅者模式实现:(仿 EventBus 实现)

class EventTarget {// EventTarget 就是一个调度中心
    constructor() {
        this.dep = {}
    }
    on(key, fn) {
        this.dep[key] = fn;
    }
    emit(key) {
        typeof this.dep[key] === 'function' ? this.dep[key]() : ''
    }
}
let eventTarget = new EventTarget()
eventTarget.on('click', function() {console.log(1)})
eventTarget.emit('click')
1
2
3
4
5
6
7
8
9
10
11
12
13
14

vue中自带的事件监听;

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }
Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      const info = `event handler for "${event}"`
      for (let i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 装饰器模式

# 定义

  • 动态地给某个对象添加一些额外的职责,是一种实现继承的替代方案
  • 在不改变原对象的基础上,通过对其进行包装扩展,使原有对象可以满足用户的更复杂需求,而不会影响从这个类中派生的其他对象

# 优点

  • 装饰类和被装饰类都只关心自身的核心业务,实现了解耦。
  • 方便动态的扩展功能,且提供了比继承更多的灵活性。

# 缺点

  • 多层装饰比较复杂。
  • 常常会引入许多小对象,看起来比较相似,实际功能大相径庭,从而使得我们的应用程序架构变得复杂起来

# 应用

  • 比如现在有4 种型号的自行车,我们为每种自行车都定义了一个单

    独的类。现在要给每种自行车都装上前灯、尾 灯和铃铛这3 种配件。如果使用继承的方式来给 每种自行车创建子类,则需要 4×3 = 12 个子类。 但是如果把前灯、尾灯、铃铛这些对象动态组 合到自行车上面,则只需要额外增加3 个类

  • ES7 Decorator

  • mixin

class Cellphone {
  create() {
    console.log('生成一个手机')
  }
}
class Decorator {
  constructor(cellphone) {
    this.cellphone = cellphone
  }
  create() {
    this.cellphone.create()
    this.createShell(cellphone)
  }
  createShell() {
    console.log('生成手机壳')
  }
}
// 测试代码
let cellphone = new Cellphone()
cellphone.create()

console.log('------------')
let dec = new Decorator(cellphone)
dec.create()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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(); //画一个圆形,画一个红色边框
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

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

封装输入控件事件

let decorator=function(input,fn){
  let input=document.getElementById(input);
  if(typeof input.onclick=='function'){
    let oldClickFn=input.onclick;//缓存事件源原有的回调函数
    input.onclick=function(){//为事件源定义新事件
      oldClickFn();//事件源原有回调函数
      fn();//执行事件源新增回调函数
    }
  }else{
    input.onclick=fn;//未绑定绑定
  }
}
decorator('textInp',function(){
  console.log('文本框执行啦');
})
decorator('btn',function(){
  console.log('按钮执行啦');
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

偏函数的部分实现;【要点】

const after = function(fn, afterFn) {
    return function() {
        fn.apply(this, arguments)
        afterFn.apply(this, arguments)
    }
}
const myAfter = after(after(fn1, fn2), fn3)
myAfter()
1
2
3
4
5
6
7
8

使用了 ES7 中的装饰器语法【要点】

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

npm i babel-plugin-transform-decorators-legacy 

//.babelrc
{
    "presets": ["es2015", "latest"],
    "plugins": ["transform-decorators-legacy"]
}
1
2
3
4
5
6
7

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

@testDec
class Demo {}

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

alert(Demo.isDec) // true
1
2
3
4
5
6
7
8

通过上例可以得出下述代码结论:

@decorator 
class A {}

// 等同于
class A {}
A = decorator(A) || A;
1
2
3
4
5
6

vue中源码的实现:重写数组的7个方法

import { def } from '../util/index'

/*取得原生数组的原型*/
const arrayProto = Array.prototype
/*创建一个新的数组对象,修改该对象上的数组的七个方法,防止污染原生数组方法*/
export const arrayMethods = Object.create(arrayProto)

 /*这里重写了数组的这些方法,在保证不污染原生数组原型的情况下重写数组的这些方法,截获数组的成员发生的变化,执行原生数组操作的同时dep通知关联的所有观察者进行响应式处理*/
[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // cache original method
  /*将数组的原生方法缓存起来,后面要调用*/
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    // avoid leaking arguments://重写的方法
    // http://jsperf.com/closure-with-arguments
    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    /*调用原生的数组方法*/
    const result = original.apply(this, args)

    /*数组新插入的元素需要重新进行observe才能响应式*/
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
        inserted = args
        break
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)//取出插入的值
      
    // notify change
    /*dep通知所有注册的观察者进行响应式处理*/
    ob.dep.notify()
    return result
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

另一个案例:

例如:现有4种型号的自行车分别被定义成一个单独的类,如果给每辆自行车都加上前灯、尾灯、铃铛这3个配件,如果用类继承的方式,需要创建4*3=12个子类。但如果通过装饰者模式,只需要创建3个类。

装饰者模式适用的场景:原有方法维持不变,在原有方法上再挂载其他方法来满足现有需求;函数的解耦,将函数拆分成多个可复用的函数,再将拆分出来的函数挂载到某个函数上,实现相同的效果但增强了复用性。

例:用AOP装饰函数实现装饰者模式

Function.prototype.before = function(beforefn) {
    var self = this;    //保存原函数引用
    return function(){  //返回包含了原函数和新函数的 '代理函数'
        beforefn.apply(this, arguments);    //执行新函数,修正this
        return self.apply(this,arguments);  //执行原函数
    }
}
Function.prototype.after = function(afterfn) {
    var self = this;
    return function(){
        var ret = self.apply(this,arguments);
        afterfn.apply(this, arguments);
        return ret;
    }
}
var func = function() {
    console.log('2');
}
//func1和func3为挂载函数
var func1 = function() {
    console.log('1');
}
var func3 = function() {
    console.log('3');
}
func = func.before(func1).after(func3);
func();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

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();
1
2
3
4
5
6
7
8
9
10
11
12
13
14

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

属性装饰器

固定语法:

function readonly(target, name, descriptor) {
    // descriptor 属性描述对象(Object.defineProperty 中会用到)
    /*
      {
          value: specifiedFunction,
          enumerable: false,
          configurable: true
          writable: true 是否可改
      }
    */
}
1
2
3
4
5
6
7
8
9
10
11

设置类属性只读:

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>'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

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

# 代理模式

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

# 定义

将抽象部分与它的实现部分分离,使它们都可以独立地变化; 代理是为了控制对对象的访问,不让外部直接访问到对象。在现实生活中,也有很多代理的场景。比如你需要买一件国外的产品,这时候你可以通过代购来购买产品。

# 优点

  • 代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用
  • 代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;

# 缺点

处理请求速度可能有差别,非直接访问存在开销

# 不同点

装饰者模式实现上和代理模式类似

  • 装饰者模式: 扩展功能,原有功能不变且可直接使用
  • 代理模式: 显示原有功能,但是经过限制之后的

# 应用

  • HTML元 素事件代理
  • jQuery.proxy()方法
  • ES6 的 proxy

比如事件代理就用到了代理模式

<ul id="ul">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>
<script>
    let ul = document.querySelector('#ul')
    ul.addEventListener('click', (event) => {
        console.log(event.target);
    })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

因为存在太多的 li,不可能每个都去绑定事件。这时候可以通过给父节点绑定一个事件,让父节点作为代理去拿到真实点击的节点。

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

$.proxy

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

// html如上例
$('#div1').click(function() {
   setTimeout(function() {
      $(this).css('background-color', 'yellow')
   },1000)
})
1
2
3
4
5
6

上述div的点击最终不会实现背景色变化,因为setTimeout的因素,导致内部函数中的this指向的是window而非相应的div。通常我们的做法是在setTimeout方法前获取当前this 指向,代码如下:

$('#div1').click(function() {
   let _this = this;
   setTimeout(function() {
      $(_this).css('background-color', 'yellow')
   },1000)
})
1
2
3
4
5
6

而如果不用上面的方法,我们就可以用$.proxy代理目标元素来实现:

$('#div1').click(function() {
    var fn = $.proxy(function() {
        $(this).css('background-color', 'yellow')
    }, this);
    
    setTimeout(fn , 1000)
})
1
2
3
4
5
6
7

ES6 proxy

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

let star = {
    name: 'samy,
    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 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

另外一个案例:

常用的虚拟代理形式:某一个花销很大的操作,可以通过虚拟代理的方式延迟到这种需要它的时候才去创建(例:使用虚拟代理实现图片懒加载)

图片懒加载的方式:先通过一张loading图占位,然后通过异步的方式加载图片,等图片加载好了再把完成的图片加载到img标签里面。

var imgFunc = (function() {
    var imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    }
})();
var proxyImage = (function() {
    var img = new Image();
    img.onload = function() {
        imgFunc.setSrc(this.src);
    }
    return {
        setSrc: function(src) {
            imgFunc.setSrc('./loading,gif');
            img.src = src;
        }
    }
})();
proxyImage.setSrc('./pic.png');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

使用代理模式实现图片懒加载的优点还有符合单一职责原则。减少一个类或方法的粒度和耦合度。

代理模式确实很方便,通常如果面临一些很大开销的操作,就可以并采用虚拟代理的方式延迟到需要它的时候再去创建,比如懒加载操作。或者一些前置条件较多的操作,比如目标操作实现的前提必须是已登录,且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();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 适配器模式

# 定义

适配器模式(Adapter)是将一个类(对象)的接口(方法或属性)转化成客户希望的另外一个接口(方法或属性),适配器模式使得原本由于接口不兼容而不能一起工作的那些类(对象)可以一些工作。速成包装器(wrapper)。适配器用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式实现两个接口的正常协作。

# 优点

  • 可以让任何两个没有关联的类一起运行。
  • 提高了类的复用。
  • 适配对象,适配库,适配数据

# 缺点

  • 额外对象的创建,非直接调用,存在一定的开销(且不像代理模式在某些功能点上可实现性能优化)
  • 如果没必要使用适配器模式的话,可以考虑重构,如果使用的话,尽量把文档完善

# 不同点

适配器与代理模式相似

  • 适配器模式: 提供一个不同的接口(如不同版本的插头);
  • 代理模式: 提供一模一样的接口;

# 应用

  • 整合第三方SDK
  • 封装旧接口
class Plug {
  getName() {
    return '港版插头'
  }
}
class Target {
  constructor() {
    this.plug = new Plug()
  }
  getName() {
    return this.plug.getName() + ' 适配器转二脚插头'
  }
}
let target = new Target()
target.getName() // 港版插头 适配器转二脚插头
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

自己封装的ajax;

// 自己封装的ajax, 使用方式如下
ajax({
    url: '/getData',
    type: 'Post',
    dataType: 'json',
    data: {
        test: 111
    }
}).done(function() {})
// 因为历史原因,代码中全都是:
// $.ajax({....})

// 做一层适配器
var $ = {
    ajax: function (options) {
        return ajax(options)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

在 Vue 中,比如父组件传递给子组件一个时间戳属性,组件内部需要将时间戳转为正常的日期显示,一般会使用 computed 来做转换这件事情,这个过程就使用到了适配器模式。

原有data 中的数据不满足当前的要求,通过计算属性的规则来适配成我们需要的格式,对原有数据并没有改变,只改变了原有数据的表现形式

<template>
    <div id="example">
        <p>Original message: "{{ message }}"</p>  <!-- Hello -->
        <p>Computed reversed message: "{{ reversedMessage }}"</p>  <!-- olleH -->
    </div>
</template>
<script type='text/javascript'>
    export default {
        name: 'demo',
        data() {
            return {
                message: 'Hello'
            }
        },
        computed: {
            reversedMessage: function() {
                return this.message.split('').reverse().join('')
            }
        }
    }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 外观模式

# 定义

为子系统中的一组接口提供一个一致的界面;定义了一个高层接口,这个接口使子系统更加容易使用;

比如在家要看电影,需要打开音响,再打开投影仪,再打开播放器等等,引入外观角色之后,只需要调用“打开电影设备”方法就可以。外观角色封装了打开投影仪等操作,给使用者提供更容易使用的方法。

# 优点

  • 减少系统相互依赖。
  • 提高灵活性。
  • 提高了安全性

# 缺点

  • 不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。

# 应用

  1. 简化复杂接口

  2. 解耦和,屏蔽使用者对子系统的直接访问;

  3. 其他案例场景;

    设计初期,应该要有意识地将不同的两个层分离,比如经典的三层结构,在数据访问层和业务逻辑层、业务逻辑层和表示层之间建立外观Facade

    在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,增加外观Facade可以提供一个简单的接口,减少他们之间的依赖。

    在维护一个遗留的大型系统时,可能这个系统已经很难维护了,这时候使用外观Facade也是非常合适的,为系系统开发一个外观Facade类,为设计粗糙和高度复杂的遗留代码提供比较清晰的接口,让新系统和Facade对象交互,Facade与遗留代码交互所有的复杂工作。

//简单模型
function a(x){
   // do something
}
function b(y){
   // do something
}
function ab( x, y ){
    a(x);
    b(y);
} 

//把阻止冒泡和阻止默认事件放到了外观角色中
//其中的stopEvent就是提供给使用者的阻止冒泡,阻止默认事件的一个方法
var N = window.N || {};
N.tools = {
    stopPropagation : function( e ){
        if( e.stopPropagation ){
            e.stopPropagation();
        }else{
            e.cancelBubble = true;
        }
    },
    preventDefault : function( e ){
        if( e.preventDefault ){
            e.preventDefault();
        }else{
            e.returnValue = false;
        }
    },
    stopEvent : function( e ){
        N.tools.stopPropagation( e );
        N.tools.preventDefault( e );
    }
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

为子系统的一组接口提供一个一致的界面,定义了一个高层接口,这个接口使子系统更加容易使用

兼容浏览器事件绑定

let addMyEvent = function (el, ev, fn) {
    if (el.addEventListener) {
        el.addEventListener(ev, fn, false)
    } else if (el.attachEvent) {
        el.attachEvent('on' + ev, fn)
    } else {
        el['on' + ev] = fn
    }
}; 
1
2
3
4
5
6
7
8
9

封装接口

let myEvent = {
    // ...
    stop: e => {
        e.stopPropagation();
        e.preventDefault();
    }
};
1
2
3
4
5
6
7

# 策略模式

# 定义

策略模式最大的好处是:减少if-else的使用,同时增加代码可读性;

策略模式的目的就是将算法的使用算法的实现分离开来

一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类(可变),策略类封装了具体的算法,并负责具体的计算过程。第二个部分是环境类Context(不变),Context接受客户的请求,随后将请求委托给某一个策略类。要做到这一点,说明Context中要维持对某个策略对象的引用。

# 优点

  • 利用组合、委托、多态等技术和思想,可以有效的避免多重条件选择语句
  • 提供了对开放-封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,理解,易于扩展
  • 利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的代替方案

# 缺点

  • 会在程序中增加许多策略类或者策略对象
  • 要使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy

# 应用

  • 如果在一个系统里面有许多类,它们之间的区别仅在于它们的'行为',那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
  • 一个系统需要动态地在几种算法中选择一种。
  • 表单验证

简单的年终奖计算。(策略模式放在必填项/规则验证会很便捷)

const bonus = {
    A: function(base) {
        return base * 4;
    },
    B: function(base) {
        return base * 3;
    },
    C: function(base) {
        return base * 2;
    },
    D: function(base) {
        return base;
    }
}
const level = "B";
const base = "1008611";
const yearBouns = bonus[level](base);
console.log(yearBouns)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*策略类*/
var levelOBJ = {
    "A": function(money) {
        return money * 4;
    },
    "B" : function(money) {
        return money * 3;
    },
    "C" : function(money) {
        return money * 2;
    } 
};
/*环境类*/
var calculateBouns =function(level,money) {
    return levelOBJ[level](money);
};
console.log(calculateBouns('A',10000)); // 40000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

再比如vue中的源码实现mixin;

/*合并两个option对象到一个新的对象中*/
export function mergeOptions(
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== "production") {
    /*检查是否是有效的组件名*/
    checkComponents(child);
  }

  if (typeof child === "function") {
    child = child.options;
  }

  /*确保所有props option序列化成正确的格式*/
  normalizeProps(child);
  /*将函数指令序列化后加入对象*/
  normalizeDirectives(child);
  /*
    https://cn.vuejs.org/v2/api/#extends
    允许声明扩展另一个组件(可以是一个简单的选项对象或构造函数),而无需使用 
    将child的extends也加入parent扩展
  */
  const extendsFrom = child.extends;
  if (extendsFrom) {//递归合并extends
    parent = mergeOptions(parent, extendsFrom, vm);
  }
  /*child的mixins加入parent中*/
  if (child.mixins) {//递归合并mixin
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm);
    }
  }
  const options = {}; //属性及生命周期的合并
  let key;
  for (key in parent) {
    mergeField(key);
  }
  /*合并parent与child*/
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key);
    }
  }
  function mergeField(key) {
    /*strats里面存了options中每一个属性(el、props、watch等等)的合并方法,先取出*/
    const strat = strats[key] || defaultStrat;
    /*根据合并方法来合并两个option*/
    options[key] = strat(parent[key], child[key], vm, key);
  }
  return options;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

表单验证

<html>
<head>
    <title>策略模式-校验表单</title>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body>
    <form id = "registerForm" method="post" action="http://xxxx.com/api/register">
        用户名:<input type="text" name="userName">
        密码:<input type="text" name="password">
        手机号码:<input type="text" name="phoneNumber">
        <button type="submit">提交</button>
    </form>
    <script type="text/javascript">
        // 策略对象
        const strategies = {
          isNoEmpty: function (value, errorMsg) {
            if (value === '') {
              return errorMsg;
            }
          },
          isNoSpace: function (value, errorMsg) {
            if (value.trim() === '') {
              return errorMsg;
            }
          },
          minLength: function (value, length, errorMsg) {
            if (value.trim().length < length) {
              return errorMsg;
            }
          },
          maxLength: function (value, length, errorMsg) {
            if (value.length > length) {
              return errorMsg;
            }
          },
          isMobile: function (value, errorMsg) {
            if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(value)) {
              return errorMsg;
            }                
          }
        }
        
        // 验证类
        class Validator {
          constructor() {
            this.cache = []
          }
          add(dom, rules) {
            for(let i = 0, rule; rule = rules[i++];) {
              let strategyAry = rule.strategy.split(':')
              let errorMsg = rule.errorMsg
              this.cache.push(() => {
                let strategy = strategyAry.shift()
                strategyAry.unshift(dom.value)
                strategyAry.push(errorMsg)
                return strategies[strategy].apply(dom, strategyAry)
              })
            }
          }
          start() {
            for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
              let errorMsg = validatorFunc()
              if (errorMsg) {
                return errorMsg
              }
            }
          }
        }

        // 调用代码
        let registerForm = document.getElementById('registerForm')

        let validataFunc = function() {
          let validator = new Validator()
          validator.add(registerForm.userName, [{
            strategy: 'isNoEmpty',
            errorMsg: '用户名不可为空'
          }, {
            strategy: 'isNoSpace',
            errorMsg: '不允许以空白字符命名'
          }, {
            strategy: 'minLength:2',
            errorMsg: '用户名长度不能小于2位'
          }])
          validator.add(registerForm.password, [ {
            strategy: 'minLength:6',
            errorMsg: '密码长度不能小于6位'
          }])
          validator.add(registerForm.phoneNumber, [{
            strategy: 'isMobile',
            errorMsg: '请输入正确的手机号码格式'
          }])
          return validator.start()
        }
        registerForm.onsubmit = function() {
          let errorMsg = validataFunc()
          if (errorMsg) {
            alert(errorMsg)
            return false
          }
        }
    </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

# 其他设计模式

# 桥接模式

# 定义

桥接模式(Bridge)将抽象部分与它的实现部分分离,使它们都可以独立地变化。

# 优点

  • 有助于独立地管理各组成部分, 把抽象化与实现化解耦
  • 提高可扩充性

# 缺点

  • 大量的类将导致开发成本的增加,同时在性能方面可能也会有所减少。

# 应用

class Color {
  constructor(name){
    this.name = name
  }
}
class Shape {
  constructor(name,color){
    this.name = name
    this.color = color 
  }
  draw(){
    console.log(`${this.color.name} ${this.name}`)
  }
}

//测试
let red = new Color('red')
let yellow = new Color('yellow')
let circle = new Shape('circle', red)
circle.draw()
let triangle = new Shape('triangle', yellow)
triangle.draw()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 模块方法模式

# 定义

模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法和封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

# 优点

  • 提取了公共代码部分,易于维护

# 缺点

  • 增加了系统复杂度,主要是增加了的抽象类和类间联系

# 应用

  • 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现
  • 子类中公共的行为应被提取出来并集中到一个公共父类中的避免代码重复
class Beverage {
  constructor({brewDrink, addCondiment}) {
    this.brewDrink = brewDrink
    this.addCondiment = addCondiment
  }
  /* 烧开水,共用方法 */
  boilWater() { console.log('水已经煮沸=== 共用') }
  /* 倒杯子里,共用方法 */
  pourCup() { console.log('倒进杯子里===共用') }
  /* 模板方法 */
  init() {
    this.boilWater()
    this.brewDrink()
    this.pourCup()
    this.addCondiment()
  }
}
/* 咖啡 */
const coffee = new Beverage({
  /* 冲泡咖啡,覆盖抽象方法 */
  brewDrink: function() { console.log('冲泡咖啡') },
  /* 加调味品,覆盖抽象方法 */
  addCondiment: function() { console.log('加点奶和糖') }
})
coffee.init() 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 命令模式

# 定义

将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。

# 优点

  • 对命令进行封装,使命令易于扩展和修改
  • 命令发出者和接受者解耦,使发出者不需要知道命令的具体执行过程即可执行

# 缺点

  • 使用命令模式可能会导致某些系统有过多的具体命令类。

# 应用

const command = {
    buy(name, cost) {
        console.log(`购买${name}消费了${cost}`)
    },
    sale(name, cost) {
        console.log(`出售了${name}赚得${cost}`)
    },
    say(name, cost) {
        console.log(`这里${cost}元可以买到${name}`)
    },
    execute(fnName) {
        const fn= this[fnName];
        (typeof fn === 'function') && fn.apply(this, [].slice.call(arguments, 1))
    }
}
command.execute('buy', 'VIP', '200');
command.execute('sale', '节操', '998');
command.execute('say', 'VIP', '123');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 接收者类
class Receiver {
    execute() {
      console.log('接收者执行请求')
    }
  }
  
// 命令者
class Command {  
    constructor(receiver) {
        this.receiver = receiver
    }
    execute () {    
        console.log('命令');
        this.receiver.execute()
    }
}
// 触发者
class Invoker {   
    constructor(command) {
        this.command = command
    }
    invoke() {   
        console.log('开始')
        this.command.execute()
    }
}
  
// 仓库
const warehouse = new Receiver();   
// 订单    
const order = new Command(warehouse);  
// 客户
const client = new Invoker(order);      
client.invoke()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 状态模式

# 定义

一个对象状态改变会导致行为变化; 允许一个对象在其内部状态改变的时候改变它的行为,对象看起来似乎修改了它的类;

# 优点

  • 定义了状态与行为之间的关系,封装在一个类里,更直观清晰,增改方便;
  • 状态与状态间,行为与行为间彼此独立互不干扰;
  • 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然;

# 缺点

  • 会在系统中定义许多状态类;
  • 逻辑分散;

# 应用

  • 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为
  • 一个操作中含有大量的分支语句,而且这些分支语句依赖于该对象的状态
// 状态 (弱光、强光、关灯)
class State {
    constructor(state) {
        this.state = state
    }
    handle(context) {
        console.log(`this is ${this.state} light`)
        context.setState(this)
    }
}
class Context {
    constructor() {
        this.state = null
    }
    getState() {
        return this.state
    }
    setState(state) {
        this.state = state
    }
}
// test 
let context = new Context()
let weak = new State('weak')
let strong = new State('strong')
let off = new State('off')

// 弱光
weak.handle(context)
console.log(context.getState())

// 强光
strong.handle(context)
console.log(context.getState())

// 关闭
off.handle(context)
console.log(context.getState())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 访问模式

# 定义

表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

# 优点

  • 符合单一职责原则
  • 优秀的扩展性
  • 灵活性

# 缺点

  • 具体元素对访问者公布细节,违反了迪米特原则
  • 违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
  • 具体元素变更比较困难

# 应用

  • 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。
// 访问者
function Visitor() {
  this.visit = function( concreteElement ) {
    concreteElement.doSomething();
  }
}

// 元素类
function ConceteElement() {
  this.doSomething = function() {
    console.log("这是一个具体元素");
  }
  this.accept = function( visitor ) {
    visitor.visit(this);
  }
}

// Client
var ele = new ConceteElement();
var v = new Visitor();
ele.accept( v );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 中介者模式

# 定义

解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的 相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知 中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者 模式使网状的多对多关系变成了相对简单的一对多关系(类似于观察者模式,但是单向的,由中介者统一管理。)

# 优点

  • 使各对象之间耦合松散,而且可以独立地改变它们之间的交互
  • 中介者和对象一对多的关系取代了对象之间的网状多对多的关系
  • 如果对象之间的复杂耦合度导致维护很困难,而且耦合度随项目变化增速很快,就需要中介者重构代码

# 缺点

  • 系统中会新增一个中介者对象,因 为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介 者对象自身往往就是一个难以维护的对象。

# 应用

  • 系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象
  • 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。

中介:撮合多个卖家 和 多个买家

class Saler {
    constructor(name, cost) {
        this.name = name;
        this.cost = cost;
    }
    send() {
        console.log(`${cost}元出售${name}`)
    }
}

class Agency {
    constructor() {
        this.cargos = []
    }
    register(saler) {
        this.cargos.push(saler);
    }
    query(name) {
        const matchCargos = this.cargos.filter(cargo => cargo && cargo.name === name);
        if (matchCargos.length) {
            console.log(`查询到正在出售的商品:${JSON.stringify(matchCargos)}`)
        } else {
            console.log(`没有${name}在出售`);
        }
    }
}

let agency = new Agency();
agency.query('cart');

const cartA = new Saler('cart', '100');
const cartB = new Saler('cart', '300');
const house = new Saler('house', '500');
agency.register(cartA);
agency.register(cartB);
agency.register(house);

agency.query('cart');
agency.query('house');
agency.query('ABC');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

例如:现实生活中,航线上的飞机只需要和机场的塔台通信就能确定航线和飞行状态,而不需要和所有飞机通信。同时塔台作为中介者,知道每架飞机的飞行状态,所以可以安排所有飞机的起降和航线安排。

中介者模式适用的场景:例如购物车需求,存在商品选择表单、颜色选择表单、购买数量表单等等,都会触发change事件,那么可以通过中介者来转发处理这些事件,实现各个事件间的解耦,仅仅维护中介者对象即可

var goods = {   //手机库存
    'red|32G': 3,
    'red|64G': 1,
    'blue|32G': 7,
    'blue|32G': 6,
};
//中介者
var mediator = (function() {
    var colorSelect = document.getElementById('colorSelect');
    var memorySelect = document.getElementById('memorySelect');
    var numSelect = document.getElementById('numSelect');
    return {
        changed: function(obj) {
            switch(obj){
                case colorSelect:
                    //TODO
                    break;
                case memorySelect:
                    //TODO
                    break;
                case numSelect:
                    //TODO
                    break;
            }
        }
    }
})();
colorSelect.onchange = function() {
    mediator.changed(this);
};
memorySelect.onchange = function() {
    mediator.changed(this);
};
numSelect.onchange = function() {
    mediator.changed(this);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

其他案例

class A {
    constructor() {
        this.number = 0
    }
    setNumber(num, m) {
        this.number = num
        if (m) {
            m.setB()
        }
    }
}
class B {
    constructor() {
        this.number = 0
    }
    setNumber(num, m) {
        this.number = num
        if (m) {
            m.setA()
        }
    }
}
class Mediator {
    constructor(a, b) {
        this.a = a
        this.b = b
    }
    setA() {
        let number = this.b.number
        this.a.setNumber(number * 10)
    }
    setB() {
        let number = this.a.number
        this.b.setNumber(number / 10)
    }
}

let a = new A()
let b = new B()
let m = new Mediator(a, b)
a.setNumber(10, m)
console.log(a.number, b.number)
b.setNumber(10, m)
console.log(a.number, b.number)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 迭代器模式

提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象的内部表示。

# 特点

  • 访问一个聚合对象的内容而无需暴露它的内部表示。
  • 为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作

# 总结

对于集合内部结果常常变化各异,不想暴露其内部结构的话,但又想让客户代码透明的访问其中的元素,可以使用迭代器模式

# 场景

  • Array.prototype.forEach
  • jQuery中的$.each()
  • ES6 Iterator
class Iterator {
  constructor(conatiner) {
    this.list = conatiner.list
    this.index = 0
  }
  next() {
    if (this.hasNext()) {
      return this.list[this.index++]
    }
    return null
  }
  hasNext() {
    if (this.index >= this.list.length) {
      return false
    }
    return true
  }
}

class Container {
  constructor(list) {
    this.list = list
  }
  getIterator() {
    return new Iterator(this)
  }
}

// 测试代码
let container = new Container([1, 2, 3, 4, 5])
let iterator = container.getIterator()
while(iterator.hasNext()) {
  console.log(iterator.next())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 组合模式

  • 将对象组合成树形结构,以表示“整体-部分”的层次结构。
  • 通过对象的多态表现,使得用户对单个对象和组合对象的使用具有一致性。

# 缺点

如果通过组合模式创建了太多的对象,那么这些对象可能会让系统负担不起。

# 场景

  • 表示对象-整体层次结构;
  • 希望用户忽略组合对象和单个对象的不同,用户将统一地使用组合结构中的所有对象(方法);
class TrainOrder {
	create () {
		console.log('创建火车票订单')
	}
}
class HotelOrder {
	create () {
		console.log('创建酒店订单')
	}
}

class TotalOrder {
	constructor () {
		this.orderList = []
	}
	addOrder (order) {
		this.orderList.push(order)
		return this
	}
	create () {
		this.orderList.forEach(item => {
			item.create()
		})
		return this
	}
}
// 可以在购票网站买车票同时也订房间
let train = new TrainOrder()
let hotel = new HotelOrder()
let total = new TotalOrder()
total.addOrder(train).addOrder(hotel).create()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 享元模式

运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式

# 优点

  • 大大减少对象的创建,降低系统的内存,使效率提高。

# 缺点

  • 提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,

# 应用

  • 文件上传需要创建多个文件实例的时候
  • 如果一个应用程序使用了大量的对象,而这些大量的对象造成了很大的存储开销时就应该考虑使用享元模式
let examCarNum = 0         // 驾考车总数
/* 驾考车对象 */
class ExamCar {
    constructor(carType) {
        examCarNum++
        this.carId = examCarNum
        this.carType = carType ? '手动档' : '自动档'
        this.usingState = false    // 是否正在使用
    }

    /* 在本车上考试 */
    examine(candidateId) {
        return new Promise((resolve => {
            this.usingState = true
            console.log(`考生- ${ candidateId } 开始在${ this.carType }驾考车- ${ this.carId } 上考试`)
            setTimeout(() => {
                this.usingState = false
                console.log(`%c考生- ${ candidateId }${ this.carType }驾考车- ${ this.carId } 上考试完毕`, 'color:#f40')
                resolve()                       // 0~2秒后考试完毕
            }, Math.random() * 2000)
        }))
    }
}

/* 手动档汽车对象池 */
ManualExamCarPool = {
    _pool: [],                  // 驾考车对象池
    _candidateQueue: [],        // 考生队列

    /* 注册考生 ID 列表 */
    registCandidates(candidateList) {
        candidateList.forEach(candidateId => this.registCandidate(candidateId))
    },

    /* 注册手动档考生 */
    registCandidate(candidateId) {
        const examCar = this.getManualExamCar()    // 找一个未被占用的手动档驾考车
        if (examCar) {
            examCar.examine(candidateId)           // 开始考试,考完了让队列中的下一个考生开始考试
              .then(() => {
                  const nextCandidateId = this._candidateQueue.length && this._candidateQueue.shift()
                  nextCandidateId && this.registCandidate(nextCandidateId)
              })
        } else this._candidateQueue.push(candidateId)
    },

    /* 注册手动档车 */
    initManualExamCar(manualExamCarNum) {
        for (let i = 1; i <= manualExamCarNum; i++) {
            this._pool.push(new ExamCar(true))
        }
    },

    /* 获取状态为未被占用的手动档车 */
    getManualExamCar() {
        return this._pool.find(car => !car.usingState)
    }
}

ManualExamCarPool.initManualExamCar(3)          // 一共有3个驾考车
ManualExamCarPool.registCandidates([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])  // 10个考生来考试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

# 职责链模式

使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止

# 优点

  • 降低耦合度。它将请求的发送者和接收者解耦。
  • 简化了对象。使得对象不需要知道链的结构
  • 增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任
  • 增加新的请求处理类很方便。

# 缺点

  • 不能保证某个请求一定会被链中的节点处理,这种情况可以在链尾增加一个保底的接受者节点来处理这种即将离开链尾的请求。
  • 使程序中多了很多节点对象,可能再一次请求的过程中,大部分的节点并没有起到实质性的作用。他们的作用仅仅是让请求传递下去,从性能当面考虑,要避免过长的职责链到来的性能损耗。

# 应用

  • JS 中的事件冒泡
  • 作用域链
  • 原型链
// 请假审批,需要组长审批、经理审批、总监审批
class Action {
    constructor(name) {
        this.name = name
        this.nextAction = null
    }
    setNextAction(action) {
        this.nextAction = action
    }
    handle() {
        console.log( `${this.name} 审批`)
        if (this.nextAction != null) {
            this.nextAction.handle()
        }
    }
}

let a1 = new Action("组长")
let a2 = new Action("经理")
let a3 = new Action("总监")
a1.setNextAction(a2)
a2.setNextAction(a3)
a1.handle()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 备忘录模式

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到保存的状态。

# 优点

  • 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态

# 缺点

  • 消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。

# 应用

  • 分页控件
  • 撤销组件
//备忘类
class Memento{
  constructor(content){
    this.content = content
  }
  getContent(){
    return this.content
  }
}
// 备忘列表
class CareTaker {
  constructor(){
    this.list = []
  }
  add(memento){
    this.list.push(memento)
  }
  get(index){
    return this.list[index]
  }
}
// 编辑器
class Editor {
  constructor(){
    this.content = null
  }
  setContent(content){
    this.content = content
  }
  getContent(){
    return this.content
  }
  saveContentToMemento(){
    return new Memento(this.content)
  }
  getContentFromMemento(memento){
    this.content = memento.getContent()
  }
}

//测试代码
let editor = new Editor()
let careTaker = new CareTaker()

editor.setContent('111')
editor.setContent('222')
careTaker.add(editor.saveContentToMemento())
editor.setContent('333')
careTaker.add(editor.saveContentToMemento())
editor.setContent('444')

console.log(editor.getContent()) //444
editor.getContentFromMemento(careTaker.get(1))
console.log(editor.getContent()) //333

editor.getContentFromMemento(careTaker.get(0))
console.log(editor.getContent()) //222
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

# 解释器模式

给定一个语言, 定义它的文法的一种表示,并定义一个解释器, 该解释器使用该表示来解释语言中的句子。

# 优点

  • 易于改变和扩展文法。
  • 由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法

# 缺点

  • 执行效率较低,在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度慢
  • 对于复杂的文法比较难维护

# 应用

class Context {
    constructor() {
      this._list = []; // 存放 终结符表达式
      this._sum = 0; // 存放 非终结符表达式(运算结果)
    }
  
    get sum() {
      return this._sum;
    }
    set sum(newValue) {
      this._sum = newValue;
    }
    add(expression) {
      this._list.push(expression);
    }
    get list() {
      return [...this._list];
    }
  }
  
  class PlusExpression {
    interpret(context) {
      if (!(context instanceof Context)) {
        throw new Error("TypeError");
      }
      context.sum = ++context.sum;
    }
  }
  class MinusExpression {
    interpret(context) {
      if (!(context instanceof Context)) {
        throw new Error("TypeError");
      }
      context.sum = --context.sum;
    }
  }
  
  /** 以下是测试代码 **/
  const context = new Context();
  
  // 依次添加: 加法 | 加法 | 减法 表达式
  context.add(new PlusExpression());
  context.add(new PlusExpression());
  context.add(new MinusExpression());
  
  // 依次执行: 加法 | 加法 | 减法 表达式
  context.list.forEach(expression => expression.interpret(context));
  console.log(context.sum);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 参考链接

https://juejin.cn/post/6844904032826294286

https://juejin.cn/post/6844903607452581896

上次更新: 2022/04/15, 05:41:29
×