读书笔记 - javascript设计模式和开发实践

基础知识

动态类型语言和鸭子类型:

编程语言按照数据类型大体可以分为两类,一类是静态类型语言,另一类是动态类型语言。静态类型语言在编译时便已确定变量的类型,而动态类型语言的变量类型要到程序运行的时候,待变量被赋予某个值之后,才会具有某种类型。

静态类型语言的优点: 1. 在编译时就能发现类型不匹配的错误 2. 在程序中明确地规定了数据类型

静态类型语言的缺点: 1.必须依照强契约来编写程序 2. 类型的声明也会增加更多的代码

动态类型语言的优点: 1. 代码数量更少,2.简洁

动态类型语言的缺点: 1;无法保证变量的类型

鸭子类型: 如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子。

多态

多态:给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。
一段多肽的代码:

案例:例如
主人家里养了两只动物,分别是一只鸭和一只鸡,当主人向它们发出“叫”的命令时,鸭会“嘎嘎嘎”地叫,而鸡会“咯咯咯”地叫。这两只动物都会以自己的方式来发出叫声。它们同样“都是动物,并且可以发出叫声”,但根据主人的指令,它们会各自发出不同的叫声。

var makeSound = function (animal) {
	if (animal instanceof Duck) {
		console.log( '嘎嘎嘎' );
	} else if (animal instanceof Chicken) {
		console.log( '咯咯' );
	}
}
对象的多态性
function makeSound = function (animal) {
	animal.sound()
}

var Duck = function () {}
Duck.prototype.sound = function () {
	console.log( '嘎嘎嘎' );
}
var Chilken = function () {}
Chilken.prototype.sound = function () {
	console.log( '咯咯' );
}

makeSound(new Duck())
makeSound(new Chilken())
javascript的多态
var gooleMap = function () {
	show: function () {
		console.log( 'google地图开始渲染' );
	}
}
var baiduMap = function () {
	show: function () {
		console.log('百度地图开始渲染')
	}
}

var renderMap = function (map) {
	if (map.show instanceof Function) {
		map.show()
	}
}
renderMap(gooleMap)
renderMap(baiduMap)
封装

封装: 封装的目的是将信息隐藏。封装可以是:封装数据,封装实现,封装类型,封装变化。

  • 封装数据
var myObject = (function () {
	var _name = 'liz' //  私有(private)变量
	return { 
		getName: function () {return _name} // 公开(public)方法
	}
})() 

console.log( myObject.getName() ); // 输出:sven 
console.log( myObject.__name ) // 输出:undefined
  • 封装实现
    封装的目的是将信息隐藏,封装应该被视为“任何形式的封装”,也就是说,封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等。

例如:迭代器的封装

  • 封装类型
    例如:如工厂方法模式、组合模式

  • 封装变化
    封装变化:找到变化并封装之。

继承(设计模式 - 原型模式)
  • 使用克隆的原型模式
    从设计的角度讲,原型模式是用于创建对象的一种模式,如果我们想要创建一个对象,一种方式是先指定它的类型,然后通过类来创建这个对象。原型模式选择了另外一种方式,我们不再关心对象的具体类型,而是找到一个对象,然后通过克隆来创建一个一模一样的对象。

如果使用原型模式,我们只需要调用负责克隆的方法,便能完成同样的功能。原型模式的实现关键,是语言本身是否提供了clone方法。ECMAScript 5提供了Object.create方法,可以用来克隆对象。

案例:

var Plane = function() {
	this.blood = 100
	this.attackLevel = 1
	this.defenseLevel = 1
}
var plane = new Plane()
plane.blood = 500
plane.attackLevel = 10
plane.defenseLevel = 7

// 克隆对象
var clonePlane = Object.create(plane)
console.log( clonePlane ); // 输出:Object {blood: 500, attackLevel: 10, defenseLevel: 7}

在不支持 Object.create 方法的浏览器中,则可以使用以下代码:

Object.create = Object.create || function( obj ){
	var F = function () {}
	F.prototype = obj
	return new F()
}
  • 克隆是创建对象的手段
    原型模式的真正目的并非在于需要得到一个一模一样的对象,而是提供了一种便捷的方式去创建某个类型的对象,克隆只是创建这个对象的过程和手段。

  • javascript中的原型继承

事实上,JavaScript 中的根对象是 Object.prototype 对象。Object.prototype 对象是一个空的对象。我们在 JavaScript 遇到的每个对象,实际上都是从 Object.prototype 对象克隆而来的,Object.prototype 对象就是它们的原型。

在 JavaScript 语言里,我们并不需要关心克隆的细节,因为这是引擎内部负责实现的。我们所需要做的只是显式地调用 var obj1 = new Object()或者 var obj2 = {}。此时,引擎内部会从Object.prototype 上面克隆一个对象出来,我们最终得到的就是这个对象。

继承

var A = function() {}
A.prototype = {name: 'seven'}

var B = function () {}
B.prototype = new A()

var b = new B()
// 测试
console.log(b.name) // seven

我们来看看执行这段代码的时候,引擎做了哪些事情。
1.首先,尝试遍历对象 b 中的所有属性,但没有找到 name 这个属性。
2. 查找 name 属性的请求被委托给对象 b 的构造器的原型,它被 b.proto 记录着并且指向B.prototype,而 B.prototype 被设置为一个通过 new A()创建出来的对象。
3. 在该对象中依然没有找到 name 属性,于是请求被继续委托给这个对象构造器的原型A.prototype。
4. 在 A.prototype 中找到了 name 属性,并返回它的值。

原型继承的未来
class Animal{
	constructor(name) { this.name = name }
	getName() { return this.name }
}

class Dog extends Animal{
	constructor(props) {
		super(props)
	}
	speak() {
		return 'woof'
	}
}

var dog = new Dog('Scamp')
console.log(dog.getName() + ' says ' + dog.speak());

单例模式:

