您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
举个栗子学习JavaScript设计模式
 
来源:www.cnblogs.com 发布于:2017-1-5
   次浏览      
 

前言

为什么要学习设计模式?

解耦合、解决复用、提高观察高度

创建型模式

1、单例模式

场景:点击页面一个按钮,弹出遮罩层。

遇到这个问题,首先我们想到用一个方法创建一个div,然后append到body上,实现点击事件,代码如下:

function createTip(){
2 var div = document.createElement("div");
3 return document.body.appendChild(div);
4 }
5 $('#user').click( function(){
6 var tip = createTip();
7 $(tip).show();
8 });

虽然功能实现了,但是这种方法有一个大问题:反复创建Dom!

所以我们需要对它进行改造,改造完成之后,代码如下:


1 var tipEl = document.createElement("div");
2 tipEl.style.display = "none";
3 document.body.appendChild(tipEl);
4 function showTip(){
5 tipEl.style.display = "";
6 }
7 $('#user').click( function(){
8 showTip();
9 });

虽然避免了上面方法重复创建dom的缺点,但是这么实现还是有很大的弊端:1、暴露全局变量tipEl,尤其是在做组件化开发的时候要尽可能不要暴露全局变量,否则容易引起全局变量污染和冲突;2、创建完成之后用户可能不去点击,导致dom浪费。

再一次升级,代码如下:

var createTip = function(){
2 var tip;
3 return function(){
4 return tip ||
5 (tip=document.body.appendChild(document.createElement("div")))
6 }
7 }();
8 $('#user').click( function(){
9 var tip = createTip();
10 $(tip).show();
11 });

好,这就是最终的单例模式实现了。

为什么createTip要return一个function,而不是直接把tip||(tip=document.body.appendChild(document.createElement("div"))) 返回出来呢?

因为这样可以做到惰性加载,只在需要的时候创建(这也是在click方法中var tip = createTip();要加上()的原因),否则浏览器遇到createTip的定义的时候就直接执行返回tip,这不是我们所希望的。

总结:

a、唯一实例:节约系统资源,提高性能;

b、命名空间:减少全局变量的数量;

c、实例控制:私有实例,提供访问接口

d、适用场景:一个类只有一个实例,提供全局访问接口;资源共享的情况下,避免性能或其他损耗,如计数器、配置;资源控制的情况下,方便资源之间的互相通信,如线程池。

2、构造器+原型

场景:酒店有多种类型的房间,如何描述各种房间之间的关系。

想到第一种,函数方法,代码如下:

function Room(roomNumber, type, money) {
2 this.roomNumber = roomNumber;
3 this.type = type; this.money = money;
4 this.getInfo = function() {
5 return this.roomNumber + ', ' + this.type + ', ' + this.money;
6 };
7 }
8 var roomA = Room('101', '标间', 200);
9 console.log(roomA);//如何获取房间信息呢???

如何获取房间信息成为一大问题,像第8行代码那样直接调用Room方法,其实this指向的是window对象,故Room方法内部的属性和方法赋值都是赋值给了window对象,所以即使可以通过window.getInfo()来调用,但是显得毫无意义。

于是,我们想到了构造器的实现方法,代码如下:

function Room(roomNumber, type, money) {
2 this.roomNumber = roomNumber;
3 this.type = type;
4 this.money = money;
5 this.getInfo = function() {
6 return this.roomNumber + ', ' + this.type + ', ' + this.money;
7 };
8 }
9 var roomA = new Room('101', '标间', 200);
10 console.log(roomA.getInfo());//101,标间,200
11var roomB = new Room('102', '海景房', 799);
12 console.log(roomB.getInfo());//102,海景房,799

每次调用使用new关键字实例化对象,然后调用getInfo方法。

够简单、够直观,但是又有新问题,getInfo每次实例化都要重新创建,消耗内存。

开启外部添加原型模式,代码如下:

function Room(roomNumber, type, money) {
2 this.roomNumber = roomNumber;
3 this.type = type;
4 this.money = money;
5 }
6 Room.prototype.getInfo = function() {
7 return this.roomNumber + ', ' + this.type + ', ' + this.money;
8 }
9 var roomA = new Room('101', '标间',200);
10 console.log(roomA.getInfo());//101,标间,200
11 Room.prototype.book = function() {
12 console.log("book success");
13 }
14 roomA.book();//book success

