# 一。面对对象的 JavaScript
# 1.1 动态类型语言和鸭子模型
俗称:如果他像鸭子,那他就是鸭子
const duck = { | |
duckSing:function(){ | |
console.log("嘎嘎嘎"); | |
} | |
} | |
const chicken = { | |
duckSing:function(){ | |
console.log("嘎嘎嘎"); | |
} | |
} | |
const choir =[]// 合唱团 | |
let joinChoir = function(){ | |
if(animal&&typeof animal,duckSing === 'function'){ | |
choir.push(animal) | |
console.log("成功加入合唱团"); | |
console.log("合唱团成员 + choir.length"); | |
} | |
} | |
joinChoir(duck); | |
joinChoir(chicken) |
# 1.2 多态
# 1.2.1 一段 “多态” 的 JavaScript 代码
这个词源于希腊文 polymorphism,拆开来看就是 poly(复数)+morph(形态)+ism,字面理解就是复数形态
let Duck = function(){} | |
let Chicken = function(){} | |
let makeSounding = function(animal){ | |
if(animal instanceof Duck){ | |
console.log("嘎嘎嘎"); | |
}else if(animal instanceof Chicken){ | |
console.log("咯咯咯"); | |
} | |
} | |
makeSounding(new Duck());// 嘎嘎嘎 | |
makeSounding(new Chicken());// 咯咯咯 |
当我们分别向鸭和鸡发出 “叫唤” 的消息时,他会根据不同的消息做出不同的反应。但当我们有狗来临时,我们必须去改变函数才去做出改变。这样的话会使 makeSounding 函数编程一个巨大的函数。
多态的思想就是将 “做什么” 和 “谁去做以及怎样去做” 分离开来。也就是将 “不变的事物” 与 “可变的事物” 分离开来。在这个例子里,动物都会叫,这是不变的,但是不同的是动物的类型。
# 1.2.2 对象的多态性
改写后的代码:
let Duck = function(){}; | |
Duck.prototype.sounding = function(){ | |
console.log("嘎嘎嘎"); | |
} | |
let Chicken = function(){}; | |
Chicken.prototype.sounding= function(){ | |
console.log("咯咯咯"); | |
} | |
makeSounding(new Duck());// 嘎嘎嘎 | |
makeSounding(new Chicken())// 咯咯咯 |
现在我们向鸭和鸡都发出 “叫唤” 的消息,它们接到消息后分别作出不同的反应。如果有一天动物世界又增加一只狗,这时候只要简单地追加一些代码就行了,而不用改动以前的 makeSounding 函数,如下所示:
let Dog =function(){ | |
Dog.prototype.sound = function(){ | |
console.log("汪汪汪"); | |
}; | |
} | |
makeSounding(new Dog());// 汪汪汪 |
# 1.2.3 多态的作用
多态最根本的好处在于,你不必再向对象询问 “你是什么类型” 而后根据得到的答案调用对象的某个行为 —— 你只管调用该行为就行了,其他一切多态机制都会为你安排妥当
最简单一个例子:
当导演喊 “action” 时,大家就各司其职,知道自己要干什么,反之,如果没有多态,导演就得一个个去告诉别人你现在要干什么。
# 1.3 封装
# 1.3.1 封装数据
除了 ECMAScript6 中提供的 let 之外,一般我们通过函数来创建作用域:
const myObj = (function() { | |
let _name = "sven" // 私有变量 (属性) | |
return { | |
getName: function(){ // 公开方法 | |
return _name | |
} | |
} | |
})() | |
console.log(myObj.getName()); | |
console.log(myObj._name); |
另外值得一提的是,在 es6 中,我们也可以通过 symbol 来创建私有属性。
# 1.4 原型模式和基于原型继承的 JavaScript 对象系统
# 1.4.1 使用克隆的原型模式
const Plane = function(){ | |
this.blood = 100; | |
this.attackLevel = 1; | |
this.defenseLevel = 1; | |
} | |
let plane = new Plane(); | |
plane.blood = 500; | |
plane.attackLevel = 10; | |
plane.defenseLevel = 7; | |
const clonePlane = Object.create(plane); | |
console.log(clonePlane.blood); | |
// 在不支持 Object.create 的浏览器中可以写成这样; | |
Object.create = Object.create||function(obj){ | |
let F = function(){}; | |
F.prototype = obj; | |
return new F(); | |
} |
# 1.4.2 克隆是创建对象的手段
当然在 JavaScript 这种类型模糊的语言中,创建对象非常容易,也不存在类型耦合的问题。从设计模式的角度来说,原型模式的意义并不算大。但 JavaScript 本身就是一门基于原型的面向对象语言,它的对象系统就是使用原型模式来搭建的。在这里称之为原型编程泛型也许更加合适。
# 1.4.3 体验 io 语言
作为一门基于原型的语言,io 中同样没有类的概念,每一个对象都是基于另外一个对象来克隆。
这节课我们依旧拿动物世界的例子来体验 io 语言。在下面的语言中,通过克隆根对象 object,就可以得到另外一个对象 Animal。虽然 Animal 是以大写开头的,但记住 io 中没有类,Aniaml 跟所有的数据一样都是对象。
# 1.4.4 JavaScript 中的原型继承
- 所有的数据都是对象
- 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆他
- 对象会记住它的原型
- 如果对象无法响应某个请求,它会把这个请求委托给自己的原型
# 2.this,call 和 apply
# 2.1 this
除去不常用的 with 和 eval 的情况,具体用到实际开发中,大致分为以下四种
- 作为对象的方法调用
- 作为普通函数调用
- 构造器调用
- function.prototype.call 或 function.prototype.apply 调用
# 1. 作为对象的方法调用
当函数作为对象的方法被调用时,this 指向该对象:
const obj = { | |
a:1, | |
getA:function(){ | |
console.log(this===obj); | |
console.log(this.a); | |
} | |
}; | |
obj.getA() |
# 2. 作为普通函数调用
window.name = "globalName"; | |
let getName = function(){ | |
return this.name; | |
} | |
cFonsole.log(getName()); |
# 3. 构造器调用
let Myclass = function(){ | |
this.name = "MyClass" | |
} | |
const obj = new Myclass(); | |
console.log(obj.name); |
# 4.Function.prototype.call 或.apply 调用
# 2.2 call 和 apply
# 2.2.1 二者的区别
apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数
//apply | |
let func =function(a,b,c){ | |
console.log([a,b,c]); | |
} | |
func.apply(null,[1,2,3]) |
在这段代码中,参数 1,2,3 被放在数组中一起传入 func 函数,他们分别对应这参数列表中的 a,b,c
call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内 this 的指向,从第二个参数开始往后,每个参数被依次传入函数:
//call | |
let func2 =function(a,b,c){ | |
console.log([a,b,c]); | |
} | |
func2.call(null,1,2,3) |
# 2.2.2 Function.prototype.bind
模拟如下
Function.prototype.mybind = function (context) { | |
const self = this;// 保存原函数 | |
return function(){ // 返回一个新的函数 | |
return self.apply(context, arguments) | |
} | |
} | |
const obj = { | |
name:"sevn" | |
}; | |
let func = function(){ | |
console.log(this.name); | |
}.mybind(obj) | |
func() | |
Function.prototype.mybind2 = function (context2) { | |
const self = this,// 保存原函数 | |
context2 = [].shift.call(arguments), | |
args =[].slice.call(arguments); | |
return function(){ | |
return self.apply(context2, [].concat.call(args,[].slice.call(arguments))); | |
// 执行新的函数的时候,会把之前传入的 context 当做新函数体内的 this | |
// 并且组合两次分别传入的参数,作为新函数的参数 | |
} | |
} | |
const obj2 = { | |
name:'yujie' | |
}; | |
let func2 = function(a,b,c,d){ | |
console.log(this.name); | |
console.log([a,b,c,d]); | |
}.bind(obj,1,2) | |
func2(3,4) |
# 2.2.3 借用其他对象方法
//apply | |
let func =function(a,b,c){ | |
console.log([a,b,c]); | |
} | |
func.apply(null,[1,2,3]) | |
//call | |
let func2 =function(a,b,c){ | |
console.log([a,b,c]); | |
} | |
func2.call(null,1,2,3) |
# 3. 闭包和高阶函数
#
# 3.1.1 变量作用域
在 JavaScript 中,函数可以创造函数作用域,函数里面可以用外面的东西,但外面用不了函数里的变量,下面这个例子能更加深入理解变量搜索
var a = 1; | |
var func = function (){ | |
var b = 2; | |
var func2 = function (){ | |
var c =3; | |
console.log(b); | |
console.log(a); | |
} | |
func2(); | |
// console.log(c);//c is not defined | |
} | |
func() |
# 3.1.2 变量的生存周期
大家可以看看下面两个例子
for (var i = 0; i < 5; i++) { | |
setTimeout(function() { | |
console.log(new Date, i); | |
}, 1000); | |
} |
打印:5,5,5,5,5
for (var i = 0; i < 5; i++) { | |
(function(j) { // j = i | |
setTimeout(function() { | |
console.log(new Date, j); | |
}, 1000); | |
})(i); | |
} |
打印:0,1,2,3,4
这就是闭包的思想!!!!
# 3.1.3 闭包的更多作用
# 1. 封装变量
闭包可以帮助一些不需要暴露在全局的变量封装成私有变量。假设有一个计算乘积的简单函数
var mult = function (){ | |
var a = 1; | |
for(var i = 0,l=arguments.length;i<l;i++) { | |
a = a * arguments[i]; | |
} | |
return a; | |
} | |
console.log(mult(1,2,3,4,5)); |
mult 函数接受一些 number 类型的参数,并返回这些参数的乘积。现在我们觉得对于那些相同的参数来说,每次计算是一种浪费,我们可以加入缓存机制来提高性能。
var cache ={} | |
var mult2 = function (){ | |
let args = Array.prototype.join.call(arguments,",") | |
if(cache[args]){ | |
return cache[args] | |
} | |
var a = 1; | |
for(var i = 0,l=arguments.length;i<l;i++) { | |
a = a * arguments[i]; | |
} | |
return cache[args]=a; | |
} | |
console.log(mult2(1,2,3)); |
# 2. 延续局部变量的寿命
var report = function(src) { | |
var img = new Image(); | |
img.src = src; | |
} | |
report("xxx") | |
// 闭包解决 | |
const report2 = (function(src) { | |
let imgs = []; | |
return function(src){ | |
let img =new Image(); | |
imgs.push(img) | |
img.src=src; | |
} | |
})() |
# 3.2.1 高阶函数之函数作为参数传递
# 1. 回调函数
在 ajax 请求中,回调函数的使用特别频繁。当我们想在 ajax 请求返回之后做一些事情,但又不知请求返回的确切时间,我们最常见就是把 cb 当做参数传入 ajax 的请求里,等请求完成再执行 cb 函数
const getUserInfo = function(useId,callback) { | |
$.ajax({"http://localhost/getUserInfo?" + useId,function(data){ | |
if(typeof callback === "function"){ | |
callback(data); | |
} | |
}}) | |
} |
# 2.Array.prototype.sort
# 3.2.2 函数作为返回值输出
# 1. 判断数据类型
let isType = function(type){ | |
return function(obj){ | |
return Object.prototype.toString.call(obj) === '[object'+type+']'; | |
} | |
} | |
let isArray = isType("Array") | |
console.log(isArray([1,2,3])); |
# 2.getSingle
下面是一个单例模型的简单例子
let getSingle = function(fn){ | |
let ret; | |
return function(){ | |
return ret||(ret = fn.apply(this, arguments)) | |
} | |
} |
# 3.2.3 高阶函数实现 AOP
AOP(面向切面编程)的主要作用是把一些核心业务逻辑无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计,安全控制,异常处理等
Function.prototype.before = function(beforefn){ | |
const _self = this;// 保存原函数的引用 | |
return function(){ // 返回包涵了原函数和新函数的 “代理” 函数 | |
beforefn.apply(this, arguments); // 执行新函数并且修正 this | |
return _self.apply(this, arguments); // 执行原函数 | |
} | |
} | |
Function.prototype.after = function(afterfn){ | |
const _self = this; | |
return function(){ | |
let ret = _self.apply(this, arguments); | |
afterfn.apply(this, arguments); | |
return ret | |
} | |
} | |
let func = function(){ | |
console.log(2); | |
} | |
func =func.before(() => { | |
console.log(1) | |
}).after(() => { | |
console.log(3); | |
}) | |
func() |
# 3.2.4 高阶函数的其他应用
# 1.currying
首先我们讨论什么是函数柯里化。
currying 又称部分求值。一个 currying 的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另一个函数,刚才传入的参数在函数形成的闭包里被保存起来。待函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求职。
let monthlyCost = 0; | |
const cost =function(money){ | |
monthlyCost += money; | |
}; | |
cost(100); | |
cost(200); | |
cost(300); | |
// cost(400); | |
console.log(monthlyCost); |
通过这段代码我们可以看到,每天结束后我们都会记录并计算到今天为止花掉的钱。但我们其实并不太关心每天花掉了多少钱,只关心在月底花掉了多少钱。
下面是一个通用的 function currying (){}, 其接受一个参数,即将要被 currying 的函数。在这个例子里,这个函数的作用遍历本月每天的开销并求出它们的总和。
代码如下:
var currying = function(fn){ | |
var args = []; | |
return function(){ | |
if(arguments.length===0){ | |
return fn.apply(this,args) | |
}else{ | |
[].push.apply(args,arguments); | |
return arguments.callee// 他可以引用该函数的函数体内当前正在执行的函数 | |
} | |
} | |
}; | |
var cost = (function(){ | |
var money = 0; | |
return function(){ | |
for(var i = 0,l=arguments.length;i<l;i++){ | |
money += arguments[i] | |
} | |
return money; | |
} | |
})() | |
var cost =currying(cost) | |
cost(100) | |
cost(200) | |
cost(300) | |
console.log(cost()); |
# 2. 函数节流
在某些情况下,函数可能被非常频繁的调用,而造成大的性能问题。
原理:
可以借助 setinmeout 来完成
实现:
var throttle =function(fn,interval){ | |
var _self =fn, // 保存需要被延迟执行的函数引用 | |
timer, // 定时器 | |
firstTime = true;// 是否为第一次调用 | |
return function(){ | |
var args =arguments; | |
_me = this; | |
if(firstTime){ | |
_self.apply(_me, args); | |
return firstTime =false; | |
} | |
if(timer){// 如果定时器还在,则说明前一次延迟执行还没有完成 | |
return false; | |
} | |
timer=setTimeout(function(){ | |
clearTimeout(timer); | |
timer = null; | |
_self.apply(_me, args); | |
},interval||500) | |
} | |
} |
# 3. 分时函数
在前面关于函数节流的讨论中,我们提供了一种限制函数被频繁调用的解决方案。下面我们将遇到另外一个问题,某些函数确实是用户主动调用的,但因为一些客观的原因,这些函数会严重影响页面性能。
一个例子是创建 WEBQQ 的 qq 好友列表。列表通常会有成百上千个好友,如果一个好友用一个节点来表示,当我们在页面中渲染这个列表的时候,可能要一次性往页面上创建成百上千个节点,
在短时间内往页面大量添加 dom 节点显然不得行:
var ary = []; | |
for(var i = 1;i<=1000;i++){ | |
ary.push(i); | |
} | |
var renderFriendList = function(data){ | |
for(var i = 0,l = data.length; i < l;i++){ | |
var div = document.createElement("div"); | |
div.innerHTML = i; | |
document.body.appendChild(div) | |
} | |
} | |
renderFriendList(ary) |
这个问题的解决方案之一就是下面的 timeChunk 函数,timeChunk 函数让创造节点的工作分批进行。
var timeChunk = function(ary,fn,count){ | |
var obj,t; | |
var len = ary.length; | |
var start = function(){ | |
for(var i = 0;i<Math.min(count||1,ary.length);i++){ | |
var obj = ary.shift(); | |
fn(obj); | |
} | |
} | |
return function(){ | |
t =setinterval(function(){ | |
if(ary.length ===0){ | |
return clearInterval(t); | |
} | |
start() | |
},200) | |
} | |
} |
# 二。设计模式
书中只介绍了 js 开发中常见的 14 中设计模式
# 设计模式原则
# S – Single Responsibility Principle 单一职责原则
- 一个程序只做好一件事
- 如果功能过于复杂就拆分开,每个部分保持独立
# O – OpenClosed Principle 开放 / 封闭原则
- 对扩展开放,对修改封闭
- 增加需求时,扩展新代码,而非修改已有代码
# L – Liskov Substitution Principle 里氏替换原则
- 子类能覆盖父类
- 父类能出现的地方子类就能出现
# I – Interface Segregation Principle 接口隔离原则
- 保持接口的单一独立
- 类似单一职责原则,这里更关注接口
# D – Dependency Inversion Principle 依赖倒转原则
- 面向接口编程,依赖于抽象而不依赖于具体
- 使用方只关注接口而不关注具体类的实现
# 2.1 单例模式
# 2.1.1 定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点
# 2.1.2 案例
- 线程池
- 全局缓存
- 浏览器的 window 对象
- 网页登录浮窗
- ...
# 优点
- 划分命名空间,减少全局变量
- 增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护
- 且只会实例化一次。简化了代码的调试和维护
# 缺点
- 由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合 从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一个单元一起测试。
# 2.1.3 实现
# 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 instance1 = Singleton.getInstance("why"); | |
var instance2 = Singleton.getInstance("www"); | |
console.log(instance1 === instance2); //true |
无非就是用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。
# 2. 闭包
var Singleton = function(name) { | |
this.name = name; | |
} | |
Singleton.prototype.getName = function() { | |
return this.name; | |
} | |
Singleton.getInstance = (function() { | |
var instance = null; | |
return function(name) { | |
if(!instance) { | |
instance = new Singleton(name) | |
} | |
return instance; | |
} | |
})() | |
var instance1 = Singleton.getInstance('why'); | |
var instance2 = Singleton.getInstance('www'); | |
console.log(instance1 === instance2); // 输出 true |
无论以上面向对象的单例实现还是闭包的单例实现,都通过 Singleton.getInstance
来获取 Singleton
类的唯一对象,这增加了这个类的不透明性,使用者必须知道 Singleton
是一个单例类,然后通过 Singleton.getInstance
方法才能获取单例对象,要解决这一问题,可以使用透明的单例设计模式
这些都是比较简单的单例模式,我们只需要稍作了解即可。
# 3. 透明的单例模式
我们现在的目标就是实现一个 “透明” 的单例类,用户从这个类创建对象的时候,可以像使用其他任何普通类一样。例子如下:
// 透明的单例模式 | |
var CreateDiv = (function(){ | |
var instance = null; | |
var CreateDiv = function(html) { | |
if(instance) { | |
return instance; | |
} | |
this.html = html; | |
this.init(); | |
instance = this; | |
return instance; | |
} | |
CreateDiv.prototype.init = function() { | |
var div = document.createElement('div'); | |
div.innerHTML = this.html; | |
document.body.appendChild(div); | |
} | |
return CreateDiv; | |
})() | |
var instance1 = new CreateDiv('why'); | |
var instance2 = new CreateDiv('www'); | |
console.log(instance1===instance2); // 输出 true |
虽然上述透明的单例设计模式解决了不用通过 Singleton.getInstance
来获取单例类的唯一对象,但是在透明的单例设计模式中,构造函数 CreateDiv
违反了单一职责,它不仅要负责创建对象,而且还要负责保证单例,假如某一天需求变化了,不再需要创建单例的 div
,则需要改写 CreateDiv
函数,解决这种问题,可以使用代理来实现单例模式
# 4. 用代理实现单例模式
// 用代理实现单例模式 | |
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); | |
} | |
var ProxyCreateDiv = (function(){ | |
var instance = null; | |
return function(html) { | |
// 惰性单例 | |
if(!instance) { | |
instance = new CreateDiv(html); | |
} | |
return instance; | |
} | |
})() | |
var divInstance1 = new ProxyCreateDiv('why'); | |
var divInstance2 = new ProxyCreateDiv('www'); | |
console.log(divInstance1===divInstance2); // 输出 true |
通过引入代理类的方式,我们同样完成了一个单例模式的书写,跟之前不同的是,现在我们把负责管理单例的逻辑移到了代理类上,这样一来二者都能满足单一原则。
# 2.1.4 JS 中的单例模式
单例模式的核心就是确保只有一个实例,并提供全局访问。
<!DOCTYPE html> | |
<html lang="en"> | |
<body> | |
<button id="btn">登录</button> | |
</body> | |
<script> | |
class Login { | |
createLayout() { | |
var oDiv = document.createElement('div') | |
oDiv.innerHTML = '我是登录框' | |
document.body.appendChild(oDiv) | |
oDiv.style.display = 'none' | |
return oDiv | |
} | |
} | |
class Single { | |
getSingle(fn) { | |
var result; | |
return function() { | |
console.log(result); | |
return result || (result = fn.apply(this, arguments)) | |
} | |
} | |
} | |
var oBtn = document.getElementById('btn') | |
var single = new Single() | |
var login = new Login() | |
// 由于闭包,createLoginLayer 对 result 的引用, | |
// 所以当 single.getSingle 函数执行完之后,内存中并不会销毁 result。 | |
// 当第二次以后点击按钮,根据 createLoginLayer 函数的作用域链中已经包含了 result, | |
// 所以直接返回 result | |
// 讲获取单例和创建登录框的方法解耦,符合开放封闭原则 | |
var createLoginLayer = single.getSingle(login.createLayout) | |
oBtn.onclick = function() { | |
var layout = createLoginLayer() | |
layout.style.display = 'block' | |
} | |
</script> | |
</html> |
# 2.1.5 惰性单例
惰性单例时单例模式的重点,这种技术在实际开发中非常有用,有用程度可能超出我们的想象,instance 实例对象总是在我们调用 Singleton.getInstance 的时候才被创建,而不是在网页加载好的时候就创建,代码如下:
Singleton.getInstance = (function(){ | |
var instance = null; | |
return function(name){ | |
if(!instance){ | |
instance = new Singleton(name); | |
} | |
return instance; | |
} | |
})() |
# 2.2 策略模式
# 2.2.1 定义
定义一一系列算法,把他们一个个封装起来,并且使他们可以相互替换
# 2.2.2 使用场景
- 表单验证
- 动态选择
# 优点
- 策略模式利用组合、委托和多态等技术的思想,可以有效的避免多重条件分支语句
- 策略模式提供了对开放 - 封闭原则的完美支持,将算法封装在独立的策略类中,使它们易于切换、易于理解、易于扩展。
- 策略模式中的算法也可以复用在系统的其他地方
- 策略模式利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻便的替代方案。
# 2.2.3 案例:计算奖金
描述:案例描述:某公司的年终奖是根据员工的工资基数和年底绩效来发放的。例如,绩效为 S 的人年终奖有 4 倍工资,绩效为 A 的人年终奖有 3 倍工资,绩效为 B 的人年终奖有 2 倍工资,财务部要求我们提供一段代码,来方便他们计算员工的年终奖。
# 最初版本:
// 计算奖金:最初版本 | |
var calculateBouns = function(level,salary) { | |
if(level=='S') { | |
return salary * 4; | |
} | |
if(level=='A') { | |
return salary * 3; | |
} | |
if(level=='B') { | |
return salary * 2; | |
} | |
} | |
console.log(calculateBouns('S',4000)); // 输出 16000 | |
console.log(calculateBouns('A',3000)); // 输出 9000 | |
console.log(calculateBouns('B',2000)); // 输出 4000 |
不足之处:
calculateBouns
函数比较庞大,包含许多if-else
的语句calculateBouns
函数缺乏弹性,如果需要增加别的,就必须修改(复制粘贴)calculateBouns
函数复用性差
# 面向对象完善版:
我们把每个绩效的计算规则都封装在对应的策略类里(es6 后可以用 class)
// 计算奖金:面向对象完善版本 | |
var PerformanceS = function(){}; | |
PerformanceS.prototype.calculate = function(salary) { | |
return salary * 4; | |
} | |
var PerformanceA = function(){}; | |
PerformanceA.prototype.calculate = function(salary) { | |
return salary * 3; | |
} | |
var PerformanceB = function(){}; | |
PerformanceB.prototype.calculate = function(salary) { | |
return salary * 2; | |
} | |
// 定义奖金类 | |
var Bouns = function() { | |
this.salary = null; | |
this.strategy = null; | |
} | |
Bouns.prototype.setSalary = function(salary) { | |
this.salary = salary; | |
} | |
Bouns.prototype.setStrategy = function(strategy) { | |
this.strategy = strategy; | |
} | |
Bouns.prototype.getBouns = function() { | |
return this.strategy.calculate(this.salary); | |
} | |
var bouns = new Bouns(); | |
bouns.setSalary(4000); | |
bouns.setStrategy(new PerformanceS());// 设置策略对象 | |
console.log(bouns.getBouns()); // 输出 16000 | |
bouns.setSalary(3000); | |
bouns.setStrategy(new PerformanceA()); | |
console.log(bouns.getBouns()); // 输出 9000 | |
bouns.setSalary(2000); | |
bouns.setStrategy(new PerformanceB()); | |
console.log(bouns.getBouns()); // 输出 4000 |
# JavaScript 完善版:
// 计算奖金:JavaScript 的完善版本 | |
var strategy = { | |
'S': function(salary) { | |
return salary * 4; | |
}, | |
'A': function(salary) { | |
return salary * 3; | |
}, | |
'B': function(salary) { | |
return salary * 2; | |
} | |
} | |
var calcluateBouns = function(level,salary) { | |
return strategy[level](salary); | |
} | |
console.log(calcluateBouns('S',4000)); // 输出 16000 | |
console.log(calcluateBouns('A',3000)); // 输出 9000 | |
console.log(calcluateBouns('B',2000)); // 输出 4000 |
# 2.2.4 案例:动画效果原理
我们的目标是编写一个动画类和一些缓动算法,让帧动画以各种各样的缓动效运动。
记录包括以下信息:
- 小球的原始位置
- 小球的目标位置
- 动画开始的准确时间点
- 动画的持续时间
// 缓动算法 | |
let tween = { | |
linner: function (t, b, c, d) { | |
return c * t / d + b; | |
}, | |
easeIn: function (t, b, c, d) { | |
return c * (t /= d) * t + b; | |
}, | |
strongEaseIn: function (t, b, c, d) { | |
return c * (t /= d) * t * t * t * t + b; | |
}, | |
strongEaseOut: function (t, b, c, d) { | |
return c * ((t = t / d - 1) * t * t * t * t + 1) + b; | |
}, | |
slinear: function (t, b, c, d) { | |
return c * (t /= d) * t * t + b; | |
}, | |
slineaseOut: function (t, b, c, d) { | |
return c * ((t = t / d - 1) * t * t + 1) + b | |
} | |
} |
# 2.2.5 案例:表单验证
表单标签
- 用户名 (验证是否为空)
- 密码 (验证长度不能小于 6 位)
- 手机号 (验证是否是手机号格式)
// 策略模式案例:表单验证 | |
var strategies = { | |
isEmpty: function(value,errMsg) { | |
if(value==='') { | |
return errMsg | |
} | |
}, | |
minLength: function(value,length,errMsg) { | |
if(value.length<length) { | |
return errMsg | |
} | |
}, | |
isMobile: function(value,errMsg) { | |
if(!(/^1[34578]\d{9}$/.test(value))) { | |
return errMsg | |
} | |
} | |
} | |
var Validator = function() { | |
this.cache = []; | |
} | |
Validator.prototype.add = function(dom,rule,msg) { | |
var ary = rule.split(':'); | |
this.cache.push(function(){ | |
var strategy = ary.shift(); | |
ary.unshift(dom.value); | |
ary.push(msg); | |
return strategies[strategy].apply(dom,ary); | |
}); | |
} | |
Validator.prototype.run = function() { | |
for (let index = 0; index < this.cache.length; index++) { | |
var msg = this.cache[index](); | |
if(msg) { | |
return msg; | |
} | |
} | |
} | |
var validateFunc = function() { | |
var validator = new Validator(); | |
validator.add(registerForm.username,'isEmpty','用户名不能为空'); | |
validator.add(registerForm.password,'minLength:6','密码长度不能小于6位'); | |
validator.add(registerForm.phone,'isMobile','手机号格式不正确'); | |
var errMsg = validator.run(); | |
return errMsg; | |
} | |
var submitBtn = document.getElementById('submitBtn'); | |
var registerForm = document.getElementById('registerForm'); | |
submitBtn.onclick = function() { | |
var errMsg = validateFunc(); | |
if(errMsg) { | |
console.log(errMsg); | |
return false; | |
} else { | |
console.log('表单验证成功') | |
} | |
} |
#
2.2.6 总结
在函数作为一等公民的语言中,其实策略模式是隐形的。strategy 的值其实就是函数的变量。在 JS 中,除了使用类来封装算法和行为之外,使用函数也是一种选择。这便是我们常说的 “高阶函数”。
# 2.3 代理模式
# 2.3.1 定义
是为一个对象提供一个代用品或占位符,以便控制对它的访问
代理模式的关键在于当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象,替身对象作出请求后再将请求转接给本体对象
# 优点
- 代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用
- 代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;
# 缺点
处理请求速度可能有差别,非直接访问存在开销
# 2.3.2 简单的代理:小明追女神
var Flower = function() {}; | |
var xiaoming = { | |
sendFlower: function(target) { | |
var flower = new Flower(); | |
target.receive(flower); | |
} | |
} | |
var classmate = { | |
receive: function(flower) { | |
girl.receive(flower); | |
} | |
} | |
var girl = { | |
receive: function(flower) { | |
console.log('女神收到了花'); | |
} | |
} | |
xiaoming.sendFlower(classmate); // 输出女神收到了花 | |
let Flower = function() {} | |
let xiaoming = { | |
sendFlower: function(target) { | |
let flower = new Flower() | |
target.receiveFlower(flower) | |
} | |
} | |
let B = { | |
receiveFlower: function(flower) { | |
A.listenGoodMood(function() { | |
A.receiveFlower(flower) | |
}) | |
} | |
} | |
let A = { | |
receiveFlower: function(flower) { | |
console.log('收到花'+ flower) | |
}, | |
listenGoodMood: function(fn) { | |
setTimeout(function() { | |
fn() | |
}, 1000) | |
} | |
} | |
xiaoming.sendFlower(B) |
# 2.3.3 保护代理
TIP: 在代理模式中,替身对象能做到过滤一些对本体不合理的请求时,这就叫做保护代理
// 保护代理 | |
function Flower() {}; | |
function Person(name,age,salary) { | |
this.age = age; | |
this.name = name; | |
this.salary = salary; | |
} | |
Person.prototype.sendFlower = function(target,person){ | |
var flower = new Flower(); | |
target.receive(flower,person); | |
} | |
var person1 = new Person('www',20,4000); | |
var person2 = new Person('AAA',25,8000); | |
var person3 = new Person('BBB',45,16000); | |
var proxyObj = { | |
receive: function(flower,person) { | |
if(person.age>=40) { | |
console.log(person.name+',你年龄太大了'); | |
return false; | |
} | |
if(person.salary<5000) { | |
console.log(person.name+',你工资太低了'); | |
return false; | |
} | |
originObj.receive(flower); | |
console.log(person.name+',恭喜你,女神收下了你的花'); | |
} | |
} | |
var originObj = { | |
receive: function(flower) { | |
} | |
} | |
person1.sendFlower(proxyObj,person1); // 输出 www, 你工资太低了 | |
person2.sendFlower(proxyObj,person2); // 输出 AAA, 恭喜你,女神收下了你的花 | |
person3.sendFlower(proxyObj,person3); // 输出 BBB, 你年龄太大了 |
# 2.3.4 虚拟代理
TIP: 将一些代价昂贵的操作放在代理对象中,待到合适时再操作,这种代理就叫做虚拟代理
// 虚拟代理 | |
function Flower() {}; | |
var xiaoming = { | |
sendFlower: function(target) { | |
target.receiveFlower(); | |
} | |
} | |
var classmate = { | |
receiveFlower: function() { | |
girl.listenMood(function() { | |
var flower = new Flower(); | |
console.log('同学帮你买了花,并送了出去'); | |
girl.receiveFlower(flower); | |
}) | |
} | |
} | |
var girl = { | |
mood: 0, | |
receiveFlower: function(flower) { | |
console.log('女神收下了你的花'); | |
}, | |
listenMood: function(fn) { | |
setTimeout(function(){ | |
fn(); | |
},1500) | |
} | |
} | |
// 首先输出:同学帮你买了花,并送了出去、 | |
// 最后输出:女神收下了你的花 | |
xiaoming.sendFlower(classmate); |
# 2.3.5 图片懒加载
// 不用代理实现图片懒加载 | |
var myImage = (function(){ | |
var imgNode = document.createElement('img'); | |
document.body.appendChild(imgNode); | |
var img = new Image(); | |
img.onload = function() { | |
imgNode.src = img.src; | |
} | |
return { | |
setSrc: function(src) { | |
imgNode.src = 'file:///C:/Users/admin/Desktop/mask/img/7.jpg' | |
img.src = src; | |
} | |
} | |
})() | |
myImage.setSrc('https://img1.sycdn.imooc.com/5c09123400014ba418720632.jpg') | |
// 用代理实现图片懒加载 | |
var myImage = (function(){ | |
var image = document.createElement('img'); | |
document.body.appendChild(image); | |
return { | |
setSrc: function(src) { | |
image.src = src; | |
} | |
} | |
})();// 立即执行函数 | |
var proxyImage = (function(){ | |
var img = new Image(); | |
img.onload = function() { | |
myImage.setSrc(this.src); | |
} | |
return { | |
setSrc: function(src) { | |
myImage.setSrc('file:///C:/Users/admin/Desktop/mask/img/7.jpg'); | |
img.src = src; | |
} | |
} | |
})() | |
proxyImage.setSrc('https://img1.sycdn.imooc.com/5c09123400014ba418720632.jpg'); |
# 2.3.6 缓存代理
TIP: 缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传进来的参数和之前的一样,则直接返还之前储存的结果。
// 缓存代理:计算乘积 | |
var mult = function() { | |
console.log('开始计算乘积'); | |
var sum = 1; | |
for(var i=0;i<arguments.length;i++) { | |
sum = sum * arguments[i]; | |
} | |
return sum; | |
} | |
var proxyMult = (function(){ | |
var cache = {}; | |
return function() { | |
var args = Array.prototype.join.call(arguments,','); | |
if(cache.hasOwnProperty(args)) { | |
return cache[args]; | |
} | |
return cache[args] = mult.apply(this,arguments) | |
} | |
})() | |
console.log(proxyMult(1,2,3,4)); // 输出:开始计算乘积 24 | |
console.log(proxyMult(1,2,3,4)); // 输出 24 |
# 2.3.7 举一反三:代理工厂
// 代理工厂 (累加和乘积) | |
var mult = function() { | |
console.log('开始计算乘积') | |
var sum = 1; | |
for (let index = 0; index < arguments.length; index++) { | |
sum = sum * arguments[index] | |
} | |
return sum; | |
} | |
var plus = function() { | |
console.log('开始计算累加') | |
var sum = 0; | |
for (let index = 0; index < arguments.length; index++) { | |
sum = sum + arguments[index] | |
} | |
return sum; | |
} | |
var createProxyFactory = function(fn) { | |
var cache = {}; | |
return function() { | |
var args = Array.prototype.join.call(arguments,','); | |
if(cache.hasOwnProperty(args)) { | |
return cache[args] | |
} | |
return cache[args] = fn.apply(this,arguments); | |
} | |
} | |
var proxyMult = createProxyFactory(mult); | |
var proxyPlus = createProxyFactory(plus); | |
console.log(proxyMult(1,2,3,4)); // 输出:开始计算乘积 24 | |
console.log(proxyMult(1,2,3,4)); // 输出: 24 | |
console.log(proxyPlus(3,4,5,6)); // 输出:开始计算累加 18 | |
console.log(proxyPlus(3,4,5,6)); // 输出 18 |
# 2.4 迭代器模式
# 2.4.1 定义
提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象的内部表示。
# 2.4.2 内部迭代器
TIP
内部迭代器在调用的时候非常方便,外界不用关心迭代器内部到底是如何实现的,跟迭代器的交互也只有一次初始调用,而这也正好是内部迭代器的缺点。
// 实现自己的 each 迭代器 | |
Array.prototype.myForEach = function(callback) { | |
for(let i=0;i<this.length;i++) { | |
callback.call(null,i,this[i]); | |
} | |
} | |
let a =[1,2,3] | |
a.myForEach(function(index,value){ | |
console.log(index);// 依次输出 0 1 2 | |
console.log(value);// 依次输出 1 2 3 | |
}) |
# 2.4.3 外部迭代器
TIP
外部迭代器必须显示的请求迭代下一个元素
// 自定义外部迭代器实现比较两个数组的值是否完全相等 | |
var Iterator = function(obj) { | |
var current = 0; | |
var next = function() { | |
current++; | |
} | |
var isDone = function() { | |
return current >=obj.length; | |
} | |
var getCurrentItem = function() { | |
return obj[current]; | |
} | |
return { | |
next: next, | |
isDone: isDone, | |
getCurrentItem:getCurrentItem, | |
length: obj.length | |
} | |
} | |
var compare = function(iterator1,iterator2) { | |
if(iterator1.length!=iterator2.length) { | |
console.log('两个数组不相等'); | |
return false; | |
} | |
while(!iterator1.isDone() && !iterator2.isDone()) { | |
if(iterator1.getCurrentItem()!=iterator2.getCurrentItem()) { | |
throw new Error('两个数组不相等') | |
} | |
iterator1.next(); | |
iterator2.next(); | |
} | |
console.log('两个数组相等') | |
} | |
var iterator1 = Iterator([1,2,3]); | |
var iterator2 = Iterator([1,2,4]); | |
compare(iterator1,iterator2); // 报错,两个数组不相等 |
# 2.5 发布 - 订阅模式(观察者模式)
# 2.5.1 定义
发布 - 订阅模式又叫观察者模式,他定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
# 优点
发布 - 订阅模式一为时间上的解耦,二为对象之间的解耦。它的应用非常广泛,既可以用在异步编程中,也可以帮助我们完成更加松耦合的代码编写。发布 - 订阅模式还可以用来帮助实现一些其他的设计模式,例如中介者模式。
#
缺点
创建订阅者本身要消耗一定的时间和内存,而当你订阅一个消息后,也许此消息最后都没有发生,但订阅者依然存在于内存中,造成了一种浪费。发布 - 订阅模式虽然弱化了对象之间的联系,但过度使用的话,对象和对象之间的必要联系也将深埋在背后,会导致程序难以追踪维护和理解。
# 2.5.2 DOM 时间中的发布 - 订阅
// DOM 事件中的发布 - 订阅模式 | |
document.body.addEventListener('click',function(){ | |
console.log(1); | |
}) | |
document.body.addEventListener('click',function() { | |
console.log(2); | |
}) | |
document.body.addEventListener('click',function() { | |
console.log(3); | |
}) | |
document.body.addEventListener('click',function(){ | |
console.log(4); | |
}) | |
document.body.click(); // 输出 1 2 3 4 |
# 2.5.3 自定义发布 - 订阅
背景:小明最近看中一套房子,到销售中心才告知已经卖完了,好在销售楼中心准备出后期工程,但不知道什么时候出,只要小明留下自己的联系方式,楼盘开启后销售中心会通知小明相关信息。而对于其他像小明那样的客户,只要同样留下联系方式都可以收到相关信息。
// 自定义发布 - 订阅事件 | |
var sales = { | |
clientList: {}, | |
listen: function(key,fn) { | |
if(!this.clientList[key]) { | |
this.clientList[key] = []; | |
} | |
this.clientList[key].push(fn) | |
}, | |
trigger: function() { | |
var type = Array.prototype.shift.call(arguments); | |
var fns = this.clientList[type]; | |
if(!fns || fns.length<1) { | |
return false; | |
} | |
for (let index = 0; index < fns.length; index++) { | |
fns[index].apply(this,arguments) | |
} | |
} | |
} | |
// 订阅 | |
sales.listen('88',function(price){ | |
console.log('88平米的房子价格是:'+price); | |
}) | |
sales.listen('100',function(price){ | |
console.log('100平米的房子价格是:'+price); | |
}) | |
// 发布 | |
sales.trigger('88',200000); // 88 平米的房子价格是:200000 | |
sales.trigger('100',300000); // 100 平米的房子价格是:300000 |
# 2.5.4 取消订阅的事件
发布 - 订阅模式中,既然可以订阅事件,那么一定可以取消订阅,假设小明突然不想买房子了,为避免销售中心发短信打搅自己,他决定取消订阅。
// 取消订阅事件 | |
var sales = { | |
clientList: {}, | |
listen: function(key,fn) { | |
if(!this.clientList[key]) { | |
this.clientList[key] = []; | |
} | |
this.clientList[key].push(fn); | |
}, | |
trigger: function() { | |
var type = Array.prototype.shift.call(arguments); | |
var fns = this.clientList[type] | |
if(!fns || fns.length<1) { | |
return false; | |
} | |
for (let index = 0; index < fns.length; index++) { | |
fns[index].apply(this,arguments); | |
} | |
}, | |
remove: function(type) { | |
var fns = this.clientList[type]; | |
if(!fns || fns.length<1) { | |
return false; | |
} | |
// 全部取消订阅 | |
fns.length = 0; | |
} | |
} | |
// 订阅 | |
sales.listen('88',function(price){ | |
console.log('88平米的房子价格是:'+price); | |
}) | |
sales.listen('100',function(price){ | |
console.log('100平米的房子价格是:'+price); | |
}) | |
// 取消订阅 | |
sales.remove('88'); | |
// 发布 | |
sales.trigger('88',200000); // 不输出 | |
sales.trigger('100',300000); // 100 平米的房子价格是:300000 |
# 2.5.5 网站登录
背景:一个商场网站,有头部 header,有导航 nav,有购物车 cart,有消息列表 message 等等模块度依赖于登录成功后的用户信息。而用户不知道什么时候会登陆。需要将以上各个模块与登录模块做一个发布 - 订阅
/ 一个真实的发布-订阅例子:网站登录 | |
var login = { | |
clientList: {}, | |
listen: function(key,fn) { | |
if(!this.clientList[key]) { | |
this.clientList[key] = []; | |
} | |
this.clientList[key].push(fn); | |
}, | |
trigger: function() { | |
var type = Array.prototype.shift.call(arguments); | |
var fns = this.clientList[type]; | |
if(!fns || fns.length<1) { | |
return false; | |
} | |
for (let index = 0; index < fns.length; index++) { | |
fns[index].apply(this,arguments); | |
} | |
} | |
} | |
// 头部 | |
var header = (function(){ | |
login.listen('loginSuccess',function(data) { | |
header.setAvatar(data.avatar); | |
}) | |
return { | |
setAvatar: function(avatar) { | |
console.log('设置header头像:'+avatar); | |
} | |
} | |
})() | |
// 导航 | |
var nav = (function(){ | |
login.listen('loginSuccess',function(data) { | |
nav.setAvatar(data.avatar); | |
}) | |
return { | |
setAvatar: function(avatar) { | |
console.log('设置nav头像:'+avatar); | |
} | |
} | |
})() | |
// 购物车 | |
var cart = (function(){ | |
login.listen('loginSuccess',function(data) { | |
cart.getOrders(data); | |
}) | |
return { | |
getOrders: function(data) { | |
console.log('获取'+data.name+'的购物车订单列表'); | |
} | |
} | |
})() | |
setTimeout(function() { | |
// 依次输出 | |
// 设置 header 头像:https://www.baidu.com/1.jpg | |
// 设置 nav 头像:https://www.baidu.com/1.jpg | |
// 获取 AAA 的购物车订单列表 | |
login.trigger('loginSuccess',{name:'AAA',avatar: 'https://www.baidu.com/1.jpg'}); | |
}, 1500) |
# 2.6 命令模式
# 2.6.1 定义
命令模式是最简单和优雅的模式之一,命令模式中的命令指的是一个执行某些特定事件的指令。
# 2.6.2 应用场景
有时候需要向某些对象发送请求,但是并不知道请求的接受者是谁,也不知道被请求的操作是什么。此时希望有一种松耦合的方式来设计程序,使得请求发送者和接受者能够消除彼此之间的耦合关系。
# 2.6.1 案例一
故事背景:有一个用户界面程序,该用户界面上至少有数十个 Button 按钮,因为项目比较复杂,所以我们觉得让某个程序员负责 Button 按钮的绘制,另外一个程序员负责编写点击按钮的具体行为,这些行为都将封装在对象里。
<button type="button" id="button1">刷新界面</button> | |
<button type="button" id="button2">添加子菜单</button> | |
<button type="button" id="button3">删除子菜单</button> |
// 面向对象版本 | |
var button1 = document.getElementById('button1'); | |
var button2 = document.getElementById('button2'); | |
var button3 = document.getElementById('button3'); | |
// 设置命令 | |
var setCommand = function(button,command) { | |
button.onclick = function() { | |
command.execute(); | |
} | |
} | |
// 具体行为 | |
var MenuBar = { | |
refresh: function() { | |
console.log('刷新界面'); | |
} | |
} | |
var SubMenu = { | |
add: function(){ | |
console.log('添加子菜单'); | |
}, | |
remove: function() { | |
console.log('删除子菜单'); | |
} | |
} | |
// 封装具体行为到对象中 | |
var RefreshBarCommand = function(receiver) { | |
this.receiver = receiver; | |
} | |
RefreshBarCommand.prototype.execute = function() { | |
this.receiver.refresh(); | |
} | |
var AddSubMenuCommand = function(receiver) { | |
this.receiver = receiver; | |
} | |
AddSubMenuCommand.prototype.execute = function() { | |
this.receiver.add(); | |
} | |
var RemoveSubMenuCommand = function(receiver) { | |
this.receiver = receiver; | |
} | |
RemoveSubMenuCommand.prototype.execute = function() { | |
this.receiver.remove(); | |
} | |
// 传入命令接受者 | |
var refreshBarCommand = new RefreshBarCommand(MenuBar); | |
var addSubMenuCommand = new AddSubMenuCommand(SubMenu); | |
var removeSubMenuCommand = new RemoveSubMenuCommand(SubMenu); | |
setCommand(button1,refreshBarCommand); // 点击按钮输出:刷新界面 | |
setCommand(button2,addSubMenuCommand); // 点击按钮输出:添加子菜单 | |
setCommand(button3,removeSubMenuCommand); // 点击按钮输出:删除子菜单 |
# 2.6.2 闭包版本
// 闭包版本 | |
var button1 = document.getElementById('button1'); | |
var button2 = document.getElementById('button2'); | |
var button3 = document.getElementById('button3'); | |
// 设置命令 | |
var setCommand = function(button,func) { | |
button.onclick = function() { | |
func(); | |
} | |
} | |
// 定义具体行为 | |
var MenuBar = { | |
refresh: function() { | |
console.log('刷新界面'); | |
} | |
} | |
var SubMenu = { | |
add: function() { | |
console.log('添加子菜单'); | |
}, | |
remove: function() { | |
console.log('删除子菜单'); | |
} | |
} | |
// 封装具体行为到对象 | |
var RefreshBarCommand = function(receiver) { | |
return function() { | |
receiver.refresh(); | |
} | |
} | |
var AddSubMenuCommand = function(receiver) { | |
return function() { | |
receiver.add(); | |
} | |
} | |
var RemoveSubMenuCommand = function(receiver) { | |
return function() { | |
receiver.remove(); | |
} | |
} | |
// 传入命令接受者 | |
var refreshBarCommand = RefreshBarCommand(MenuBar); | |
var addSubMenuCommand = AddSubMenuCommand(SubMenu); | |
var removeSubMenuCommand = RemoveSubMenuCommand(SubMenu); | |
setCommand(button1,refreshBarCommand); // 点击按钮输出:刷新界面 | |
setCommand(button2,addSubMenuCommand); // 点击按钮输出:添加子菜单 | |
setCommand(button3,removeSubMenuCommand); // 点击按钮输出:删除子菜单 |
# 2.6.3 回调函数版本
// 绑定事件 | |
var bindClick = function(button,func) { | |
button.onclick = func; | |
} | |
// 定义具体行为 | |
var MenuBar = { | |
refresh: function() { | |
console.log('刷新界面'); | |
} | |
} | |
var SubMenu = { | |
add: function(){ | |
console.log('添加子菜单'); | |
}, | |
remove: function() { | |
console.log('删除子菜单'); | |
} | |
} | |
// 回调函数 | |
bindClick(button1,MenuBar.refresh); // 点击按钮输出:刷新界面 | |
bindClick(button2,SubMenu.add); // 点击按钮输出:添加子菜单 | |
bindClick(button3,SubMenu.remove); // 点击按钮输出:删除子菜单 |
# 2.6.4 宏命令
# 定义
宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行多个命令。
// 基础命令 | |
var CloseDoorCommand = { | |
execute: function() { | |
console.log('关门') | |
} | |
} | |
var OpenTVCommand = { | |
execute: function() { | |
console.log('打开电视') | |
} | |
} | |
var OpenQQComand = { | |
execute: function() { | |
console.log('登QQ') | |
} | |
} | |
// 宏命令 | |
var MacroCommand = function() { | |
return { | |
commandList: [], | |
add: function(command) { | |
this.commandList.push(command) | |
}, | |
execute: function() { | |
for (let index = 0; index < this.commandList.length; index++) { | |
this.commandList[index].execute(); | |
} | |
} | |
} | |
} | |
// 添加命令到宏命令 | |
var macroCommand = MacroCommand(); | |
macroCommand.add(CloseDoorCommand); | |
macroCommand.add(OpenTVCommand); | |
macroCommand.add(OpenQQComand); | |
// 执行宏命令 | |
macroCommand.execute(); // 依次输出:关门 打开电视 登 QQ |
# 2.7 组合模式
# 2.7.1 定义
组合模式将对象组合成树形结构,以表示 "部分 - 整体" 的层次结构。
# 传递顺序
对宏命令为例,请求从树最顶端的对象往下传递,如果当前处理请求的对象是叶对象,叶对象自身会对请求做出相应的处理。如果当前处理请求的是组合对象,则遍历该组合对象下的子节点,将请求继续传递给这些子节点。
# 2.7.2 更强大的宏命令
万能遥控器:
- 打开空调
- 打开电视和音响
- 关门、打开电脑、登录 QQ
var MacroCommand = function() { | |
return { | |
commandList: [], | |
add: function(command) { | |
this.commandList.push(command) | |
}, | |
execute: function() { | |
for (let index = 0; index < this.commandList.length; index++) { | |
this.commandList[index].execute(); | |
} | |
} | |
} | |
} | |
// 打开空调 | |
var openAcCommand = { | |
execute: function() { | |
console.log('打开空调'); | |
} | |
} | |
// 打开电视和音响 | |
var openTVCommand = { | |
execute: function() { | |
console.log('打开电视'); | |
} | |
} | |
var openSoundCommand = { | |
execute: function() { | |
console.log('打开音响') | |
} | |
} | |
var macroCommand1 = MacroCommand(); | |
macroCommand1.add(openTVCommand); | |
macroCommand1.add(openSoundCommand); | |
// 关门、开电脑、登 QQ | |
var closeCommand = { | |
execute: function() { | |
console.log('关门') | |
} | |
} | |
var openPcCommand = { | |
execute: function() { | |
console.log('打开电脑') | |
} | |
} | |
var openQQCommand = { | |
execute: function() { | |
console.log('登录QQ') | |
} | |
} | |
var macroCommand2 = MacroCommand(); | |
macroCommand2.add(closeCommand); | |
macroCommand2.add(openPcCommand); | |
macroCommand2.add(openQQCommand); | |
// 宏命令 | |
var macroCommand = MacroCommand(); | |
macroCommand.add(openAcCommand); | |
macroCommand.add(macroCommand1); | |
macroCommand.add(macroCommand2); | |
// 触发宏命令 | |
var setCommand = (function(command){ | |
document.getElementById('SuperButton').onclick = function() { | |
// 依次输出:打开空调 打开电视 打开音响 关门 打开电脑 登录 QQ | |
command.execute(); | |
} | |
})(macroCommand) |
# 2.7.3 扫描文件
// 组合模式案例:文件扫描 | |
// 文件夹类 | |
var Folder = function(name) { | |
this.name = name; | |
this.files = []; | |
} | |
Folder.prototype.add = function(file) { | |
this.files.push(file); | |
} | |
Folder.prototype.scan = function() { | |
console.log('开始扫描文件夹:'+this.name); | |
for (let index = 0; index < this.files.length; index++) { | |
this.files[index].scan(); | |
} | |
} | |
// 文件类 | |
var File = function(name) { | |
this.name = name; | |
} | |
File.prototype.add = function() { | |
throw new Error('文件下面不能添加文件'); | |
} | |
File.prototype.scan = function() { | |
console.log('开始扫描文件:'+this.name) | |
} | |
var folder = new Folder('学习资料'); | |
var folder1 = new Folder('JavaScript'); | |
var folder2 = new Folder('jQuery'); | |
var folder3 = new Folder('重构与实现'); | |
var folder4 = new Folder('NodeJs'); | |
var file1 = new File('JavaScript设计模式'); | |
var file2 = new File('精通jQuery'); | |
var file3 = new File('JavaScript语言精粹'); | |
var file4 = new File('深入浅出的Node.js'); | |
folder1.add(file1); | |
folder2.add(file2); | |
folder4.add(file4); | |
folder.add(folder1); | |
folder.add(folder2); | |
folder.add(file3); | |
folder.add(folder3); | |
folder.add(folder4); | |
// 执行扫描 | |
// 开始扫描文件夹:学习资料 | |
// 开始扫描文件夹:JavaScript | |
// 开始扫描文件:JavaScript 设计模式 | |
// 开始扫描文件夹:jQuery | |
// 开始扫描文件:精通 jQuery | |
// 开始扫描文件:JavaScript 语言精粹 | |
// 开始扫描文件夹:重构与实现 | |
// 开始扫描文件夹:NodeJs | |
// 开始扫描文件:深入浅出的 Node.js | |
folder.scan(); |
# 2.8 模板方法模式
# 2.8.1 定义
模板方法是一种只需使用继承就可以实现的非常简单的模式。模板方法由两部分组成,一部分是抽象的父类,另一部分是具体的子类。
通常而言,在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承抽象的父类,也继承了整个算法结构。
# 2.8.2 经典案例
泡咖啡的步骤:
- 把水煮沸
- 用沸水泡咖啡
- 把咖啡倒进杯子
- 加糖和牛奶
// 泡咖啡 | |
var Coffee = function(){}; | |
Coffee.prototype.boilWater = function() { | |
console.log('把水煮沸'); | |
} | |
Coffee.prototype.brewCoffee = function() { | |
console.log('冲泡咖啡'); | |
} | |
Coffee.prototype.purInCup = function() { | |
console.log('把咖啡倒进杯子里'); | |
} | |
Coffee.prototype.addSugarAndMilk = function() { | |
console.log('加牛奶和糖') | |
} | |
Coffee.prototype.init = function() { | |
this.boilWater(); | |
this.brewCoffee(); | |
this.purInCup(); | |
this.addSugarAndMilk(); | |
} | |
var coffee = new Coffee(); | |
// 依次输出: 把水煮沸 冲泡咖啡 把咖啡倒进杯子里 加牛奶和糖 | |
coffee.init(); |
泡茶的步骤:
- 把水煮沸
- 用沸水浸泡茶叶
- 把茶水倒进杯子
- 加柠檬
// 泡茶 | |
var Tea = function() {}; | |
Tea.prototype.boilWater = function() { | |
console.log('把水煮沸'); | |
} | |
Tea.prototype.brewTea = function() { | |
console.log('用沸水浸泡茶叶'); | |
} | |
Tea.prototype.purInCup = function() { | |
console.log('把茶水倒进杯子') | |
} | |
Tea.prototype.addLemon = function() { | |
console.log('加柠檬'); | |
} | |
Tea.prototype.init = function() { | |
this.boilWater(); | |
this.brewTea(); | |
this.purInCup(); | |
this.addLemon(); | |
} | |
var tea = new Tea(); | |
// 依次输出: 把水煮沸 用沸水浸泡茶叶 把茶水倒进杯子 加柠檬 | |
tea.init(); |
# 2.8.3 案例重构
经过对比分析,泡咖啡和泡茶虽然具体实现的方法是不一样的,但是步骤大致是类似的:
- 把水煮沸
- 用沸水
- 倒进杯子
- 加调料
泡咖啡和泡茶主要的不同点
- 原料不同,一个是咖啡,一个是茶,统称为饮料
- 泡的方式不同,一个是冲泡,一个是浸泡,统称为泡
- 加入的调料不同,一个是牛奶和糖,另一个是柠檬。
其实在 js 中,我们很多时候不需要去实现一个模版模式,高阶函数是更好的选择
# 2.9 享元模式
# 2.9.1 定义
亨元模式是一种用于性能优化的模式,其核心是运用共享技术来有效支持大量细粒度的对象。亨元模式要求将对象的属性划分为内部状态和外部状态。
# 2.9.2 享元模式雏形
背景:某内衣厂生产有 50 种男士内衣和 50 种女士内衣,正常情况下,需要 50 个男模特和 50 个女模特来完成对内衣的试穿拍照,在不使用亨元模式的情况下,在程序里也许会这样写
// 亨元模式雏形 | |
var Model = function(sex,underwear) { | |
this.sex = sex; | |
this.underwear = underwear; | |
} | |
Model.prototype.takePhoto = function() { | |
console.log('sex='+this.sex+',underwear='+this.underwear); | |
} | |
for (let index = 0; index < 50; index++) { | |
var model = new Model('male','underwear'+index); | |
model.takePhoto(); | |
} | |
for (let index = 0; index < 50; index++) { | |
var model = new Model('female','underwear'+index); | |
model.takePhoto(); | |
} |
# 2.9.3 初应用
思考:
- 在上列中,要得到一张照片,每次需要传入 sex 和 underwear 参数,一共有 50 种男士内衣和 50 种女士内衣,一共需要 100 个对象,将来如果生产 1000 种内衣,则需要的对象会更多。
- 利用享元模式后,虽然有 100 种内衣,但只需要男、女两个模特即可,即只需要两个对象
var Model = function (sex) { | |
this.sex = sex; | |
} | |
Model.prototype.takePhoto = function () { | |
console.log('sex=' + this.sex + ',underwear=' + this.underwear); | |
} | |
var male = new Model('male'); | |
var female = new Model('female'); | |
for (let index = 0; index < 50; index++) { | |
male.underwear = index+1; | |
male.takePhoto(); | |
} | |
for (let index = 0; index < 50; index++) { | |
female.underwear = index+1; | |
female.takePhoto(); | |
} |
亨元模式初运用思考:
- 我们通过 new 来创建男女两个 model 对象,在其他情况下,也许并不是一开始就需要共享所有的对象。
- 给 model 手动添加了 underwear 属性,在更加复杂的系统中,这并不是一个最好的方法,因为外部状态可能是相对比较复杂的,他们与共享对象的联系会变得更加困难。
# 2.9.4 真实案件:文件上传
背景:在微云文件上传模块的开发中,曾爆发过对象爆炸的问题。微云文件上传分为浏览器插件上传,flash 上传和表单上传等。用户对于不用的上传模式,都能一个一个上传,或者批量上传。在最初版时,同时上传 2000 个文件,在 IE 浏览器中直接进入假死状态。
// 真实案例:文件上传 | |
// 文件上传对象 | |
var Upload = function (uploadType, fileName, fileSize) { | |
this.uploadType = uploadType; | |
this.fileName = fileName; | |
this.fileSize = fileSize; | |
this.dom = null; | |
} | |
Upload.prototype.init = function (id) { | |
var _self = this; | |
this.id = id; | |
this.dom = document.createElement('div'); | |
this.dom.innerHTML = '<span>文件名称:' + this.fileName + ',文件大小:' | |
+ this.fileSize + 'kb</span>' + '<button type="button" class="delFile">删除</button>'; | |
this.dom.querySelector('.delFile').onclick = function () { | |
_self.deleteFile(); | |
} | |
document.body.appendChild(this.dom); | |
} | |
Upload.prototype.deleteFile = function () { | |
if (this.fileSize < 3000) { | |
console.log('成功删除' + this.fileName + '文件'); | |
return this.dom.parentNode.removeChild(this.dom); | |
} | |
if (confirm('是否确定删除此文件?')) { | |
return this.dom.parentNode.removeChild(this.dom); | |
} | |
} | |
// 上传方法 | |
var id = 0; | |
window.startUpload = function (uploadType, fileList) { | |
for (let index = 0; index < fileList.length; index++) { | |
var file = fileList[index]; | |
var upload = new Upload(uploadType, file.name, file.size); | |
upload.init(id++); | |
} | |
} | |
// 用户上传 | |
startUpload('plugin', [ | |
{ name: '1.txt', size: 1000 }, | |
{ name: '2.txt', size: 3000 }, | |
{ name: '3.txt', size: 5000 } | |
]) | |
startUpload('flash', [ | |
{ name: '4.txt', size: 1000 }, | |
{ name: '5.txt', size: 3000 }, | |
{ name: '6.txt', size: 5000 } | |
]) |
# 2.9.5 享元模式重构
如何划分内部状态和外部状态
- 内部状态存储于对象内部
- 内部状态可以被一些对象共享
- 内部状态独立于具体的场景,通常不会改变
- 外部状态取决于具体的场景,并根据场景的变化而变化,外部状态通常是不能被共享的。
文件上传内部状态划分
内部状态:uploadType
外部状态:fileName,fileSize (文件名和文件大小不能被共享,它随不同的文件不同而不同)
// 真实案例:亨元模式重构文件上传 | |
// 文件上传对象 | |
var Upload = function (uploadType) { | |
this.uploadType = uploadType; | |
} | |
// 工厂模式:解决一开始就共享所有对象的问题 | |
var UploadFactory = (function(){ | |
var createFactoryList = {}; | |
return { | |
create: function(uploadType) { | |
if(createFactoryList[uploadType]) { | |
return createFactoryList[uploadType] | |
} | |
return createFactoryList[uploadType] = new Upload(uploadType); | |
} | |
} | |
})(); | |
// 管理器:封装外部状态,使程序在运行时给 upload 对象设置外部状态 | |
var uploadManage = (function(){ | |
var uploadDataBase = {}; | |
return { | |
add: function(id,type,name,size) { | |
var uploadObj = UploadFactory.create(type); | |
var dom = document.createElement('div'); | |
dom.innerHTML = '<span>文件名称:' + name + ',文件大小:' + | |
size + 'kb</span>' + '<button type="button" class="delFile">删除</button>'; | |
dom.querySelector('.delFile').onclick = function () { | |
uploadObj.deleteFile(id); | |
} | |
document.body.appendChild(dom); | |
uploadDataBase[id] = { | |
fileName: name, | |
fileSize: size, | |
dom: dom | |
} | |
return uploadObj; | |
}, | |
setExternalState: function(id,uploadObj) { | |
var uploadData = uploadDataBase[id]; | |
for (var i in uploadData) { | |
uploadObj[i] = uploadData[i]; | |
} | |
} | |
} | |
})(); | |
Upload.prototype.deleteFile = function () { | |
uploadManage.setExternalState(id,this); | |
if (this.fileSize < 3000) { | |
return this.dom.parentNode.removeChild(this.dom); | |
} | |
if (confirm('是否确定删除此文件?')) { | |
return this.dom.parentNode.removeChild(this.dom); | |
} | |
} | |
// 上传方法 | |
var id = 0; | |
window.startUpload = function (uploadType, fileList) { | |
for (let index = 0; index < fileList.length; index++) { | |
var file = fileList[index]; | |
var upload = uploadManage.add(++id,uploadType,file.name,file.size); | |
} | |
} | |
// 用户上传 | |
startUpload('plugin', [ | |
{ name: '1.txt', size: 1000 }, | |
{ name: '2.txt', size: 3000 }, | |
{ name: '3.txt', size: 5000 } | |
]) | |
startUpload('flash', [ | |
{ name: '4.txt', size: 1000 }, | |
{ name: '5.txt', size: 3000 }, | |
{ name: '6.txt', size: 5000 } | |
]) |
# 2.10 职责链模式
# 2.10.1 定义
使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系,将这些对象连成一条链传递该请求,直到有一个对象处理它为止。
优点:
- 解耦发送者和 N 个接受者之间的关系
- 可以手动设置起始节点,并不是必须从第一个开始
- 可以与其他设计模式在一起实现更加复杂的功能,例如职责链模式 + 命令模式
缺点:
- 请求不能保证一定能在接受者中被处理
- 请求链过长的情况下,可能某些节点并没有起到实质性的作用,造成性能损耗。
现实中的职责链模式
- 高峰坐公交时,从后门上车的乘客需要把卡一个一个传递,最后一个人打卡或者投币。
- 考试写小纸条,往后一个一个传递,直到有一个人把正确答案给你为止。
# 2.10.2 实际开发中的职责链模式: if-else 版
背景:某公司电商网站,准备做一个活动,用户分别交纳 500 元定金,可得 100 元优惠券;交纳 200 元定金,可得 50 元优惠券;不交纳定金,正常购买,不享受优惠券,且在库存不充足时,不一定保证能买到商品。
字段描述:
- orderType: 1 代表 500 元定金用户;2 代表 200 元定金用户;3 代表普通用户
- pay:是否已支付定金
- stock:库存,支付了定金的用户不受库存限制
//if else 版 | |
var order = function(orderType,pay,stock) { | |
if(orderType==1) { | |
if(pay) { | |
console.log('500元定金预购,享受100元优惠券'); | |
} else { | |
// 未支付定金,降级到普通订单 | |
if(stock>0) { | |
console.log('普通订单,无优惠券'); | |
} else { | |
console.log('库存不足'); | |
} | |
} | |
} else if(orderType==2) { | |
if(pay) { | |
console.log('200元定金预购,享受50元优惠券'); | |
} else { | |
// 未支付定金,降级到普通订单 | |
if(stock>0) { | |
console.log('普通订单,无优惠券'); | |
} else { | |
console.log('库存不足'); | |
} | |
} | |
} else if(orderType==3) { | |
if(stock>0) { | |
console.log('普通订单,无优惠券'); | |
} else { | |
console.log('库存不足'); | |
} | |
} | |
} | |
// 订单测试 | |
order(1, true, 500); // 输出:500 元定金预购,享受 100 元优惠券 | |
order(1, false,500); // 输出:普通订单,无优惠券 | |
order(2, true, 500); // 输出:200 元定金预购,享受 50 元优惠券 | |
order(3, true, 500); // 输出:普通订单,无优惠券 |
# 2.10.3 重构版
// 职责链重构版 | |
var order500 = function(orderType,pay,stock) { | |
if(orderType==1 && pay) { | |
console.log('500元定金预购,享受100元优惠券'); | |
} else { | |
order200(orderType,pay,stock); | |
} | |
} | |
var order200 = function(orderType,pay,stock) { | |
if(orderType==2 && pay) { | |
console.log('200元定金预购,享受50元优惠券'); | |
} else { | |
orderNormal(orderType,pay,stock); | |
} | |
} | |
var orderNormal = function(orderType,pay,stock) { | |
if(stock>0) { | |
console.log('普通订单,无优惠券'); | |
} else { | |
console.log('库存不足'); | |
} | |
} | |
// 订单测试 | |
order500(1, true, 500); // 输出:500 元定金预购,享受 100 元优惠券 | |
order500(1, false,500); // 输出:普通订单,无优惠券 | |
order500(2, true, 500); // 输出:200 元定金预购,享受 50 元优惠券 | |
order500(3, true, 500); // 输出:普通订单,无优惠券 |
存在的问题:
- 传递请求的代码被严格耦合在了一起,违反开放 - 封闭原则
- 当要新增 300 元订单时,必须把原来的职责链拆解,移动后才能运行起来
# 2.10.4 重构完善版
约定
我们约定,在某个节点处理不了请求时,返回一个字段,把请求往后传递
// 职责链重构完善版 | |
var order500 = function(orderType,pay,stock) { | |
if(orderType==1 && pay) { | |
console.log('500元定金预购,享受100元优惠券'); | |
} else { | |
return 'next'; | |
} | |
} | |
var order200 = function(orderType,pay,stock) { | |
if(orderType==2 && pay) { | |
console.log('200元定金预购,享受50元优惠券'); | |
} else { | |
return 'next'; | |
} | |
} | |
var orderNormal = function(orderType,pay,stock) { | |
if(stock>0) { | |
console.log('普通订单,无优惠券'); | |
} else { | |
console.log('库存不足'); | |
} | |
} | |
// 新增职责链类 | |
var Chain = function(fn) { | |
this.fn = fn; | |
this.receiver = null; | |
} | |
Chain.prototype.setReceiver = function(receiver) { | |
this.receiver = receiver; | |
} | |
Chain.prototype.passRequest = function() { | |
var returnMsg = this.fn.apply(this,arguments); | |
if(returnMsg=='next') { | |
return this.receiver && this.receiver.passRequest.apply(this.receiver,arguments); | |
} | |
return returnMsg; | |
} | |
var chainOrder500 = new Chain(order500); | |
var chainOrder200 = new Chain(order200); | |
var chainOrderNormal = new Chain(orderNormal); | |
chainOrder500.setReceiver(chainOrder200); | |
chainOrder200.setReceiver(chainOrderNormal); | |
// 订单测试 | |
chainOrder500.passRequest(1, true, 500); // 输出:500 元定金预购,享受 100 元优惠券 | |
chainOrder500.passRequest(1, false,500); // 输出:普通订单,无优惠券 | |
chainOrder500.passRequest(2, true, 500); // 输出:200 元定金预购,享受 50 元优惠券 | |
chainOrder500.passRequest(3, true, 500); // 输出:普通订单,无优惠券 |
//AOP 实现 | |
// AOP 实现职责链模式 | |
Function.prototype.after = function(fn) { | |
var self = this; | |
return function() { | |
var returnMsg = self.apply(this,arguments); | |
if(returnMsg=='next') { | |
return fn.apply(this,arguments); | |
} | |
return returnMsg; | |
} | |
} | |
var order500 = function(orderType,pay,stock) { | |
if(orderType==1 && pay) { | |
console.log('500元定金预购,享受100元优惠券'); | |
} else { | |
return 'next'; | |
} | |
} | |
var order200 = function(orderType,pay,stock) { | |
if(orderType==2 && pay) { | |
console.log('200元定金预购,享受50元优惠券'); | |
} else { | |
return 'next'; | |
} | |
} | |
var orderNormal = function(orderType,pay,stock) { | |
if(stock>0) { | |
console.log('普通订单,无优惠券'); | |
} else { | |
console.log('库存不足'); | |
} | |
} | |
var order = order500.after(order200).after(orderNormal); | |
// 订单测试 | |
order(1, true, 500); // 输出:500 元定金预购,享受 100 元优惠券 | |
order(1, false,500); // 输出:普通订单,无优惠券 | |
order(2, true, 500); // 输出:200 元定金预购,享受 50 元优惠券 | |
order(3, true, 500); // 输出:普通订单,无优惠券 |
# 2.11 中介者模式
# 2.11.1 定义
中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者即可。
# 中介者模式的缺点
- 中介者模式本身就要新增一个中介者对象
- 将对象与对象之间交互的复杂性,转移到对象与中介者之间的复杂性,使得中介者对象经常是巨大的
- 中介者本身就是一个难以维护的对象。
# 现实中的案例
- 机场指挥塔 机场指挥塔扮演者中介者,而不同的飞机扮演对象,他们通过与指挥塔进行通信,从而得知消息,什么时候可以起飞,什么时候可以降落。
- 博彩公司 在世界杯期间,博彩公司通过扮演中介者,把成千上万的用户投注情况进行汇总,根据比赛的输赢,进行计算相关的赔率。
# 2.11.3 泡泡堂(原始版)
TIP
- 分为红蓝两队
- 只有某队全部死亡,才算失败
// 中介者模式案例:泡泡堂 (原始版) | |
var Player = function(name,teamColor) { | |
this.partners = []; // 队友列表 | |
this.emeies = []; // 敌人列表 | |
this.name = name; // 名字 | |
this.teamColor = teamColor; // 队伍颜色 | |
this.state = 'live'; // 生存状态 | |
} | |
Player.prototype.win = function() { | |
console.log(this.name+'胜利了'); | |
} | |
Player.prototype.lose = function() { | |
console.log(this.name+'失败了'); | |
} | |
Player.prototype.die = function() { | |
var allDie = true; | |
this.state = 'dead'; | |
// 遍历队友是否全部阵亡 | |
for (let index = 0,len = this.partners.length; index < len; index++) { | |
if(this.partners[index].state!='dead') { | |
allDie = false; | |
break; | |
} | |
} | |
// 如果全部阵亡,遍历通知队友失败,通知敌人胜利 | |
if(allDie) { | |
this.lose(); | |
for (let index = 0,len = this.partners.length; index < len; index++) { | |
this.partners[index].lose(); | |
} | |
for (let index = 0,len = this.emeies.length; index < len; index++) { | |
this.emeies[index].win(); | |
} | |
} | |
} | |
// 工厂方法创建玩家 | |
var players = []; | |
var playerFactory = function(name,teamColor) { | |
var newPlayer = new Player(name,teamColor); | |
for (let index = 0,len = players.length; index < len; index++) { | |
if(players[index].teamColor==teamColor) { | |
players[index].partners.push(newPlayer); | |
newPlayer.partners.push(players[index]); | |
} else { | |
players[index].emeies.push(newPlayer); | |
newPlayer.emeies.push(players[index]); | |
} | |
} | |
players.push(newPlayer); | |
return newPlayer; | |
} | |
// 红队 | |
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'); | |
// 淘汰玩家 | |
// 依次输出:辰四失败了 辰大失败了 辰二失败了 辰三失败了 张三胜利了 张四胜利了 | |
// 张五胜利了 张六胜利了 | |
player5.die(); | |
player6.die(); | |
player7.die(); |
思考:
- 虽然我们可以随意创建任意对个玩家,但玩家和其他玩家紧紧耦合在了一起
- 某一个玩家的状态改变,必须通知其他对象,当其它对象很多时,会非常不合适。
- 不利于扩展,今后如果要添加新的功能,如:玩家掉线,解除队伍,添加到别的队伍会非常不方便。
# 2.11.4 引入中介者
// 中介者模式案例:泡泡堂 (引入中介者) | |
var playerDirector = (function(){ | |
var players = {}; | |
var operations = { | |
addPlayer: function(player) { | |
var teamColor = player.teamColor; | |
if(!players[teamColor]) { | |
players[teamColor] = []; | |
} | |
players[teamColor].push(player) | |
}, | |
removePlayer: function(player) { | |
var teamColor = player.teamColor; | |
var teamPlayers = players[teamColor] || []; | |
for (let index = 0,len=teamPlayers.length; index < len; index++) { | |
if(teamPlayers[index]==player) { | |
teamPlayers.splice(index,1); | |
break; | |
} | |
} | |
}, | |
changeTeam: function(player,newTeamColor) { | |
operations.removePlayer(player); | |
player.teamColor = newTeamColor; | |
operations.addPlayer(player); | |
}, | |
playerDead: function(player) { | |
var teamColor = player.teamColor; | |
var teamPlayers = players[teamColor]; | |
var allDead = true; | |
player.state = 'dead'; | |
for (let index = 0,len=teamPlayers.length; index < len; index++) { | |
if(teamPlayers[index].state!='dead') { | |
allDead = false; | |
break; | |
} | |
} | |
if(allDead) { | |
for (let index = 0,len=teamPlayers.length; index < len; index++) { | |
teamPlayers[index].lose(); | |
} | |
for (var color in players) { | |
if(color!=teamColor) { | |
for (let index = 0,len=players[color].length; index < len; index++) { | |
players[color][index].win(); | |
} | |
} | |
} | |
} | |
} | |
}; | |
var ReceiveMessage = function() { | |
var message = Array.prototype.shift.call(arguments); | |
operations[message].apply(this,arguments); | |
} | |
return { | |
ReceiveMessage: ReceiveMessage | |
} | |
})() | |
var Player = function(name,teamColor) { | |
this.name = name; | |
this.teamColor = teamColor; | |
this.state = 'live'; | |
} | |
Player.prototype.win = function() { | |
console.log(this.name+'胜利了'); | |
} | |
Player.prototype.lose = function() { | |
console.log(this.name+'失败了'); | |
} | |
Player.prototype.remove = function() { | |
console.log(this.name+'掉线了'); | |
playerDirector.ReceiveMessage('removePlayer',this); | |
} | |
Player.prototype.die = function() { | |
console.log(this.name+'死亡'); | |
playerDirector.ReceiveMessage('playerDead',this); | |
} | |
Player.prototype.changeTeam = function(color) { | |
console.log(this.name+'换队'); | |
playerDirector.ReceiveMessage('changeTeam',this,color); | |
} | |
// 工厂模式创建玩家 | |
var playerFactory = function(name,teamColor) { | |
var newPlayer = new Player(name,teamColor); | |
playerDirector.ReceiveMessage('addPlayer',newPlayer); | |
return newPlayer; | |
} | |
// 红队 | |
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.remove(); | |
player2.remove(); | |
// 更换队伍 | |
// 依次输出:张五换队 张五死亡 | |
player3.changeTeam('blue'); | |
// 阵亡 | |
// 依次输出:辰大死亡 辰二死亡 辰三死亡 辰四死亡 | |
// 辰大失败了 辰二失败了 辰三失败了 辰四失败了 张五失败了 | |
// 张六胜利了 | |
player3.die(); | |
player5.die(); | |
player6.die(); | |
player7.die(); | |
player8.die(); |
# 2.12 装饰者模式
# 2.12.1 定义
装饰者模式可以动态的给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。
# 继承的问题
在传统的面向对象语言中,给对象添加功能常常使用继承的方式,但继承的方式并不灵活,还会带来许多问题
- 超类和子类之间存在强耦合关系,当改变超类时,子类也会随之改变。
- 超类的内部细节对子类是可见的,继承常常被认为破坏了封装性。
- 在完成一些功能复用的同时,有可能创建出大量的子类,使子类的数量呈爆炸式增长。
# 状态模式的优缺点
现在我们已经大概掌握了状态模式,现在是时候来总结一下状态模式的优缺点了。
优点:
- 状态模式定义了状态和行为之间的关系,并将他们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
- 避免了 Context 无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了 Context 中原本过多的条件分支
- 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
- Context 中的请求动作和状态类中封装的行为可以非常容易的独立变化而不影响。
缺点:
- 状态模式会根据系统中多少种状态来定义多少个类,这将是一项枯燥和无味的过程
- 状态模式会将逻辑分散在各个状态类中,虽然可以避免条件分支语句判断,但也造成了逻辑分散,我们无法在一个地方就看出整个状态机的逻辑。
# 2.12.2 模拟对象的装饰者模式
// 飞机大战案例:面向对象版 | |
var Plane = function() {}; | |
var Missile = function(plane) { | |
this.plane = plane; | |
}; | |
var Atom = function(plane) { | |
this.plane = plane; | |
} | |
Plane.prototype.fire = function() { | |
console.log('发射普通子弹'); | |
}; | |
Missile.prototype.fire = function() { | |
this.plane.fire(); | |
console.log('发射导弹'); | |
} | |
Atom.prototype.fire = function() { | |
this.plane.fire(); | |
console.log('发射原子弹'); | |
} | |
// 调用 | |
var plane = new Plane(); | |
plane = new Missile(plane); | |
plane = new Atom(plane); | |
plane.fire(); // 依次输出:发射普通子弹 发射导弹 发射原子弹 |
# 2.12.3 JavaScript 中的装饰者
// JavaScript 版 | |
var plane = { | |
fire: function() { | |
console.log('发射普通子弹'); | |
} | |
} | |
var missile = function() { | |
console.log('发射导弹'); | |
} | |
var atom = function() { | |
console.log('发射原子弹'); | |
} | |
var fire1 = plane.fire; | |
plane.fire = function() { | |
fire1(); | |
missile(); | |
} | |
var fire2 = plane.fire; | |
plane.fire = function() { | |
fire2(); | |
atom(); | |
} | |
plane.fire();// 依次输出:发射普通子弹 发射导弹 发射原子弹 |
解析:这种给对象添加职责的方式,并没有真正的改动对象自身,而是将对象放入另外一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。
# 2.12.4 AOP
用 AOP 装饰函数的技巧在实际开发中非常有用,无论是业务代码的编写,还是在框架层面,我们都可以把行为依照职责分成粒度更细的函数,随后通过装饰把他们合并在一起,这有助于我们编写一个松耦合和高复用性的系统。
# AOP 的两个装饰函数
/ before函数 | |
Function.prototype.before = function(beforeFn) { | |
var _self = this; | |
return function() { | |
beforeFn.apply(this,arguments); | |
return _self.apply(this,arguments); | |
} | |
} | |
//after 函数 | |
Function.prototype.after = function(afterFn) { | |
var _self = this; | |
return function() { | |
var ret = _self.apply(this,arguments); | |
afterFn.apply(this,arguments); | |
return ret; | |
} | |
} |
# 应用案例一:数据上报
<button id="btnLogin">点击打开登录浮层</button> | |
// 数据上报 | |
Function.prototype.before = function (beforeFn) { | |
var _self = this; | |
return function () { | |
beforeFn.apply(this, arguments); | |
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 showLogin = function() { | |
console.log('打开登录浮层'); | |
} | |
// 依次输出:按钮点击之前上报 打开登录浮层 按钮点击之后上报 | |
document.getElementById('btnLogin').onclick = showLogin.before(function(){ | |
console.log('按钮点击之前上报'); | |
}).after(function() { | |
console.log('按钮点击之后上报'); | |
}); |
# 2.13 状态模式
# 2.13.1 定义
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。
# 2.13.2 初识
我们想象这样一个场景:有一个电灯,电灯上面只有一个开关。当电灯开着的时候,我们按一下开关,电灯会切换到关闭状态;再按一次开关,电灯又将被打开。同一个开关按钮,在不同的状态下,表现出来的行为是不一样的。
// 电灯类 | |
var Light = function() { | |
this.state = 'off'; | |
this.button = null; | |
} | |
// 初始化方法 | |
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.buttonPresssed(); | |
} | |
} | |
// 开关点击 | |
Light.prototype.buttonPresssed = function() { | |
if (this.state == 'off') { | |
this.state = 'on'; | |
console.log('开灯'); | |
} else if(this.state == 'on') { | |
this.state = 'off'; | |
console.log('关灯'); | |
} | |
} | |
var light = new Light(); | |
light.init(); |
分析:现在看来,我们已经编写了一个强壮的状态机,这个状态机的逻辑既简单又缜密,看起来这段代码设计得无懈可击。但令人遗憾的是,这个世界上的电灯并非只有一种。许多酒店里有另外一种电灯,这种电灯也只有一种开关,但它的表现是:当第一次按下时,出现弱光;第二次按下时,出现强光;第三次按下时才是关闭电灯。
// 改写开关点击事件 | |
Light.prototype.buttonPresssed = function() { | |
if (this.state == 'off') { | |
this.state = 'weakLight'; | |
console.log('弱光'); | |
} else if(this.state == 'weakLight') { | |
this.state = 'strongLight'; | |
console.log('强光'); | |
}else if(this.state == 'strongLight') { | |
this.state = 'off'; | |
console.log('关灯'); | |
} | |
} |
再次分析:以上代码存在如下的缺点
- 开关点击事件中的代码,违反了开放 - 封闭原则,每次新增或者修改 light 的状态,都要改动开关点击事件中的代码
- 所有跟状态有关的行为,都被封装在 buttonPresssed 方法里,后续如果再扩展一种灯光的话,将十分难以维护。
- 状态的切换不明显,仅仅表现在对 state 变量的赋值。
- buttonPresssed 方法里,对于状态的判断,仅仅是 if-else 的堆砌,不利于后续的维护和扩展
# 2.13.3 状态模式改写
// 关闭 | |
var OffLightState = function(light) { | |
this.light = light; | |
} | |
OffLightState.prototype.buttonPressed = function() { | |
console.log('弱光'); | |
this.light.setState(this.light.weakLightState); | |
} | |
// 弱光 | |
var WeakLightState = function(light) { | |
this.light = light; | |
} | |
WeakLightState.prototype.buttonPressed = function() { | |
console.log('强光'); | |
this.light.setState(this.light.strongLightState); | |
} | |
// 强光 | |
var StrongLightState = function(light) { | |
this.light = light; | |
} | |
StrongLightState.prototype.buttonPressed = function() { | |
console.log('关闭'); | |
this.light.setState(this.light.offLightState); | |
} | |
// 电灯类 | |
var Light = function() { | |
this.offLightState = new OffLightState(this); | |
this.weakLightState = new WeakLightState(this); | |
this.strongLightState = new StrongLightState(this); | |
this.button = null; | |
} | |
Light.prototype.init = function() { | |
var button = document.createElement('button'); | |
var _self = this; | |
this.button = document.body.appendChild(button); | |
this.button.innerHTML = '开关'; | |
this.currState = this.offLightState; | |
this.button.onclick = function() { | |
_self.currState.buttonPressed(); | |
} | |
} | |
Light.prototype.setState = function(state) { | |
this.currState = state; | |
} | |
// 初始化测试 | |
var light = new Light(); | |
light.init(); |
# 2.14 适配器模式
# 2.14.1 定义
适配器模式的作用是解决两个软件实体间的接口不兼容的问题,使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。
# 2.14.2 地图渲染
// 谷歌地图 | |
var googleMap = { | |
show: function() { | |
console.log('开始渲染谷歌地图'); | |
} | |
} | |
// 百度地图 | |
var baiduMap = { | |
// 地图渲染接口不兼容 | |
display: function() { | |
console.log('开始渲染百度地图'); | |
} | |
} | |
// 百度地图适配器 | |
var baiduMapAdapter = { | |
show: function() { | |
return baiduMap.display(); | |
} | |
} | |
// 地图渲染 | |
var renderMap = function(map) { | |
if(map.show instanceof Function) { | |
map.show(); | |
} | |
} | |
// 测试地图渲染 | |
renderMap(googleMap); // 开始渲染谷歌地图 | |
renderMap(baiduMapAdapter); // 开始渲染百度地图 |