单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏 览器中的 window 对象等。在 JavaScript 开发中,单例模式的用途同样非常广泛。试想一下,当我 们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少 次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。

单例模式实现:

要实现一个标准的单例模式并不复杂,无非是用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。

方法1:

var Singleton = function(name) {
      this.name = name
      this.instance = null
    }
    Singleton.prototype.getName = function() {
      return this.name
    }
    Singleton.getInstance = function (name) {
      if (!this.instance) {
        this.instance = new Singleton(name)
      }
      return this.instance
    }
    // 测试:
    var a = Singleton.getInstance('sven1')
    var b = Singleton.getInstance('sven2')
    alert(a === b) // true

等同于:
但是下面的方法用到了匿名函数和自执行

 var Singleton = function (name) {
      this.name = name
    }
    Singleton.getInstance = (function() {
      var instance = null
      return function(name) {
        if (!instance) {
          instance = new Singleton(name)
        }
        return instance
      }
    })()
// 测试:
var a = Singleton.getInstance( 'sven1' ); 
var b = Singleton.getInstance( 'sven2' ); 
alert ( a === b ); // true

以上方式创建的单例有一个缺点:不透明性。因为:使用者:不一定知道这是个单例。跟以往通过 new XXX 的方式来获取对象不同,这里偏要使用 Singleton.getInstance 来获取对象。

透明的单例模式
var CreateDiv = (function() {
      var instance;
      var CreateDiv = function(html) {
        if (instance) {
          return instance
        }
        this.html = html
        this.init()
        return instance = this
      }
      CreateDiv.prototype.init = function() {
        var div = document.createElement('div')
        div.innerHTML = this.html
        document.body.appendChild(div)
      }
      return CreateDiv
    })()
    var a = new CreateDiv( 'sven1' ); 
    var b = new CreateDiv( 'sven2' ); 
    alert ( a === b ); // true

虽然现在完成了一个透明的单例类的编写,但它同样有一些缺点。
为了把 instance 封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的 Singleton 构造方法,这增加了一些程序的复杂度,阅读起来也不是很舒服。

var CreateDiv = function( html ){ 
	 if ( instance ){ 
	 	return instance; 
	 } 
	 this.html = html; 
	 this.init(); 
	 return instance = this; 
}; 

在这段代码中,CreateDiv 的构造函数实际上负责了两件事情。第一是创建对象和执行初始化 init 方法,第二是保证只有一个对象。虽然我们目前还没有接触过“单一职责原则”的概念,但可以明确的是,这是一种不好的做法,至少这个构造函数看起来很奇怪。

使用代理模式实现单例模式(重点):

// 创建 div 的类
 var CreateDiv = function(html) {
      this.html = html
      this.init()
}
CreateDiv.prototype.init = function() {
  var div = document.createElement( 'div' ); 
  div.innerHTML = this.html; 
  document.body.appendChild( div );
}

// 接下来引入代理类 proxySingletonCreateDiv:
var proxySingletonCreateDiv = function () {
  var instance;
  return function (html) {
    if (!instance) {
      instance = new CreateDiv(html)
    }
    return instance
  }
}
var a = new ProxySingletonCreateDiv( 'sven1' ); 
var b = new ProxySingletonCreateDiv( 'sven2' );
alert ( a === b );

通过引入代理类的方式,我们同样完成了一个单例模式的编写,跟之前不同的是,现在我们把负责管理单例的逻辑移到了代理类 proxySingletonCreateDiv 中。这样一来,CreateDiv 就变成了一个普通的类,它跟 proxySingletonCreateDiv 组合起来可以达到单例模式的效果。

单例模式的核心是确保只有一个实例,并提供全局访问。
全局变量不是单例模式

惰性单例
我们可以用一个变量来判断是否已经创建过登录浮窗
var createLoginLayer = (function(){ 
 	var div; 
	 return function(){ 
		 if ( !div ){ 
		 	div = document.createElement( 'div' ); 
		 	div.innerHTML = '我是登录浮窗'; 
		 	div.style.display = 'none'; 
		 	document.body.appendChild( div ); 
		 } 
		 return div; 
	 } 
})();

策略模式

策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

在程序设计中,我们也常常遇到类似的情况,要实现某一个功能有多种方案可以选择。比如一个压缩文件的程序,既可以选择 zip 算法,也可以选择 gzip 算法。这些算法灵活多样,而且可以随意互相替换。这种解决方案就是本章将要介绍的策略模式。

使用策略模式计算奖金:

我们可以编写一个名为 calculateBonus 的函数来计算每个人的奖金数额。很显然,calculateBonus 函数要正确工作,就需要接收两个参数:员工的工资数额和他的绩效考核等级。代码如下:

var caculateBouns = function(level, salary) {
	if (level === 'S') {
	   return salary * 4
	 }
	 if (level === 'A') {
	   return salary * 3
	 }
	 if (level === 'B') {
	   return salary * 2
	 }
}
caculateBouns('B', 2000)
caculateBouns('A', 6000)

这样写代码的缺点:

  1. calculateBonus 函数比较庞大,包含了很多 if-else 语句,这些语句需要覆盖所有的逻辑
    分支。
  2. calculateBonus 函数缺乏弹性,如果增加了一种新的绩效等级 C,或者想把绩效 S 的奖金
    系数改为 5,那我们必须深入 calculateBonus 函数的内部实现,这是违反开放封闭原则的。
  3. 算法的复用性差,如果在程序的其他地方需要重用这些计算奖金的算法呢?我们的选择
    只有复制和粘贴。
    因此,我们需要重构这段代码。
使用组合函数重构代码
var performanceS = function(leval, salary) {
  return salary * 4
}
var performanceA = function(salary) {
  return salary * 3
}
var performanceB = function(salary) {
  return salary * 2
}
var caculateBouns = function(level, salary) {
 	if (leval === 'S') return performanceS(salary)
 	if (leval === 'A') return performanceA(salary)
 	if (leval === 'B') return performanceB(salary)
}
// 测试:
caculateBouns('B', 2000)
caculateBouns('A', 6000)