优点:方法共享,节约内存;随时添加方法且可以被实例化使用;

缺点:方法零散,不利于管理。

那就把原型统一起来,代码如下:

function Room(roomNumber, type, money) {
2 this.roomNumber = roomNumber;
3 this.type = type;
4 this.money = money;
5 }
6 Room.prototype = {
7 constructor : Room,
//这行代码很重要,因为已经重新定义了prototype的“指针”
,如果不自己指定的话默认就是object,而不是Room
8 getInfo : function() {
9 return this.roomNumber + ', ' + this.type + ', ' +this.money
10 }
11 }
12 var roomA = new Room('101', '标间', 200);
13 console.log(roomA.getInfo());//101,标间,200
14 console.log(roomA.constructor == Room);//true
15 console.log(roomA instanceof Room);//true

其中,所有的方法和变量都会暴露出来,这显然不够合理,更加优化的方式是把Room.prototype设计成揭示型的原型,利用自执行函数可以有效地保护私有变量和方法,只暴露自己想暴露的东西,代码如下:

Room.prototype = function(){
2 var getInfo = function(){
3 return this.roomNumber + ', ' + this.type + ', ' + this.money;
4 }
5 return {
6 constructor:Room,
7 getInfo : getInfo
8 }
9 }()

总结:

a、适用场景:一个类有多个实例,且相互独立,如UI组件。

3、简单工厂模式

场景:某旅游网站有机票预订和酒店预订功能,如何实现它们的关系。

场景实现,代码如下:

 var PRODUCT_TYPE = {
2 AIR_TICKET : "01",
3 HOTEL : "02"
4 }
5 function User(){
6 this.shopCart = [];
7 }
8 User.prototype = {
9 constructor : User,
10 order : function(productType){
11 var product = null;
12 switch(productType){
13 case PRODUCT_TYPE.AIR_TICKET:
14 product = new Flight();
15 case PRODUCT_TYPE.HOTEL:
16 product = new Hotel();
17 default:
18 }
19 this.shopCart.push(product);
20 }
21 }

此时如果想增加火车票业务,删除机票业务,该怎么实现?需要改动的代码一点都不灵活,这时就需要我们理清用户和具体业务之间的关系,用户仅仅只和订单有关系。

使用简单工厂模式,代码如下:

var productFactory = (function(){
2 var productFoctories = {
3 "airTicket" : function(){
4 return new AirTicket();
5 },
6 "hotel" : function(){
7 return new Hotel();
8 }
9 }
10 return {
11 create : function(productType){
12 return productFoctories[productType];
13 }
14 }
15 })();
16 User.prototype = {
17 constructor : User,
18 order : function(productType){
19 var product = productFactory.create(productType);
20 this.shopCart.push(product);
21 }
22 }

总结:

对象的创建和使用分离,使用一个类生成实例。

a、使用类User:使用类仅使用产品,职责单一;

b、工厂类Factory:对象类的集中管理;

c、对象类Hotel:易于扩展,仅影响工厂类;

d、适用场景:根据不同参数产生不同实例,这些实例有些共性的场景,使用者只需使用产品,无需关注产品的创建细节。

4、工厂模式

场景:某旅游网站有很多种酒店,不同的酒店内也有很多种房间,如何清楚地描述它们之间的关系。

工厂实现,代码如下:

/*********RoomBase ***********/
2 var RoomBase = function(){};
3 RoomBase.prototype = {
4 constructor : RoomBase,
5 create : function(){
6 throw new Error("room create not impl!");
7 },
8 book : function(){
9 this.state = "1";
10 },
11 hasWindow : function(){
12 return true;
13 }
14 }
15 /********************/
16 var RoomA = function(){};
17 extend(RoomA, RoomBase);//继承
18 RoomA.prototype.create = function(type){
19 console.log("这是标间");
20 }
21 RoomA.prototype.hasWindow = function(){
22 return false;
23 }
24 var RoomB = function(){};
25 extend(RoomB, RoomBase);
26 RoomB.prototype.create = function(type){
27 console.log("这是大床房");
28 }
29 /*********HotelBase ***********/
30 var HotelBase = function(){
31 this.roomFoctory = {};
32 };
33 HotelBase.prototype = {
34 constructor : HotelBase,
35 getRoom : function(type){
36 return this.roomFactory[type];
37 },
38 bookRoom : function(type){
39 var room = this.getRoom(type);
40 room.book();//预订
41 }
42 }
43 var HotelA = function(){
44 this.roomFoctory = {
45 "RoomA" : function(){
46 return new RoomA();
47 },
48 "RoomB" : function(){
49 return new RoomB();
50 }
51 }
52 };
53 extend(HotelA, HotelBase);
54 var HotelB = function(){
55 this.roomFoctory = {
56 "RoomA" : function(){
57 return new RoomA();
58 },
59 "RoomC" : function(){
60 return new RoomC();
61 }
62 }
63 };
64 extend(HotelA, HotelBase);

针对该旅游网站,网站管理酒店,酒店管理房间,网站不能直接管理房间,所有房间预订的功能全部走酒店的接口,跟网站没有关系,条理才比较清晰。

总结:

a、对象的创建和使用分离,子类决定成员变量的具体类;

b、使用类HotelBase:提供对象类的管理;

c、创建类HotelA:管理对象的实例化,多态;

d、对象类Room:职责单一,扩展仅影响相关创建类;

e、根据不同场景产生不同实例,这些实例有些共性的场景,使用者需要明确产品使用的场景,无需关心产品创建细节。

5、创建型模式比较

工厂模式:

①可创建多个相似对象;

②屏蔽了子对象类型(如上例中,使用Hotel预订的时候我们不知道它是A酒店还是B酒店,预订的时候是调用了HotelBase的基类中的book实现,根本没有关心它是酒店A还是酒店B)。

构造函数模式:

①可创建多个相似对象;

②可识别子对象类型;

③创建多个完成相同任务的实例。

原型模式:

①可创建多个相似对象;

②可识别子对象类型;

③多个实例共享公共方法。

结构型模式

1、模块模式

var productFactory = (function(){
2 var productFoctories = {
3 "airTicket" : function(){
4 return new AirTicket();
5 },
6 "hotel" : function(){
7 return new Hotel();
8 }
9 }
10 return {
11 create : function(productType){
12 return productFoctories[productType];
13 }
14 }
15 })();

总结:

a、模块模式就是我们常说的命名空间,里面的东西根本不重要;

b、保护私有接口,开放公共接口;

c、模块化是面向对象的第一步;

d、遵循单一职责原则。

2、外观模式

1 /****例1*****/
2 var product;
3 function getProduct(){
4 if(!product){
5 product = productFactory.create();
6 }
7 return product;
8 }
9 /****例2*****/
10 /** * 同时阻止事件默认行为和冒泡 * @param e */
11 var stopEvent = function(e){
12 e.stopPropagation();
13 e.preventDefault();
14 }

对相关的操作进行一层包装,比如例2中需要我们每次阻止事件冒泡的时候打一个日志console.log('日志'),那我们只需要在stopEvent的方法中写就行了,而不需要在每个需要阻止事件冒泡的地方去打日志。

总结:

a、更高层次的接口抽象,隐藏底层的真实复杂性;

b、遵循最少知识原则。(你只需要知道有一个接口可以实现你的需求,而你不需要知道接口中到底调用了哪些方法去实现你的需求)

3、混入模式

场景:一个房子可以用来出租或出售,在不同的场景下有不同的功能,比如出售还涉及到房产证等等其他的一系列东西,所以单单通过一个属性是办不到的,这个时候就需要使用混入模式。代码如下:

/******基础对象********/
2 var Room = function(){};
3 Room.prototype = {
4 constructor:Room,
5 create:function(){
6 console.log('create');
7 },
8 book:function(){
9 console.log('book');
10 }
11 };
12 /******混入对象********/
13 var Goods = function(){};
14 Goods.prototype = {
15 sell:function(){
16 console.log('sell');
17 }
18 };
19 /******混入方法********/
20 var mixin = function (receiver, mixinObj) {
21 // 如果存在三个以上的参数的话,从第三个开始表示要混入的方法名
22 if (arguments[2]) {
23 for (var i = 2, len = arguments.length; i < len; i++) {
24 receiver.prototype[arguments[i]] = mixinObj.prototype[arguments[i]];
25 }
26 }
27 // 否则混入全部方法
28 else{
29 for(var methodName in mixinObj.prototype){
30 // 防止混入覆盖
31 if(!receiver.prototype[methodName]){
32 receiver.prototype[methodName] = mixinObj.prototype[methodName];
33 }
34 }
35 }
36 }
37
38 /******混入操作********/
39 mixin(Room,Goods);
40 var roomA = new Room();
41 roomA.sell();//调用混入后的方法

 

“房屋类”混入“商品类”的出售属性,房屋就具有了出售的功能,同理,如果混入了出租的属性,就具备了出租的功能。

总结:

a、通过函数复用达到对象扩充的目的;

b、混入和继承的区别:混入只完成了方法的转移,两个对象还是两个对象,毫无任何关系;

c、混入和组合的区别:组合是拥有的关系,比如酒店拥有房间,酒店可以操作房间,但是房间的属性还是房间的,不属于酒店。

4、装饰模式

场景:一个酒店的房间有淡季价格和旺季价格之分,如何实现。

var Room = function(price){
2 this.price = price;
3 };
4 Room.prototype = {
5 constructor:Room,
6 getPrice:function(){
7 return this.price;
8 }
9 };
10 var LowSeasonRoom = function(room){
11 this.room = room;//注入Room,Room和LowSeasonRoom是组合的关系
12 };
13 LowSeasonRoom.prototype.getPrice = function(){
14 return this.room.getPrice()*0.9;
15 };
16 var HighSeasonRoom = function(room){
17 this.room = room;
18 };
19 HighSeasonRoom.prototype.getPrice = function(){
20 return this.room.getPrice()*1.2;
21 };
22 /****************使用*****************/
23 var roomA = new LowSeasonRoom(new Room(200));
24 console.log(roomA.getPrice());//180

总结:

a、动态改变对象实现对象扩充;(比如密码输入框是文本输入框的装饰模式,从文本输入框中单独剥离出来单独实现密码输入框的业务需求)

b、缺点是增加了架构的复杂度。

5、适配模式

总结:

a、主要用来解决对象之间的不兼容性;

b、sum函数:可以不只是两个数的sum,可以通过arguments适配多个参数的sum;

c、jQuery版本升级:比如版本升级之后废弃了某个方法一定是有其他的方法来兼容;

d、apply函数:指定作用域随时都可以调用。

行为型模式

1、观察者模式

场景:酒店的某个房间需要客房服务,酒店如何响应。

 var Room = function(){};
2 Room.prototype = {
3 constructor : Room,
4 service : function(){
5 this.fire("service", this.roomId);
6 }
7 }
8 var Hotel = function(){};
9 Hotel.prototype.addRoom = function(room){
10 room.on("service", function(){
11 //TODO 服务
12 });
13 }

总结:

a、命令的发起者和执行者解耦合,don't call us,we will call you;

b、一个发起者可能对应多个执行者;

c、一个执行者也可能是其他执行者的命令发起者

d、常用的场景:比如我们页面中经常会用到iframe,一个子页面需要调用父页面的方法,经常会使用parent.functionNmae()去调用,但是一旦取不到父页面,这里就会报错,所以正确的处理方式是在父页面监听事件,在子页面触发事件,这样就算服务不到也不会报错,通常在父子页面中都存在的是body,固我们习惯性把事件的监听和触发放到body上执行。

2、中介者模式

场景:一个酒店某个房间需要服务,酒店中有多个服务员,这时候该怎么实现呢。

这个时候就不能用观察者模式,因为观察者模式是不确定谁来服务的,万一酒店中所有的服务员都来服务怎么办,得由酒店来确定到底哪个服务员过来提供服务。

