JavaScript设计模式.docx
- 文档编号:26898082
- 上传时间:2023-06-23
- 格式:DOCX
- 页数:9
- 大小:23.83KB
JavaScript设计模式.docx
《JavaScript设计模式.docx》由会员分享,可在线阅读,更多相关《JavaScript设计模式.docx(9页珍藏版)》请在冰豆网上搜索。
JavaScript设计模式
JavaScript设计模式
1.弱类型语言在JavaScript中,定义变量时不必声明其类型。
但这并不意味着变量没有类型。
一个变量可以属于几种类型之一,这取决于其包含的数据。
JavaScript中有三种原始类型:
布尔型、数值型和字符串类型(不区分整数和浮点数是JavaScript与大多数其他主流语言的一个不同之处)。
此外,还有对象类型和包含可执行代码的函数类型,前者是一种复合数据类型(数组是一种特殊的对象,它包含着一批值的有序集合)。
最后,还有空类型(null)和未定义类型(undefined)这两种数据类型。
原始数据类型按值传送,而其他数据类型则按引用传送。
与其他弱类型语言一样,JavaScript中的变量可以根据所赋的值改变类型。
原始类型之间也可以进行类型转换。
toString可以把数值或布尔值转为字符串。
parseFloat和parseInt函数可以把字符串转变为数值。
双重“非”可以把字符串或数值转变为布尔值:
varbool=!
!
num;2.初谈闭包匿名函数最有趣的用途是用来创建闭包。
闭包是一个受到保护的变量空间,由内嵌函数生成。
JavaScript具有函数级的作用域。
这意味着定义在函数内部的变量在函数外部不能被访问。
JavaScript的作用域又是词法性质的。
这意味着函数运行在定义它的作用域中,而不是在调用它的作用域中。
把这两个因素结合起来,就能通过把变量包裹在匿名函数中而对其加以保护。
3.依赖于接口的设计模式下面列出的设计模式,尤其依赖接口:
工厂模式。
对象工厂所创建的具体对象会因具体情况而异。
使用接口可以确保所创建出来的这些对象可以互换使用。
也就是说,对象工厂可以保证其生产出来的对象都实现了必需的方法。
组合模式。
如果不用接口你就不可能用这个模式。
组合模式的中心思想在于可以将对象群体与其组成对象同等对待。
这是通过让它们实现同样的接口来做到的。
如果不进行某种形式的鸭式辨型或类型检查,组合模式就会失去大部分作用。
装饰者模式。
装饰者通过透明地为另一对象提供包装而发挥作用。
这是通过实现与另外那个对象完全相同的接口而做到的。
对于外界而言,一个装饰者和它所包装的对象看不出有什么区别。
命令模式。
代码中所有的命令对象都要实现同一批方法。
通过使用接口,你为执行这些命令对象而创建的类可以不必知道这些对象具体是什么,只要知道它们都实现了正确的接口即可。
4.用命名规范区别私用成员在一些方法和属性的名称前面加下划线以示其私用性。
下划线的这种用法是一个众所周知的命名规范,它表明一个属性(或方法)仅供对象内部使用,直接访问它或设置它可能会导致意想不到的后果。
这有助于防止程序员对它的无意使用,却不能防止对它的有意使用。
后一个目标的实现需要有真正私用性的方法。
5.作用域下面这个示例说明了JavaScript中作用域的特点:
functionfoo(){vara=10;functionbar(){a*=2;}bar();returna;}在这个示例中,a定义在函数foo中,但函数bar可以访问它,因为bar也定义在foo中。
bar在执行过程中将a设置为a乘以2。
当bar在foo中被调用时它能够访问a,这可以理解。
但是如果bar是在foo外部被调用呢?
functionfoo(){vara=10;functionbar(){a*=2;returna;}returnbar;}varbaz=foo();console.log(baz());//20console.log(baz());//40console.log(baz());//80varblat=foo();console.log(blat());//20在上述代码中,所返回的对bar函数的引用被赋给变量baz。
这个函数现在是在foo外部被调用,但它依然能够访问a。
这是因为JavaScript的作用域是词法性的。
函数是运行在定义它们的作用域中(本例中是foo内部的作用域),而不是运行在调用它们的作用域中。
只要bar被定义在foo中,它就能访问在foo中定义的所有变量,即使foo的执行已经结束。
这就是闭包的一个例子。
在foo返回后,它的作用域被保存下来,但只有它返回的那个函数能够访问这个作用域。
在前面的示例中,baz和blat各有这个作用域及a的一个副本,而且只有它们自己能对其进行修改。
返回一个内嵌函数是创建闭包最常用的手段。
6.用闭包实现私用成员的弊端在门户打开型对象创建模式中,所有方法都创建在原型对象中,因此不管派生多少对象实例,这些方法在内存中只存在一份。
而包含特权方法、私用成员的创建模式中,每生成一个新的对象示例都将为每一个私用方法和特权方法生成一个新的副本。
这会比其他做法耗费更多内存,所以只宜用在需要真正的私用成员的场合。
这种对象创建模式也不利于派生子类,因为所派生出的子类不能访问超类的任何私用属性或方法。
相比之下,在大多数语言中,子类都能访问超类的所有私有属性和方法。
故在JavaScript中用闭包实现私用成员导致的派生问题称为“继承破坏封装”。
7.静态方法和属性前面所讲的作用域和闭包的概念可用于创建静态成员,包括公用和私用的。
大多数方法和属性所关联的是类的实例,而静态成员所关联的则是类本身。
换句话说,静态成员是在累的层次上操作,而不是在实例的层次上操作。
每个静态成员都只有一份。
稍后将会看到,静态成员是直接通过类对象访问的。
下面是添加了静态属性和方法的Book类:
varBook=(function(){//私有静态变量varnumOfBooks=0;//私有静态方法functioncheckIsbn(isbn){}//返回一个构造器returnfunction(newIsbn,newTitle,newAuthor){//私有属性varisbn,title,author;//特权方法this.getIsbn=function(){returnisbn;};this.setIsbn=function(newIsbn){if(!
checkIsbn(newIsbn)){thrownewError('Book:
InvalidISBN.');}isbn=newIsbn;};this.getTitle=function(){returntitle;};this.setTitle=function(newTitle){title=newTitle||'Notitlespecified';};this.getAuthor=function(){returnauthor;};this.setAuthor=function(newAuthor){author=newAuthor||'Noauthorspecified';};//Constructedcode.numOfBooks++;if(numOfBooks>50){thrownewError('.');}this.setIsbn(newIsbn);this.setTitle(newTitle);this.setAuthor(newAuthor);}})();//公共静态方法Book.convertToTitleCase=function(inputString){};//公共非特权方法Book.prototype={display:
function(){}};这里的私用成员和特权成员仍然被声明在构造器中(分别使用var和this关键字)。
但哪个构造器却从原来的普通函数变成了一个内嵌函数,并且被作为包含它的函数的返回值赋给变量Book。
这就创建了一个闭包,你可以把静态的私用成员声明在里面。
位于外层函数声明之后的一对空括号很重要,其作用是一段代码载入就立即执行这个函数(而不是在调用Book构造函数时)。
这个函数的返回值是另一个函数,它被赋给Book变量,Book因此成了一个构造函数。
在实例化Book时,所调用的是这个内层函数。
外层那个函数只是用于创建一个可以用来存放静态私用成员的闭包。
在本例中,checkIsbn被设计为静态方法,原因是为Book的每个实例都生成这个方法的一个新副本毫无道理。
此外还有一个静态属性numOfBooks,其作用在于跟踪Book构造器的总调用次数。
本例利用这个属性将Book实例的个数限制为不超过50个。
这些私用的静态成员可以从构造器内部访问,这意味着所有私用函数和特权函数都能访问它们。
与其他方法相比,它们有一个明显的优点,那就是内存中只会存放一份。
因为其中那些静态方法被声明在构造器之外,所以它们不是特权方法,不能访问任何定义在构造器中的私用属性。
定义在构造器中的私用方法能够调用那些私用静态方法,反之则不然。
要判断一个私用方法是否应该被设计为静态方法,一条经验法则是看它是否需要访问任何实例数据。
如果它不需要,那么将其设计为静态方法会更有效率(从内存占用的意义上来讲),因为它只会被创建一份。
创建公用的静态成员则容易得多,只需直接将其作为构造函数这个对象的属性创建即可,前述代码中的方法converToTitleCase就是一例。
这实际上相当于把构造器作为命名空间来使用。
所有公用静态方法如果作为独立的函数来声明其实也同样简单,但最好还是像这样把相关行为集中在一起。
这些方法用于与类这个整体相关的任务,而不是与类的任一特定实例相关的任务。
它们并不直接依赖于对象实例中包含的任何数据。
8.私用变量模仿常量通过创建只有取值器而没有赋值器的私用变量可以模仿常量。
varClass=(function(){varUPPER_BOUND=100;//构造器varctor=function(constructorArgument){};//静态特权方法ctor.getUPPER_BOUND=function(){returnUPPER_BOUND;};returnctor;})();9.封装之弊私用方法很难进行单元测试。
因为它们及其内部变量都是私用的,所以在对象外部无法访问到它们。
这个问题没有什么很好的应对之策。
你要么通过使用公用方法来提供访问途径(这样一来就葬送了使用私有方法所带来的大多数好处),要么设法在对象内部定义并执行所有单元测试。
最好的解决办法是只对公用方法进行单元测试。
这应该能覆盖到所有私用方法,尽管对它们的测试只是间接的。
这种问题不是JavaScript所独有的,只对公用方法进行单元测试是一种广为接收的处理方式。
使用封装意味着不得不与复杂的作用域链打交道。
封装可能会损害类的灵活性,致使其无法被用于某些你未曾想到过的目的。
10.单体模式单体模式是JavaScript中最基本但又最有用的模式之一,它可能比其他任何模式都更常用。
这种模式提供了一种将代码组织为一个逻辑单元的手段,这个逻辑单元中的代码可以通过单一的变量进行访问。
通过确保单体对象只存在一份实例,你就可以确信自己的所有代码使用的都是同样的全局资源。
单体类在JavaScript中有许多用处。
它们可以用来划分命名空间,以减少网页中全局变量的数目。
更重要的是,借助于单体模式,你可以把代码组织得更为一致,从而使其更容易阅读和维护。
11.单体的基本结构varSingleton={attribute1:
true,attribute2:
10,method1:
function(){},method2:
function(args){}};这个单体对象可以被修改。
你可以为其添加新成员,这一点与别的对象字面量没有什么不同。
你也可以用delete运算符删除其现有成员。
这实际上违背了面向对象设计的一条原则:
类可以被扩展,但不应该被修改。
按传统的定义,单体是一个只能被实例化一次并且可以通过一个众所周知的访问点访问的类。
要是严格按照这个定义来说,前面的例子所示的并不是一个单体,因为它不是一个可实例化的类。
我们打算把单体模式定义的更广义一些:
单体是一个用来划分命名空间并将一批相关方法和属性组织在一起的对象,如果可以被实例化,那么它只能被实例化一次。
12.划分命名空间为了避免无意中改写变量,最好的解决办法之一是用单体对象将代码组织在命名空间之中。
下面是前面的例子用单体模式改良后的结果:
varMyNamespace={findProduct:
function(id){}};现在findProduct函数是MyNamespace中的一个方法,它不会被全局命名空间中声明的任何新变量改写。
要注意,该方法仍然可以从各个地方访问。
不同之处在于现在其调用方式不是findProduct(id),而是MyNamespace.findProduct(id)。
还有一个好处就是,这可以让其他程序员大体知道这个方法的声明地点及其作用。
用命名空间把类似的方法组织到一起,也有助于增强代码的文档性。
13.模块模式有一种单体模式被称为模块模式,因为它可以把一批相关方法和属性组织为模块并起到划分命名空间的作用。
例如:
MyNamespace.Singleton=(function(){//私有成员varprivateAttribute1=false;varprivateAttribute2=[1,2,3];functionprivateMethod1(){}functionprivateMethod2(){}return{//publicmemberspublicAttribute1:
true,publicAttribute2:
10,publicMethod1:
function(){},publicMethod2:
function(args){}}})();14.简单工厂模式最好用一个例子来说明简单工厂模式的概念。
假设你想开几个自行车商店,每个店都有几种型号的自行车出售。
这可以用一个类来表示:
/*BicycleShopclass.*/varBicycleShop=function(){};BicycleShop.prototype={sellBicycle:
function(model){varbicycle;switch(model){case'TheSpeedster':
bicycle=newSpeedSter();break;case'TheLowrider':
bicycle=newLowrider();break;case'TheComfortCruiser':
default:
bicycle=newComfortCruiser();}Interface.ensureImplements(bicycle,Bicycle);bicycle.assemble();bicycle.wash();returnbicycle;}};sellBicycle方法根据所要求的自行车型号用switch语句创建一个自行车的实例。
各种型号的自行车实例可以互换使用,因为它们都实现了Bicycle接口:
/*TheBicycleinterface.*/varBicycle=newInterface('Bicycle',['assemble','wash','ride','repair']);/*Speedsterclass.*/varSpeedster=function(){};Speedster.prototype={assemble:
function(){},wash:
function(){},ride:
function(){},repair:
function(){}};要出售某种型号的自行车,只要调用sellBicycle方法即可:
varcaliforniaCruisers=newBicycleShop();varyourNewBike=californiaCruisers.sellBicycle('TheSpeedster');在情况发生变化之前,这倒也挺管用。
但要是你想在供货目录中加入一款新车型又会怎么样呢?
你得为此修改BicycleShop的代码,哪怕这个类的实际功能实际上并没有发生改变——依旧是创建一个自行车的新实例,组装它,清洗它,然后把它交给顾客。
更好的解决办法是把sellBicycle方法中“创建新实例”这部分工作转交给一个简单工厂对象:
/*BicycleFactorynamespace.*/varBicycleFactory={createBicycle:
function(model){varbicycle;switch(model){case'TheSpeedster':
bicycle=newSpeedSter();break;case'TheLowrider':
bicycle=newLowrider();break;case'TheComfortCruiser':
default:
bicycle=newComfortCruiser();}Interface.ensureImplements(bicycle,Bicycle);returnbicycle;}};BicycleFactory是一个单体,用来把createBicycle方法封装在一个命名空间中。
这个方法返回一个实现了Bicycle接口的对象,然后你可以照常对其进行组装和清洗:
/*BicycleShopclass,improved.*/varBicycleShop=function(){};BicycleShop.prototype={sellBicycle:
function(model){varbicycle=BicycleFactory.createBicycle(model);bicycle.assemble();bicycle.wash();returnbicycle;}};这个BicycleFactory对象可以供各种类用来创建新的自行车实例。
有关可供车型的所有信息集中在一个地方管理,所以添加更多车型很容易:
/*BicycleFactorynamespace,withmoremodels.*/varBicycleFactory={createBicycle:
function(model){varbicycle;switch(model){case'TheSpeedster':
bicycle=newSpeedSter();break;case'TheLowrider':
bicycle=newLowrider();break;case'TheFlatlander':
bicycle=newFlatlander();break;case'TheComfortCruiser':
default:
bicycle=newComfortCruiser();}Interface.ensureImplements(bicycle,Bicycle);returnbicycle;}};15.工厂模式真正的工厂模式与简单工厂模式的区别在于,它不是另外使用一个类或对象来创建自行车,而是使用一个子类。
按照正式定义,工厂是一个将其成员对象的实例化推迟到子类中进行的类。
16.工厂模式的适用场合动态实现:
如果需要创建一些用不同方式实现同一接口的对象,那么可以使用一个工厂方法或简单工厂对象来简化选择实现的过程。
节省设置开销:
如果对象需要进行复杂并且彼此相关的设置,那么使用工厂模式可以减少每种对象所需的代码量。
如果这种设置只需要为特定类型的所有实例执行一次即可,这种作用尤为突出。
把这种设置代码放到类的构造函数中并不是一种高效的做法,这是因为即便设置工作已经完成,每次创建新实例的时候这些代码还是会执行,而且这样做会把设置代码分散到不同的类中。
工厂方法非常适合于这种场合。
它可以在实例化所有需要的对象之前先一次性地进行设置。
无论有多少类会被实例化,这种办法都可以让设置代码集中在一个地方。
用许多小型对象组成一个大对象17.工厂模式之利工厂模式的主要好处在于消除对象间的耦合。
通过使用工厂方法而不是new关键字及具体类,你可以把所有实例化的代码集中在一个位置。
这可以大大简化更换所用的类或在运行期间动态选择所用的类的工作。
在派生子类时它也提供了更强大的灵活性。
所有这些好处都与面向对象设计的这两条原则有关:
弱化对象间的耦合;防止代码的重复。
在一个方法中进行类的实例化,可以消除重复性的代码。
这是在用一个对接口的调用取代一个具体的实现。
这些都有助于创建模块化的代码。
18.桥接模式桥接模式最常见和实际的应用场合之一就是事件监听器回调函数。
假设有一个名为getBeerById的API函数,它根据一个标识符返回有关某种啤酒的信息。
你希望用户在点击的时候获取这种信息。
那个被点击的元素很可能有啤酒的标识符信息,它可能是作为元素自身的ID保存,也可能是作为别的自定义属性保存。
下面是一种做法:
addEvent(element,'click',getBeerById);functiongetBeerById(e){varid=this.id;asyncRequest('GET','beer.uri?
id='+id,function(resp){console.log(resp.responseText);});}这个API只能工作在浏览器中,如果要对这个API函数做单元测试,或者在命令行中执行,可能会报错。
一个优良的API设计,不应该把它与任何特定的实现搅在一起。
functiongetBeerById(id,callback){asyncRequest('GET','beer.uri?
id='+id,function(resp){callback(resp.responseText);})}现在我们将针对接口而不是实现进行编程,用桥接模式把抽象隔离开来:
addEvent(element,'click',getBeerByIdBridge);functiongetBeerBIdBridge(e){getBeerById(this.id,function(beer){console.log(beer);});}这下getBeerById并没有和事件对象捆绑在一起了。
19.用桥接模式联结多个类varClass1=function(a,b,c){this.a=a;this.b=b;this.c=c;};varClass2=function(d){this.d=d;};varBridgeClass=function(a,b,c,d){this.one=newClass1(a,b,c);this.two=newClass2(d);};20.适配器模式适配器模式可以用来在现有接口和不兼容的类之间进行适配。
使用这种模式的对象又叫包装器,因为它们是在用一个新的接口包装另一个对象。
21.适配器的特点适配器可以被添加到现有代码中以协调两个不同的接口。
如果现有代码的接口能很好地满足需要,那就可能没有必要使用适配器。
从表面上看,适配器模式很像门面模式。
它们都要对别的对象进行包装并改变其呈现的接口。
二者的差别在于它们如何改变接口。
门面元
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- JavaScript 设计 模式