目前,我们的程序得到了一定的改善,但这种改善非常有限,我们依然没有解决最重要的问题:calculateBonus 函数有可能越来越庞大,而且在系统变化的时候缺乏弹性。

使用策略模式重构代码

刚刚我们用策略模式重构了这段计算年终奖的代码,可以看到通过策略模式重构之后,代码
变得更加清晰,各个类的职责更加鲜明。但这段代码是基于传统面向对象语言的模仿,下一节我
们将了解用 JavaScript 实现的策略模式。

javascript版本的策略模式

上面的案例中我们让 strategy 对象从各个策略类中创建而来,这是模拟一些传统面向对象语言的实现。实际上在 JavaScript 语言中,函数也是对象,所以更简单和直接的做法是把 strategy直接定义为函数:

var  strategies = {
	"S": function (salary) {
		return salary * 4
	},
	"A": function (salary) {
		return salary * 3
	},
	"B": function (salary) {
		return salary * 2
	},
}

var calculateBonus = function( level, salary ){ 
 return strategies[ level ]( salary ); 
}; 
// 测试
console.log( calculateBonus( 'S', 20000 ) ); // 输出:80000 
console.log( calculateBonus( 'A', 10000 ) ); // 输出:30000

在接下来的缓动动画和表单验证的例子中,我们用到的都是这种函数形式的策略对象。

表单校验

在一个 Web 项目中,注册、登录、修改用户信息等功能的实现都离不开提交表单。在将用户输入的数据交给后台之前,常常要做一些客户端力所能及的校验工作,比如注册的时候需要校验是否填写了用户名,密码的长度是否符合规定,等等。这样可以避免因为提交不合法数据而带来的不必要网络开销。

假设我们正在编写一个注册的页面,在点击注册按钮之前,有如下几条校验逻辑。
 用户名不能为空。
 密码长度不能少于 6 位。
 手机号码必须符合格式。

  • 表单校验的第一个版本:
    现在编写表单校验的第一个版本,可以提前透露的是,目前我们还没有引入策略模式。代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <form action="http://xxx.com/register" id="registerForm" method="post">
    请输入用户名:<input type="text" name="userName"></input>
    请输入密码:<input type="text" name="password"></input>
    请输入手机号码:<input type="text" name="phone"></input>
    <button>提交</button>
  </form>
  <script>
    var registerForm = document.getElementById('registerForm')
    registerForm.onsubmit = function() {
      if (registerForm.userName.value === '') {
        alert('用户名不能为空')
        return false
      }
      if (registerForm.password.value.length < 6) {
        alert ( '密码长度不能少于 6 位' ); 
        return false;
      }
      if ( !/(^1[3|5|8][0-9]{9}$)/.test( registerForm.phoneNumber.value ) ){ 
        alert ( '手机号码格式不正确' )
        return false
      }
    }
  </script>
</body>
</html>

这是一种很常见的代码编写方式,它的缺点跟计算奖金的最初版本一模一样。
 registerForm.onsubmit 函数比较庞大,包含了很多 if-else 语句,这些语句需要覆盖所有
的校验规则。
 registerForm.onsubmit 函数缺乏弹性,如果增加了一种新的校验规则,或者想把密码的长
度校验从 6 改成 8,我们都必须深入 registerForm.onsubmit 函数的内部实现,这是违反开
放—封闭原则的。
 算法的复用性差,如果在程序中增加了另外一个表单,这个表单也需要进行一些类似的
校验,那我们很可能将这些校验逻辑复制得漫天遍野。

  • 用策略模式重构表单校验:
    下面我们将用策略模式来重构表单校验的代码,很显然第一步我们要把这些校验逻辑都封装
    成策略对象:
var strategies = {
     // 不为空
	isNonEmpty: function (value, errorMsg) {
		if (value === '') {
			return errorMsg
		}
	},
	// 限制最小长度
	minLength: function (value, length, errorMsg) {
		if (value.length < length) {
			return errorMsg
		}
	},
	isMobile: function (value, errorMsg) {
		// 手机号码格式
		if ( !/(^1[3|5|8][0-9]{9}$)/.test( value ) ){
			return errorMsg
		}
	}
}


接下来我们准备实现 Validator 类。Validator 类在这里作为 Context,负责接收用户的请求
并委托给 strategy 对象。在给出 Validator 类的代码之前,有必要提前了解用户是如何向 Validator
类发送请求的,这有助于我们知道如何去编写 Validator 类的代码。代码如下:

var validateFunc = function () {
	// // 创建一个 validator 对象
	var validator = new  Validator()
	
	/***************添加一些校验规则****************/
	validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空' ); 
 	validator.add( registerForm.password, 'minLength:6', '密码长度不能少于 6 位' ); 
 	validator.add( registerForm.phoneNumber, 'isMobile', '手机号码格式不正确' );
	
	var errorMsg = validator.start(); // 获得校验结果
    return errorMsg; // 返回校验结果
}


 var registerForm = document.getElementById( 'registerForm' ); 
 registerForm.onsubmit = function(){ 
	 var errorMsg = validataFunc(); // 如果 errorMsg 有确切的返回值,说明未通过校验
	 if ( errorMsg ){ 
	 	alert ( errorMsg ); 
	 	return false; // 阻止表单提交
	 } 
};

从这段代码中可以看到,我们先创建了一个 validator 对象,然后通过 validator.add 方法,往 validator 对象中添加一些校验规则。validator.add 方法接受 3 个参数.
当我们往 validator 对象里添加完一系列的校验规则之后,会调用 validator.start()方法来启动校验。如validator.start()返回了一个确切的 errorMsg 字符串当作返回值,说明该次校验没有通过,此时需让 registerForm.onsubmit 方法返回 false 来阻止表单的提交。

最后是 Validator 类的实现:

var Validator = function () {
	this.cache = [] // 保存校验规则
}
Validator.Prototype.add = function (dom, rule errorMsg) {
	var ary = rule.split(";") // 把 strategy 和参数分开
	this.cache.push(function () { // 把校验的步骤用空函数包装起来,并且放入 cache
		var strategy = ary.shift() //  用户挑选的 strategy
		ary.unshift(dom.value) // 把 input 的 value 添加进参数列表
		ary.push(errorMsg) //  把 errorMsg 添加进参数列表
		return  strategies[strategy].apply(dom, ary)
	})
}
Validator.prototype.start = function () {
	for (var i = 0, validatorFunc; validatorFunc = this.cache[i++]){
		var msg = validatorFunc() // 开始校验,并取得校验后的返回信息
	}

}

  • 给某个文本输入框添加多种校验规则
    目前我们的表单校验实现留有一点小遗憾:一个文本输入框只能对应一种校验规则,比如,用户名输入框只能校验输入是否为空:
validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空' );

如果我们既想校验它是否为空,又想校验它输入文本的长度不小于 10 呢?我们期望以这样的形式进行校验:

validator.add( registerForm.userName, 
	[{ 
	 	strategy: 'isNonEmpty', 
	 	errorMsg: '用户名不能为空'
	 }, { 
	 	strategy: 'minLength:6', 
	 	errorMsg: '用户名长度不能小于 10 位' 
	 }]
 );

下面提供的代码可用于一个文本输入框对应多种校验规则:

<script>
	/***********************策略对象**************************/
	var strategies = {
     // 不为空
	isNonEmpty: function (value, errorMsg) {
		if (value === '') {
			return errorMsg
		}
	},
	// 限制最小长度
	minLength: function (value, length, errorMsg) {
		if (value.length < length) {
			return errorMsg
		}
	},
	isMobile: function (value, errorMsg) {
		// 手机号码格式
		if ( !/(^1[3|5|8][0-9]{9}$)/.test( value ) ){
			return errorMsg
		}
	}
}

/***********************Validator 类**************************/
var Validator = function () {
	this.cache = []
}
Validator.prototype.add = function (dom, rules) {
	var self = this
	for (let i = 0; i < rules.length; i++) {
		const rule = rules[i]
		var strategyAry = rule.strategy.split( ':' ); 
	 	var errorMsg = rule.errorMsg;
	 	self.cache.push(function () {
			var strategy = strategyAry.shift();
			strategyAry.unshift( dom.value );
			strategyAry.push( errorMsg );
			return strategies[ strategy ].apply( dom, strategyAry );
		})
	}
}
Validator.prototype.start = function () {
	for (let i = 0, validatorFunc = this.cache.length ; i <validatorFunc; i++ ) {
		var errorMsg = validatorFunc();
		if ( errorMsg ){ 
 			return errorMsg; 
 		}
	}
}

/***********************客户调用代码**************************/
var registerForm = document.getElementById( 'registerForm' );
var validataFunc = function(){ 
    var validator = new Validator();
	validator.add( registerForm.userName, [{ 
	 	strategy: 'isNonEmpty', 
	 	errorMsg: '用户名不能为空' 
	 }, { 
	 	strategy: 'minLength:6', 
	 	errorMsg: '用户名长度不能小于 10 位' 
	 }]); 
	 validator.add( registerForm.password, [{ 
	 	strategy: 'minLength:6', 
	 	errorMsg: '密码长度不能小于 6 位' 
	 }]); 
	 validator.add( registerForm.phoneNumber, [{ 
	 	strategy: 'isMobile', 
	 	errorMsg: '手机号码格式不正确' 
	 }]); 
	 var errorMsg = validator.start(); 
	 return errorMsg; 
 }
 // 提交表单
registerForm.onsubmit = function () {
	var errorMsg = validataFunc()
	if (errorMsg) {
		alert(errorMsg)
		return false
	}
}

</script>
策略模式的优缺点

策略模式是一种常用且有效的设计模式,本章提供了计算奖金、缓动动画、表单校验这三个例子来加深大家对策略模式的理解。从这三个例子中,我们可以总结出策略模式的一些优点。
 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它
们易于切换,易于理解,易于扩展。
 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
 在策略模式中利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻
便的替代方案。
当然,策略模式也有一些缺点,但这些缺点并不严重。
首先,使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的逻辑堆砌在 Context 中要好。
其次,要使用策略模式,必须了解所有的 strategy,必须了解各个 strategy 之间的不同点,这样才能选择一个合适的 strategy。比如,我们要选择一种合适的旅游出行路线,必须先了解选择飞机、火车、自行车等方案的细节。此时 strategy 要向客户暴露它的所有实现,这是违反最少知识原则的。

一等函数对象与策略模式

strategy 就是值为函数的变量。

var S = function( salary ){ 
 	return salary * 4; 
}; 
var A = function( salary ){ 
 	return salary * 3; 
}; 
var B = function( salary ){ 
 	return salary * 2; 
}; 
var calculateBonus = function( func, salary ){ 
 	return func( salary ); 
}; 
calculateBonus( S, 10000 ); // 输出:40000

代理模式

迭代器模式

发布订阅模式

发布订阅模式: dom的自定义事件

class EventEmitter{
	constructor() {
		this.subs = {} // { onclick: [fn1, fn2] }
	}
	/** 订阅事件
	* eventType: 事件类型
	* fn:处理函数	
	*/
	listener(eventType, fn) {
		this.subs[eventType] = this.subs[eventType] || []
		this.subs[eventType].push(fn)
	}
	// 触发事件
	trigger(eventType) {
		if (this.subs[eventType].length) {
			this.subs[eventType].forEach((fn) => {
				fn(arguments)
			})
		}
	}
	// 移除事件监听
	remove(eventType, fn) {
		var eventType = this.subs[eventType]
		if (!eventType.lenght) return false  // 如果 对应的事件没有被人订阅,则直接返回
		if (!fn) eventType.lenght = 0 // 如果没有传入具体的回调函数,表示需要取消 key 对应消息的所有订阅
		for (let i = 0; i < eventType.length; i++) {
			var _fn = eventType[i]
			if (_fn === fn) { 
				eventType.splice(i, 1) // 删除订阅者的回调函数
			}
		}
		
	}
}
必须先订阅再发布吗