var Room = function(){};
2 Room.prototype = {
3 constructor : Room,
4 service : function(){
5 this.fire("service", this.roomId, type);
6 }
7 }
8 var Waiter = function(){};
9 Waiter.prototype = {
10 constructor : Waiter,
11 service : function(){
12 //TODO 服务
13 }
14 }
15 var Hotel = function(){};
16 Hotel.prototype.addRoom = function(room){
17 room.on("service", function(roomId, type){
18 if(type == "设备修理"){
19 waiterA.service();
20 }else{
21 if(roomId.startWith("3")){
22 waiterB.service();
23 }else{
24 waiterC.service();
25 }
26 }
27 });
28 }

总结:

a、管理复杂对象关系:把多对多拆分成多对一;

b、对象间的耦合转移至中介者;

c、中介者的稳定性至关重要。

3、命令模式

场景:如果酒店提供的服务有多种多样,该如何实现。

此时就需要用到命令模式,代码如下:

 var Services = {
2 clean : function(){
3 console.log("打扫卫生");
4 },
5 consult : function(){
6 console.log("咨询");
7 }
8 }
9 var ServcieCommand = function(){
10 return {
11 execute : function(receiver, command){
12 receiver[command]();
13 }
14 }
15 }();

总结:

a、命令的发起和实现解耦合

4、责任链模式

场景:假设某个酒店发生了斗殴,如果酒店内部可以处理则自己处理,否则就让警察处理,责任一层层转移。

var Hotel = function(){
2 this.on("fight", function(){
3 if(this.canDo()){//内部处理
4 this.do();
5 }else{//报警
6 Police.do();
7 }
8 });
9 };
10 Police = function(){
11 return {
12 do : function(){
13 if(this.canDo()){
14 console.log("over");
15 }else{
16 //TODO 转上级处理。。。
17 }
18 }
19 }
20 }();

总结:

a、每个节点只知道下线,不知道最终执行者;

b、可能出现无人处理的情况;

c、缺点调试困难(所以需要我们在每个执行环节打日志)。

设计原则

1、单一职责原则

如何确定一个职责or两个职责:

a、职责被定义为“引起变化的原因”;

b、若两个职责总是同时变化,则可以作为一个职责;(比如请求数据之后渲染图表)

c、发生变化才对职责的限定有意义,不变没有必要分离

违反不一定是坏事:

比如:$.attr()这个方法有赋值也有取值的职责,但是能很好地找到平衡点。

2、最少知识原则

减少对象之间的联系:

a、两个对象之间不必直接通信则不直接通信,通过第三方实现通信;

b、如中介者模式和外观模式

违反不一定是坏事:

比如:$.ajax 和 $.post

3、开放-封闭原则

开放和封闭(对扩展开放,对修改关闭):

a、当需要改变时,可以使用增加代码的方式,但不允许改动程序的源代码,比如组件有bug或不满足需求时;

b、比如一个很老系统新的需求window.onload的时候需要统一打一下日志,如何处理?

法1:使用jQuery,因为jQuery的事件可以监听多个;

法2:重写onload方法,重写的时候先调它的方法,再调自己的方法。

找出变化的地方很重要:

a、挑选出最容易发生变化的地方进行抽象,来封闭这些变化;

b、不可避免发生修改时,尽量修改容易修改的地方,如修改配置比修改源码简单。

   
次浏览       
相关文章 相关文档 相关课程



深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程
最新课程计划
信息架构建模(基于UML+EA)3-21[北京]
软件架构设计师 3-21[北京]
图数据库与知识图谱 3-25[北京]
业务架构设计 4-11[北京]
SysML和EA系统设计与建模 4-22[北京]
DoDAF规范、模型与实例 5-23[北京]

Android手机开发(一)
理解Javascript
非典型ajax实践
彻底的Ajax
javascript 使用Cookies
使用 jQuery 简化 Ajax 开发
更多...   

Struts+Spring+Hibernate
基于J2EE的Web 2.0应用开发
J2EE设计模式和性能调优
Java EE 5企业级架构设计
Java单元测试方法与技术
Java编程方法与技术

某航空公司IT部 JavaScript实践
某电视软件 HTML5和JavaScript
中航信 JavaScript高级应用开发
大庆油田 web界面Ajax开发技术
和利时 使用AJAX进行WEB应用开发
更多...