(结局篇)
最后更新于:2022-04-01 14:59:10
## 介绍
最近几个月忙得实在是不可开交,终于把《深入理解JavaScript系列》的最后两篇“补全”了,所谓的全是不准确的,因为很多内容都没有写呢,比如高性能、Ajax安全、DOM详解、JavaScript架构等等。但因为经历所限,加上大叔希望接下来写点其它东西,所以此篇文字就暂且当前完结篇的总结吧,以后有时间的话,可以继续加上一些未涉及的专题内容。
## 网络文章来源
本系列文章参考了大量的互联网网站,在此向各位网站拥有者、博主、提到的以及未提到的作者们说一声:多谢感谢了。
本系列文章主要参考了如下站点:
五大原则:http://freshbrewedcode.com/derekgreer
ECMAScript262系列:http://dmitrysoshnikov.com/
DOM系列文章:http://net.tutsplus.com
设计模式系列文章参考如下三个网站:
http://www.addyosmani.com/resources/essentialjsdesignpatterns/book/
http://shichuan.github.com/javascript-patterns/
https://github.com/tcorral/Design-Patterns-in-Javascript/
其它文章,总结自自己的收藏、心得,结合了互联网上的各位大牛的博客总结整理而成,因为参考地址太多,无法一一列出,如果忘记了各位各种的版权声明,请及时告知,以便及时处理,多谢!
## 参考书籍
这里列出的书籍是大叔曾经读过的,也是在整理博文的时候经常参考的书籍,推荐给大家阅读。
**初级读物**:
《JavaScript高级程序设计》:一本非常完整的经典入门书籍,被誉为JavaScript圣经之一,详解的非常详细,最新版第三版已经发布了,建议购买。
**中级读物**:
《JavaScript权威指南》:另外一本JavaScript圣经,讲解的也非常详细,属于中级读物,建议购买。
《JavaScript.The.Good.Parts》:Yahoo大牛,JavaScript精神领袖Douglas Crockford的大作,虽然才100多页,但是字字珠玑啊!强烈建议阅读。
《高性能JavaScript》:《JavaScript高级程序设计》作者Nicholas C. Zakas的又一大作。
《Eloquent JavaScript》:这本书才200多页,非常短小,但是改变了我写作的习惯,本书通过几个非常经典的例子(艾米丽姨妈的猫、悲惨的隐士、模拟生态圈、推箱子游戏等等)来介绍JavaScript方方面面的知识和应用方法,非常值得一读,同时这本书的中文版也是大叔翻译的,点击屏幕右上角可以订购,希望大家多多支持。
**高级读物**:
《JavaScript Patterns 》:书中介绍到了各种经典的模式,如构造函数、单例、工厂等等,值得学习。
《Pro.JavaScript.Design.Patterns》:Apress出版社讲解JavaScript设计模式的书,非常不错。
《Developing JavaScript Web Applications》:构建富应用的好书,针对MVC模式有较为深入的讲解,同时也对一些流程的库进行了讲解。
《Developing Large Web Applications》:将这本书归结在这里,貌似有点不妥,因为这里不仅有JavaScript方面的介绍,还有CSS、HTML方面的介绍,但是介绍的内容却都非常不错,真正考虑到了一个大型的Web程序下,如何进行JavaScript架构设计,值得一读。
**其它参考书籍**:
《大话设计模式》:博文里关于设计模式的文章,有些总结性的文字来自于此。
《设计模式——可复用面向对象软件的基础》:博文里关于设计模式的文章,有些介绍性和总结性的文章来自于此。
## 总结
在写此系列文章期间,大叔也学到了很多很多内容。同时为了不误人子弟,大叔参考了很多很多文章,同时也阅读了那么多书籍,但博客里的文章,可能依然有很多错误,希望各位如果发现错误的话,请及时告知,以便及时修正而不再继续误导其它人。
同时,大家在阅读过程中,有任何问题都可以在相应的文章里留言,大叔将在不耽误工作的情况下尽力回复。
(50)Function模式(下篇)
最后更新于:2022-04-01 14:59:08
## 介绍
本篇我们介绍的一些模式称为初始化模式和性能模式,主要是用在初始化以及提高性能方面,一些模式之前已经提到过,这里只是做一下总结。
## 立即执行的函数
在本系列第4篇的《[立即调用的函数表达式](http://www.cnblogs.com/TomXu/archive/2011/12/31/2289423.html)》中,我们已经对类似的函数进行过详细的描述,这里我们只是再举两个简单的例子做一下总结。
~~~
// 声明完函数以后,立即执行该函数
(function () {
console.log('watch out!');
} ());
//这种方式声明的函数,也可以立即执行
!function () {
console.log('watch out!');
} ();
// 如下方式也都可以哦
~function () { /* code */ } ();
-function () { /* code */ } ();
+function () { /* code */ } ();
~~~
## 立即执行的对象初始化
该模式的意思是指在声明一个对象(而非函数)的时候,立即执行对象里的某一个方法来进行初始化工作,通常该模式可以用在一次性执行的代码上。
~~~
({
// 这里你可以定义常量,设置其它值
maxwidth: 600,
maxheight: 400,
// 当然也可以定义utility方法
gimmeMax: function () {
return this.maxwidth + "x" + this.maxheight;
},
// 初始化
init: function () {
console.log(this.gimmeMax());
// 更多代码...
}
}).init(); // 这样就开始初始化咯
~~~
## 分支初始化
分支初始化是指在初始化的时候,根据不同的条件(场景)初始化不同的代码,也就是所谓的条件语句赋值。之前我们在做事件处理的时候,通常使用类似下面的代码:
~~~
var utils = {
addListener: function (el, type, fn) {
if (typeof window.addEventListener === 'function') {
el.addEventListener(type, fn, false);
} else if (typeof document.attachEvent !== 'undefined') {
el.attachEvent('on' + type, fn);
} else {
el['on' + type] = fn;
}
},
removeListener: function (el, type, fn) {
}
};
~~~
我们来改进一下,首先我们要定义两个接口,一个用来add事件句柄,一个用来remove事件句柄,代码如下:
~~~
var utils = {
addListener: null,
removeListener: null
};
~~~
实现代码如下:
~~~
if (typeof window.addEventListener === 'function') {
utils.addListener = function (el, type, fn) {
el.addEventListener(type, fn, false);
};
} else if (typeof document.attachEvent !== 'undefined') { // IE
utils.addListener = function (el, type, fn) {
el.attachEvent('on' + type, fn);
};
utils.removeListener = function (el, type, fn) {
el.detachEvent('on' + type, fn);
};
} else { // 其它旧浏览器
utils.addListener = function (el, type, fn) {
el['on' + type] = fn;
};
utils.removeListener = function (el, type, fn) {
el['on' + type] = null;
};
}
~~~
用起来,是不是就很方便了?代码也优雅多了。
## 自声明函数
一般是在函数内部,重写同名函数代码,比如:
~~~
var scareMe = function () {
alert("Boo!");
scareMe = function () {
alert("Double boo!");
};
};
~~~
这种代码,非常容易使人迷惑,我们先来看看例子的执行结果:
~~~
// 1\. 添加新属性
scareMe.property = "properly";
// 2\. scareMe赋与一个新值
var prank = scareMe;
// 3\. 作为一个方法调用
var spooky = {
boo: scareMe
};
// 使用新变量名称进行调用
prank(); // "Boo!"
prank(); // "Boo!"
console.log(prank.property); // "properly" // 使用方法进行调用
spooky.boo(); // "Boo!"
spooky.boo(); // "Boo!"
console.log(spooky.boo.property); // "properly"
~~~
通过执行结果,可以发现,将定于的函数赋值与新变量(或内部方法),代码并不执行重载的scareMe代码,而如下例子则正好相反:
~~~
// 使用自声明函数
scareMe(); // Double boo!
scareMe(); // Double boo!
console.log(scareMe.property); // undefined
~~~
大家使用这种模式时,一定要非常小心才行,否则实际结果很可能和你期望的结果不一样,当然你也可以利用这个特殊做一些特殊的操作。
## 内存优化
该模式主要是利用函数的属性特性来避免大量的重复计算。通常代码形式如下:
~~~
var myFunc = function (param) {
if (!myFunc.cache[param]) {
var result = {};
// ... 复杂操作 ...
myFunc.cache[param] = result;
}
return myFunc.cache[param];
};
// cache 存储
myFunc.cache = {};
~~~
但是上述代码有个问题,如果传入的参数是toString或者其它类似Object拥有的一些公用方法的话,就会出现问题,这时候就需要使用传说中的`hasOwnProperty`方法了,代码如下:
~~~
var myFunc = function (param) {
if (!myFunc.cache.hasOwnProperty(param)) {
var result = {};
// ... 复杂操作 ...
myFunc.cache[param] = result;
}
return myFunc.cache[param];
};
// cache 存储
myFunc.cache = {};
~~~
或者如果你传入的参数是多个的话,可以将这些参数通过JSON的stringify方法生产一个cachekey值进行存储,代码如下:
~~~
var myFunc = function () {
var cachekey = JSON.stringify(Array.prototype.slice.call(arguments)),
result;
if (!myFunc.cache[cachekey]) {
result = {};
// ... 复杂操作 ...
myFunc.cache[cachekey] = result;
}
return myFunc.cache[cachekey];
};
// cache 存储
myFunc.cache = {};
~~~
或者多个参数的话,也可以利用arguments.callee特性:
~~~
var myFunc = function (param) {
var f = arguments.callee,
result;
if (!f.cache[param]) {
result = {};
// ... 复杂操作 ...
f.cache[param] = result;
}
return f.cache[param];
};
// cache 存储
myFunc.cache = {};
~~~
## 总结
就不用总结了吧,大家仔细看代码就行咯
(49)Function模式(上篇)
最后更新于:2022-04-01 14:59:05
## 介绍
本篇主要是介绍Function方面使用的一些技巧(上篇),利用Function特性可以编写出很多非常有意思的代码,本篇主要包括:回调模式、配置对象、返回函数、分布程序、柯里化(Currying)。
## 回调函数
在JavaScript中,当一个函数A作为另外一个函数B的其中一个参数时,则函数A称为回调函数,即A可以在函数B的周期内执行(开始、中间、结束时均可)。
举例来说,有一个函数用于生成node
~~~
var complexComputation = function () { /* 内部处理,并返回一个node*/};
~~~
有一个findNodes函数声明用于查找所有的节点,然后通过callback回调进行执行代码。
~~~
var findNodes = function (callback) {
var nodes = [];
var node = complexComputation();
// 如果回调函数可用,则执行它
if (typeof callback === "function") {
callback(node);
}
nodes.push(node);
return nodes;
};
~~~
关于callback的定义,我们可以事先定义好来用:
~~~
// 定义callback
var hide = function (node) {
node.style.display = "none";
};
// 查找node,然后隐藏所有的node
var hiddenNodes = findNodes(hide);
~~~
也可以直接在调用的时候使用匿名定义,如下:
~~~
// 使用匿名函数定义callback
var blockNodes = findNodes(function (node) {
node.style.display = 'block';
});
~~~
我们平时用的最多的,估计就数jQuery的ajax方法的调用了,通过在done/faild上定义callback,以便在ajax调用成功或者失败的时候做进一步处理,代码如下(本代码基于jquery1.8版):
~~~
var menuId = $("ul.nav").first().attr("id");
var request = $.ajax({
url: "script.php",
type: "POST",
data: {id : menuId},
dataType: "html"
});
//调用成功时的回调处理
request.done(function(msg) {
$("#log").html( msg );
});
//调用失败时的回调处理
request.fail(function(jqXHR, textStatus) {
alert( "Request failed: " + textStatus );
});
~~~
## 配置对象
如果一个函数(或方法)的参数只有一个参数,并且参数为对象字面量,我们则称这种模式为配置对象模式。例如,如下代码:
~~~
var conf = {
username:"shichuan",
first:"Chuan",
last:"Shi"
};
addPerson(conf);
~~~
则在addPerson内部,就可以随意使用conf的值了,一般用于初始化工作,例如jquery里的ajaxSetup也就是这种方式来实现的:
~~~
// 事先设置好初始值
$.ajaxSetup({
url: "/xmlhttp/",
global: false,
type: "POST"
});
// 然后再调用
$.ajax({ data: myData });
~~~
另外,很多jquery的插件也有这种形式的传参,只不过也可以不传,不传的时候则就使用默认值了。
## 返回函数
返回函数,则是指在一个函数的返回值为另外一个函数,或者根据特定的条件灵活创建的新函数,示例代码如下:
~~~
var setup = function () {
console.log(1);
return function () {
console.log(2);
};
};
// 调用setup 函数
var my = setup(); // 输出 1
my(); // 输出 2
// 或者直接调用也可
setup()();
~~~
或者你可以利用闭包的特性,在setup函数里记录一个私有的计数器数字,通过每次调用来增加计数器,代码如下:
~~~
var setup = function () {
var count = 0;
return function () {
return ++count;
};
};
// 用法
var next = setup();
next(); // 返回 1
next(); // 返回 2
next(); // 返回 3
~~~
## 偏应用
这里的偏应用,其实是将参数的传入工作分开进行,在有的时候一系列的操作可能会有某一个或几个参数始终完全一样,那么我们就可以先定义一个偏函数,然后再去执行这个函数(执行时传入剩余的不同参数)。
举个例子,代码如下:
~~~
var partialAny = (function (aps) {
// 该函数是你们自执行函数表达式的结果,并且赋值给了partialAny变量
function func(fn) {
var argsOrig = aps.call(arguments, 1);
return function () {
var args = [],
argsPartial = aps.call(arguments),
i = 0;
// 变量所有的原始参数集, // 如果参数是partialAny._ 占位符,则使用下一个函数参数对应的值 // 否则使用原始参数里的值
for (; i < argsOrig.length; i++) {
args[i] = argsOrig[i] === func._
? argsPartial.shift()
: argsOrig[i];
}
// 如果有任何多余的参数,则添加到尾部
return fn.apply(this, args.concat(argsPartial));
};
}
// 用于占位符设置
func._ = {};
return func;
})(Array.prototype.slice);
~~~
使用方式如下:
~~~
// 定义处理函数
function hex(r, g, b) {
return '#' + r + g + b;
}
//定义偏函数, 将hex的第一个参数r作为不变的参数值ff
var redMax = partialAny(hex, 'ff', partialAny._, partialAny._);
// 新函数redMax的调用方式如下,只需要传入2个参数了:
console.log(redMax('11', '22')); // "#ff1122"
~~~
如果觉得partialAny._太长,可以用__代替哦。
~~~
var __ = partialAny._;
var greenMax = partialAny(hex, __, 'ff');
console.log(greenMax('33', '44'));
var blueMax = partialAny(hex, __, __, 'ff');
console.log(blueMax('55', '66'));
var magentaMax = partialAny(hex, 'ff', __, 'ff');
console.log(magentaMax('77'));
~~~
这样使用,就简洁多了吧。
## Currying
Currying是函数式编程的一个特性,将多个参数的处理转化成单个参数的处理,类似链式调用。
举一个简单的add函数的例子:
~~~
function add(x, y) {
var oldx = x, oldy = y;
if (typeof oldy === "undefined") { // partial
return function (newy) {
return oldx + newy;
}
}
return x + y;
}
~~~
这样调用方式就可以有多种了,比如:
~~~
// 测试
typeof add(5); // "function"
add(3)(4); // 7
// 也可以这样调用
var add2000 = add(2000);
add2000(10); // 2010
~~~
接下来,我们来定义一个比较通用的currying函数:
~~~
// 第一个参数为要应用的function,第二个参数是需要传入的最少参数个数
function curry(func, minArgs) {
if (minArgs == undefined) {
minArgs = 1;
}
function funcWithArgsFrozen(frozenargs) {
return function () {
// 优化处理,如果调用时没有参数,返回该函数本身
var args = Array.prototype.slice.call(arguments);
var newArgs = frozenargs.concat(args);
if (newArgs.length >= minArgs) {
return func.apply(this, newArgs);
} else {
return funcWithArgsFrozen(newArgs);
}
};
}
return funcWithArgsFrozen([]);
}
~~~
这样,我们就可以随意定义我们的业务行为了,比如定义加法:
~~~
var plus = curry(function () {
var result = 0;
for (var i = 0; i < arguments.length; ++i) {
result += arguments[i];
}
return result;
}, 2);
~~~
使用方式,真实多种多样哇。
~~~
plus(3, 2) // 正常调用
plus(3) // 偏应用,返回一个函数(返回值为3+参数值)
plus(3)(2) // 完整应用(返回5)
plus()(3)()()(2) // 返回 5
plus(3, 2, 4, 5) // 可以接收多个参数
plus(3)(2, 3, 5) // 同理
~~~
如下是减法的例子
~~~
var minus = curry(function (x) {
var result = x;
for (var i = 1; i < arguments.length; ++i) {
result -= arguments[i];
}
return result;
}, 2);
~~~
或者如果你想交换参数的顺序,你可以这样定义
~~~
var flip = curry(function (func) {
return curry(function (a, b) {
return func(b, a);
}, 2);
});
~~~
更多资料,可以参考如下地址:
[http://www.cnblogs.com/rubylouvre/archive/2009/11/09/1598761.html](http://www.cnblogs.com/rubylouvre/archive/2009/11/09/1598761.html)
[http://www.cnblogs.com/sanshi/archive/2009/02/17/javascript_currying.html](http://www.cnblogs.com/sanshi/archive/2009/02/17/javascript_currying.html)
## 总结
JavaScript里的Function有很多特殊的功效,可以利用闭包以及arguments参数特性实现很多不同的技巧,下一篇我们将继续介绍利用Function进行初始化的技巧。
参考地址:http://shichuan.github.com/javascript-patterns/#function-patterns
(48)对象创建模式(下篇)
最后更新于:2022-04-01 14:59:03
## 介绍
本篇主要是介绍创建对象方面的模式的下篇,利用各种技巧可以极大地避免了错误或者可以编写出非常精简的代码。
## 模式6:函数语法糖
函数语法糖是为一个对象快速添加方法(函数)的扩展,这个主要是利用prototype的特性,代码比较简单,我们先来看一下实现代码:
~~~
if (typeof Function.prototype.method !== "function") {
Function.prototype.method = function (name, implementation) {
this.prototype[name] = implementation;
return this;
};
}
~~~
扩展对象的时候,可以这么用:
~~~
var Person = function (name) {
this.name = name;
}
.method('getName',
function () {
return this.name;
})
.method('setName', function (name) {
this.name = name;
return this;
});
~~~
这样就给Person函数添加了getName和setName这2个方法,接下来我们来验证一下结果:
~~~
var a = new Person('Adam');
console.log(a.getName()); // 'Adam'
console.log(a.setName('Eve').getName()); // 'Eve'
~~~
## 模式7:对象常量
对象常量是在一个对象提供set,get,ifDefined各种方法的体现,而且对于set的方法只会保留最先设置的对象,后期再设置都是无效的,已达到别人无法重载的目的。实现代码如下:
~~~
var constant = (function () {
var constants = {},
ownProp = Object.prototype.hasOwnProperty,
// 只允许设置这三种类型的值
allowed = {
string: 1,
number: 1,
boolean: 1
},
prefix = (Math.random() + "_").slice(2);
return {
// 设置名称为name的属性
set: function (name, value) {
if (this.isDefined(name)) {
return false;
}
if (!ownProp.call(allowed, typeof value)) {
return false;
}
constants[prefix + name] = value;
return true;
},
// 判断是否存在名称为name的属性
isDefined: function (name) {
return ownProp.call(constants, prefix + name);
},
// 获取名称为name的属性
get: function (name) {
if (this.isDefined(name)) {
return constants[prefix + name];
}
return null;
}
};
} ());
~~~
验证代码如下:
~~~
// 检查是否存在
console.log(constant.isDefined("maxwidth")); // false
// 定义
console.log(constant.set("maxwidth", 480)); // true
// 重新检测
console.log(constant.isDefined("maxwidth")); // true
// 尝试重新定义
console.log(constant.set("maxwidth", 320)); // false
// 判断原先的定义是否还存在
console.log(constant.get("maxwidth")); // 480
~~~
## 模式8:沙盒模式
沙盒(Sandbox)模式即时为一个或多个模块提供单独的上下文环境,而不会影响其他模块的上下文环境,比如有个Sandbox里有3个方法event,dom,ajax,在调用其中2个组成一个环境的话,和调用三个组成的环境完全没有干扰。Sandbox实现代码如下:
~~~
function Sandbox() {
// 将参数转为数组
var args = Array.prototype.slice.call(arguments),
// 最后一个参数为callback
callback = args.pop(),
// 除最后一个参数外,其它均为要选择的模块
modules = (args[0] && typeof args[0] === "string") ? args : args[0],
i;
// 强制使用new操作符
if (!(this instanceof Sandbox)) {
return new Sandbox(modules, callback);
}
// 添加属性
this.a = 1;
this.b = 2;
// 向this对象上需想添加模块
// 如果没有模块或传入的参数为 "*" ,则以为着传入所有模块
if (!modules || modules == '*') {
modules = [];
for (i in Sandbox.modules) {
if (Sandbox.modules.hasOwnProperty(i)) {
modules.push(i);
}
}
}
// 初始化需要的模块
for (i = 0; i < modules.length; i += 1) {
Sandbox.modules[modules[i]](this);
}
// 调用 callback
callback(this);
}
// 默认添加原型对象
Sandbox.prototype = {
name: "My Application",
version: "1.0",
getName: function () {
return this.name;
}
};
~~~
然后我们再定义默认的初始模块:
~~~
Sandbox.modules = {};
Sandbox.modules.dom = function (box) {
box.getElement = function () {
};
box.getStyle = function () {
};
box.foo = "bar";
};
Sandbox.modules.event = function (box) {
// access to the Sandbox prototype if needed:
// box.constructor.prototype.m = "mmm";
box.attachEvent = function () {
};
box.detachEvent = function () {
};
};
Sandbox.modules.ajax = function (box) {
box.makeRequest = function () {
};
box.getResponse = function () {
};
};
~~~
调用方式如下:
~~~
// 调用方式
Sandbox(['ajax', 'event'], function (box) {
console.log(typeof (box.foo));
// 没有选择dom,所以box.foo不存在
});
Sandbox('ajax', 'dom', function (box) {
console.log(typeof (box.attachEvent));
// 没有选择event,所以event里定义的attachEvent也不存在
});
Sandbox('*', function (box) {
console.log(box); // 上面定义的所有方法都可访问
});
~~~
通过三个不同的调用方式,我们可以看到,三种方式的上下文环境都是不同的,第一种里没有foo; 而第二种则没有attachEvent,因为只加载了ajax和dom,而没有加载event; 第三种则加载了全部。
## 模式9:静态成员
静态成员(Static Members)只是一个函数或对象提供的静态属性,可分为私有的和公有的,就像C#或Java里的public static和private static一样。
我们先来看一下公有成员,公有成员非常简单,我们平时声明的方法,函数都是公有的,比如:
~~~
// 构造函数
var Gadget = function () {
};
// 公有静态方法
Gadget.isShiny = function () {
return "you bet";
};
// 原型上添加的正常方法
Gadget.prototype.setPrice = function (price) {
this.price = price;
};
// 调用静态方法
console.log(Gadget.isShiny()); // "you bet"
// 创建实例,然后调用方法
var iphone = new Gadget();
iphone.setPrice(500);
console.log(typeof Gadget.setPrice); // "undefined"
console.log(typeof iphone.isShiny); // "undefined"
Gadget.prototype.isShiny = Gadget.isShiny;
console.log(iphone.isShiny()); // "you bet"
~~~
而私有静态成员,我们可以利用其闭包特性去实现,以下是两种实现方式。
**第一种实现方式:**
~~~
var Gadget = (function () {
// 静态变量/属性
var counter = 0;
// 闭包返回构造函数的新实现
return function () {
console.log(counter += 1);
};
} ()); // 立即执行
var g1 = new Gadget(); // logs 1
var g2 = new Gadget(); // logs 2
var g3 = new Gadget(); // logs 3
~~~
可以看出,虽然每次都是new的对象,但数字依然是递增的,达到了静态成员的目的。
**第二种方式:**
~~~
var Gadget = (function () {
// 静态变量/属性
var counter = 0,
NewGadget;
//新构造函数实现
NewGadget = function () {
counter += 1;
};
// 授权可以访问的方法
NewGadget.prototype.getLastId = function () {
return counter;
};
// 覆盖构造函数
return NewGadget;
} ()); // 立即执行
var iphone = new Gadget();
iphone.getLastId(); // 1
var ipod = new Gadget();
ipod.getLastId(); // 2
var ipad = new Gadget();
ipad.getLastId(); // 3
~~~
数字也是递增了,这是利用其内部授权方法的闭包特性实现的。
## 总结
这是对象创建模式的下篇,两篇一起总共9种模式,是我们在日常JavaScript编程中经常使用的对象创建模式,不同的场景起到了不同的作用,希望大家根据各自的需求选择适用的模式。
参考:http://shichuan.github.com/javascript-patterns/#object-creation-patterns
(47)对象创建模式(上篇)
最后更新于:2022-04-01 14:59:01
## 介绍
本篇主要是介绍创建对象方面的模式,利用各种技巧可以极大地避免了错误或者可以编写出非常精简的代码。
## 模式1:命名空间(namespace)
命名空间可以减少全局命名所需的数量,避免命名冲突或过度。一般我们在进行对象层级定义的时候,经常是这样的:
~~~
var app = app || {};
app.moduleA = app.moduleA || {};
app.moduleA.subModule = app.moduleA.subModule || {};
app.moduleA.subModule.MethodA = function () {
console.log("print A");
};
app.moduleA.subModule.MethodB = function () {
console.log("print B");
};
~~~
如果层级很多的话,那就要一直这样继续下去,很是混乱。namespace模式就是为了解决这个问题而存在的,我们看代码:
~~~
// 不安全,可能会覆盖已有的MYAPP对象
var MYAPP = {};
// 还好
if (typeof MYAPP === "undefined") {
var MYAPP = {};
}
// 更简洁的方式
var MYAPP = MYAPP || {};
//定义通用方法
MYAPP.namespace = function (ns_string) {
var parts = ns_string.split('.'),
parent = MYAPP,
i;
// 默认如果第一个节点是MYAPP的话,就忽略掉,比如MYAPP.ModuleA
if (parts[0] === "MYAPP") {
parts = parts.slice(1);
}
for (i = 0; i < parts.length; i += 1) {
// 如果属性不存在,就创建
if (typeof parent[parts[i]] === "undefined") {
parent[parts[i]] = {};
}
parent = parent[parts[i]];
}
return parent;
};
~~~
调用代码,非常简单:
~~~
// 通过namespace以后,可以将返回值赋给一个局部变量
var module2 = MYAPP.namespace('MYAPP.modules.module2');
console.log(module2 === MYAPP.modules.module2); // true
// 跳过MYAPP
MYAPP.namespace('modules.module51');
// 非常长的名字
MYAPP.namespace('once.upon.a.time.there.was.this.long.nested.property');
~~~
## 模式2:定义依赖
有时候你的一个模块或者函数可能要引用第三方的一些模块或者工具,这时候最好将这些依赖模块在刚开始的时候就定义好,以便以后可以很方便地替换掉。
~~~
var myFunction = function () {
// 依赖模块
var event = YAHOO.util.Event,
dom = YAHOO.util.dom;
// 其它函数后面的代码里使用局部变量event和dom
};
~~~
## 模式3:私有属性和私有方法
JavaScript本书不提供特定的语法来支持私有属性和私有方法,但是我们可以通过闭包来实现,代码如下:
~~~
function Gadget() {
// 私有对象
var name = 'iPod';
// 公有函数
this.getName = function () {
return name;
};
}
var toy = new Gadget();
// name未定义,是私有的
console.log(toy.name); // undefined
// 公有方法访问name
console.log(toy.getName()); // "iPod"
var myobj; // 通过自执行函数给myobj赋值
(function () {
// 自由对象
var name = "my, oh my";
// 实现了公有部分,所以没有var
myobj = {
// 授权方法
getName: function () {
return name;
}
};
} ());
~~~
## 模式4:Revelation模式
也是关于隐藏私有方法的模式,和《[深入理解JavaScript系列(3):全面解析Module模式](http://www.cnblogs.com/TomXu/archive/2011/12/30/2288372.html)》里的Module模式有点类似,但是不是return的方式,而是在外部先声明一个变量,然后在内部给变量赋值公有方法。代码如下:
~~~
var myarray;
(function () {
var astr = "[object Array]",
toString = Object.prototype.toString;
function isArray(a) {
return toString.call(a) === astr;
}
function indexOf(haystack, needle) {
var i = 0,
max = haystack.length;
for (; i < max; i += 1) {
if (haystack[i] === needle) {
return i;
}
}
return -1;
}
//通过赋值的方式,将上面所有的细节都隐藏了
myarray = {
isArray: isArray,
indexOf: indexOf,
inArray: indexOf
};
} ());
//测试代码
console.log(myarray.isArray([1, 2])); // true
console.log(myarray.isArray({ 0: 1 })); // false
console.log(myarray.indexOf(["a", "b", "z"], "z")); // 2
console.log(myarray.inArray(["a", "b", "z"], "z")); // 2
myarray.indexOf = null;
console.log(myarray.inArray(["a", "b", "z"], "z")); // 2
~~~
## 模式5:链模式
链模式可以你连续可以调用一个对象的方法,比如obj.add(1).remove(2).delete(4).add(2)这样的形式,其实现思路非常简单,就是将this原样返回。代码如下:
~~~
var obj = {
value: 1,
increment: function () {
this.value += 1;
return this;
},
add: function (v) {
this.value += v;
return this;
},
shout: function () {
console.log(this.value);
}
};
// 链方法调用
obj.increment().add(3).shout(); // 5
// 也可以单独一个一个调用
obj.increment();
obj.add(3);
obj.shout();
~~~
## 总结
本篇是对象创建模式的上篇,敬请期待明天的下篇。
参考:http://shichuan.github.com/javascript-patterns/#object-creation-patterns
(46)代码复用模式(推荐篇)
最后更新于:2022-04-01 14:58:59
## 介绍
本文介绍的四种代码复用模式都是最佳实践,推荐大家在编程的过程中使用。
## 模式1:原型继承
原型继承是让父对象作为子对象的原型,从而达到继承的目的:
~~~
function object(o) {
function F() {
}
F.prototype = o;
return new F();
}
// 要继承的父对象
var parent = {
name: "Papa"
};
// 新对象
var child = object(parent);
// 测试
console.log(child.name); // "Papa"
// 父构造函数
function Person() {
// an "own" property
this.name = "Adam";
}
// 给原型添加新属性
Person.prototype.getName = function () {
return this.name;
};
// 创建新person
var papa = new Person();
// 继承
var kid = object(papa);
console.log(kid.getName()); // "Adam"
// 父构造函数
function Person() {
// an "own" property
this.name = "Adam";
}
// 给原型添加新属性
Person.prototype.getName = function () {
return this.name;
};
// 继承
var kid = object(Person.prototype);
console.log(typeof kid.getName); // "function",因为是在原型里定义的
console.log(typeof kid.name); // "undefined", 因为只继承了原型
~~~
同时,ECMAScript5也提供了类似的一个方法叫做Object.create用于继承对象,用法如下:
~~~
/* 使用新版的ECMAScript 5提供的功能 */
var child = Object.create(parent);
var child = Object.create(parent, {
age: { value: 2} // ECMA5 descriptor
});
console.log(child.hasOwnProperty("age")); // true
~~~
而且,也可以更细粒度地在第二个参数上定义属性:
~~~
// 首先,定义一个新对象man
var man = Object.create(null);
// 接着,创建包含属性的配置设置
// 属性设置为可写,可枚举,可配置
var config = {
writable: true,
enumerable: true,
configurable: true
};
// 通常使用Object.defineProperty()来添加新属性(ECMAScript5支持)
// 现在,为了方便,我们自定义一个封装函数
var defineProp = function (obj, key, value) {
config.value = value;
Object.defineProperty(obj, key, config);
}
defineProp(man, 'car', 'Delorean');
defineProp(man, 'dob', '1981');
defineProp(man, 'beard', false);
~~~
所以,继承就这么可以做了:
~~~
var driver = Object.create( man );
defineProp (driver, 'topSpeed', '100mph');
driver.topSpeed // 100mph
~~~
但是有个地方需要注意,就是Object.create(null)创建的对象的原型为undefined,也就是没有toString和valueOf方法,所以alert(man);的时候会出错,但alert(man.car);是没问题的。
## 模式2:复制所有属性进行继承
这种方式的继承就是将父对象里所有的属性都复制到子对象上,一般子对象可以使用父对象的数据。
先来看一个浅拷贝的例子:
~~~
/* 浅拷贝 */
function extend(parent, child) {
var i;
child = child || {};
for (i in parent) {
if (parent.hasOwnProperty(i)) {
child[i] = parent[i];
}
}
return child;
}
var dad = { name: "Adam" };
var kid = extend(dad);
console.log(kid.name); // "Adam"
var dad = {
counts: [1, 2, 3],
reads: { paper: true }
};
var kid = extend(dad);
kid.counts.push(4);
console.log(dad.counts.toString()); // "1,2,3,4"
console.log(dad.reads === kid.reads); // true
~~~
代码的最后一行,你可以发现dad和kid的reads是一样的,也就是他们使用的是同一个引用,这也就是浅拷贝带来的问题。
我们再来看一下深拷贝:
~~~
/* 深拷贝 */
function extendDeep(parent, child) {
var i,
toStr = Object.prototype.toString,
astr = "[object Array]";
child = child || {};
for (i in parent) {
if (parent.hasOwnProperty(i)) {
if (typeof parent[i] === 'object') {
child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
extendDeep(parent[i], child[i]);
} else {
child[i] = parent[i];
}
}
}
return child;
}
var dad = {
counts: [1, 2, 3],
reads: { paper: true }
};
var kid = extendDeep(dad);
kid.counts.push(4);
console.log(kid.counts.toString()); // "1,2,3,4"
console.log(dad.counts.toString()); // "1,2,3"
console.log(dad.reads === kid.reads); // false
kid.reads.paper = false;
~~~
深拷贝以后,两个值就不相等了,bingo!
## 模式3:混合(mix-in)
混入就是将一个对象的一个或多个(或全部)属性(或方法)复制到另外一个对象,我们举一个例子:
~~~
function mix() {
var arg, prop, child = {};
for (arg = 0; arg < arguments.length; arg += 1) {
for (prop in arguments[arg]) {
if (arguments[arg].hasOwnProperty(prop)) {
child[prop] = arguments[arg][prop];
}
}
}
return child;
}
var cake = mix(
{ eggs: 2, large: true },
{ butter: 1, salted: true },
{ flour: '3 cups' },
{ sugar: 'sure!' }
);
console.dir(cake);
~~~
mix函数将所传入的所有参数的子属性都复制到child对象里,以便产生一个新对象。
那如何我们只想混入部分属性呢?该个如何做?其实我们可以使用多余的参数来定义需要混入的属性,例如mix(child,parent,method1,method2)这样就可以只将parent里的method1和method2混入到child里。上代码:
~~~
// Car
var Car = function (settings) {
this.model = settings.model || 'no model provided';
this.colour = settings.colour || 'no colour provided';
};
// Mixin
var Mixin = function () { };
Mixin.prototype = {
driveForward: function () {
console.log('drive forward');
},
driveBackward: function () {
console.log('drive backward');
}
};
// 定义的2个参数分别是被混入的对象(reciving)和从哪里混入的对象(giving)
function augment(receivingObj, givingObj) {
// 如果提供了指定的方法名称的话,也就是参数多余3个
if (arguments[2]) {
for (var i = 2, len = arguments.length; i < len; i++) {
receivingObj.prototype[arguments[i]] = givingObj.prototype[arguments[i]];
}
}
// 如果不指定第3个参数,或者更多参数,就混入所有的方法
else {
for (var methodName in givingObj.prototype) {
// 检查receiving对象内部不包含要混入的名字,如何包含就不混入了
if (!receivingObj.prototype[methodName]) {
receivingObj.prototype[methodName] = givingObj.prototype[methodName];
}
}
}
}
// 给Car混入属性,但是值混入'driveForward' 和 'driveBackward'*/
augment(Car, Mixin, 'driveForward', 'driveBackward');
// 创建新对象Car
var vehicle = new Car({ model: 'Ford Escort', colour: 'blue' });
// 测试是否成功得到混入的方法
vehicle.driveForward();
vehicle.driveBackward();
~~~
该方法使用起来就比较灵活了。
## 模式4:借用方法
一个对象借用另外一个对象的一个或两个方法,而这两个对象之间不会有什么直接联系。不用多解释,直接用代码解释吧:
~~~
var one = {
name: 'object',
say: function (greet) {
return greet + ', ' + this.name;
}
};
// 测试
console.log(one.say('hi')); // "hi, object"
var two = {
name: 'another object'
};
console.log(one.say.apply(two, ['hello'])); // "hello, another object"
// 将say赋值给一个变量,this将指向到全局变量
var say = one.say;
console.log(say('hoho')); // "hoho, undefined"
// 传入一个回调函数callback
var yetanother = {
name: 'Yet another object',
method: function (callback) {
return callback('Hola');
}
};
console.log(yetanother.method(one.say)); // "Holla, undefined"
function bind(o, m) {
return function () {
return m.apply(o, [].slice.call(arguments));
};
}
var twosay = bind(two, one.say);
console.log(twosay('yo')); // "yo, another object"
// ECMAScript 5给Function.prototype添加了一个bind()方法,以便很容易使用apply()和call()。
if (typeof Function.prototype.bind === 'undefined') {
Function.prototype.bind = function (thisArg) {
var fn = this,
slice = Array.prototype.slice,
args = slice.call(arguments, 1);
return function () {
return fn.apply(thisArg, args.concat(slice.call(arguments)));
};
};
}
var twosay2 = one.say.bind(two);
console.log(twosay2('Bonjour')); // "Bonjour, another object"
var twosay3 = one.say.bind(two, 'Enchanté');
console.log(twosay3()); // "Enchanté, another object"
~~~
## 总结
就不用总结了吧。
(45)代码复用模式(避免篇)
最后更新于:2022-04-01 14:58:56
## 介绍
任何编程都提出代码复用,否则话每次开发一个新程序或者写一个新功能都要全新编写的话,那就歇菜了,但是代码复用也是有好要坏,接下来的两篇文章我们将针对代码复用来进行讨论,第一篇文避免篇,指的是要尽量避免使用这些模式,因为或多或少有带来一些问题;第二排是推荐篇,指的是推荐大家使用的模式,一般不会有什么问题。
## 模式1:默认模式
代码复用大家常用的默认模式,往往是有问题的,该模式使用Parent()的构造函数创建一个对象,并且将该对象赋值给Child()的原型。我们看一下代码:
~~~
function inherit(C, P) {
C.prototype = new P();
}
// 父构造函数
function Parent(name) {
this.name = name || 'Adam';
}
// 给原型添加say功能
Parent.prototype.say = function () {
return this.name;
};
// Child构造函数为空
function Child(name) {
}
// 执行继承
inherit(Child, Parent);
var kid = new Child();
console.log(kid.say()); // "Adam"
var kiddo = new Child();
kiddo.name = "Patrick";
console.log(kiddo.say()); // "Patrick"
// 缺点:不能让参数传进给Child构造函数
var s = new Child('Seth');
console.log(s.say()); // "Adam"
~~~
这种模式的缺点是Child不能传进参数,基本上也就废了。
## 模式2:借用构造函数
该模式是Child借用Parent的构造函数进行apply,然后将child的this和参数传递给apply方法:
~~~
// 父构造函数
function Parent(name) {
this.name = name || 'Adam';
}
// 给原型添加say功能
Parent.prototype.say = function () {
return this.name;
};
// Child构造函数
function Child(name) {
Parent.apply(this, arguments);
}
var kid = new Child("Patrick");
console.log(kid.name); // "Patrick"
// 缺点:没有从构造函数上继承say方法
console.log(typeof kid.say); // "undefined"
~~~
缺点也很明显,say方法不可用,因为没有继承过来。
## 模式3:借用构造函数并设置原型
上述两个模式都有自己的缺点,那如何把两者的缺点去除呢,我们来尝试一下:
~~~
// 父构造函数
function Parent(name) {
this.name = name || 'Adam';
}
// 给原型添加say功能
Parent.prototype.say = function () {
return this.name;
};
// Child构造函数
function Child(name) {
Parent.apply(this, arguments);
}
Child.prototype = new Parent();
var kid = new Child("Patrick");
console.log(kid.name); // "Patrick"
console.log(typeof kid.say); // function
console.log(kid.say()); // Patrick
console.dir(kid);
delete kid.name;
console.log(kid.say()); // "Adam"
~~~
运行起来,一切正常,但是有没有发现,Parent构造函数执行了两次,所以说,虽然程序可用,但是效率很低。
## 模式4:共享原型
共享原型是指Child和Parent使用同样的原型,代码如下:
~~~
function inherit(C, P) {
C.prototype = P.prototype;
}
// 父构造函数
function Parent(name) {
this.name = name || 'Adam';
}
// 给原型添加say功能
Parent.prototype.say = function () {
return this.name;
};
// Child构造函数
function Child(name) {
}
inherit(Child, Parent);
var kid = new Child('Patrick');
console.log(kid.name); // undefined
console.log(typeof kid.say); // function
kid.name = 'Patrick';
console.log(kid.say()); // Patrick
console.dir(kid);
~~~
确定还是一样,Child的参数没有正确接收到。
## 模式5:临时构造函数
首先借用构造函数,然后将Child的原型设置为该借用构造函数的实例,最后恢复Child原型的构造函数。代码如下:
~~~
/* 闭包 */
var inherit = (function () {
var F = function () {
};
return function (C, P) {
F.prototype = P.prototype;
C.prototype = new F();
C.uber = P.prototype;
C.prototype.constructor = C;
}
} ());
function Parent(name) {
this.name = name || 'Adam';
}
// 给原型添加say功能
Parent.prototype.say = function () {
return this.name;
};
// Child构造函数
function Child(name) {
}
inherit(Child, Parent);
var kid = new Child();
console.log(kid.name); // undefined
console.log(typeof kid.say); // function
kid.name = 'Patrick';
console.log(kid.say()); // Patrick
var kid2 = new Child("Tom");
console.log(kid.say());
console.log(kid.constructor.name); // Child
console.log(kid.constructor === Parent); // false
~~~
问题照旧,Child不能正常接收参数。
## 模式6:klass
这个模式,先上代码吧:
~~~
var klass = function (Parent, props) {
var Child, F, i;
// 1.
// 新构造函数
Child = function () {
if (Child.uber && Child.uber.hasOwnProperty("__construct")) {
Child.uber.__construct.apply(this, arguments);
}
if (Child.prototype.hasOwnProperty("__construct")) {
Child.prototype.__construct.apply(this, arguments);
}
};
// 2.
// 继承
Parent = Parent || Object;
F = function () {
};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.uber = Parent.prototype;
Child.prototype.constructor = Child;
// 3.
// 添加实现方法
for (i in props) {
if (props.hasOwnProperty(i)) {
Child.prototype[i] = props[i];
}
}
// return the "class"
return Child;
};
var Man = klass(null, {
__construct: function (what) {
console.log("Man's constructor");
this.name = what;
},
getName: function () {
return this.name;
}
});
var first = new Man('Adam'); // logs "Man's constructor"
first.getName(); // "Adam"
var SuperMan = klass(Man, {
__construct: function (what) {
console.log("SuperMan's constructor");
},
getName: function () {
var name = SuperMan.uber.getName.call(this);
return "I am " + name;
}
});
var clark = new SuperMan('Clark Kent');
clark.getName(); // "I am Clark Kent"
console.log(clark instanceof Man); // true
console.log(clark instanceof SuperMan); // true
~~~
怎么样?看着是不是有点晕,说好点,该模式的语法和规范拧得和别的语言一样,你愿意用么?咳。。。
## 总结
以上六个模式虽然在某种特殊情况下实现了某些功能,但是都存在各自的缺点,所以一般情况,大家要避免使用。
参考:http://shichuan.github.com/javascript-patterns/#code-reuse-patterns
(44)设计模式之桥接模式
最后更新于:2022-04-01 14:58:54
## 介绍
桥接模式(Bridge)将抽象部分与它的实现部分分离,使它们都可以独立地变化。
## 正文
桥接模式最常用在事件监控上,先看一段代码:
~~~
addEvent(element, 'click', getBeerById);
function getBeerById(e) {
var id = this.id;
asyncRequest('GET', 'beer.uri?id=' + id, function(resp) {
// Callback response.
console.log('Requested Beer: ' + resp.responseText);
});
}
~~~
上述代码,有个问题就是getBeerById必须要有浏览器的上下文才能使用,因为其内部使用了this.id这个属性,如果没用上下文,那就歇菜了。所以说一般稍微有经验的程序员都会将程序改造成如下形式:
~~~
function getBeerById(id, callback) {
// 通过ID发送请求,然后返回数据
asyncRequest('GET', 'beer.uri?id=' + id, function(resp) {
// callback response
callback(resp.responseText);
});
}
~~~
实用多了,对吧?首先ID可以随意传入,而且还提供了一个callback函数用于自定义处理函数。但是这个和桥接有什么关系呢?这就是下段代码所要体现的了:
~~~
addEvent(element, 'click', getBeerByIdBridge);
function getBeerByIdBridge (e) {
getBeerById(this.id, function(beer) {
console.log('Requested Beer: '+beer);
});
}
~~~
这里的getBeerByIdBridge就是我们定义的桥,用于将抽象的click事件和getBeerById连接起来,同时将事件源的ID,以及自定义的call函数(console.log输出)作为参数传入到getBeerById函数里。
这个例子看起来有些简单,我们再来一个复杂点的实战例子。
## 实战XHR连接队列
我们要构建一个队列,队列里存放了很多ajax请求,使用队列(queue)主要是因为要确保先加入的请求先被处理。任何时候,我们可以暂停请求、删除请求、重试请求以及支持对各个请求的订阅事件。
### 基础核心函数
在正式开始之前,我们先定义一下核心的几个封装函数,首先第一个是异步请求的函数封装:
~~~
var asyncRequest = (function () {
function handleReadyState(o, callback) {
var poll = window.setInterval(
function () {
if (o && o.readyState == 4) {
window.clearInterval(poll);
if (callback) {
callback(o);
}
}
},
50
);
}
var getXHR = function () {
var http;
try {
http = new XMLHttpRequest;
getXHR = function () {
return new XMLHttpRequest;
};
}
catch (e) {
var msxml = [
'MSXML2.XMLHTTP.3.0',
'MSXML2.XMLHTTP',
'Microsoft.XMLHTTP'
];
for (var i = 0, len = msxml.length; i < len; ++i) {
try {
http = new ActiveXObject(msxml[i]);
getXHR = function () {
return new ActiveXObject(msxml[i]);
};
break;
}
catch (e) { }
}
}
return http;
};
return function (method, uri, callback, postData) {
var http = getXHR();
http.open(method, uri, true);
handleReadyState(http, callback);
http.send(postData || null);
return http;
};
})();
~~~
上述封装的自执行函数是一个通用的Ajax请求函数,相信属性Ajax的人都能看懂了。
接下来我们定义一个通用的添加方法(函数)的方法:
~~~
Function.prototype.method = function (name, fn) {
this.prototype[name] = fn;
return this;
};
~~~
最后再添加关于数组的2个方法,一个用于遍历,一个用于筛选:
~~~
if (!Array.prototype.forEach) {
Array.method('forEach', function (fn, thisObj) {
var scope = thisObj || window;
for (var i = 0, len = this.length; i < len; ++i) {
fn.call(scope, this[i], i, this);
}
});
}
if (!Array.prototype.filter) {
Array.method('filter', function (fn, thisObj) {
var scope = thisObj || window;
var a = [];
for (var i = 0, len = this.length; i < len; ++i) {
if (!fn.call(scope, this[i], i, this)) {
continue;
}
a.push(this[i]);
}
return a;
});
}
~~~
因为有的新型浏览器已经支持了这两种功能(或者有些类库已经支持了),所以要先判断,如果已经支持的话,就不再处理了。
### 观察者系统
观察者在队列里的事件过程中扮演着重要的角色,可以队列处理时(成功、失败、挂起)订阅事件:
~~~
window.DED = window.DED || {};
DED.util = DED.util || {};
DED.util.Observer = function () {
this.fns = [];
}
DED.util.Observer.prototype = {
subscribe: function (fn) {
this.fns.push(fn);
},
unsubscribe: function (fn) {
this.fns = this.fns.filter(
function (el) {
if (el !== fn) {
return el;
}
}
);
},
fire: function (o) {
this.fns.forEach(
function (el) {
el(o);
}
);
}
};
~~~
### 队列主要实现代码
首先订阅了队列的主要属性和事件委托:
~~~
DED.Queue = function () {
// 包含请求的队列.
this.queue = [];
// 使用Observable对象在3个不同的状态上,以便可以随时订阅事件
this.onComplete = new DED.util.Observer;
this.onFailure = new DED.util.Observer;
this.onFlush = new DED.util.Observer;
// 核心属性,可以在外部调用的时候进行设置
this.retryCount = 3;
this.currentRetry = 0;
this.paused = false;
this.timeout = 5000;
this.conn = {};
this.timer = {};
};
~~~
然后通过DED.Queue.method的链式调用,则队列上添加了很多可用的方法:
~~~
DED.Queue.
method('flush', function () {
// flush方法
if (!this.queue.length > 0) {
return;
}
if (this.paused) {
this.paused = false;
return;
}
var that = this;
this.currentRetry++;
var abort = function () {
that.conn.abort();
if (that.currentRetry == that.retryCount) {
that.onFailure.fire();
that.currentRetry = 0;
} else {
that.flush();
}
};
this.timer = window.setTimeout(abort, this.timeout);
var callback = function (o) {
window.clearTimeout(that.timer);
that.currentRetry = 0;
that.queue.shift();
that.onFlush.fire(o.responseText);
if (that.queue.length == 0) {
that.onComplete.fire();
return;
}
// recursive call to flush
that.flush();
};
this.conn = asyncRequest(
this.queue[0]['method'],
this.queue[0]['uri'],
callback,
this.queue[0]['params']
);
}).
method('setRetryCount', function (count) {
this.retryCount = count;
}).
method('setTimeout', function (time) {
this.timeout = time;
}).
method('add', function (o) {
this.queue.push(o);
}).
method('pause', function () {
this.paused = true;
}).
method('dequeue', function () {
this.queue.pop();
}).
method('clear', function () {
this.queue = [];
});
~~~
代码看起来很多,折叠以后就可以发现,其实就是在队列上定义了flush, setRetryCount, setTimeout, add, pause, dequeue, 和clear方法。
### 简单调用
~~~
var q = new DED.Queue;
// 设置重试次数高一点,以便应付慢的连接
q.setRetryCount(5);
// 设置timeout时间
q.setTimeout(1000);
// 添加2个请求.
q.add({
method: 'GET',
uri: '/path/to/file.php?ajax=true'
});
q.add({
method: 'GET',
uri: '/path/to/file.php?ajax=true&woe=me'
});
// flush队列
q.flush();
// 暂停队列,剩余的保存
q.pause();
// 清空.
q.clear();
// 添加2个请求.
q.add({
method: 'GET',
uri: '/path/to/file.php?ajax=true'
});
q.add({
method: 'GET',
uri: '/path/to/file.php?ajax=true&woe=me'
});
// 从队列里删除最后一个请求.
q.dequeue();
// 再次Flush
q.flush();
~~~
### 桥接呢?
上面的调用代码里并没有桥接,那桥呢?看一下下面的完整示例,就可以发现处处都有桥哦:
~~~
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Ajax Connection Queue</title>
<script src="utils.js"></script>
<script src="queue.js"></script>
<script type="text/javascript">
addEvent(window, 'load', function () {
// 实现.
var q = new DED.Queue;
q.setRetryCount(5);
q.setTimeout(3000);
var items = $('items');
var results = $('results');
var queue = $('queue-items');
// 在客户端保存跟踪自己的请求
var requests = [];
// 每个请求flush以后,订阅特殊的处理步骤
q.onFlush.subscribe(function (data) {
results.innerHTML = data;
requests.shift();
queue.innerHTML = requests.toString();
});
// 订阅时间处理步骤
q.onFailure.subscribe(function () {
results.innerHTML += ' <span style="color:red;">Connection Error!</span>';
});
// 订阅全部成功的处理步骤x
q.onComplete.subscribe(function () {
results.innerHTML += ' <span style="color:green;">Completed!</span>';
});
var actionDispatcher = function (element) {
switch (element) {
case 'flush':
q.flush();
break;
case 'dequeue':
q.dequeue();
requests.pop();
queue.innerHTML = requests.toString();
break;
case 'pause':
q.pause();
break;
case 'clear':
q.clear();
requests = [];
queue.innerHTML = '';
break;
}
};
var addRequest = function (request) {
var data = request.split('-')[1];
q.add({
method: 'GET',
uri: 'bridge-connection-queue.php?ajax=true&s=' + data,
params: null
});
requests.push(data);
queue.innerHTML = requests.toString();
};
addEvent(items, 'click', function (e) {
var e = e || window.event;
var src = e.target || e.srcElement;
try {
e.preventDefault();
}
catch (ex) {
e.returnValue = false;
}
actionDispatcher(src.id);
});
var adders = $('adders');
addEvent(adders, 'click', function (e) {
var e = e || window.event;
var src = e.target || e.srcElement;
try {
e.preventDefault();
}
catch (ex) {
e.returnValue = false;
}
addRequest(src.id);
});
});
</script>
<style type="text/css" media="screen">
body
{
font: 100% georgia,times,serif;
}
h1, h2
{
font-weight: normal;
}
#queue-items
{
height: 1.5em;
}
#add-stuff
{
padding: .5em;
background: #ddd;
border: 1px solid #bbb;
}
#results-area
{
padding: .5em;
border: 1px solid #bbb;
}
</style>
</head>
<body id="example">
<div id="doc">
<h1>
异步联接请求</h1>
<div id="queue-items">
</div>
<div id="add-stuff">
<h2>向队列里添加新请求</h2>
<ul id="adders">
<li><a href="#" id="action-01">添加 "01" 到队列</a></li>
<li><a href="#" id="action-02">添加 "02" 到队列</a></li>
<li><a href="#" id="action-03">添加 "03" 到队列</a></li>
</ul>
</div>
<h2>队列控制</h2>
<ul id='items'>
<li><a href="#" id="flush">Flush</a></li>
<li><a href="#" id="dequeue">出列Dequeue</a></li>
<li><a href="#" id="pause">暂停Pause</a></li>
<li><a href="#" id="clear">清空Clear</a></li>
</ul>
<div id="results-area">
<h2>
结果:
</h2>
<div id="results">
</div>
</div>
</div>
</body>
</html>
~~~
在这个示例里,你可以做flush队列,暂停队列,删除队列里的请求,清空队列等各种动作,同时相信大家也体会到了桥接的威力了。
## 总结
桥接模式的优点也很明显,我们只列举主要几个优点:
1. 分离接口和实现部分,一个实现未必不变地绑定在一个接口上,抽象类(函数)的实现可以在运行时刻进行配置,一个对象甚至可以在运行时刻改变它的实现,同将抽象和实现也进行了充分的解耦,也有利于分层,从而产生更好的结构化系统。
2. 提高可扩充性
3. 实现细节对客户透明,可以对客户隐藏实现细节。
同时桥接模式也有自己的缺点:
大量的类将导致开发成本的增加,同时在性能方面可能也会有所减少。
(43)设计模式之状态模式
最后更新于:2022-04-01 14:58:52
## 介绍
状态模式(State)允许一个对象在其内部状态改变的时候改变它的行为,对象看起来似乎修改了它的类。
## 正文
举个例子,就比如我们平时在下载东西,通常就会有好几个状态,比如准备状态(ReadyState)、下载状态(DownloadingState)、暂停状态(DownloadPausedState)、下载完毕状态(DownloadedState)、失败状态(DownloadFailedState),也就是说在每个状态都只可以做当前状态才可以做的事情,而不能做其它状态能做的事儿。
由于State模式描述了下载(Download)如何在每一种状态下表现出不同的行为。这一模式的关键思想就是引入了一个叫做State的抽象类(或JS里的函数)来表示下载状态,State函数(作为原型)为每个状态的子类(继承函数)声明了一些公共接口。其每个继承函数实现与特定状态相关的行为,比如DownloadingState和DownloadedState分别实现了正在下载和下载完毕的行为。这些行为可以通过Download来来维护。
让我们来实现一把,首先定义作为其他基础函数的原型的State函数:
~~~
var State = function () {
};
State.prototype.download = function () {
throw new Error("该方法必须被重载!");
};
State.prototype.pause = function () {
throw new Error("该方法必须被重载!");
};
State.prototype.fail = function () {
throw new Error("该方法必须被重载!");
};
State.prototype.finish = function () {
throw new Error("该方法必须被重载!");
};
~~~
我们为State的原型定义了4个方法接口,分别对应着下载(download)、暂停(pause)、失败(fail)、结束(finish)以便子函数可以重写。
在编写子函数之前,我们先来编写一个ReadyState函数,以便可以将状态传递给第一个download状态:
~~~
var ReadyState = function (oDownload) {
State.apply(this);
this.oDownload = oDownload;
};
ReadyState.prototype = new State();
ReadyState.prototype.download = function () {
this.oDownload.setState(this.oDownload.getDownloadingState());
// Ready以后,可以开始下载,所以设置了Download函数里的状态获取方法
console.log("Start Download!");
};
ReadyState.prototype.pause = function () {
throw new Error("还没开始下载,不能暂停!");
};
ReadyState.prototype.fail = function () {
throw new Error("文件还没开始下载,怎么能说失败呢!");
};
ReadyState.prototype.finish = function () {
throw new Error("文件还没开始下载,当然也不能结束了!");
};
~~~
该函数接收了一个Download维护函数的实例作为参数,Download函数用于控制状态的改变和获取(类似于中央控制器,让外部调用),ReadyState重写了原型的download方法,以便开始进行下载。我们继续来看Download函数的主要功能:
~~~
var Download = function () {
this.oState = new ReadyState(this);
};
Download.prototype.setState = function (oState) {
this.oState = oState;
};
// 对外暴露的四个公共方法,以便外部调用
Download.prototype.download = function () {
this.oState.download();
};
Download.prototype.pause = function () {
this.oState.pause();
};
Download.prototype.fail = function () {
this.oState.fail();
};
Download.prototype.finish = function () {
this.oState.finish();
};
//获取各种状态,传入当前this对象
Download.prototype.getReadyState = function () {
return new ReadyState(this);
};
Download.prototype.getDownloadingState = function () {
return new DownloadingState(this);
};
Download.prototype.getDownloadPausedState = function () {
return new DownloadPausedState(this);
};
Download.prototype.getDownloadedState = function () {
return new DownloadedState(this);
};
Download.prototype.getDownloadedFailedState = function () {
return new DownloadFailedState(this);
};
~~~
Download函数的原型提供了8个方法,4个是对用于下载状态的操作行为,另外4个是用于获取当前四个不同的状态,这4个方法都接收this作为参数,也就是将Download实例自身作为一个参数传递给处理该请求的状态对象(ReadyState 以及后面要实现的继承函数),这使得状态对象比必要的时候可以访问oDownlaod。
接下来,继续定义4个相关状态的函数:
~~~
var DownloadingState = function (oDownload) {
State.apply(this);
this.oDownload = oDownload;
};
DownloadingState.prototype = new State();
DownloadingState.prototype.download = function () {
throw new Error("文件已经正在下载中了!");
};
DownloadingState.prototype.pause = function () { this.oDownload.setState(this.oDownload.getDownloadPausedState());
console.log("暂停下载!");
};
DownloadingState.prototype.fail = function () { this.oDownload.setState(this.oDownload.getDownloadedFailedState());
console.log("下载失败!");
};
DownloadingState.prototype.finish = function () {
this.oDownload.setState(this.oDownload.getDownloadedState());
console.log("下载完毕!");
};
~~~
DownloadingState的主要注意事项就是已经正在下载的文件,不能再次开始下载了,其它的状态都可以连续进行。
~~~
var DownloadPausedState = function (oDownload) {
State.apply(this);
this.oDownload = oDownload;
};
DownloadPausedState.prototype = new State();
DownloadPausedState.prototype.download = function () {
this.oDownload.setState(this.oDownload.getDownloadingState());
console.log("继续下载!");
};
DownloadPausedState.prototype.pause = function () {
throw new Error("已经暂停了,咋还要暂停呢!");
};
DownloadPausedState.prototype.fail = function () { this.oDownload.setState(this.oDownload.getDownloadedFailedState());
console.log("下载失败!");
};
DownloadPausedState.prototype.finish = function () {
this.oDownload.setState(this.oDownload.getDownloadedState());
console.log("下载完毕!");
};
~~~
DownloadPausedState函数里要注意的是,已经暂停的下载,不能再次暂停。
~~~
var DownloadedState = function (oDownload) {
State.apply(this);
this.oDownload = oDownload;
};
DownloadedState.prototype = new State();
DownloadedState.prototype.download = function () {
this.oDownload.setState(this.oDownload.getDownloadingState());
console.log("重新下载!");
};
DownloadedState.prototype.pause = function () {
throw new Error("对下载完了,还暂停啥?");
};
DownloadedState.prototype.fail = function () {
throw new Error("都下载成功了,咋会失败呢?");
};
DownloadedState.prototype.finish = function () {
throw new Error("下载成功了,不能再为成功了吧!");
};
~~~
DownloadedState函数,同理成功下载以后,不能再设置finish了,只能设置重新下载状态。
~~~
var DownloadFailedState = function (oDownload) {
State.apply(this);
this.oDownload = oDownload;
};
DownloadFailedState.prototype = new State();
DownloadFailedState.prototype.download = function () {
this.oDownload.setState(this.oDownload.getDownloadingState());
console.log("尝试重新下载!");
};
DownloadFailedState.prototype.pause = function () {
throw new Error("失败的下载,也不能暂停!");
};
DownloadFailedState.prototype.fail = function () {
throw new Error("都失败了,咋还失败呢!");
};
DownloadFailedState.prototype.finish = function () {
throw new Error("失败的下载,肯定也不会成功!");
};同理,DownloadFailedState函数的失败状态,也不能再次失败,但可以和finished以后再次尝试重新下载。
~~~
调用测试代码,就非常简单了,我们在HTML里演示吧,首先是要了jquery,然后有3个按钮分别代表:开始下载、暂停、重新下载。(注意在Firefox里用firebug查看结果,因为用了 console.log方法)。
~~~
<html>
<head>
<link type="text/css" rel="stylesheet" href="http://www.cnblogs.com/css/style.css" />
<title>State Pattern</title>
<script type="text/javascript" src="/jquery.js"></script>
<script type="text/javascript" src="Download.js"></script>
<script type="text/javascript" src="states/State.js"></script>
<script type="text/javascript" src="states/DownloadFailedState.js"></script>
<script type="text/javascript" src="states/DownloadPausedState.js"></script>
<script type="text/javascript" src="states/DownloadedState.js"></script>
<script type="text/javascript" src="states/DownloadingState.js"></script>
<script type="text/javascript" src="states/ReadyState.js"></script>
</head>
<body>
<input type="button" value="开始下载" id="download_button" />
<input type="button" value="暂停" id="pause_button" />
<input type="button" value="重新下载" id="resume_button" />
<script type="text/javascript">
var oDownload = new Download();
$("#download_button").click(function () {
oDownload.download();
});
$("#pause_button").click(function () {
oDownload.pause();
});
$("#resume_button").click(function () {
oDownload.download();
});
</script>
</body>
</html>
~~~
## 总结
状态模式的使用场景也特别明确,有如下两点:
1. 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为。
2. 一个操作中含有大量的分支语句,而且这些分支语句依赖于该对象的状态。状态通常为一个或多个枚举常量的表示。
参考:https://github.com/tcorral/Design-Patterns-in-Javascript/blob/master/State/1/index.html
(42)设计模式之原型模式
最后更新于:2022-04-01 14:58:49
## 介绍
原型模式(prototype)是指用原型实例指向创建对象的种类,并且通过拷贝这些原型创建新的对象。
## 正文
对于原型模式,我们可以利用JavaScript特有的原型继承特性去创建对象的方式,也就是创建的一个对象作为另外一个对象的prototype属性值。原型对象本身就是有效地利用了每个构造器创建的对象,例如,如果一个构造函数的原型包含了一个name属性(见后面的例子),那通过这个构造函数创建的对象都会有这个属性。
在现有的文献里查看原型模式的定义,没有针对JavaScript的,你可能发现很多讲解的都是关于类的,但是现实情况是基于原型继承的JavaScript完全避免了类(class)的概念。我们只是简单从现有的对象进行拷贝来创建对象。
真正的原型继承是作为最新版的ECMAScript5标准提出的,使用Object.create方法来创建这样的对象,该方法创建指定的对象,其对象的prototype有指定的对象(也就是该方法传进的第一个参数对象),也可以包含其他可选的指定属性。例如Object.create(prototype, optionalDescriptorObjects),下面的例子里也可以看到这个用法:
~~~
// 因为不是构造函数,所以不用大写
var someCar = {
drive: function () { },
name: '马自达 3'
};
// 使用Object.create创建一个新车x
var anotherCar = Object.create(someCar);
anotherCar.name = '丰田佳美';
~~~
Object.create运行你直接从其它对象继承过来,使用该方法的第二个参数,你可以初始化额外的其它属性。例如:
~~~
var vehicle = {
getModel: function () {
console.log('车辆的模具是:' + this.model);
}
};
var car = Object.create(vehicle, {
'id': {
value: MY_GLOBAL.nextId(),
enumerable: true // 默认writable:false, configurable:false
},
'model': {
value: '福特',
enumerable: true
}
});
~~~
这里,可以在Object.create的第二个参数里使用对象字面量传入要初始化的额外属性,其语法与Object.defineProperties或Object.defineProperty方法类型。它允许您设定属性的特性,例如enumerable, writable 或 configurable。
如果你希望自己去实现原型模式,而不直接使用Object.create 。你可以使用像下面这样的代码为上面的例子来实现:
~~~
var vehiclePrototype = {
init: function (carModel) {
this.model = carModel;
},
getModel: function () {
console.log('车辆模具是:' + this.model);
}
};
function vehicle(model) {
function F() { };
F.prototype = vehiclePrototype;
var f = new F();
f.init(model);
return f;
}
var car = vehicle('福特Escort');
car.getModel();
~~~
## 总结
原型模式在JavaScript里的使用简直是无处不在,其它很多模式有很多也是基于prototype的,就不多说了,这里大家要注意的依然是浅拷贝和深拷贝的问题,免得出现引用问题。
(41)设计模式之模板方法
最后更新于:2022-04-01 14:58:47
## 介绍
模板方法(TemplateMethod)定义了一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
模板方法是一种代码复用的基本技术,在类库中尤为重要,因为他们提取了类库中的公共行为。模板方法导致一种反向的控制结构,这种结构就是传说中的“好莱坞法则”,即“别找找我们,我们找你”,这指的是父类调用一个类的操作,而不是相反。具体体现是面向对象编程编程语言里的抽象类(以及其中的抽象方法),以及继承该抽象类(和抽象方法)的子类。
## 正文
举个例子,泡茶和泡咖啡有同样的步骤,比如烧开水(boilWater)、冲泡(brew)、倒在杯子里(pourOnCup),加小料(addCondiments)等等。但每种饮料冲泡的方法以及所加的小料不一样,所以我们可以利用模板方法实现这个主要步骤。
首先先来定义抽象步骤:
~~~
var CaffeineBeverage = function () {
};
CaffeineBeverage.prototype.prepareRecipe = function () {
this.boilWater();
this.brew();
this.pourOnCup();
if (this.customerWantsCondiments()) {
// 如果可以想加小料,就加上
this.addCondiments();
}
};
CaffeineBeverage.prototype.boilWater = function () {
console.log("将水烧开!");
};
CaffeineBeverage.prototype.pourOnCup = function () {
console.log("将饮料到再杯子里!");
};
CaffeineBeverage.prototype.brew = function () {
throw new Error("该方法必须重写!");
};
CaffeineBeverage.prototype.addCondiments = function () {
throw new Error("该方法必须重写!");
};
// 默认加上小料
CaffeineBeverage.prototype.customerWantsCondiments = function () {
return true;
};
~~~
该函数在原型上扩展了所有的基础步骤,以及主要步骤,冲泡和加小料步骤没有实现,供具体饮料所对应的函数来实现,另外是否加小料(customerWantsCondiments )默认返回true,子函数重写的时候可以重写该值。
下面两个函数分别是冲咖啡和冲茶所对应的函数:
~~~
// 冲咖啡
var Coffee = function () {
CaffeineBeverage.apply(this);
};
Coffee.prototype = new CaffeineBeverage();
Coffee.prototype.brew = function () {
console.log("从咖啡机想咖啡倒进去!");
};
Coffee.prototype.addCondiments = function () {
console.log("添加糖和牛奶");
};
Coffee.prototype.customerWantsCondiments = function () {
return confirm("你想添加糖和牛奶吗?");
};
//冲茶叶
var Tea = function () {
CaffeineBeverage.apply(this);
};
Tea.prototype = new CaffeineBeverage();
Tea.prototype.brew = function () {
console.log("泡茶叶!");
};
Tea.prototype.addCondiments = function () {
console.log("添加柠檬!");
};
Tea.prototype.customerWantsCondiments = function () {
return confirm("你想添加柠檬嘛?");
};
~~~
另外使用confirm,可以让用户自己选择加不加小料,很不错,不是嘛?
## 总结
模板方法应用于下列情况:
1. 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现
2. 各子类中公共的行为应被提取出来并集中到一个公共父类中的避免代码重复,不同之处分离为新的操作,最后,用一个钓鱼这些新操作的模板方法来替换这些不同的代码
3. 控制子类扩展,模板方法只在特定点调用“hook”操作,这样就允许在这些点进行扩展
和策略模式不同,模板方法使用继承来改变算法的一部分,而策略模式使用委托来改变整个算法。
参考:https://github.com/tcorral/Design-Patterns-in-Javascript/blob/master/Template/withHook/index.html
(40)设计模式之组合模式
最后更新于:2022-04-01 14:58:45
## 介绍
组合模式(Composite)将对象组合成树形结构以表示“部分-整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性。
常见的场景有asp.net里的控件机制(即control里可以包含子control,可以递归操作、添加、删除子control),类似的还有DOM的机制,一个DOM节点可以包含子节点,不管是父节点还是子节点都有添加、删除、遍历子节点的通用功能。所以说组合模式的关键是要有一个抽象类,它既可以表示子元素,又可以表示父元素。
## 正文
举个例子,有家餐厅提供了各种各样的菜品,每个餐桌都有一本菜单,菜单上列出了该餐厅所偶的菜品,有早餐糕点、午餐、晚餐等等,每个餐都有各种各样的菜单项,假设不管是菜单项还是整个菜单都应该是可以打印的,而且可以添加子项,比如午餐可以添加新菜品,而菜单项咖啡也可以添加糖啊什么的。
这种情况,我们就可以利用组合的方式将这些内容表示为层次结构了。我们来逐一分解一下我们的实现步骤。
**第一步,先实现我们的“抽象类”函数MenuComponent:**
~~~
var MenuComponent = function () {
};
MenuComponent.prototype.getName = function () {
throw new Error("该方法必须重写!");
};
MenuComponent.prototype.getDescription = function () {
throw new Error("该方法必须重写!");
};
MenuComponent.prototype.getPrice = function () {
throw new Error("该方法必须重写!");
};
MenuComponent.prototype.isVegetarian = function () {
throw new Error("该方法必须重写!");
};
MenuComponent.prototype.print = function () {
throw new Error("该方法必须重写!");
};
MenuComponent.prototype.add = function () {
throw new Error("该方法必须重写!");
};
MenuComponent.prototype.remove = function () {
throw new Error("该方法必须重写!");
};
MenuComponent.prototype.getChild = function () {
throw new Error("该方法必须重写!");
};
~~~
该函数提供了2种类型的方法,一种是获取信息的,比如价格,名称等,另外一种是通用操作方法,比如打印、添加、删除、获取子菜单。
**第二步,创建基本的菜品项:**
~~~
var MenuItem = function (sName, sDescription, bVegetarian, nPrice) {
MenuComponent.apply(this);
this.sName = sName;
this.sDescription = sDescription;
this.bVegetarian = bVegetarian;
this.nPrice = nPrice;
};
MenuItem.prototype = new MenuComponent();
MenuItem.prototype.getName = function () {
return this.sName;
};
MenuItem.prototype.getDescription = function () {
return this.sDescription;
};
MenuItem.prototype.getPrice = function () {
return this.nPrice;
};
MenuItem.prototype.isVegetarian = function () {
return this.bVegetarian;
};
MenuItem.prototype.print = function () {
console.log(this.getName() + ": " + this.getDescription() + ", " + this.getPrice() + "euros");
};
~~~
由代码可以看出,我们只重新了原型的4个获取信息的方法和print方法,没有重载其它3个操作方法,因为基本菜品不包含添加、删除、获取子菜品的方式。
**第三步,创建菜品:**
~~~
var Menu = function (sName, sDescription) {
MenuComponent.apply(this);
this.aMenuComponents = [];
this.sName = sName;
this.sDescription = sDescription;
this.createIterator = function () {
throw new Error("This method must be overwritten!");
};
};
Menu.prototype = new MenuComponent();
Menu.prototype.add = function (oMenuComponent) {
// 添加子菜品
this.aMenuComponents.push(oMenuComponent);
};
Menu.prototype.remove = function (oMenuComponent) {
// 删除子菜品
var aMenuItems = [];
var nMenuItem = 0;
var nLenMenuItems = this.aMenuComponents.length;
var oItem = null;
for (; nMenuItem < nLenMenuItems; ) {
oItem = this.aMenuComponents[nMenuItem];
if (oItem !== oMenuComponent) {
aMenuItems.push(oItem);
}
nMenuItem = nMenuItem + 1;
}
this.aMenuComponents = aMenuItems;
};
Menu.prototype.getChild = function (nIndex) {
//获取指定的子菜品
return this.aMenuComponents[nIndex];
};
Menu.prototype.getName = function () {
return this.sName;
};
Menu.prototype.getDescription = function () {
return this.sDescription;
};
Menu.prototype.print = function () {
// 打印当前菜品以及所有的子菜品
console.log(this.getName() + ": " + this.getDescription());
console.log("--------------------------------------------");
var nMenuComponent = 0;
var nLenMenuComponents = this.aMenuComponents.length;
var oMenuComponent = null;
for (; nMenuComponent < nLenMenuComponents; ) {
oMenuComponent = this.aMenuComponents[nMenuComponent];
oMenuComponent.print();
nMenuComponent = nMenuComponent + 1;
}
};
~~~
注意上述代码,除了实现了添加、删除、获取方法外,打印print方法是首先打印当前菜品信息,然后循环遍历打印所有子菜品信息。
**第四步,创建指定的菜品:**
我们可以创建几个真实的菜品,比如晚餐、咖啡、糕点等等,其都是用Menu作为其原型,代码如下:
~~~
var DinnerMenu = function () {
Menu.apply(this);
};
DinnerMenu.prototype = new Menu();
var CafeMenu = function () {
Menu.apply(this);
};
CafeMenu.prototype = new Menu();
var PancakeHouseMenu = function () {
Menu.apply(this);
};
PancakeHouseMenu.prototype = new Menu();
~~~
**第五步,创建最顶级的菜单容器——菜单本:**
~~~
var Mattress = function (aMenus) {
this.aMenus = aMenus;
};
Mattress.prototype.printMenu = function () {
this.aMenus.print();
};
~~~
该函数接收一个菜单数组作为参数,并且值提供了printMenu方法用于打印所有的菜单内容。
**第六步,调用方式:**
~~~
var oPanCakeHouseMenu = new Menu("Pancake House Menu", "Breakfast");
var oDinnerMenu = new Menu("Dinner Menu", "Lunch");
var oCoffeeMenu = new Menu("Cafe Menu", "Dinner");
var oAllMenus = new Menu("ALL MENUS", "All menus combined");
oAllMenus.add(oPanCakeHouseMenu);
oAllMenus.add(oDinnerMenu);
oDinnerMenu.add(new MenuItem("Pasta", "Spaghetti with Marinara Sauce, and a slice of sourdough bread", true, 3.89));
oDinnerMenu.add(oCoffeeMenu);
oCoffeeMenu.add(new MenuItem("Express", "Coffee from machine", false, 0.99));
var oMattress = new Mattress(oAllMenus);
console.log("---------------------------------------------");
oMattress.printMenu();
console.log("---------------------------------------------");
~~~
熟悉asp.net控件开发的同学,是不是看起来很熟悉?
## 总结
组合模式的使用场景非常明确:
1. 你想表示对象的部分-整体层次结构时;
2. 你希望用户忽略组合对象和单个对象的不同,用户将统一地使用组合结构中的所有对象(方法)
另外该模式经常和装饰者一起使用,它们通常有一个公共的父类(也就是原型),因此装饰必须支持具有add、remove、getChild操作的 component接口。
参考:https://github.com/tcorral/Design-Patterns-in-Javascript/blob/master/Composite/index.html
(39)设计模式之适配器模式
最后更新于:2022-04-01 14:58:43
## 介绍
适配器模式(Adapter)是将一个类(对象)的接口(方法或属性)转化成客户希望的另外一个接口(方法或属性),适配器模式使得原本由于接口不兼容而不能一起工作的那些类(对象)可以一些工作。速成包装器(wrapper)。
## 正文
我们来举一个例子,鸭子(Dock)有飞(fly)和嘎嘎叫(quack)的行为,而火鸡虽然也有飞(fly)的行为,但是其叫声是咯咯的(gobble)。如果你非要火鸡也要实现嘎嘎叫(quack)这个动作,那我们可以复用鸭子的quack方法,但是具体的叫还应该是咯咯的,此时,我们就可以创建一个火鸡的适配器,以便让火鸡也支持quack方法,其内部还是要调用gobble。
OK,我们开始一步一步实现,首先要先定义鸭子和火鸡的抽象行为,也就是各自的方法函数:
~~~
//鸭子
var Duck = function(){
};
Duck.prototype.fly = function(){
throw new Error("该方法必须被重写!");
};
Duck.prototype.quack = function(){
throw new Error("该方法必须被重写!");
}
//火鸡
var Turkey = function(){
};
Turkey.prototype.fly = function(){
throw new Error(" 该方法必须被重写 !");
};
Turkey.prototype.gobble = function(){
throw new Error(" 该方法必须被重写 !");
};
~~~
然后再定义具体的鸭子和火鸡的构造函数,分别为:
~~~
//鸭子
var MallardDuck = function () {
Duck.apply(this);
};
MallardDuck.prototype = new Duck(); //原型是Duck
MallardDuck.prototype.fly = function () {
console.log("可以飞翔很长的距离!");
};
MallardDuck.prototype.quack = function () {
console.log("嘎嘎!嘎嘎!");
};
//火鸡
var WildTurkey = function () {
Turkey.apply(this);
};
WildTurkey.prototype = new Turkey(); //原型是Turkey
WildTurkey.prototype.fly = function () {
console.log("飞翔的距离貌似有点短!");
};
WildTurkey.prototype.gobble = function () {
console.log("咯咯!咯咯!");
};
~~~
为了让火鸡也支持quack方法,我们创建了一个新的火鸡适配器TurkeyAdapter:
~~~
var TurkeyAdapter = function(oTurkey){
Duck.apply(this);
this.oTurkey = oTurkey;
};
TurkeyAdapter.prototype = new Duck();
TurkeyAdapter.prototype.quack = function(){
this.oTurkey.gobble();
};
TurkeyAdapter.prototype.fly = function(){
var nFly = 0;
var nLenFly = 5;
for(; nFly < nLenFly;){
this.oTurkey.fly();
nFly = nFly + 1;
}
};
~~~
该构造函数接受一个火鸡的实例对象,然后使用Duck进行apply,其适配器原型是Duck,然后要重新修改其原型的quack方法,以便内部调用oTurkey.gobble()方法。其fly方法也做了一些改变,让火鸡连续飞5次(内部也是调用自身的oTurkey.fly()方法)。
调用方法,就很明了了,测试一下便可以知道结果了:
~~~
var oMallardDuck = new MallardDuck();
var oWildTurkey = new WildTurkey();
var oTurkeyAdapter = new TurkeyAdapter(oWildTurkey);
//原有的鸭子行为
oMallardDuck.fly();
oMallardDuck.quack();
//原有的火鸡行为
oWildTurkey.fly();
oWildTurkey.gobble();
//适配器火鸡的行为(火鸡调用鸭子的方法名称)
oTurkeyAdapter.fly();
oTurkeyAdapter.quack();
~~~
## 总结
那合适使用适配器模式好呢?如果有以下情况出现时,建议使用:
1. 使用一个已经存在的对象,但其方法或属性接口不符合你的要求;
2. 你想创建一个可复用的对象,该对象可以与其它不相关的对象或不可见对象(即接口方法或属性不兼容的对象)协同工作;
3. 想使用已经存在的对象,但是不能对每一个都进行原型继承以匹配它的接口。对象适配器可以适配它的父对象接口方法或属性。
另外,适配器模式和其它几个模式可能容易让人迷惑,这里说一下大概的区别:
1. 适配器和桥接模式虽然类似,但桥接的出发点不同,桥接的目的是将接口部分和实现部分分离,从而对他们可以更为容易也相对独立的加以改变。而适配器则意味着改变一个已有对象的接口。
2. 装饰者模式增强了其它对象的功能而同时又不改变它的接口,因此它对应程序的透明性比适配器要好,其结果是装饰者支持递归组合,而纯粹使用适配器则是不可能的。
3. 代理模式在不改变它的接口的条件下,为另外一个对象定义了一个代理。
参考:https://github.com/tcorral/Design-Patterns-in-Javascript/blob/master/Adapter/index.html
(38)设计模式之职责链模式
最后更新于:2022-04-01 14:58:40
## 介绍
职责链模式(Chain of responsibility)是使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理他为止。
也就是说,请求以后,从第一个对象开始,链中收到请求的对象要么亲自处理它,要么转发给链中的下一个候选者。提交请求的对象并不明确知道哪一个对象将会处理它——也就是该请求有一个隐式的接受者(implicit receiver)。根据运行时刻,任一候选者都可以响应相应的请求,候选者的数目是任意的,你可以在运行时刻决定哪些候选者参与到链中。
## 正文
对于JavaScript实现,我们可以利用其原型特性来实现职责链模式。
~~~
var NO_TOPIC = -1;
var Topic;
function Handler(s, t) {
this.successor = s || null;
this.topic = t || 0;
}
Handler.prototype = {
handle: function () {
if (this.successor) {
this.successor.handle()
}
},
has: function () {
return this.topic != NO_TOPIC;
}
};
~~~
Handler只是接受2个参数,第一个是继任者(用于将处理请求传下去),第二个是传递层级(可以用于控制在某个层级下是否执行某个操作,也可以不用),Handler原型暴露了一个handle方法,这是实现该模式的重点,先来看看如何使用上述代码。
~~~
var app = new Handler({
handle: function () {
console.log('app handle');
}
}, 3);
var dialog = new Handler(app, 1);
var button = new Handler(dialog, 2);
button.handle();
~~~
改代码通过原型特性,调用代码从button.handle()->dialog.handle()->app.handle()->参数里的handle(),前三个都是调用原型的handle,最后才查找到传入的参数里的handle,然后输出结果,也就是说其实只有最后一层才处理。
那如何做到调用的时候,只让dialog的这个对象进行处理呢?其实可以定义dialog实例对象的handle方法就可以了,但需要在new button的之前来做,代码如下:
~~~
var app = new Handler({
handle: function () {
console.log('app handle');
}
}, 3);
var dialog = new Handler(app, 1);
dialog.handle = function () {
console.log('dialog before ...')
// 这里做具体的处理操作
console.log('dialog after ...')
};
var button = new Handler(dialog, 2);
button.handle();
~~~
该代码的执行结果即时dialog.handle里的处理结果,而不再是给app传入的参数里定义的handle的执行操作。
那能不能做到自身处理完以后,然后在让继任者继续处理呢?答案是肯定的,但是在调用的handle以后,需要利用原型的特性调用如下代码:
~~~
Handler.prototype.handle.call(this);
~~~
该句话的意思说,调用原型的handle方法,来继续调用其继任者(也就是successor )的handle方法,以下代码表现为:button/dialog/app三个对象定义的handle都会执行。
~~~
var app = new Handler({
handle: function () {
console.log('app handle');
}
}, 3);
var dialog = new Handler(app, 1);
dialog.handle = function () {
console.log('dialog before ...')
// 这里做具体的处理操作
Handler.prototype.handle.call(this); //继续往上走
console.log('dialog after ...')
};
var button = new Handler(dialog, 2);
button.handle = function () {
console.log('button before ...')
// 这里做具体的处理操作
Handler.prototype.handle.call(this);
console.log('button after ...')
};
button.handle();
~~~
通过代码的运行结果我们可以看出,如果想先自身处理,然后再调用继任者处理的话,就在末尾执行Handler.prototype.handle.call(this);代码,如果想先处理继任者的代码,就在开头执行Handler.prototype.handle.call(this);代码。
## 总结
职责链模式经常和组合模式一起使用,这样一个构件的父构件可以作为其继任者。
同时,DOM里的事件冒泡机制也和此好像有点类似,比如点击一个按钮以后,如果不阻止冒泡,其click事件将一直向父元素冒泡,利用这个机制也可以处理很多相关的问题,比如本系列设计模式享元模式里的《例1:事件集中管理》的示例代码。
参考代码:https://gist.github.com/1174982
(37)设计模式之享元模式
最后更新于:2022-04-01 14:58:38
## 介绍
享元模式(Flyweight),运行共享技术有效地支持大量细粒度的对象,避免大量拥有相同内容的小类的开销(如耗费内存),使大家共享一个类(元类)。
享元模式可以避免大量非常相似类的开销,在程序设计中,有时需要生产大量细粒度的类实例来表示数据,如果能发现这些实例除了几个参数以外,开销基本相同的 话,就可以大幅度较少需要实例化的类的数量。如果能把那些参数移动到类实例的外面,在方法调用的时候将他们传递进来,就可以通过共享大幅度第减少单个实例 的数目。
那么如果在JavaScript中应用享元模式呢?有两种方式,第一种是应用在数据层上,主要是应用在内存里大量相似的对象上;第二种是应用在DOM层上,享元可以用在中央事件管理器上用来避免给父容器里的每个子元素都附加事件句柄。
## 享元与数据层
Flyweight中有两个重要概念--内部状态intrinsic和外部状态extrinsic之分,内部状态就是在对象里通过内部方法管理,而外部信息可以在通过外部删除或者保存。
说白点,就是先捏一个的原始模型,然后随着不同场合和环境,再产生各具特征的具体模型,很显然,在这里需要产生不同的新对象,所以Flyweight模式中常出现Factory模式,Flyweight的内部状态是用来共享的,Flyweight factory负责维护一个Flyweight pool(模式池)来存放内部状态的对象。
### 使用享元模式
让我们来演示一下如果通过一个类库让系统来管理所有的书籍,每个书籍的元数据暂定为如下内容:
~~~
ID
Title
Author
Genre
Page count
Publisher ID
ISBN
~~~
我们还需要定义每本书被借出去的时间和借书人,以及退书日期和是否可用状态:
~~~
checkoutDate
checkoutMember
dueReturnDate
availability
~~~
因为book对象设置成如下代码,注意该代码还未被优化:
~~~
var Book = function( id, title, author, genre, pageCount,publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate,availability ){
this.id = id;
this.title = title;
this.author = author;
this.genre = genre;
this.pageCount = pageCount;
this.publisherID = publisherID;
this.ISBN = ISBN;
this.checkoutDate = checkoutDate;
this.checkoutMember = checkoutMember;
this.dueReturnDate = dueReturnDate;
this.availability = availability;
};
Book.prototype = {
getTitle:function(){
return this.title;
},
getAuthor: function(){
return this.author;
},
getISBN: function(){
return this.ISBN;
},
/*其它get方法在这里就不显示了*/
// 更新借出状态
updateCheckoutStatus: function(bookID, newStatus, checkoutDate,checkoutMember, newReturnDate){
this.id = bookID;
this.availability = newStatus;
this.checkoutDate = checkoutDate;
this.checkoutMember = checkoutMember;
this.dueReturnDate = newReturnDate;
},
//续借
extendCheckoutPeriod: function(bookID, newReturnDate){
this.id = bookID;
this.dueReturnDate = newReturnDate;
},
//是否到期
isPastDue: function(bookID){
var currentDate = new Date();
return currentDate.getTime() > Date.parse(this.dueReturnDate);
}
};
~~~
程序刚开始可能没问题,但是随着时间的增加,图书可能大批量增加,并且每种图书都有不同的版本和数量,你将会发现系统变得越来越慢。几千个book对象在内存里可想而知,我们需要用享元模式来优化。
我们可以将数据分成内部和外部两种数据,和book对象相关的数据(title, author 等)可以归结为内部属性,而(checkoutMember, dueReturnDate等)可以归结为外部属性。这样,如下代码就可以在同一本书里共享同一个对象了,因为不管谁借的书,只要书是同一本书,基本信息是一样的:
~~~
/*享元模式优化代码*/
var Book = function(title, author, genre, pageCount, publisherID, ISBN){
this.title = title;
this.author = author;
this.genre = genre;
this.pageCount = pageCount;
this.publisherID = publisherID;
this.ISBN = ISBN;
};
~~~
### 定义基本工厂
让我们来定义一个基本工厂,用来检查之前是否创建该book的对象,如果有就返回,没有就重新创建并存储以便后面可以继续访问,这确保我们为每一种书只创建一个对象:
~~~
/* Book工厂 单例 */
var BookFactory = (function(){
var existingBooks = {};
return{
createBook: function(title, author, genre,pageCount,publisherID,ISBN){
/*查找之前是否创建*/
var existingBook = existingBooks[ISBN];
if(existingBook){
return existingBook;
}else{
/* 如果没有,就创建一个,然后保存*/
var book = new Book(title, author, genre,pageCount,publisherID,ISBN);
existingBooks[ISBN] = book;
return book;
}
}
}
});
~~~
### 管理外部状态
外部状态,相对就简单了,除了我们封装好的book,其它都需要在这里管理:
~~~
/*BookRecordManager 借书管理类 单例*/
var BookRecordManager = (function(){
var bookRecordDatabase = {};
return{
/*添加借书记录*/
addBookRecord: function(id, title, author, genre,pageCount,publisherID,ISBN, checkoutDate, checkoutMember, dueReturnDate, availability){
var book = bookFactory.createBook(title, author, genre,pageCount,publisherID,ISBN);
bookRecordDatabase[id] ={
checkoutMember: checkoutMember,
checkoutDate: checkoutDate,
dueReturnDate: dueReturnDate,
availability: availability,
book: book;
};
},
updateCheckoutStatus: function(bookID, newStatus, checkoutDate, checkoutMember, newReturnDate){
var record = bookRecordDatabase[bookID];
record.availability = newStatus;
record.checkoutDate = checkoutDate;
record.checkoutMember = checkoutMember;
record.dueReturnDate = newReturnDate;
},
extendCheckoutPeriod: function(bookID, newReturnDate){
bookRecordDatabase[bookID].dueReturnDate = newReturnDate;
},
isPastDue: function(bookID){
var currentDate = new Date();
return currentDate.getTime() > Date.parse(bookRecordDatabase[bookID].dueReturnDate);
}
};
});
~~~
通过这种方式,我们做到了将同一种图书的相同信息保存在一个bookmanager对象里,而且只保存一份;相比之前的代码,就可以发现节约了很多内存。
## 享元模式与DOM
关于DOM的事件冒泡,在这里就不多说了,相信大家都已经知道了,我们举两个例子。
### 例1:事件集中管理
举例来说,如果我们又很多相似类型的元素或者结构(比如菜单,或者ul里的多个li)都需要监控他的click事件的话,那就需要多每个元素进行事件绑定,如果元素有非常非常多,那性能就可想而知了,而结合冒泡的知识,任何一个子元素有事件触发的话,那触发以后事件将冒泡到上一级元素,所以利用这个特性,我们可以使用享元模式,我们可以对这些相似元素的父级元素进行事件监控,然后再判断里面哪个子元素有事件触发了,再进行进一步的操作。
在这里我们结合一下jQuery的bind/unbind方法来举例。
**HTML**:
~~~
<div id="container">
<div class="toggle" href="#">更多信息 (地址)
<span class="info">
这里是更多信息
</span></div>
<div class="toggle" href="#">更多信息 (地图)
<span class="info">
<iframe src="http://www.map-generator.net/extmap.php?name=London&address=london%2C%20england&width=500...gt;"</iframe>
</span>
</div>
</div>
~~~
**JavaScript**:
~~~
stateManager = {
fly: function(){
var self = this;
$('#container').unbind().bind("click", function(e){
var target = $(e.originalTarget || e.srcElement);
// 判断是哪一个子元素
if(target.is("div.toggle")){
self.handleClick(target);
}
});
},
handleClick: function(elem){
elem.find('span').toggle('slow');
}
});
~~~
### 例2:应用享元模式提升性能
另外一个例子,依然和jQuery有关,一般我们在事件的回调函数里使用元素对象是会后,经常会用到$(this)这种形式,其实它重复创建了新对象,因为本身回调函数里的this已经是DOM元素自身了,我们必要必要使用如下这样的代码:
~~~
$('div').bind('click', function(){
console.log('You clicked: ' + $(this).attr('id'));
});
// 上面的代码,要避免使用,避免再次对DOM元素进行生成jQuery对象,因为这里可以直接使用DOM元素自身了。
$('div').bind('click', function(){
console.log('You clicked: ' + this.id);
});
~~~
其实,如果非要用$(this)这样的形式,我们也可以实现自己版本的单实例模式,比如我们来实现一个jQuery.signle(this)这样的函数以便返回DOM元素自身:
~~~
jQuery.single = (function(o){
var collection = jQuery([1]);
return function(element) {
// 将元素放到集合里
collection[0] = element;
// 返回集合
return collection;
};
});
~~~
使用方法:
~~~
$('div').bind('click', function(){
var html = jQuery.single(this).next().html();
console.log(html);
});
~~~
这样,就是原样返回DOM元素自身了,而且不进行jQuery对象的创建。
## 总结
Flyweight模式是一个提高程序效率和性能的模式,会大大加快程序的运行速度.应用场合很多:比如你要从一个数据库中读取一系列字符串,这些字符串中有许多是重复的,那么我们可以将这些字符串储存在Flyweight池(pool)中。
如果一个应用程序使用了大量的对象,而这些大量的对象造成了很大的存储开心时就应该考虑使用享元模式;还有就是对象的大多数状态可以外部状态,如果删除对象的外部状态,那么就可以用相对较少的共享对象取代很多组对象,此时可以考虑使用享元模式。
参考地址:http://www.addyosmani.com/resources/essentialjsdesignpatterns/book/#detailflyweight
(36)设计模式之中介者模式
最后更新于:2022-04-01 14:58:36
## 介绍
中介者模式(Mediator),用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
主要内容来自:http://www.addyosmani.com/resources/essentialjsdesignpatterns/book/#mediatorpatternjavascript
## 正文
软件开发中,中介者是一个行为设计模式,通过提供一个统一的接口让系统的不同部分进行通信。一般,如果系统有很多子模块需要直接沟通,都要创建一个中央控制点让其各模块通过该中央控制点进行交互。中介者模式可以让这些子模块不需要直接沟通,而达到进行解耦的目的。
打个比方,平时常见的机场交通控制系统,塔台就是中介者,它控制着飞机(子模块)的起飞和降落,因为所有的沟通都是从飞机向塔台汇报来完成的,而不是飞机之前相互沟通。中央控制系统就是该系统的关键,也就是软件设计中扮演的中介者角色。
我们先用伪代码来理解一下:
~~~
// 如下代码是伪代码,请不要过分在意代码
// 这里app命名空间就相当于扮演中介者的角色
var app = app || {};
// 通过app中介者来进行Ajax请求
app.sendRequest = function ( options ) {
return $.ajax($.extend({}, options);
}
// 请求URL以后,展示View
app.populateView = function( url, view ){
$.when(app.sendRequest({url: url, method: 'GET'})
.then(function(){
//显示内容
});
}
// 清空内容
app.resetView = function( view ){
view.html('');
}
~~~
在JavaScript里,中介者非常常见,相当于观察者模式上的消息Bus,只不过不像观察者那样通过调用pub/sub的形式来实现,而是通过中介者统一来管理,让我们在观察者的基础上来给出一个例子:
~~~
var mediator = (function () {
// 订阅一个事件,并且提供一个事件触发以后的回调函数
var subscribe = function (channel, fn) {
if (!mediator.channels[channel]) mediator.channels[channel] = [];
mediator.channels[channel].push({ context: this, callback: fn });
return this;
},
// 广播事件
publish = function (channel) {
if (!mediator.channels[channel]) return false;
var args = Array.prototype.slice.call(arguments, 1);
for (var i = 0, l = mediator.channels[channel].length; i < l; i++) {
var subscription = mediator.channels[channel][i];
subscription.callback.apply(subscription.context, args);
}
return this;
};
return {
channels: {},
publish: publish,
subscribe: subscribe,
installTo: function (obj) {
obj.subscribe = subscribe;
obj.publish = publish;
}
};
} ());
~~~
调用代码,相对就简单了:
~~~
(function (Mediator) {
function initialize() {
// 默认值
mediator.name = "dudu";
// 订阅一个事件nameChange
// 回调函数显示修改前后的信息
mediator.subscribe('nameChange', function (arg) {
console.log(this.name);
this.name = arg;
console.log(this.name);
});
}
function updateName() {
// 广播触发事件,参数为新数据
mediator.publish('nameChange', 'tom'); // dudu, tom
}
initialize(); // 初始化
updateName(); // 调用
})(mediator);
~~~
### 中介者和观察者
到这里,大家可能迷糊了,中介者和观察者貌似差不多,有什么不同呢?其实是有点类似,但是我们来看看具体的描述:
观察者模式,没有封装约束的单个对象,相反,观察者Observer和具体类Subject是一起配合来维护约束的,沟通是通过多个观察者和多个具体类来交互的:每个具体类通常包含多个观察者,而有时候具体类里的一个观察者也是另一个观察者的具体类。
而中介者模式所做的不是简单的分发,却是扮演着维护这些约束的职责。
### 中介者和外观模式
很多人可能也比较迷糊中介者和外观模式的区别,他们都是对现有各模块进行抽象,但有一些微妙的区别。
中介者所做的是在模块之间进行通信,是多向的,但外观模式只是为某一个模块或系统定义简单的接口而不添加额外的功能。系统中的其它模块和外观模式这个概念没有直接联系,可以认为是单向性。
## 完整的例子
再给出一个完整的例子:
~~~
<!doctype html>
<html lang="en">
<head>
<title>JavaScript Patterns</title>
<meta charset="utf-8">
</head>
<body>
<div id="results"></div>
<script>
function Player(name) {
this.points = 0;
this.name = name;
}
Player.prototype.play = function () {
this.points += 1;
mediator.played();
};
var scoreboard = {
// 显示内容的容器
element: document.getElementById('results'),
// 更新分数显示
update: function (score) {
var i, msg = '';
for (i in score) {
if (score.hasOwnProperty(i)) {
msg += '<p><strong>' + i + '<\/strong>: ';
msg += score[i];
msg += '<\/p>';
}
}
this.element.innerHTML = msg;
}
};
var mediator = {
// 所有的player
players: {},
// 初始化
setup: function () {
var players = this.players;
players.home = new Player('Home');
players.guest = new Player('Guest');
},
// play以后,更新分数
played: function () {
var players = this.players,
score = {
Home: players.home.points,
Guest: players.guest.points
};
scoreboard.update(score);
},
// 处理用户按键交互
keypress: function (e) {
e = e || window.event; // IE
if (e.which === 49) { // 数字键 "1"
mediator.players.home.play();
return;
}
if (e.which === 48) { // 数字键 "0"
mediator.players.guest.play();
return;
}
}
};
// go!
mediator.setup();
window.onkeypress = mediator.keypress;
// 30秒以后结束
setTimeout(function () {
window.onkeypress = null;
console.log('Game over!');
}, 30000);
</script>
</body>
</html>
~~~
## 总结
中介者模式一般应用于一组对象已定义良好但是以复杂的方式进行通信的场合,一般情况下,中介者模式很容易在系统中使用,但也容易在系统里误用,当系统出现了多对多交互复杂的对象群时,先不要急于使用中介者模式,而是要思考一下是不是系统设计有问题。
另外,由于中介者模式把交互复杂性变成了中介者本身的复杂性,所以说中介者对象会比其它任何对象都复杂。
(35)设计模式之迭代器模式
最后更新于:2022-04-01 14:58:34
## 介绍
迭代器模式(Iterator):提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象内部表示。
迭代器的几个特点是:
1. 访问一个聚合对象的内容而无需暴露它的内部表示。
2. 为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。
3. 遍历的同时更改迭代器所在的集合结构可能会导致问题(比如C#的foreach里不允许修改item)。
## 正文
一般的迭代,我们至少要有2个方法,hasNext()和Next(),这样才做做到遍历所有对象,我们先给出一个例子:
~~~
var agg = (function () {
var index = 0,
data = [1, 2, 3, 4, 5],
length = data.length;
return {
next: function () {
var element;
if (!this.hasNext()) {
return null;
}
element = data[index];
index = index + 2;
return element;
},
hasNext: function () {
return index < length;
},
rewind: function () {
index = 0;
},
current: function () {
return data[index];
}
};
} ());
~~~
使用方法和平时C#里的方式是一样的:
~~~
// 迭代的结果是:1,3,5
while (agg.hasNext()) {
console.log(agg.next());
}
~~~
当然,你也可以通过额外的方法来重置数据,然后再继续其它操作:
~~~
// 重置
agg.rewind();
console.log(agg.current()); // 1
~~~
## jQuery应用例子
jQuery里一个非常有名的迭代器就是$.each方法,通过each我们可以传入额外的function,然后来对所有的item项进行迭代操作,例如:
~~~
$.each(['dudu', 'dudu', '酸奶小妹', '那个MM'], function (index, value) {
console.log(index + ': ' + value);
});
//或者
$('li').each(function (index) {
console.log(index + ': ' + $(this).text());
});
~~~
## 总结
迭代器的使用场景是:对于集合内部结果常常变化各异,我们不想暴露其内部结构的话,但又响让客户代码透明底访问其中的元素,这种情况下我们可以使用迭代器模式。
(34)设计模式之命令模式
最后更新于:2022-04-01 14:58:31
## 介绍
命令模式(Command)的定义是:用于将一个请求封装成一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及执行可撤销的操作。也就是说改模式旨在将函数的调用、请求和操作封装成一个单一的对象,然后对这个对象进行一系列的处理。此外,可以通过调用实现具体函数的对象来解耦命令对象与接收对象。
## 正文
我们来通过车辆购买程序来展示这个模式,首先定义车辆购买的具体操作类:
~~~
$(function () {
var CarManager = {
// 请求信息
requestInfo: function (model, id) {
return 'The information for ' + model +
' with ID ' + id + ' is foobar';
},
// 购买汽车
buyVehicle: function (model, id) {
return 'You have successfully purchased Item '
+ id + ', a ' + model;
},
// 组织view
arrangeViewing: function (model, id) {
return 'You have successfully booked a viewing of '
+ model + ' ( ' + id + ' ) ';
}
};
})();
~~~
来看一下上述代码,通过调用函数来简单执行manager的命令,然而在一些情况下,我们并不想直接调用对象内部的方法。这样会增加对象与对象间的依赖。现在我们来扩展一下这个CarManager 使其能够接受任何来自包括model和car ID 的CarManager对象的处理请求。根据命令模式的定义,我们希望实现如下这种功能的调用:
~~~
CarManager.execute({ commandType: "buyVehicle", operand1: 'Ford Escort', operand2: '453543' });
~~~
根据这样的需求,我们可以这样啦实现CarManager.execute方法:
~~~
CarManager.execute = function (command) {
return CarManager[command.request](command.model, command.carID);
};
~~~
改造以后,调用就简单多了,如下调用都可以实现(当然有些异常细节还是需要再完善一下的):
~~~
CarManager.execute({ request: "arrangeViewing", model: 'Ferrari', carID: '145523' });
CarManager.execute({ request: "requestInfo", model: 'Ford Mondeo', carID: '543434' });
CarManager.execute({ request: "requestInfo", model: 'Ford Escort', carID: '543434' });
CarManager.execute({ request: "buyVehicle", model: 'Ford Escort', carID: '543434' });
~~~
## 总结
命令模式比较容易设计一个命令队列,在需求的情况下比较容易将命令计入日志,并且允许接受请求的一方决定是否需要调用,而且可以实现对请求的撤销和重设,而且由于新增的具体类不影响其他的类,所以很容易实现。
但敏捷开发原则告诉我们,不要为代码添加基于猜测的、实际不需要的功能,如果不清楚一个系统是否需要命令模式,一般就不要着急去实现它,事实上,在需求的时通过重构实现这个模式并不困难,只有在真正需求如撤销、恢复操作等功能时,把原来的代码重构为命令模式才有意义。
(33)设计模式之策略模式
最后更新于:2022-04-01 14:58:29
## 介绍
策略模式定义了算法家族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化不会影响到使用算法的客户。
## 正文
在理解策略模式之前,我们先来一个例子,一般情况下,如果我们要做数据合法性验证,很多时候都是按照swith语句来判断,但是这就带来几个问题,首先如果增加需求的话,我们还要再次修改这段代码以增加逻辑,而且在进行单元测试的时候也会越来越复杂,代码如下:
~~~
validator = {
validate: function (value, type) {
switch (type) {
case 'isNonEmpty ':
{
return true; // NonEmpty 验证结果
}
case 'isNumber ':
{
return true; // Number 验证结果
break;
}
case 'isAlphaNum ':
{
return true; // AlphaNum 验证结果
}
default:
{
return true;
}
}
}
};
// 测试
alert(validator.validate("123", "isNonEmpty"));
~~~
那如何来避免上述代码中的问题呢,根据策略模式,我们可以将相同的工作代码单独封装成不同的类,然后通过统一的策略处理类来处理,OK,我们先来定义策略处理类,代码如下:
~~~
var validator = {
// 所有可以的验证规则处理类存放的地方,后面会单独定义
types: {},
// 验证类型所对应的错误消息
messages: [],
// 当然需要使用的验证类型
config: {},
// 暴露的公开验证方法
// 传入的参数是 key => value对
validate: function (data) {
var i, msg, type, checker, result_ok;
// 清空所有的错误信息
this.messages = [];
for (i in data) {
if (data.hasOwnProperty(i)) {
type = this.config[i]; // 根据key查询是否有存在的验证规则
checker = this.types[type]; // 获取验证规则的验证类
if (!type) {
continue; // 如果验证规则不存在,则不处理
}
if (!checker) { // 如果验证规则类不存在,抛出异常
throw {
name: "ValidationError",
message: "No handler to validate type " + type
};
}
result_ok = checker.validate(data[i]); // 使用查到到的单个验证类进行验证
if (!result_ok) {
msg = "Invalid value for *" + i + "*, " + checker.instructions;
this.messages.push(msg);
}
}
}
return this.hasErrors();
},
// helper
hasErrors: function () {
return this.messages.length !== 0;
}
};
~~~
然后剩下的工作,就是定义types里存放的各种验证类了,我们这里只举几个例子:
~~~
// 验证给定的值是否不为空
validator.types.isNonEmpty = {
validate: function (value) {
return value !== "";
},
instructions: "传入的值不能为空"
};
// 验证给定的值是否是数字
validator.types.isNumber = {
validate: function (value) {
return !isNaN(value);
},
instructions: "传入的值只能是合法的数字,例如:1, 3.14 or 2010"
};
// 验证给定的值是否只是字母或数字
validator.types.isAlphaNum = {
validate: function (value) {
return !/[^a-z0-9]/i.test(value);
},
instructions: "传入的值只能保护字母和数字,不能包含特殊字符"
};
~~~
使用的时候,我们首先要定义需要验证的数据集合,然后还需要定义每种数据需要验证的规则类型,代码如下:
~~~
var data = {
first_name: "Tom",
last_name: "Xu",
age: "unknown",
username: "TomXu"
};
validator.config = {
first_name: 'isNonEmpty',
age: 'isNumber',
username: 'isAlphaNum'
};
~~~
最后,获取验证结果的代码就简单了:
~~~
validator.validate(data);
if (validator.hasErrors()) {
console.log(validator.messages.join("\n"));
}
~~~
## 总结
策略模式定义了一系列算法,从概念上来说,所有的这些算法都是做相同的事情,只是实现不同,他可以以相同的方式调用所有的方法,减少了各种算法类与使用算法类之间的耦合。
从另外一个层面上来说,单独定义算法类,也方便了单元测试,因为可以通过自己的算法进行单独测试。
实践中,不仅可以封装算法,也可以用来封装几乎任何类型的规则,是要在分析过程中需要在不同时间应用不同的业务规则,就可以考虑是要策略模式来处理各种变化。
(32)设计模式之观察者模式
最后更新于:2022-04-01 14:58:27
## 介绍
观察者模式又叫发布订阅模式(Publish/Subscribe),它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。
使用观察者模式的好处:
1. 支持简单的广播通信,自动通知所有已经订阅过的对象。
2. 页面载入后目标对象很容易与观察者存在一种动态关联,增加了灵活性。
3. 目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用。
## 正文(版本一)
JS里对观察者模式的实现是通过回调来实现的,我们来先定义一个pubsub对象,其内部包含了3个方法:订阅、退订、发布。
~~~
var pubsub = {};
(function (q) {
var topics = {}, // 回调函数存放的数组
subUid = -1;
// 发布方法
q.publish = function (topic, args) {
if (!topics[topic]) {
return false;
}
setTimeout(function () {
var subscribers = topics[topic],
len = subscribers ? subscribers.length : 0;
while (len--) {
subscribers[len].func(topic, args);
}
}, 0);
return true;
};
//订阅方法
q.subscribe = function (topic, func) {
if (!topics[topic]) {
topics[topic] = [];
}
var token = (++subUid).toString();
topics[topic].push({
token: token,
func: func
});
return token;
};
//退订方法
q.unsubscribe = function (token) {
for (var m in topics) {
if (topics[m]) {
for (var i = 0, j = topics[m].length; i < j; i++) {
if (topics[m][i].token === token) {
topics[m].splice(i, 1);
return token;
}
}
}
}
return false;
};
} (pubsub));
~~~
使用方式如下:
~~~
//来,订阅一个
pubsub.subscribe('example1', function (topics, data) {
console.log(topics + ": " + data);
});
//发布通知
pubsub.publish('example1', 'hello world!');
pubsub.publish('example1', ['test', 'a', 'b', 'c']);
pubsub.publish('example1', [{ 'color': 'blue' }, { 'text': 'hello'}]);
~~~
怎么样?用起来是不是很爽?但是这种方式有个问题,就是没办法退订订阅,要退订的话必须指定退订的名称,所以我们再来一个版本:
~~~
//将订阅赋值给一个变量,以便退订
var testSubscription = pubsub.subscribe('example1', function (topics, data) {
console.log(topics + ": " + data);
});
//发布通知
pubsub.publish('example1', 'hello world!');
pubsub.publish('example1', ['test', 'a', 'b', 'c']);
pubsub.publish('example1', [{ 'color': 'blue' }, { 'text': 'hello'}]);
//退订
setTimeout(function () {
pubsub.unsubscribe(testSubscription);
}, 0);
//再发布一次,验证一下是否还能够输出信息
pubsub.publish('example1', 'hello again! (this will fail)');
~~~
## 版本二
我们也可以利用原型的特性实现一个观察者模式,代码如下:
~~~
function Observer() {
this.fns = [];
}
Observer.prototype = {
subscribe: function (fn) {
this.fns.push(fn);
},
unsubscribe: function (fn) {
this.fns = this.fns.filter(
function (el) {
if (el !== fn) {
return el;
}
}
);
},
update: function (o, thisObj) {
var scope = thisObj || window;
this.fns.forEach(
function (el) {
el.call(scope, o);
}
);
}
};
//测试
var o = new Observer;
var f1 = function (data) {
console.log('Robbin: ' + data + ', 赶紧干活了!');
};
var f2 = function (data) {
console.log('Randall: ' + data + ', 找他加点工资去!');
};
o.subscribe(f1);
o.subscribe(f2);
o.update("Tom回来了!")
//退订f1
o.unsubscribe(f1);
//再来验证
o.update("Tom回来了!");
~~~
如果提示找不到filter或者forEach函数,可能是因为你的浏览器还不够新,暂时不支持新标准的函数,你可以使用如下方式自己定义:
~~~
if (!Array.prototype.forEach) {
Array.prototype.forEach = function (fn, thisObj) {
var scope = thisObj || window;
for (var i = 0, j = this.length; i < j; ++i) {
fn.call(scope, this[i], i, this);
}
};
}
if (!Array.prototype.filter) {
Array.prototype.filter = function (fn, thisObj) {
var scope = thisObj || window;
var a = [];
for (var i = 0, j = this.length; i < j; ++i) {
if (!fn.call(scope, this[i], i, this)) {
continue;
}
a.push(this[i]);
}
return a;
};
}
~~~
## 版本三
如果想让多个对象都具有观察者发布订阅的功能,我们可以定义一个通用的函数,然后将该函数的功能应用到需要观察者功能的对象上,代码如下:
~~~
//通用代码
var observer = {
//订阅
addSubscriber: function (callback) {
this.subscribers[this.subscribers.length] = callback;
},
//退订
removeSubscriber: function (callback) {
for (var i = 0; i this.subscribers.length; i++) {
if (this.subscribers[i] === callback) {
delete (this.subscribers[i]);
}
}
},
//发布
publish: function (what) {
for (var i = 0; i this.subscribers.length; i++) {
if (typeof this.subscribers[i] === 'function') {
this.subscribers[i](what);
}
}
},
// 将对象o具有观察者功能
make: function (o) {
for (var i in this) {
o[i] = this[i];
o.subscribers = [];
}
}
};
~~~
然后订阅2个对象blogger和user,使用observer.make方法将这2个对象具有观察者功能,代码如下:
~~~
var blogger = {
recommend: function (id) {
var msg = 'dudu 推荐了的帖子:' + id;
this.publish(msg);
}
};
var user = {
vote: function (id) {
var msg = '有人投票了!ID=' + id;
this.publish(msg);
}
};
observer.make(blogger);
observer.make(user);
~~~
使用方法就比较简单了,订阅不同的回调函数,以便可以注册到不同的观察者对象里(也可以同时注册到多个观察者对象里):
~~~
var tom = {
read: function (what) {
console.log('Tom看到了如下信息:' + what)
}
};
var mm = {
show: function (what) {
console.log('mm看到了如下信息:' + what)
}
};
// 订阅
blogger.addSubscriber(tom.read);
blogger.addSubscriber(mm.show);
blogger.recommend(123); //调用发布
//退订
blogger.removeSubscriber(mm.show);
blogger.recommend(456); //调用发布
//另外一个对象的订阅
user.addSubscriber(mm.show);
user.vote(789); //调用发布
~~~
## jQuery版本
根据jQuery1.7版新增的on/off功能,我们也可以定义jQuery版的观察者:
~~~
(function ($) {
var o = $({});
$.subscribe = function () {
o.on.apply(o, arguments);
};
$.unsubscribe = function () {
o.off.apply(o, arguments);
};
$.publish = function () {
o.trigger.apply(o, arguments);
};
} (jQuery));
~~~
调用方法比上面3个版本都简单:
~~~
//回调函数
function handle(e, a, b, c) {
// `e`是事件对象,不需要关注
console.log(a + b + c);
};
//订阅
$.subscribe("/some/topic", handle);
//发布
$.publish("/some/topic", ["a", "b", "c"]); // 输出abc
$.unsubscribe("/some/topic", handle); // 退订
//订阅
$.subscribe("/some/topic", function (e, a, b, c) {
console.log(a + b + c);
});
$.publish("/some/topic", ["a", "b", "c"]); // 输出abc
//退订(退订使用的是/some/topic名称,而不是回调函数哦,和版本一的例子不一样
$.unsubscribe("/some/topic");
~~~
可以看到,他的订阅和退订使用的是字符串名称,而不是回调函数名称,所以即便传入的是匿名函数,我们也是可以退订的。
## 总结
观察者的使用场合就是:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。
总的来说,观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。
参考地址:
https://github.com/shichuan/javascript-patterns/blob/master/design-patterns/observer.html
http://www.addyosmani.com/resources/essentialjsdesignpatterns/book/#observerpatternjavascript
https://gist.github.com/661855