不是的。
我们所了解到的发布—订阅模式,都是订阅者必须先订阅一个消息,随后才能接收到发布者发布的消息。如果把顺序反过来,发布者先发布一条消息,而在此之前并没有对象来订阅它,这条消息无疑将消失在宇宙中。

但是在某些情况下,我们需要先将这条消息保存下来,等到有对象来订阅它的时候,再重新把消息发布给订阅者。就如同 QQ 中的离线消息一样,离线消息被保存在服务器中,接收人下次登录上线之后,可以重新收到这条消息。

为了满足某种需求,我们要建立一个存放离线事件的堆栈,当事件发布的时候,如果此时还没有订阅者来订阅这个事件,我们暂时把发布事件的动作包裹在一个函数里,这些包装函数将被存入堆栈中,等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。当然离线事件的生命周期只有一次,就像 QQ 的未读消息只会被重新阅读一次,所以刚才的操作我们只能进行一次。

职责链模式

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

实际开发中的职责链模式

假设我们负责一个售卖手机的电商网站,经过分别交纳 500 元定金和 200 元定金的两轮预定后(订单已在此时生成),现在已经到了正式购买的阶段。

公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过 500 元定金的用户会收到 100 元的商城优惠券,200 元定金的用户可以收到 50 元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没
有优惠券,且在库存有限的情况下不一定保证能买到。

后端返回的数据字段:
orderType:表示订单类型(定金用户或者普通购买用户),code 的值为 1 的时候是 500 元定金用户,为 2 的时候是 200 元定金用户,为 3 的时候是普通购买用户。
 pay:表示用户是否已经支付定金,值为 true 或者 false, 虽然用户已经下过 500 元定金的订单,但如果他一直没有支付定金,现在只能降级进入普通购买模式。
 stock:表示当前用于普通购买的手机库存数量,已经支付过 500 元或者 200 元定金的用户不受此限制。

使用职责链模式重构代码:

var order500 = function (orderType, pay, stock) {
	if (orderType === 1 && pay === true) {
		console.log('500 元定金预购, 得到 100 优惠券')
	} else {
		order200(orderType, pay, stock) //  将请求传递给 200 元订单
	}
}

var order200 = function (orderType, pay, stock) {
	if (orderType === 2 && pay === true) {
		console.log('200 元定金预购, 得到 50 优惠券')
	} else {
		if (stock > 0) {
			console.log('普通购买')
		} else {
			console.log( '手机库存不足' )
		}
	}
}

// 测试结果:
order500( 1 , true, 500); // 输出:500 元定金预购, 得到 100 优惠券
order500( 1, false, 500 ); // 输出:普通购买, 无优惠券
order500( 2, true, 500 ); // 输出:200 元定金预购, 得到 500 优惠券
order500( 3, false, 500 ); // 输出:普通购买, 无优惠券
order500( 3, false, 0 ); // 输出:手机库存不足
灵活可拆分的职责链节点

本节我们采用一种更灵活的方式,来改进上面的职责链模式,目标是让链中的各个节点可以灵活拆分和重组。
首先需要改写一下分别表示 3 种购买模式的节点函数,我们约定,如果某个节点不能处理请求,则返回一个特定的字符串 'nextSuccessor’来表示该请求需要继续往后面传递:

function order500 = function (orderType, pay, stock) {
	
}

中介者模式

中介者模式的作用就是:解决对象与对象之间的紧耦关系。增加一个中介对象后,所有相关对象都通过中介者对象来通信,而不是相互引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使对象之间耦合松散,而
且可以独立地改变他们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系。

普通模式:
读书笔记 - javascript设计模式和开发实践_第1张图片

中介者模式:
读书笔记 - javascript设计模式和开发实践_第2张图片

现实中的中介者
  1. 机场指挥塔
    中介者也被称为调停者,我们想象一下机场的指挥塔,如果没有指挥塔的存在,每一架飞机要和方圆 100 公里内的所有飞机通信,才能确定航线以及飞行状况,后果是不可想象的。现实中的情况是,每架飞机都只需要和指挥塔通信。指挥塔作为调停者,知道每一架飞机的飞行状况,所以它可以安排所有飞机的起降时间,及时做出航线调整。

  2. 博彩公司

打麻将的人经常遇到这样的问题,打了几局之后开始计算钱,A 自摸了两把,B 杠了三次,C 点炮一次给 D,谁应该给谁多少钱已经很难计算清楚,而这还是在只有 4 个人参与的情况下。
在世界杯期间购买足球彩票,如果没有博彩公司作为中介,上千万的人一起计算赔率和输赢绝对是不可能实现的事情。有了博彩公司作为中介,每个人只需和博彩公司发生关联,博彩公司会根据所有人的投注情况计算好赔率,彩民们赢了钱就从博彩公司拿,输了钱就交给博彩公司。

中介者模式的例子——泡泡堂游戏
// 玩家类
function Player(name, teamColor) {
	this.name = name // 角色名字
	this.teamColor = teamColor // 队伍颜色
	this.state = 'alive'// 玩家生存状态
}
Player.prototype.win = function () {
	console.log(this.name + 'win')
}
Player.prototype.lose = function () {
	console.log(this.name + 'lose')
}

/*******************玩家死亡*****************/ 
Player.prototype.die = function () {
	this.state = 'dead'
	playerDirector.reciveMessage('playerDead', this) // 给中介者发消息,玩家死亡
}

/*******************移除玩家*****************/
Player.prototype.remove = function () {
	playerDirector.reciveMessage('removePlayer', this) // 给中介者发消息,移除玩家
}

