阅读《JavaScript设计模式》(2009),电子版资源:
链接:https://pan.baidu.com/s/1smWE-Xcwsapn65jnlw8cvg
提取码:hkke
《JavaScript设计模式》
第一部分 面向对象的JavaScript
第1章 富有表现力的JavaScript
1.编程风格:
0)目的,普通方法:
function startAnimation(){
...
}
function stopAnimation(){
...
}
1)定义一个类,为这个类添加方法
var Anim=function(){
...
};
Anim.prototype.start=function(){
...
};
Anim.prototype.stop=function(){
...
};
2)定义一个类,为这个类添加的方法写在一起
var Anim=function(){
...
};
Anim.prototype={
start:function(){
...
},
stop=function(){
...
}
};
3)定义一个类,为这个类封装一个方法用于添加方法
Function.prototype.method=function(name,fn){
this.prototype[name]=fn;
};
var Anim=function(){
...
};
Anim.method('start',function(){
...
});
Anim.method('stop',function(){
...
});
4)定义一个类,为这个类封装一个方法用于添加方法,并链式调用
Function.prototype.method=function(name,fn){
this.prototype[name]=fn;
return this;
};
var Anim=function(){
...
};
Anim.
method('start',function(){
...
}).
method('stop',function(){
...
});
第2章 接口
1.提供一种规定必需方法的手段;提供一种检查这些方法是否确实得到实现的手段;在结果为否定时能提供有用的错误信息。
2.谨慎地使用接口Interface类有助于创建更健壮的类和更稳定的代码。
第3章 封装和信息隐藏
1.一些概念
信息隐藏原则有助于减轻系统中两个参与者之间的依赖性。两个参与者必须通过明确的通道传送信息。
信息隐藏是目的,而封装则是借以达到这个目的的技术。
封装:对对象的内部数据表现形式和实现细节进行隐藏。
封装是面向对象的设计的基石。
2.创建对象的基本模式:
1)门户大开型
2)下划线表示私有性
3)使用闭包创建真正私用的成员
3.封装的优点
(1)保护内部数据的完整性。
(2)对象的重构变得轻松。
(3)弱化模块间的耦合,提高了对象的重用性。
(4)使用私用变量避免命名空间冲突。
4.封装的缺点:
(1)很难进行单元测试。
(2)不得不与复杂的作用域链打交道,这会使错误调试更加困难。
(3)过度封装。
(4)实现封装的难度大。
第4章 继承
1.继承的优点:减少重复代码。
2.类式继承
//超类Person
function Person(name)={
this.name=name;
}
Person.prototype.getName=function(){
return this.name;
}
//子类Author
function Author(name,books){
Person.call(this,name);
this.books=books;
}
1)原型链
利用访问属性时从本对象开始查找,找不到再往该对象的prototype上找,找不到再找往根节点找这个特性,可将父类赋值给本对象的prototype。
举例:
Author.prototype=new Person();//设置原型链,将子类的prototype设置为指向超类的一个实例
Author.prototype.constructor=Author;
Author.prototype.getBooks=function(){
return this.books;
}
2)extend函数
简化类的声明,将派生子类的整个过程包装在一个函数中,基于一个给定的类结构创建一个新的类。
function extend(subClass,superClass){
var F=function(){};
F.prototype=superClass.prototype;
subClass.prototype=new F();
subClass.prototype.constructor=subClass;
}
举例:
extend(Author,Person);
Author.prototype.getBooks=function(){
return this.books;
}
弱化超类与子类的耦合,可以为子类添加一个superClass属性存放超类的原型对象。使用superClass便于在子类中重定义超类的方法。
function extend(subClass,superClass){
var F=function(){};
F.prototype=superClass.prototype;
subClass.prototype=new F();
subClass.prototype.constructor=subClass;
subClass.superClass=superClass.prototype;
if(superClass.prototype.constructor==Object.prototype.constructor){
superClass.prototype.constructor=superClass;
}
}
3.原型式继承
定义父类的结构——定义一个对象;使用clone函数复制一个新对象出来,整个新对象就是子类。
使用clone函数创建一个原型对象为超类的空对象。在这个空对象中查找某个方法或属性时,如果找不到,那么查找过程会在其原型对象中继续进行。
举例:
//超类
var Person={
name:'default name',
getName:function(){
return this.name;
}
}
//子类
var reader=clone(Person);
1)缺点
对继承而来的成员的读写不对等性。易改动原型对象。
- 为clone对象新添超类属性。
- 在超类中定义工厂函数,返回对象。
2)clone函数
function clone(object){
function F(){}
F.prototype=object;
return new F();
}
3)其他
有风险。
更节约内存。
4.掺元类mixin class
如果想把一个函数用到多个类中,可以通过扩充的方式让这些类共享该函数。
先创建一个包含各种通用方法的类,然后再用它扩充其他类。
mixin class通常不会被实例化或直接调用,其存在的目的只是向其他类提供自己的方法。
可以被视为多亲继承(multiple inheritance)在js中的一种实现方式。
1)augment函数
function augment(receivingClass,givingClass){
for(methodName in givingClass.prototype){
if(!receivingClass.prototype[methodName]){
receivingClass.prototype[methodName]=givingClass.prototype[methodName];
}
}
}
用一个for…in循环遍历予类(givingClass)的prptotype中的每一个成员,并将其添加到受类(receivingClass)的prototype中。如果受类中已经存在同名成员,则跳过这个成员,转而处理下一个。受类中的成员不会被改写。
5.三种方式适合场景
1)类式继承
试图模仿C++和Java等面向对象语言中类的继承方式。最适合内存效率要求不高,程序员不熟悉不太知名的继承方式的场合。
2)原型式继承
用这种方法创建的对象有较高的内存效率,因为会共享那些未被改写的属性和方法。
3)掺元类
适合彼此有较大差异的类共享一些通用方法。
第5章 单体模式
这种模式提供了一种将代码组织为一个逻辑单元的手段,这个逻辑单元中的代码可以通过单一的变量进行访问。
1.在js中的用途:
-
划分命名空间,减少全局变量的数目
-
在分支(branching)技术中用来封装浏览器之间的差异
-
把代码组织得更为一致,提高可读性和可维护性
-
划分命名空间
-
用作特定网页专用代码的包装器的单体
-
拥有私用成员的单体
2.基本结构
示例:
var Singleton={
attribute1:true,
attribute2:10,
method1:function(){},
method2:function(arg){}
}
这个示例与普通对象字面量没什么不同,可以被修改。
按传统定义:单体是一个只能被实例化一次并且可以通过一个众所周知的访问点访问的类。
广义定义:单体是一个用来划分命名空间并将一批相关方法和属性组织在一起的对象,如果它可以被实例化,那么它只能被实例化一次。
3.惰性实例化
对于资源密集型的或配置开销甚大的单体,更合理的做法是将其实例化推迟到需要使用它的时候。这种技术称为惰性加载(lazy loading)。常用语必须加载大量数据的单体。
1)getInstance
对惰性加载单体的访问必须借助于一个getInstance()
方法,检查该单体是否已经被实例化,若还没有就创建并返回其实例。
Singleton.getInstance().methodName();//惰性加载单体
Singleton.methodName();//普通单体
2)如何将普通单体转化成惰性加载单体
使用闭包创建的单体,将闭包内所有代码放到名为constructor的方法中,constructor即为私有方法。
单体中抛出getInstance公有方法,控制单体类实例化时机,它要做2件事:判断该类是否已经被实例化;如果已经实例化过,那就返回这个实例。
通过一个私有属性uniqueInstance记录该单体类是否被实例化过,若uniqueInstance==‘undefined’,则赋值uniqueInstance=constructor();
3)缺点
复杂,创建这种单体的代码并不直观。
命名空间长,例如MyNamespace.Singleton.methodName()
。
4.分支branching
是一种用来把浏览器间的差异封装到在运行期间进行设置的动态方法中的技术。
在脚本加载时一次性地确定针对特定浏览器的代码,在之后调用该方法时不必再运行检查代码,直接返回第一次的结果,提高效率。
具有分支的单体示例:
MyNamespace.Singleton=(function(){
var objectA={
method1:function(){...},
method2:function(){...}
};
var objectB={
method1:function(){...},
method2:function(){...}
};
return (someCondition)?objectA:objectB;
})();
objectA和objectB拥有相同的方法,只是实现过程不同。根据条件确定选择哪个分支。
这种条件通常是某种能力检测的结果,意在确保运行代码的js环境实现了所需要的特性。
在考虑是否使用分支时,需要在程序运行时间和占用内存这两方面权衡。是个分支对象较小而判断使用哪个分支的开销较大的情况。
5.单体模式使用场合
- 用作命名空间
- 把相关代码组织在一起便于维护
- 把数据或代码安置在一个众所周知的单一位置
- 开销较大却很少使用的组件可以被包装到惰性加载单体中
- 针对特定环境的代码可以被包装到分支型单体中
第6章 方法的链式调用
return this;
第二部分 设计模式
第7章 工厂模式
1.简单工厂
2.工厂模式
3.适合场景
- 主要用在所实例化的类的类型不能在开发期间确定,而只能在运行期间确定的情况。
- 存在许多具有复杂的设置开销的相关对象。
- 想创建一个包含了一些成员对象的类,但又想避免把它们紧密耦合在一起。
4.优缺点
优点:
-
消除对象间的耦合。
-
在派生子类时也提供了更大的灵活性。
缺点:
如果根本不可能另外换用一个类,或者不需要在运行期间在一系列可互换的类中进行选择,那就不应该使用工厂方法。
(大多数类最好使用new关键字和构造函数公开地进行实例化;工厂方法可以在重构代码时使用。)
第8章 桥接模式
1.作用
将抽象与其实现隔离开来,以便二者独立变化。
2.使用场景
在设计一个js api时,可以用这个模式来弱化它与使用它的类和对象之间的耦合。
这种模式对于js中常见的事件驱动的变成大有裨益。
最常见和实际的应用场合之一是事件监听器的回调函数:
addEvent(element,'click',getBeerByIdBridge);
function getBeerByIdBridge(e){
getBeerById(this.id,function(beer){
console.log('Requested Beer:'+beer);
})
}
getBeerByIdBridge就是连接监听器和回调的桥。
3.优缺点
优点:
- 独立管理软件的各组成部分
- 容易查找bug,减小发生严重故障的可能性
- 代码易维护
- 促进代码的模块化、促成更简洁的实现并提高抽象的灵活性
- 提供了一种借助于特权函数访问私用数据的手段
缺点:
-
每使用一个桥接元素都要增加一次函数调用,对应用程序的性能会有一些负面影响。
-
提高了系统的复杂程度
如果一个桥接函数被用于连接2个函数,而其中某个函数根本不会在桥接函数之外被调用,那么此时这个桥接函书就是不必要的,可删除。
第9章 组合模式
1.概念
组合对象(集合:中间人,循环/递归调用叶对象方法,使得外部只需调用组合对象的一个方法)、叶对象
把一批子对象组织为树形结构,只要一条命令就可以操作树中的所有对象。
2.适用场景
这种模式特别适合于动态的HTML用户界面,可以在不知道用户界面的最终格局的情况下进行开发。
3.优缺点
优点:
- 不必编写大量手工遍历数组或其他数据结构的粘合代码,只需对最顶层的对象执行操作,让每一个子对象自己传递这个操作即可。这对于那些再三执行的操作尤其有用。
- 各个对象之间的耦合非常松散。提高了代码的模块化程度,促进了代码的重用,有利于代码重构。
- 用组合模式组织起来的对象形成了一个出色的层次体系。每当对顶层组合对象执行一个操作时,实际上是在对整个结构进行深度优先的搜索以查找节点。
缺点:
- 如果组织的层次体系很大的话,系统的性能会受到影响。
- 表格很难转化为一个组合对象。
- 组合模式的正常运作需要用到某种形式的接口,接口检查越严格组合对象类就越可靠。但这会为系统增加一些复杂性。
第10章 门面模式
添加一些便利方法(这种方法是对原有的一些方法的组合利用)。在幕后进行错误检查、浏览器兼容性。是一种组织性的模式。
1.实现的一般步骤
- 找到程序中适合使用门面方法的地方
- 函数名称要与用途相称
- 处理浏览器API的不一致性:把分支代码放在新创建的门面函数中,辅以对象检查或浏览器嗅探等技术。
2.适用场合
-
反复成组出现的代码
-
应对js内置函数在不同浏览器中的不同表现
3.优缺点
优点:
- 创建便利函数。编写一次组合代码就可以反复使用。为复杂、重复性任务提供简单接口。
- 降低对外部代码的依赖程度,提高开发灵活性。
缺点:
- 易被滥用,门面函数可能常常会执行一些你并不需要的任务。
第11章 适配器模式
用于在现有借口和不兼容的类之间进行适配。这种模式的对象又叫“包装器”,因为是在用一个新的接口包装另一个对象。
适配器所适配的两个方法执行的应该是类似的任务。协调两个不同的接口。
1.适用场合
- 适用于客户系统期待的接口与现有API提供的接口不兼容这种场合。只能用来协调语法上的差异问题。
- 如果客户想要的是一个不同的接口,比如使用更容易的接口,也可使用适配器,可以把抽象与其实现隔离开来,以便二者独立变化。
2.优缺点
优点:
- 有助于避免大规模改写现有客户代码。
缺点:
- 实际上开发需要彻底重写代码,适配器只适用于一个过渡,有人认为适配器是一种不必要的开销。
第12章 装饰者模式
关键特点是能够与其组件互换使用。这样的好处之一就是可以透明地用新对象装饰现有系统中的对象,而这并不会改变代码中的其他东西。
1.如何实现装饰
-
装饰对象的类继承自原系统的类,定义好与原类同名的默认方法。
-
装饰对象的子类原型式继承自装饰对象的类,并重写同名方法。
-
调用时先创建原对象,再将原对象作为组件传入装饰对象即可。
-
可以使用多个装饰者
举例自行车售卖代码:
//原对象类:Bicycle(自行车)
//装饰对象类:BicycleDecorator(自行车装饰类)
//第一步
var BicycleDecorator = function(bicycle){
Interface.ensureImplements(bicycle,Bicycle);
this.bicycle = bicycle;
}
BicycleDecorator.prototype = {
assemble:function(){
return tis.bicycle.assemble;
},
wash:...
ride:...
repair:...
getPrice:function(){
return this.bicycle.getPrice();
}
};
//第二步
//装饰对象类:HeadlightDecorator(自行车带前灯)
var HeadlightDecorator = function(bicycle){
HeadlightDecorator.superclass.constructor.call(this,bicycle);
}
extend(HeadlightDecorator,BicycleDecorator);
HeadlightDecorator.prototype.getPrice = function(){
return this.bicycle.getPrice()+15.00;
};
//装饰对象类:TaillightDecorator(自行车带尾灯)
var TaillightDecorator = function(bicycle){
TaillightDecorator.superclass.constructor.call(this,bicycle);
}
extend(TaillightDecorator,BicycleDecorator);
TaillightDecorator.prototype.getPrice = function(){
return this.bicycle.getPrice()+9.00;
};
//第三步
var myBicycle = new Bicycle();
myBicycle = new HeadlightDecorator(myBicycle);
myBicycle = new TaillightDecorator(myBicycle);
alert(myBicycle.getPrice());//价格会加上前灯和尾灯
2.装饰者修改其组件的方式
以下“方法”指的是继承自的组件的方法。
1)在方法之后添加行为
2)在方法之前添加行为
3)替换方法
4)添加新方法
3.工厂
如果必须确保某种特定顺序,那么可以为此使用工厂对象。
将装饰内容放到工厂对象的创建方法中。
自行车售卖举例:
//原对象类:Bicycle
var BicycleShop = function() {};
BicycleShop.prototype.createBicycle = function(options){
var bicycle = new Bicycle();
for(var i = 0, len = options.length; i < len; i++){
var decorator = BicycleShop.options[options[i].name];
if(typeof decorator !== 'function'){
throw new Error('装饰者'+options[i].name+'不存在');
}
var argument = options[i].arg;
bicycle = new decorator(bicycle,argument);
}
Interface.ensureImplements(bicycle,Bicycle);
return bicycle();
}
BicycleShop.options = {
'headlight':HeadlightDecorator,
'taillight':TaillightDecorator
};
//调用
var bicycleShop = new BicycleShop();
var myBicycle = bicycleShop.createBicycle([
{name:'headlight'},{name:'taillight'}
]);
使用工厂方法可以直接使用它创建出来的对此昂就行,不必关心它是一个自行车对象还是一个装饰者对象。
工厂方法的装饰者模式适用于对选件需要排序的情况。
4.函数装饰者
在对另一个函数的输出应用某种格式或执行某种转换这方面很有用处。
举例:
//将被包装者的返回结果改为大写形式
function upperCaseDecorator(func){
return function(){
return func.apply(this,arguments).toUpperCase();
}
}
//使用装饰者
function getDate(){
return new Date().toString();
}
getDateCaps = upperCaseDecorator(getDate);
//调用
alert(getDate());//Wed Sep 26 2007 20:11:02 GMT-0700(PDT)
alert(getDateCaps());//WED SEP 26 2007 20:11:02 GMT-0700(PDT)
5.适用场合
-
需要为类增添特性或职责,而从该类派生子类的解决办法并不实际的话,就应该使用装饰者模式。
-
需要为对象增添特性而又不想改变使用该对象的代码的话,也可使用装饰者模式。
6.优缺点
优点:
-
在运行期间动态为对象增添特性或职责。
-
运作过程透明,即可以用它包装其他对象,然后继续按之前使用那些对象的方法来使用它。
缺点:
- 遇到用装饰者包装起来的对象时,那些依赖于类型检查的代码会出问题。(尽管在js中很少使用严格的类型检查,但是如果有,那么装饰者是无法匹配所需要的类型的。)
- 增加架构的复杂程度。
第13章 享元模式
是一种优化模式,目的在于提高系统使用内存的效率。
1.适用场合
- 网页中必须使用了大量资源密集型对象。(数量多到引起这方面的问题)
- 这些对象中所保存的数据至少有一部分能被转化为外在数据。能把对象内部的部分数据分离出来,然后将其作为参数提供给各个方法。
- 将外在数据分离出去之后,独一无二的对象的数目相对较少。
2.实现享元模式的一般步骤
- 将所有外在数据从目标类剥离
- 创建一个用来控制该类的实例化工厂
- 创建一个用来保存外在数据的管理器
3.优缺点
优点:
- 可以把网页的资源负荷降低几个数量级
- 实现这种节省不需要大量修改原有代码
缺点:
- 不是什么地方都能用,如果被用在不必要的地方,反而有损代码的运行效率
- 追踪数据问题变困难了,因为数据来源可能有管理器、工厂和享元
- 代码比较散,维护变得困难
第14章 代理模式
创建另一个对象–代理,对本体对象的方法访问进行权限控制。不会修改或添加本体对象的方法。
1.三种代理
1)虚拟代理
与普通代理的关键区别在于,虚拟代理不会立即创建本体的实例,直到有方法被调用时才真正执行本体的实例化。
_initializeLibrary:function(){
if(this.library===null){
this.library=new PublicLibrary();
}
}
2)远程代理
用于访问位于另一个环境中的对象。不适合js中使用。一种更有可能的用途是控制对其他语言中的本体的访问。
3)保护代理
通常用来根据客户的身份控制对特定方法的访问。不适合js中使用。
2.适用场合
- 如果有些类或对象需要使用大量内存保存其数据,而并不需要在实例化完成之后立即访问这些数据;或者,其构造函数需要进行大量计算。那就应该使用虚拟代理将设置开销的产生推迟到真正需要使用数据的时候。
- 如果有某种远程资源,并且要为该资源提供的所有功能实现对应的方法,可以使用远程代理。
3.优缺点
优点:
- 借助远程代理,可以把远程资源当做本地js对象使用。
- 虚拟代理优化效率。当资源的创建或保有开销较大时,可以使用代理控制创建时间和方式,还可提示“正在加载”。
缺点:
-
远程代理获取远程资源的请求响应比访问本地资源要慢很多。而且需要和回调函数结合使用,增加了代码的复杂性。另外只有能够与远程资源通信的条件下才能工作。
-
使项目增加了不必要的复杂性和代码。
第15章 观察者模式
多对多的关系
实质是你可以对程序中某个对象的状态进行观察,并且在其发生改变时能够得到通知。(addEventListener)
1.适用场合
把人的行为和应用程序的行为分开。如事件监听器。
2.优缺点
优点:
基于行为的应用程序。让观察对象借助一个事件监听器替你处理各种行为并将信息委托给它的所有订阅者,从而降低内存消耗和提高互动性能。
缺点:
创建可观察对象会有加载时间的开销。可以使用惰性加载技术(把新的可观察对象的实例化推迟到需要发送事件通知的时候)。
第16章 命令模式
主要用途:把调用对象(用户界面、API和代理等)与实现操作的对象隔离开。
这是一种用来封装单个操作的结构型模式。
1.适用场合
- 需要对操作进行规范化处理的场合。
- 用来封装用于XHR调用或其他延迟性调用场合的回调函数。
- 在应用程序中实现取消机制。
2.优缺点
优点:
-
提高程序的模块化程度和灵活性。
-
实现取消和状态恢复等复杂的有用特性。
缺点:
- 运用不当造成浪费。
- 增加代码调试难度。
第17章 职责链模式
可以用来消除请求的客户和处理程序之间的耦合。
通过实现一个由隐式地对请求进行处理的对象组成的链而做到的,链中的每个对象可以处理请求,也可以将其传给下一个对象。
js事件委托
1.适用场合
- 事先不知道在几个对象中有哪些能够处理请求。
- 一批处理器对象在开发期间不可知,而是需要动态指定。
2.优缺点
优点:
- 动态选择由哪个对象处理请求。
- 在已有现成的链或层次体系的情况下,职责链模式更加有效。与组合模式的结合使用。
缺点:
- 请求与具体的处理程序被隔离开来,因此无法保证它一定会被处理,而不是径直从链尾离开。
- 代码的复杂性。
读后感
很多设计模式没有看懂,并且两者之间混淆,还需要继续学习和实践。