JS设计模式笔记
一、概述
1.1 js的灵活性
完成一项任务的5中不同方法
- 函数过程式实现
- 原型函数new挂载方法
- 对象原型挂载方式
- 原型注册方法方式
- 在4的基础上return this 可实现链式调用
1.2 弱类型语言
在定义变量时并不指定其类型
1.3 函数是一等对象
可以动态创建并且可以创建闭包
1.4 对象的易变性
对象和类都是异变的
内省:可以在运行时检查对象所具有的属性和方法
反射:可以用这种信息动态实例化类和执行其方法
1.5 继承
原型继承、类式继承
1.6 js中的设计模式
设计模式优点
- 可维护性。设计模式有助于降低模块之间的耦合度。使得对代码进行重构和换用不同的模块变得更加容易
- 沟通。设计模式为处理不同类型的对象提供了一套通用的属于。“工厂模式”
- 性能。提高程序运行速度减少需要传送到客户端的代码量。亨元模式和代理模式
设计模式缺点 - 复杂性。代码会变得复杂更难被新手理解
- 性能。尽管对某些性能提升性能,但多数模式对代码的性能都有所拖累。
二、接口
2.1 接口的概念
接口提供了一种用以说明一个对象应该具有那些方法的手段
objecy.compare ???
2.1.1接口之利
- 既定的一批接口具有自我描述性,并能促进代码的重用。接口可以告诉我们一个类实现了那些方法,从而帮助使用这个类。
- 有助于稳定不同类之间的通信方式。如果事先知道了接口就能减少在集成两个对象的过程中出现的问题。(实现说明希望一个类具有那些特性和操作。可以针对所需的类定义一个接口,并转交给另一个程序员,第二个程序员可以随心所欲编写自己的代码,只要他定义的类实现了哪个接口就行)
- 测试和调试也变得轻松
2.1.2 接口之弊
- js是一种极强表现力的语言,但接口的使用在一定程度上强化了类型的作用,降低了语言的灵活性
- js并没有提供对接口的内置支持。js没有Interface这个关键词
- js任何实现接口的方法都会对性能造成一些影响,在某种程度上这的归咎于额外的方法调用的开销
firebug这类性能分析器可以帮助判断是否真有必要剔除接口代码 - 无法强迫其他程序员遵守你定义的接口。
2.2 其他面向对象语言处理接口的方式
2.3 在js中模仿接口
js中模仿接口的三种方法:注释法、属性检查法、鸭式辩型法
2.3.1 注释法
将想实现的接口用interface写出来加上注释后再用js实现
/*
interface Composite{
function add(child);
function remove(child);
function getChild(index);
}
interface formItem{
function save();
}
*/
var CompositeForm = function (id,method,action){
...
}
CompositeForm.prototype.add = function(child){}
CompositeForm.prototype.remove = function(child){}
CompositeForm..prototype.getChild = function(index){}
//implement the FormItem interface
CompositeForm.prototype.save = function(){}
弊端:
没有确保函数真正实现了正确的方法集而进行检查,也不会抛出错误已告知程序员程序中有问题。对接口约定的遵守完全依靠自觉。
优点:
易于实现,不需要额外的类或函数。
2.3.2 用属性检查模仿接口
接口自身仍然是注释,可以通过检查一个属性得知某个类自称实现了什么接口
/*
interface Composite{
function add(child);
function remove(child);
function getChild(index);
}
interface formItem{
function save();
}
*/
var CompositeForm = function(id,method,action){
//CompositeForm宣称自己实现了Composite、FormItem两个接口
this.implementsInterfaces = ['Composite','FormItem'];
}
function addForm(formInstance){
if(!implements(formInstance,'Composite','FormItem')){
throw new Error('Object dose not implement a reqired interface')
}
}
function implements(object){
//i == 1
for(var i = 1;i < arguments.length;i++){
var interfaceName = arguments[i];
var interfaceFound = false;
for(var j = 0;j < object.implementsInterfaces.length;j++){
if(object.implementsInterfaces[j] == interfaceName){
interfaceFound = true;
breack;
}
}
if(!interfaceFound){return false;}
}
return true
}
优点:
对类所实现的接口提供了文档说明。如果需要的接口不在一个类宣称的接口之列,会看到错误消息。
弊端:
并未确保类真正实现了自称实现的接口
2.3.3 鸭式辩型模仿接口
如果对象具有于接口定义的方法同名的所有方法,那么久认为它实现了这个接口。可以用一个辅助函数来确保对象具有所有必需的方法。
//Interface
var Composite = new Interface('Composite',['add','remove','getChild']);
var FormItem = new Interface('FormItem',['save']);
//CompositeForm class
var CompositeForm = function(id,method,active){}
function addForm(formInstance){
//ensureImplements函数至少需要两个参数,一个是想要检查的对象,其余参数是对哪个对象进行检查的接口。
//ensureImplements检查第一个参数代表的对象是否实现了那些接口所声明的所有方法。
//如果发现漏掉任意一个方法,就会抛出错误,其中包含了所缺少的哪个方法和未被正确实现的接口名称等有用信息。
ensureImplements(formInstance,Composite,FormItem);
//This function will throw an error if required method is not implemented
}
弊端:
类并不生明自己是实现了那些接口,降低了代码的重用性,并且缺乏其他两种方法那样的自我描述性。需要一个辅助类Interface和一个辅助函数ensureImplements。而且只关心方法的名称,并不检查其参数的名称数目或类型。
2.4 接口实现方法
结合注释法和鸭式辩型模仿法。用注释声明类支持的接口,从而提高代码的可重用性以及文档的完整性。还用辅助类Interface及其类方法Interface.ensureImplements来对对象实现的方法进行显示检查。如果对象未能通过检查,这个方法将返回一条有用的错误信息。
//Interfaces
var Composite = new Interface('Composite',['add','remove','getChild']);
var FormItem = new Interface('FormItem',['save']);
//CompositeForm class
var CompositeForm = function(id,method,active){
}
function addForm(formInstance){
Interface.ensureImplements(formInstance,Composite,FormItem);
//如果发现有问题就会抛出一个错误。
//这个错误要买被其他代码捕捉并得到处理,要么中断程序的执行。
}
2.5 Interface类
该类的所有方法对其参数都有严格的要求,如果参数未能通过检查,将导致错误的抛出。特地加入这种检查的目的在于:如果没有错误抛出,那么可以肯定接口已经得到了正确的声明和实现
//Constructor
var Interface = function(name,methods){
if(arguments.length != 2){
throw new Error('Interface constructor called with ' + arguments.length + 'arguments,but expected exactly 2.');
this.name = name;
this.methods = [];
for(var i = 0;len = methods.length;i< len;i++){
if(typeof methods[i] !== 'string'){
throw new Error('Interface construtor expects method names to be passed in as a string');
}
this.methods.push(methods[i])
}
}
}
//Static class method
Interface.ensureImplements = fucntion(object){
if(arguments.length < 2){
throw new Error('Function Interface.ensureImplements called with' + arguments.length + 'arguments, but exoected at least 2.')
}
for(var i = 1,len = atguments.length;i < len;i++){
var interface = arguments[i];
if(interface.construtor !== Interface){
throw new Error('Function Interface.ensureImplements expects arguments two and above to be instances of Interface')
}
for(var j = 0,methodsLen = interface.methods.length;j < methodsLen;j++){
var method = interface.methods[j];
if(object[method] || typeof object[method] !== 'function'){
throw new Error ('Function Interface.ensureImplements:object dose not implement the' + interface.name + 'interface.Method' + method + 'was not found')
}
}
}
}
2.5.1 Interface类的使用场合
2.5.2 Interface类的用法
- 将Interface类纳入HTML文件
- 逐一检查代码中所有以对象为参数的方法。搞清代码的正常运转要求这些对象参数具有那些方法
- 为需要的每一个不同的方法创建一个Interface对象
- 剔除所有针对构造器的显示检查,因为使用鸭式辩型,所以对象的类型不在重要
- 以Interface.ensureImplements取代原来的构造器检查
2.6 依赖于接口的设计模式
- 工厂模式
对象工厂所创建的具体对象会因具体情况而异。使用接口可以确保所创建的这些对象可以互换使用。对象工厂 可以保证其生产出来的对象都实现了必须的方法。 - 组合模式
组合模式的中心思想在于可以将对象群体与组成对象同等对待。这是组合让他们实现同样的接口来做到做到的。如果不进行某种形式的鸭式辩型或类型检查,组合模式就会失去大部分作用。 - 装饰模式
装饰者通过透明的为另一对象提供包装而发挥作用。这是通过实现与另外哪个对象完全相同的接口做到的。对于外界而言,一个装饰者和他所包装的对象看不出有什么区别。我们将使用Interface类来确保所创建的装饰着对象实现了必需的方法。 - 命令模式
代码中所有的命令对象都要实现同一批方法。通过使用接口,为执行这些命令对象而创建的类可以不必知道这些对象具体是什么,只要知道他们都实现了正确的接口即可。借此可以创建出模块化成都很高而耦合程度很低的用户界面和api
三、封装和信息隐藏
通过将一个方法或属性声明为私用的,可以让对象的实现细节对其他对象保密以降低对象之间的耦合程度,可以保持数据的完整性并对其修改方式加以约束。
3.1 信息隐藏原则
3.1.1. 封装与信息隐藏
信息隐藏是目的,封装是到达这个目的的技术。
封装可以被定义为对象内部数据表现形式和实现细节进行隐藏。
在js中没有私用的关键字,但是可以通过闭包的方式实现该功能
3.1.2 接口扮演的角色
尽量避免公开未定义与接口中的方法
一个理想的软件系统应该为所有类定义接口。
3.2 创建对象的基本模式
- 门户大开型:只能提供公用成员
- 使用下划线来表示方法或属性的私用性
- 使用闭包来创建真正私用的成员:这些成员只能通过一些特权方法访问
3.2.1 门户大开型对象
var Book = function(isbn,title,author){
if(isbn == undefined)throw new Error('Book constructor requires an isbn');
this.isbn = isbn;
this.title = title || 'No title specified';
this.author = author || 'No author specified';
}
Book.propotypr.dispaly = function(){}
上述方法没有校验isbn数据的完整性
var Book = function(isbn,title,author){
if(isbn == undefined)throw new Error('Book constructor requires an isbn');
this.isbn = isbn;
this.title = title || 'No title specified';
this.author = author || 'No author specified';
}
Book.prototype = {
checkIsbn: function(isbn){
//进行isbn校验
},
display: function(){}
}
上述方法没有对属性进行内部隐藏,存在被外部随意修改的风险
3.2.2 用命名规范区别私用成员
在门户打开基础上在一些方法和属性的名称前加上下划线用以标识自己的私用性。
3.2.3 作用域、嵌套函数和闭包
3.2.4 用闭包实现私用成员
var Book = function(newIsbn,newTitle,newAuthor){
//Private attributes
var isbn,title,author;
//Private method
function checkIsBN(){}
//Privileged methods 特权方法
this.getIsbn = function(){
retun isbn;
}
this.setIsbn = function(newIsbn){
if(!checkIsbn(newIsbn)) throw new Error('Book: Invalid ISBN');
isbn = newIsbn;
}
this.getTitle = function(){
return title;
}
this.setTitle = function(newTitle){
title = newTitle || 'No title specified';
}
this.getAuthor = function(){
return author;
}
this.setAuthor = function(newAuthor){
author = newAuthor || 'No author specified'
}
// Constructor code
this.setIsbn(newIsbn)
this.setTitle(newTitle)
this.setAuthor(newAuthor)
};
//Public non-privileged methods
Book.prototype = {
display:function(){}
}
门户大型对象创建模式中,所有方法都创建在原型对象中,不管生成多少对象实例,这些方法在内存中只存在一份。
利用闭包创建对象每生成一个新的对象实例都将为每一个私用方法和特权方法生成一个新的副本,更消耗内存。
闭包实现私用成员导致派生问题被称为“继承破坏封装”。子类不能访问超类的私用属性和方法
3.3 更多高级对象创建模式
3.3.1 静态方法和属性
var Book = (function(newIsbn,newTitle,newAuthor){
var numOfBooks = 0;
//Private method
function checkIsBN(){}
Return the constructor
return function(newIdbn,newTitle,newAuthoe){
//Private attributes
var isbn,title,author;
//Privileged methods 特权方法
this.getIsbn = function(){
retun isbn;
}
this.setIsbn = function(newIsbn){
if(!checkIsbn(newIsbn)) throw new Error('Book: Invalid ISBN');
isbn = newIsbn;
}
this.getTitle = function(){
return title;
}
this.setTitle = function(newTitle){
title = newTitle || 'No title specified';
}
this.getAuthor = function(){
return author;
}
this.setAuthor = function(newAuthor){
author = newAuthor || 'No author specified'
}
// Constructor code
numOfBooks++;
if(numOfBooks > 50)throw new Error('Book:Only 50 instances of Book can ne created')
// Constructor code
this.setIsbn(newIsbn)
this.setTitle(newTitle)
this.setAuthor(newAuthor)
}
})();
//Public static methods
Book.converTotitlrCase = function(inputString){}
//Public non-privileged methods
Book.prototype = {
display:function(){}
}
判断一个私用方法是否应该被设计为静态方法:看他是否需要访问实例数据,如果不需要则设计为静态方法更有效率。
3.3.2 常量
常量是不能被修改的变量,可以通过只有取值器没有赋值其的私用变量来模仿常量。
Class.getUPPER_BOUND()
//特权静态方法
var Class = (function(){
//Constants(created as private static attributes)
var UPPER_BOUND = 100;
// Constructor
var ctor = function(constructorArgumrnt){
}
//Privileged static method
ctor.getUPPER_BOUND = function(){
return UPPER_BOUND;
}
//Return the Constructor
return ctor
})()
Class.getUPPER_BOUND()
//特权静态方法
var Class = (function(){
//Private static attributes
var constants = {
UPPER_BOUND: 100,
LOWER_BOUND: -100
}
// Constructor
var ctor = function(constructorArgumrnt){
}
//Privileged static method
ctor.getUPPER_BOUND = function(name){
return constants [name];
}
//Return the Constructor
return ctor
})()
Class.getConstant('UPPER_BOUND')
3.3 单体和对象工厂
单体模式:使用一个有外层函数返回的对象字面量来公开特权成员,而私用成员则被保护性的封装在外层函数的作用域中。
对象工厂:也可以使用闭包来创建具有私用成员的对象,最简形式就是一个类构造器。
3.4 封装之利
通过之公开接口的规定的方法,可以弱化模块间的耦合。
3.5 封装之弊
私用方法很难进行单元测试
四、继承
4.2 类式继承
function Person(name){
this.name = name;
}
Person.prototype.getName = function(){return this.name}
var reader = new Pserson('John');
reader.getName()
4.2.1 原型链
function Author(name.books){
Person.call(this,name)
this.books = books;
}
//创建原型继承
Author.prototype = new Person();
Author.prototype.constructor = Author;
Author.prototype.getBooks = function(){return this.books}
var author = []'
author[0] = new Author('A',['bbbb])
4.2.2 extend函数
function extend(subClass,superClass){
var F = function(){};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
}
上述方法存在超类Person的名称被固化在了Author类的声明中。
function extend(subClass,superClass){
var F = function(){};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
subClass.supecless = superClass.prototype;
if(superClass.prototyoe.constructor == Object.prototype.constructor){
superClass.protype.constructor = superClass;
}
}
4.3 原型式继承
var Person = {
name: 'default name',
getName:function(){
return this.name;
}
}
var reader = clone(Person);
alert(reader.getName()); //'default name'
reader.name = 'John';
alert(reader.getName()); // 'John'
var Author = clone(Pserson);
Author.books = [];
Author.getBooks = function(){return this.books}
4.3.1 对继承而来的成员的读和写的不对等性
当实例改变引用地址的属性时会引起原类属性值的改变
可以用工厂方法来创建
var CompoundObject = {};
CompoundObject.string1 = 'defult value';
CompouneObject.createChildObject = function(){
return {
bool:true,
num:10
}
}
CompoundOject.childObject = CompoundOject.createChildObject ();
var compundObjectClone = clone(CompoundOject);
compundObjectClone.childObject = CompoundOject.createChildObject ();
compundObjectClone.childObject.num = 5;
4.3.2 clone 函数
function clone(object){
function F(){}
F.prototype = object; //prototype属性是用来指向原型对象的,通过原型链机制,提供了所有继承而来的成员的链接
return new F;
}
4.4 类式继承和原型继承的对比
如果设计的是API时使用类式继承。
原型式继承更能节约内存。原型链读取成员的方式使得克隆出来的对象都共享每个属性和方法的唯一一份实例,只有在直接设置了某个克隆出来的对象和方法时情况才会有所变化。
类式继承方式中创建的每一个对象在内存中都有自己的一套属性的副本。
4.5 继承与封装
4.6 掺元类
创建一个包含各种通用方法的类,然后再用他扩充其他类,这种包含通用方法的类称为掺元类
/**Mixin class**/
/**没有必要让所有类都继承serialize方法**/
var Mixin = function(){
serialize:function(){
var output = [];
for(key in this){
output.push(key + ':' + this[key])
}
return output.join(', ')
}
}
augment(Author,Mixin);
/**多亲继承**/
var author = new Author('Ross Harmes',['Java']);
var serializedString = author.serialize();
function augment(receivingClass,givingClass){
if(arguments[2]){
for(var i = 2,len = auguments.length;i< len;i++){
receivingClass.prototype[arguments[i]] = givingClass.prototype[auguemnts[i]];
}
}else{
for(methodsName in givingClass.prototype){
if(!receivingClass.prototype[methodsName]){
receivingClass.prototype[arguments[i]] = givingClass.prototype[auguemnts[i]];
}
}
}
}
4.7 就地编辑
4.8 继承使用场合
再内存效率比较重要的场合原型式继承是最佳函数(clone函数)
对象写法最好使用类式继承(extend函数)】一上两种方法都适合于类间较小的类层次体系,类之间差异较大可以使用掺元类。
五、单体模式
用途:
- 划分命名空间,以减少网页中全局变量的数目
- 借助分支技术来封装浏览器之间的差异
- 借助单体模式可以把代码组织的更为一致,从而使其更容易阅读和维护
5.1 单体的基本结构
简单单体
/** Basic Singleton
单体是一个用来划分命名空间并将一批相关方法和属性组织在一起的对象,如果他可以被实例化,那么只能被实例化一次
**/
var Singleton = {
attribute1:true,
attribute2:10,
method1:function(){},
method2:function(){},
}
Singleton.attribute1 = false;
var total = Singleton.attribute2 + 5;
var result = Singleton.method1();
5.2 划分命名空间
单体对象由两部分组成:包含着方法和属性成员和对象自身,以及用于访问它的变量。由于这些成员只能通过这个单体对象变量进行访问,因此在某种意义上,可以说他们被单体对象圈在一个命名空间中。
5.3 用作特定网页专用代码的包装器的单体
Namespace.PageName = {
CONSTANT_1:true,
CONSTANT_2:10,
method1:function(){},
method2:function(){},
init:function(){}
}
addLoadEvent(Namespace.pageName,init)
5.4 拥有私用成员的单体
弊端:耗费内存
5.4.1 使用下划线表示
GiantCorp.DataParser = {
// provate methods
_sttipWhitespave:function(str){return str.replace(/\s+/,'')},
_stringSplit:function(str,delimiter){return str.split(delimiter)};
stringToArray:function(str,delimiter,stripWS){
if(stripWS){
str = this._stripWhitespace(str)
}
var putputArray = this._stringSplit(str,delimiter);
return putputArray;
}
}
5.4.2 使用闭包
单体只会被实例化一次,所以可以把他们都声明在构造函数内部。
下述这种模式又称模块模式,指的是他可以把一批相关方法和属性组织为模块并起到划分命名空间的作用
/**Singleton as an Object Literal**/
MyNamespace.Singleton = {};
MyNamespace.Singleton = (function(){
// Private members
var privateAttribute1 = false;
var privateAttribute2 = [1,2,3];
function privateMethod1(){}
function privateMethod2(args ){}
return{ //Public membersn
publicAttribute1:true,
publicAttribute2:10,
publicMethod1:function(){},
publicMethod2:function(args){}
}
})()
5.5 惰性实例化
对于资源密集型的或配置开销大的单体更适合在时需要使用的时候在进行实例化,这种技术被称为惰性加载。
惰性单体的特别之处子啊与,对于他们的访问必须借助于一个静态方法,这个方法会检查该单体是否已经被实例化,如果还没有那么他将创建并返回实例。
MyNamespave.Singleton = (function(){
var uniqueInstance;
function constructor(){
//Private member
var privateAttribute1 = false;
var privateAttribute2 = [1,2,3];
function privateMethod1(){}
function privateMethod2(args){}
return {
publicAttribute1:true,
publicAttribute2:10,
publicMethod1:function(){}
publicMtehod2:function(args){}
}
}
return {
getInstance:function(){//Control code goes here
if(!uniqueInstance){
uniqueInstance = constructor()
}
return uniqueInstance
}
}
})()
把一个单体转化为惰性加载单体后必须要对调用他的代码进行修改
MyNameSpace.Singleton.publicMethod1()
MyNameSpce.getInstance().publicMethod1()
惰性加载单体的缺点在于其复杂性。
5.6 分支
分支是一种用来吧浏览器间的差异封装在运行期间进行设置的动态方法中的技术。
MyNamespace.Singleton = (function(){
var objectA = {
method1:function(){},
method2:function(){}
}
var obejctB = {
method1:function(){},
method2:function(){}
}
return (someCondition) ? objectA : obejctB
})()
5.7 用分支技术创建XHR对象
var SimpleChrFactory = (function(){
//the three branches
var standard = {
createXhrObject: function(){
return new HMLHttpRequest();
}
}
var activeXNew = {
createXhrObject: function(){
return new ActiveXObject('Msml2.HMLHTTP')
}
}
var activeXOld = {
createXhrObject: fucntion(){
return nre ActiveVObject('Microsoft.XMLHTTP')
}
}
var testObject ;
try{
testObject = standard.createXhrObject();
return standard;
}catch(err){
try{
testObject = standard.createXhrObject();
return activeXNew ;
}catch(err){
testObject = standard.createXhrObject();
return activeXOld;
}
}
})()
使用SimpleChrFactory.createXhrObject()就能得到合适特定的运行环境XHRduix。用了分支技术后所有那些特性嗅探代码都只会执行一次,而不是每生成一个对象就要执行一次。
5.8 单体模式的适用场合
多使用单体模式
5.9 单体模式之利
5.10 单体模式之弊
六、方法的链式调用
通过重用一个初始操作来达到用少量代码表达复杂操作的目的。
//without chaining
addEvent($('example'),'click',function(){
setStyle(this,'color','green');
show(this);
})
//with chaining
$('example').addevent('click',function(){
$(this).setStryle('color','green').show()
})
6.1 调用链的结构
(function(){
//use a pricate class
function _$(els){
this.elements = [];
for(var i = 0,len = els.length;i < len;++i){
var element = els[i];
if(typeof element == 'string'){
element = document.getElementById(element);
}
this.elments.push(element)
}
}
_$.prototype = {
each:function(fn){
each:function(fn){
for(var i = 0,len = arguments.length;i < len; ++i){
fn.call(this,this.elements[i];)
}
return this //将this返回
}
},
}
//the public interface remains the same
widow.$ = function(){
return new _$(atguments)
}
})()