/*******************玩家换队*****************/
Player.prototype.changeTeam = function () {
	playerDirector.reciveMessage('changeTeam', this)
}

// 创建中介者类
var playerDirector = (function() {
	var players = {} //保存所有的玩家 {teamColor: [player1, player2]} // teamColor:队伍
	var options = {} // 中介者可以执行的操作
	 
	 /****************新增一个玩家***************************/
	 options.addPlayer = function (player) {
		var teamColor = player.teamColor // 玩家的队伍颜色
		players[teamColor] = players[teamColor] || [] //  如果该颜色的玩家还没有成立队伍,则新成立一个队伍
		players[teamColor].push(player) // 添加玩家进队伍
	}
	/****************移除一个玩家***************************/
	options.removePlayer = function (player) {
		var teamColor = player.teamColor // 玩家的队伍颜色
		var teamPlayers = players[teamColor] || [] // 获取当前队伍的所有的玩家
		for (let i = 0; i < teamPlayers.length; i++) {
		  const item = teamPlayers[i]
			if (item === player) {
				teamPlayers.splice(i, 1)  // 删除当前玩家
			}
		}
	}
	/****************玩家换队***************************/
	options.changeTeam = function (player, newTeamColor) {
		options.removePlayer(player) // 从原来的队伍移除
		player.teamColor = newTeamColor // 更换玩家的队伍
		options.addPlayer(player) // 增加到新队伍中
	}
	/****************玩家死亡***************************/
	options.playerDead = function (player) {
		var teamColor = player.teamColor
		var teamPlayers = players[teamColor] || [] // 玩家所在的队伍
		
		var all_dead = true
		for (let i = 0; i <teamPlayers.length; i++) {
			const player = teamPlayers[i]
			if (player.state !=='dead') {
				all_dead = false
				break
			}
		}
		// 本队全部死亡:输出自己队伍lose,其他所有队伍win
		if (all_dead === true) {
			for (let i = 0; i <teamPlayers.length; i++) {
				const player = teamPlayers[i]
				player.lose() // 本队所有玩家 lose
			}
			for (let color in players) {
				if (color !== teamColor) {
					var teamPlayers = players[ color ]; // 其他队伍的玩家
					for (let i = 0; i< teamPlayers.length; i++) {
						const player = teamPlayers[i]
						player.win() //  其他队伍所有玩家 win 
					}
				}
			}
		}
		
	}

  var reciveMessage = function() {
    var message = Array.prototype.shift.call(arguments)
    options[message].apply(this, arguments)
  }
  return {
    reciveMessage
  }

})()


测试添加玩家:

//测试数据
// 红队:
var player1 = playerFactory( '皮蛋', 'red' ),
 player2 = playerFactory( '小乖', 'red' ),
 player3 = playerFactory( '宝宝', 'red' ),
 player4 = playerFactory( '小强', 'red' );
// 蓝队:
var player5 = playerFactory( '黑妞', 'blue' ),
 player6 = playerFactory( '葱头', 'blue' ),
 player7 = playerFactory( '胖墩', 'blue' ),
 player8 = playerFactory( '海盗', 'blue' );
player1.die();
player2.die();
player3.die();
player4.die(); 

读书笔记 - javascript设计模式和开发实践_第3张图片

测试:假设皮蛋和小乖掉线

player1.remove();
player2.remove();
player3.die();
player4.die(); 

读书笔记 - javascript设计模式和开发实践_第4张图片

假设皮蛋从红队叛变到蓝队

player1.changeTeam( 'blue' );
player2.die();
player3.die();
player4.die(); 

读书笔记 - javascript设计模式和开发实践_第5张图片

中介者模式的例子——购买商品
<body>
	 选择颜色: <select id="colorSelect">
	 <option value="">请选择</option>
	 <option value="red">红色</option>
	 <option value="blue">蓝色</option>
	 </select> 
	
	选择内存: <select id="memorySelect">
	 <option value="">请选择</option>
	 <option value="32G">32G</option>
	 <option value="16G">16G</option>
	 </select>
	 
	输入购买数量: <input type="text" id="numberInput"/><br/>
	
	// 信息展示
	您选择了颜色: <div id="colorInfo"></div><br/>
	您选择了内存: <div id="memoryInfo"></div><br/>
	您输入了数量: <div id="numberInfo"></div><br/>
	<button id="nextBtn" disabled="true">请选择手机颜色和购买数量</button> 
</body>

<script>
	// 手机库存 
	var goods = {
		'red|32G': 3, // 红色32G,库存数量为3
		'red|16G': 0,
		'blue|32G': 1,
		'blue|16G': 6,
	}
	// 中介者
	var mediator = (function () {		
		var colorSelect = document.getElementById('colorSelect')
		var numberInput = document.getElementById('numberInput')
		var memorySelect = document.getElementById('memorySelect')
		var colorInfo = document.getElementById(colorInfo)
		var memoryInfo = document.getElementById(memoryInfo)
		var numberInfo = document.getElementById(numberInfo)
		var nextBtn = document.getElementById(nextBtn)
	
		return {
			changed: function (obj) {		
				var color = colorSelect.value
				var memory = memorySelect.value
				var number = numberInput.value // 用户输入的手机库存数量
				var stocks = goods[color + '|' + memory] // 手机库存实际数量
				if(obj === colorSelect){
					colorInfo.innerHTML = color
				} else if(obj === numberInput){
					numberInfo.innerHTML = number
				} else if(obj === memorySelect){
					memoryInfo.innerHTML = memory
				}
				
				if (!color) {
					nextBtn.disabled = true
					nextBtn.innerHTML = "请选择手机颜色"
				}
				if (!memory) {
					nextBtn.disabled = true
					nextBtn.innerHTML = "请选择手机内存"
				}
				if ( ( ( number - 0 ) | 0 ) !== number - 0) {
					nextBtn.disabled = true
					nextBtn.innerHTML = "请选择手机购买数量"
				}
				nextBtn.diabled = false
				nextBtn.innerHTML = "请放入购物车"
			}
		}
	})()
	
	colorSelect.onchange = function () {
		mediator.changed(this)
	}
	numberInput.onchange = function () {
		mediator.changed(this)
	}
	memorySelect.onchange = function () {
		mediator.changed(this)
	}
</script>

装饰器模式

装饰器模式:就是将i 个对象嵌入另一个对象之中,实际上相当于这个对象被另一个对象包装起来,形成一条包装链。

var flane = {
	fire: function () {
		console.log('发射普通子弹')
	}
}
var missleDecorator = function () {
	console.log('发射导弹')
}
var atomDecorator = function () {
	console.log('发射原子弹')
}

var fire1 = plane.fire
plane.fire = function () {
	fire1()
	missleDecorator()
}

var fire2 = plane.fire
plane.fire = function () {
	fire2()
	atomDecorator()
}

状态模式

状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。

状态模式的定义:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

例如:开关。按一下:打开,再按一下:关闭。

代码模拟:

var Light = function(){ 
 this.state = 'off'; // 给电灯设置初始状态 off 
 this.button = null; // 电灯开关按钮
};
接下来定义 Light.prototype.init 方法,该方法负责在页面中创建一个真实的 button 节点,
假设这个 button 就是电灯的开关按钮,当 button 的 onclick 事件被触发时,就是电灯开关被按下的时候,代码如下:

Light.prototype.init = function(){ 
 var button = document.createElement( 'button' )
 var self = this; 
 button.innerHTML = '开关'; 
 this.button = document.body.appendChild( button ); 
 this.button.onclick = function(){ 
 	self.buttonWasPressed(); 
 } 
};

当开关被按下时,程序会调用 self.buttonWasPressed 方法, 开关按下之后的所有行为,
都将被封装在这个方法里,代码如下:

Light.prototype.buttonWasPressed = function(){ 
 if ( this.state === 'off' ){ 
 	console.log( '开灯' ); 
 	this.state = 'on'; 
 }else if ( this.state === 'on' ){ 
 	console.log( '关灯' ); 
 	this.state = 'off'; 
 } 
};

var light = new Light(); 
light.init();

令人遗憾的是,这个世界上的电灯并非只有一种。许多酒店里有另外一种电灯,这种电灯也只有一个开关,但它的表现是:第一次按下打开弱光,第二次按下打开强光,第三次才是关闭电灯。现在必须改造上面的代码来完成这种新型电灯的制造:

Light.prototype.buttonWasPressed = function(){ 
 if ( this.state === 'off' ){ 
 	console.log( '弱光' ); 
 	this.state = 'weakLight'; 
 }else if ( this.state === 'weakLight' ){ 
 	console.log( '强光' ); 
 	this.state = 'strongLight'; 
 }else if ( this.state === 'strongLight' ){ 
 	console.log( '关灯' ); 
 	this.state = 'off'; 
 } 
};
状态模式的优缺点

状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
 避免 Context 无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了 Context 中原本过多的条件分支。
 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
 Context 中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。状态模式的缺点是会在系统中定义许多状态类,编写 20 个状态类是一项枯燥乏味的工作,而且系统中会因此而增加不少对象。另外,由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑。

状态模式和策略模式的关系

状态模式和策略模式像一对双胞胎,它们都封装了一系列的算法或者行为,它们的类图看起来几乎一模一样,但在意图上有很大不同,因此它们是两种迥然不同的模式。

策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行。

它们之间的区别是策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在。

JavaScript 版本的状态机
var Ligth = function () {
      this.currState = FSM.off // 设置当前状态
      this.button = null
    }
    Ligth.prototype.init = function() {
      var button = document.createElement('button')
      var self = this
      button.innerHTML = '已关灯'
      this.button.onClick = function () {
        self.currState.buttonWasPressed.call(self) // 把请求委托给 FSM 状态机
      }
    }

    var FSM = {
      off: {
        buttonWasPressed: function() {
          console.log('关灯')
          this.button.innerHTML = '下一次按我是开灯'
          this.currState = FSM.on
        }
      },
      on: {
        buttonWasPressed: function() {
          console.log('开灯')
          this.button.innerHTML = '下一次按我是关灯'
          this.currState = FSM.off
        }
      }
    }

    var lignt = new Ligth()
    lignt.init()

接下来尝试另外一种方法,即利用下面的 delegate 函数来完成这个状态机编写。这是面向对象设计和闭包互换的一个例子,前者把变量保存为对象的属性,而后者把变量封闭在闭包形成的环境中:

var delegate = function (client, delegation) {
      return {
        buttonWasPressed: function() {
          // 将客户的操作委托给 delegation 对象
          return delegation.buttonWasPressed.apply(client, arguments)
        }
      }
    }

    var FSM = {
      off: {
        buttonWasPressed: function() {
          console.log('关灯')
          this.button.innerHTML = '下一次按我是开灯'
          this.currState = FSM.on
        }
      },
      on: {
        buttonWasPressed: function() {
          console.log('开灯')
          this.button.innerHTML = '下一次按我是关灯'
          this.currState = FSM.off
        }
      }
    }
    var Ligth = function() {
      this.offState = delegate(this, FSM.off)
      this.onState = delegate(this, FSM.on)
      this.currState = this.offState // 设置初始状态为关闭状态
      this.button = null
    }
    Ligth.prototype.init = function() {
      var button = document.createElement('button')
      var self = this
      button.innerHTML = '已关灯'
      this.button = document.body.createElement(button)
      this.button.onclick = function() {
        self.currState.buttonWasPressed()
      }
    }
    var lignt = new Ligth()
    lignt.init()
表驱动的有限状态机

其实还有另外一种实现状态机的方法,这种方法的核心是基于表驱动的。我们可以在表中很清楚地看到下一个状态是由当前状态和行为共同决定的。这样一来,我们就可以在表中查找状态,而不必定义很多条件分支。
读书笔记 - javascript设计模式和开发实践_第6张图片

var fsm = StateMachine.create({ 
	 initial: 'off', 
	 events: [ 
	 	{ name: 'buttonWasPressed', from: 'off', to: 'on' }, 
	 	{ name: 'buttonWasPressed', from: 'on', to: 'off' } 
	 ], 
	 callbacks: { 
	 	onbuttonWasPressed: function( event, from, to ){ 
	 		console.log( arguments ); 
	 	} 
	 }, 
	 error: function( eventName, from, to, args, errorCode, errorMessage ) { 
	 	console.log( arguments ); // 从一种状态试图切换到一种不可能到达的状态的时候
	 } 
}); 
button.onclick = function(){ 
	fsm.buttonWasPressed(); 
}
实际项目中的其他状态机

在实际开发中,很多场景都可以使用状态机来模拟,比如一个菜单在hover动作下有显示,悬浮,隐藏等状态;一次 TCP 请求有建立连接、监听、关闭等状态;一个格斗游戏中人物有攻击、防御、跳跃、跌倒等状态。
状态机在游戏开发中也有着广泛的用途,特别是游戏 AI 的逻辑编写。在我曾经开发的HTML5 版街头霸王游戏里,游戏主角 Ryu 有走动、攻击、防御、跌倒、跳跃等多种状态。这些状态之间既互相联系又互相约束。比如 Ryu 在走动的过程中如果被攻击,就会由走动状态切换为跌倒状态。在跌倒状态下,Ryu 既不能攻击也不能防御。同样,Ryu 也不能在跳跃的过程中切换到防御状态,但是可以进行攻击。这种场景就很适合用状态机来描述。代码如下:

var FSM = { 
	 walk: { 
		 attack: function(){ 
		 	console.log( '攻击' ); 
		 }, 
		 defense: function(){ 
		 	console.log( '防御' ); 
		 }, 
		 jump: function(){ 
		 	console.log( '跳跃' ); 
		 } 
	 }, 
	 attack: { 
		 walk: function(){ 
		 	console.log( '攻击的时候不能行走' ); 
		 }, 
		 defense: function(){ 
		 	console.log( '攻击的时候不能防御' ); 
		 }, 
		 jump: function(){ 
		 	console.log( '攻击的时候不能跳跃' ); 
		 } 
	 } 
}

适配器模式

现实中的适配器

港式插头转换器:港式的电器插头比大陆的电器插头体积要大一些。如果从香港买了一个 Mac book,我们会发现充电器无法插在家里的插座上,为此而改造家里的插座显然不方便,所以我们需要一个适配器。

电源适配器:Mac book 电池支持的电压是 20V,我们日常生活中的交流电压一般是 220V。除了我们了解的 220V 交流电压,日本和韩国的交流电压大多是 100V,而英国和澳大利亚的是 240V。笔记本电脑的电源适配器就承担了转换电压的作用,电源适配器使笔记本电脑在 100V~240V 的电压之内都能正常工作,这也是它为什么被称为电源“适配器”的原因。

USB 转接口: 在以前的电脑上,PS2 接口是连接鼠标、键盘等其他外部设备的标准接口。但随着技术的发展,越来越多的电脑开始放弃了 PS2 接口,转而仅支持 USB 接口。所以那些过去生产出来的只拥有 PS2 接口的鼠标、键盘、游戏手柄等,需要一个 USB 转接口才能继续正常工作,这是 PS2-USB适配器诞生的原因。

适配器模式的应用

如果现有的接口已经能够正常工作,那我们就永远不会用上适配器模式。适配器模式是一种“亡羊补牢”的模式,没有人会在程序的设计之初就使用它。因为没有人可以完全预料到未来的事情,也许现在好好工作的接口,未来的某天却不再适用于新系统,那么我们可以用适配器模式把旧接口包装成一个新的接口,使它继续保持生命力。

例如:当我们向 googleMap 和 baiduMap 都发出“显示”请求时,googleMap和 baiduMap 分别以各自的方式在页面中展现了地图:

var gooleMap = {
	show: function () {
		console.log( '开始渲染谷歌地图' );
	},
}
var baiduMap = {
	show: function () {
		console.log( '开始渲染百度地图' );
	},
}

var renderMap = function( map ){ 
 	if ( map.show instanceof Function ){ 
 		map.show(); 
 	} 
}; 
renderMap( googleMap ); // 输出:开始渲染谷歌地图
renderMap( baiduMap ); // 输出:开始渲染百度地图

这段程序得以顺利运行的关键是 googleMap 和 baiduMap 提供了一致的 show 方法,但第三方的接口方法并不在我们自己的控制范围之内,假如 baiduMap 提供的显示地图的方法不叫 show 而叫display 呢?baiduMap 这个对象来源于第三方,正常情况下我们都不应该去改动它。此时我们可以通过增加 baiduMapAdapter 来解决问题:

增加适配代码:
var baiduMapAdapter = { 
 	show: function(){ 
 		return baiduMap.display();
	}
}
renderMap( googleMap ); // 输出:开始渲染谷歌地图
renderMap( baiduMapAdapter ); // 输出:开始渲染百度地图
总结:

适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实现的,也不考虑它们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够使它们协同作用。
 装饰者模式和代理模式也不会改变原有对象的接口,但装饰者模式的作用是为了给对象增加功能。装饰者模式常常形成一条长的装饰链,而适配器模式通常只包装一次。代理模式是为了控制对对象的访问,通常也只包装一次。
 外观模式的作用倒是和适配器比较相似,有人把外观模式看成一组对象的适配器,但外观模式最显著的特点是定义了一个新的接口。

享元模式

模版方法模式

组合模式

命令模式

你可能感兴趣的:(javascript,javascript,开发语言,ecmascript)