JavaScript设计模式
徐涛 译
人民邮电出版社
北京
设计模式是解决软件设计中常见问题的可复用方案。学习任何编程语言,设计模式都是一个令人兴奋和极具吸引力的话题。
本书是JavaScript设计模式的学习指南。全书分为14章。首先介绍了什么是模式、模式的结构、类别、模式的分类、如何编写模式等等;然后,集中介绍了很多流行的设计模式在JavaScript中的应用,包括Module(模块)模式、Observer(观察者)模式、Facade(外观)模式和Mediator(中介者)模式;最后,还探讨了模块化的JavaScript模式、jQuery及其插件中的设计模式。
本书适合专业的 Web 开发人员和前端工程师阅读。通过阅读本书,他们将能够提高对设计模式的认识,并学会如何将设计模式应用到JavaScript编程语言中。
设计模式是解决软件设计中常见问题的可复用方案。探索任何编程语言时,设计模式都是一个令人兴奋和极具吸引力的话题。
原因之一是:设计模式是许多先前开发人员总结出的经验,我们可以借鉴这些经验进行编程,以确保能够以优化的方式组织代码,为我们解决棘手的问题提供参考。
设计模式还是我们用来描述解决方案的常用词汇。当我们想要向其他人表述一种以代码形式构建解决方案的方式时,描述设计模式比描述语法和语义要简单得多。
在本书中,我们将探讨JavaScript编程语言中经典的与现代的设计模式的应用。
目标读者
本书的目标读者是专业开发人员,希望提高对设计模式的认识,并学会如何将设计模式应用到JavaScript编程语言中。
本书对有些概念(闭包、原型继承)只做了一些基本介绍,以便于理解。如果想要进一步了解这些概念,下面列出了一些推荐书目,方便大家阅读。
如果想要学习如何编写出美观、结构化和组织良好的代码,相信这本书就是为你而准备的。
致谢
本书中提到的很多设计模式是根据我的个人经验总结出来的,还有很多设计模式是JavaScript 社区前辈总结出来的经验。本作品是众多开发人员的经验结合产物。与斯托扬·斯蒂凡诺夫为防止致谢名单中出现错漏的明智做法(在《JavaScript模式》中)类似,我列出了致谢名单以及本书参考的文献。
如果参考文献中遗漏了任何著作或链接,我深表歉意。如果您与我联系,我一定会及时将您的著作或链接添加到参考文献中。
其他读物
虽然本书也面向初学者和中级开发人员,但也需要读者对JavaScript有基本的了解。如果想要深入了解该语言,我很乐意向您推荐以下书目。
• 《JavaScript权威指南》,作者:David Flanagan
• 《JavaScript编程精解》,作者:Marijn Haverbeke
• 《JavaScript模式》,作者:Stoyan Stefanov
• 《编写可维护的 JavaScript》1,作者:Nicholas Zakas
注释:1 编者注:《编写可维护的JavaScript》已由人民邮电出版社出版(ISBN9787115310088,定价55元, 2013年3月)。
• 《JavaScript语言精粹》,作者:Douglas Crockford
本书约定
本书使用下列排版约定。
斜体(Italic)
表示专业词汇、链接(URL)、文件名和文件扩展名。
等宽字体(Constant width)
表示广义上的计算机编码,它们包括变量或函数名、数据库、数据类型、环境变量、语句和关键字。
等宽粗体(Constant width bold)
表示应该由用户按照字面引入的命令或其他文本。
等宽斜体(Constant width italic)
表示应该由用户替换或取决于上下文的值。
这个图标表示提示、建议或一般说明。
这个图标表示警告或提醒。
代码示例
这本书是为了帮助你做好工作。一般来说,你可以在程序和文档中使用本书的代码。你无须联系我们获取许可。例如,使用来自本书的几段代码写一个程序是不需要许可的。出售和散布O’ Reilly书中用例的光盘(CD-ROM)是需要许可的。通过引用本书用例和代码来回答问题是不需要许可的。把本书中大量的用例代码并入到你的产品文档中是需要许可的。
我们赞赏但不强求注明信息来源。一条信息来源通常包括标题、作者、出版者和国际标准书号(ISBN)。例如:“Learning JavaScript Design Patterns by Addy Osmani (O’Reilly). Copyright 2012 Addy Osmani, 978-1-449-33181-8”。
如果你感到对示例代码的使用超出了正当引用或这里给出的许可范围,请随时通过permissions@oreilly.com联系我们。
Safari®在线图书
Safari在线图书(Safari Books Online)是一家按需服务的数字图书馆,提供来自领先出版商的技术类和商业类专业参考书目和视频。
专业技术人员、软件开发人员、Web设计师、商业和创意专家将Safari Books Online作为他们研究、解决问题、学习和认证培训的主要资源。
Safari Books Online为组织、政府机构和个人提供一系列的产品组合和定价计划。用户可以在一个来自各个出版社的可完全搜索的数据库中访问成千上万的书籍、培训视频和正式出版前的手稿,这些出版社包括:O’Reilly Media、Prentice Hall Professional、Addison-Wesley Professional、微软出版社、Sams、Que、Peachpit Press、Focal Press、Cisco Press、John Wiley & Sons、Syngress、Morgan Kaufmann、IBM Redbooks、Packt、Adobe Press、FT Press、Apress、Manning、New Riders、McGraw-Hill、Jones & Bartlett、Course Technology等等。欲获得有关Safari Books Online的更多信息,请在线访问我们。
联系我们
关于本书的建议和疑问,可以与下面的出版社联系:
美国:
O’Reilly Media, Inc.
1005 Gravenstein Highway North
Sebastopol, CA 95472
中国:
北京市西城区西直门南大街2号成铭大厦C座807室(100035)
奥莱利技术咨询(北京)有限公司
我们将关于本书的勘误表,例子以及其他信息列在本书的网页上,网页地址是:
http://www.oreilly.com/catalog/9781449302146
如果要评论本书或者咨询关于本书的技术问题,请发邮件到:
bookquestions@oreilly.com
想了解关于O’Reilly图书、课程、会议和新闻的更多信息,请访问以下网站:
http://www.oreilly.com.cn
http://www.oreilly.com
致谢
我要感谢热情的技术审稿人帮助我检查和改进本书,这些审稿人还包括整个社区里的同行。他们的知识和热情对这一工作做出了不可思议的贡献。这些技术审稿人的官方tweet和博客也经常为我提供一些想法和灵感,我竭诚向大家推荐他们的tweet和博客。
• Nicholas Zakas (http://nczonline.net, @slicknet(http://twitter.com/slicknet))
• Andrée Hansson (http://andreehansson.se, @peolanha(http://twitter.com/peolanha))
• Luke Smith (http://lucassmith.name, @ls_n(http://twitter.com/ls_n))
• Eric Ferraiuolo (http://ericf.me/, @ericf (http://ericf.me/))
• Peter Michaux (http://michaux.ca, @petermichaux)
• Alex Sexton (http://alexsexton.com, @slexaxton(http://twitter.com/slexaxton))
我还要感谢丽贝卡·墨菲(http://rebeccamurphey.com, @rmurphey (http://twitter.com/rmurphey))给我写作的灵感,更重要的是,本书在GitHub和O’Reilly出版公司都可以获取。
最后,我要感谢我的妻子埃莉,她非常支持我的出书工作。
在本章中,我们将探索一些经典与现代设计模式的JavaScript实现。
开发人员通常想知道他们是否应该在工作中使用一种“理想”的模式或模式集。这个问题没有明确的唯一答案,我们研究的每个脚本和 Web应用程序可能都有它自己的个性化需求,我们需要思考模式的哪些方面能够为实现提供实际价值。
例如,一些项目可能会受益于观察者模式提供的解耦好处(这可以减少应用程序的某些部分对彼此的依赖度),而有些项目可能只是因为太小,而根本无需考虑解耦。
也就是说,一旦我们牢牢掌握了设计模式和与它们最为相配的具体问题,那么将它们集成到我们的应用程序架构中就会变得更加容易。
在本节中将要探索的模式包括:
• Constructor(构造器)模式;
• Module(模块)模式;
• Revealing Module(揭示模块)模式;
• Singleton(单例)模式;
• Observer(观察者)模式;
• Mediator(中介者)模式;
• Prototype(原型)模式;
• Command(命令)模式;
• Facade(外观)模式;
• Factory(工厂)模式;
• Mixin(混入)模式;
• Decorator(装饰者)模式;
• Flyweight(享元)模式。
在经典面向对象编程语言中,Constructor是一种在内存已分配给该对象的情况下,用于初始化新创建对象的特殊方法。在JavaScript中,几乎所有东西都是对象,我们通常最感兴趣的是object构造器。
Object构造器用于创建特定类型的对象——准备好对象以备使用,同时接收构造器可以使用的参数,以在第一次创建对象时,设置成员属性和方法的值(见图9-1)。
图9-1 Constructor(构造器)模式
});
在JavaScript中,创建新对象的两种常用方法如下所示:
//下面每种方式都将创建一个新的空对象
var newObject = {};
// object构造器的简洁记法
var newObject = new Object();
在 Object 构造器为特定的值创建对象封装,或者没有传递值时,它将创建一个空对象并返回它。
有四种方法可以将键值赋值给一个对象:
// ECMAScript 3兼容方式
// 1.“点”语法
// 设置属性
newObject.someKey = "Hello World";
// 获取属性
var key = newObject.someKey;
// 2.中括号语法
// 设置属性
newObject["someKey"] = "Hello World";
// 获取属性
var key = newObject["someKey"];
//只适用ECMAScript 5的方式
// 更多信息查看:http://kangax.github.com/es5-compat-table/
// 3. Object.defineProperty
// 设置属性
Object.defineProperty(newObject, "someKey", {
value: "for more control of the property's behavior",
writable: true,
enumerable: true,
configurable: true
// 如果上面的看着麻烦,可以使用下面的简便方式
var defineProp = function (obj, key, value) {
config.value = value;
Object.defineProperty(obj, key, config);
};
// 使用上述方式,先创建一个空的person对象
var person = Object.create(null);
// 然后设置各个属性
defineProp(person, "car", "Delorean");
defineProp(person, "dateOfBirth", "1981");
defineProp(person, "hasBeard", false);
//4.Object.defineProperties
// 设置属性
Object.defineProperties(newObject, {
"someKey": {
value: "Hello World",
writable: true
},
"anotherKey": {
value: "Foo bar",
writable: false
}
});
// 可以用1和2中获取属性的方式获取3和4方式中的属性
正如我们将在本书稍后看到的,这些方法甚至可以用于继承,如下所示:
// 用法
// 创建赛车司机driver对象,继承于person对象
var driver = Object.create(person);
// 为driver设置一些属性
defineProp(driver, "topSpeed", "100mph");
// 获取继承的属性
console.log(driver.dateOfBirth);
// 获取我们设置的100mph的属性
console.log(driver.topSpeed);
正如我们在前面所看到的,JavaScript 不支持类的概念,但它确实支持与对象一起用的特殊 constructor(构造器)函数。通过在构造器前面加 new关键字,告诉JavaScript像使用构造器一样实例化一个新对象,并且对象成员由该函数定义。
在构造器内,关键字this引用新创建的对象。回顾对象创建,基本的构造器看起来可能是这样的:
function Car(model, year, miles) {
this.model = model;
this.year = year;
this.miles = miles;
this.toString = function () {
return this.model + " has done " + this.miles + " miles";
};
}
// 用法
// 可以创建car的新实例
var civic = new Car("Honda Civic", 2009, 20000);
var mondeo = new Car("Ford Mondeo", 2010, 5000);
// 打开浏览器控制台,查看这些对象上调用的toString()方法的输出
console.log(civic.toString());
console.log(mondeo.toString());
上面是一个简单的构造器模式版本,但它确实有一些问题。其中一个问题是,它使继承变得困难,另一个问题是,toString()这样的函数是为每个使用Car构造器创建的新对象而分别重新定义的。这不是最理想的,因为这种函数应该在所有的Car类型实例之间共享。
值得庆幸的是,因为有很多ES3和ES5兼容替代方法能够用于创建对象,所以很容易解决这个限制问题。
JavaScript中有一个名为prototype的属性。调用JavaScript构造器创建一个对象后,新对象就会具有构造器原型的所有属性。通过这种方式,可以创建多个Car对象,并访问相同的原型。因此我们可以扩展原始示例,如下所示:
function Car(model, year, miles) {
this.model = model;
this.year = year;
this.miles = miles;
}
// 注意这里我们使用Object.prototype.newMethod而不是Object.prototype是为了避免重新定义prototype对象
Car.prototype.toString = function () {
return this.model + " has done " + this.miles + " miles";
};
//用法:
var civic = new Car("Honda Civic", 2009, 20000);
var mondeo = new Car("Ford Mondeo", 2010, 5000);
console.log(civic.toString());
console.log(mondeo.toString());
现在toString()的单一实例就能够在所有Car对象之间共享。
模块是任何强大应用程序架构中不可或缺的一部分,它通常能够帮助我们清晰地分离和组织项目中的代码单元。
在JavaScript中,有几种用于实现模块的方法,包括:
• 对象字面量表示法
• Module模式
• AMD模块
• CommonJS模块
• ECMAScript Harmony模块
我们稍后将在本书第 11 章探索后三种方法。Module 模式在某种程度上是基于对象字面量,因此首先重新认识对象字面量是有意义的。
在对象字面量表示法中,一个对象被描述为一组包含在大括号({})中、以逗号分隔的name/value 对。对象内的名称可以是字符串或标识符,后面跟着一个冒号。对象中最后的一个name/value对的后面不用加逗号,如果加逗号将会导致出错。
var myObjectLiteral = {
variableKey: variableValue,
functionKey: function () {
// ...
}
};
对象字面量不需要使用 new 运算符进行实例化,但不能用在一个语句的开头,因为开始的可能被解读为一个块的开始。在对象的外部,新成员可以使用如下赋值语句添加到对象字面量上,如:myModule.property = "someValue";
下面我们可以看到一个更完整的示例:使用对象字面量表示法定义的模块:
var myModule = {
myProperty: "someValue",
// 对象字面量可以包含属性和方法
// 例如,可以声明模块的配置对象
myConfig: {
useCaching: true,
language: "en"
},
// 基本方法
myMethod: function () {
console.log("Where in the world is Paul Irish today?");
},
// 根据当前配置输出信息
myMethod2: function () {
console.log("Caching is:" + (this.myConfig.useCaching) ? "enabled" : "disabled");
},
// 重写当前的配置
myMethod3: function (newConfig) {
if (typeof newConfig === "object") {
this.myConfig = newConfig;
console.log(this.myConfig.language);
}
}
};
//输出:Where in the world is Paul Irish today?
myModule.myMethod();
//输出:enabled
myModule.myMethod2();
//输出:fr
myModule.myMethod3({
language: "fr",
useCaching: false
});
使用对象字面量有助于封装和组织代码,如果想进一步了解有关对象字面量的信息,丽贝卡·墨菲曾对这一主题进行了深入解析,可阅读其文章进行了解(地址:http://rmurphey.com/blog/2009/10/15/using-objects-to-organize-your-code/)。
也就是说,如果我们选择了这种技术,我们可能同样也对Module模式感兴趣。它仍然使用对象字面量,但只是作为一个作用域函数的返回值。
Module模式最初被定义为一种在传统软件工程中为类提供私有和公有封装的方法。
在JavaScript中,Module模式用于进一步模拟类的概念,通过这种方式,能够使一个单独的对象拥有公有/私有方法和变量,从而屏蔽来自全局作用域的特殊部分。产生的结果是:函数名与在页面上其他脚本定义的函数冲突的可能性降低(见图9-2)。
图9-2 Module模式
9.2.2.1 私有
Module模式使用闭包封装“私有”状态和组织。它提供了一种包装混合公有/私有方法和变量的方式,防止其泄露至全局作用域,并与别的开发人员的接口发生冲突。通过该模式,只需返回一个公有API,而其他的一切则都维持在私有闭包里。
这为我们提供了一个屏蔽处理底层事件逻辑的整洁解决方案,同时只暴露一个接口供应用程序的其他部分使用。该模式除了返回一个对象而不是一个函数之外,非常类似于一个立即调用的函数表达式1。
注释:1 IIFE(http: //benalman. com/news/2010/11/immediately-invoked- function- expression/)。请参阅第200页的“命名空间模式”了解更多信息。
应该指出的是,在 JavaScript 中没有真正意义上的“私有”,因为不像有些传统语言,JavaScript没有访问修饰符。从技术上来说,我们不能称变量为公有或是私有,因此我们需使用函数作用域来模拟这个概念。在Module模式内,由于闭包的存在,声明的变量和方法只在该模式内部可用。但在返回对象上定义的变量和方法,则对外部使用者都是可用的。
9.2.2.2 历史
从历史的角度来看,Module模式最初是在2003年由多人共同开发出来的,其中包括理查德•康佛德(http:// groups.google. com/group/ comp.lang. javascript/ msg/9f58bd11bd67d937)。后来由道格拉斯·克劳克福德在其讲座中推广开来。除此之外,如果你曾体验过雅虎的 YUI 库,它的一些特性看起来可能相当熟悉,原因是在创建它们的组件时,Module模式对YUI有很大的影响。
9.2.2.3 示例
让我们通过创建一个自包含的模块来看一下Module模式的实现。
var testModule = (function () {
var counter = 0;
return {
incrementCounter: function () {
return ++counter;
},
resetCounter: function () {
console.log("counter value prior to reset: " + counter);
counter = 0;
}
};
})();
//用法:
//增加计数器
testModule.incrementCounter();
// 检查计数器值并重置
//输出:1
testModule.resetCounter();
在这里,代码的其他部分无法直接读取incrementCounter()或resetCounter()。counter变量实际上是完全与全局作用域隔离的,因此它表现得就像是一个私有变量,它的存在被局限于模块的闭包内,因此唯一能够访问其作用域的代码就是这两个函数。上述方法进行了有效的命名空间设置,所以在测试代码中,所有的调用都需要加上前缀(如:“testModule”)。
使用Module模式时,可能会觉得它可以用来定义一个简单的模板来入门使用。下面是一个包含命名空间、公有和私有变量的Module模式:
var myNamespace = (function () {
// 私有计数器变量
var myPrivateVar = 0;
// 记录所有参数的私有函数
var myPrivateMethod = function (foo) {
console.log(foo);
};
return {
// 公有变量
myPublicVar: "foo",
// 调用私有变量和方法的公有函数
myPublicFunction: function (bar) {
// 增加私有计数器值
myPrivateVar++;
// 传入bar调用私有方法
myPrivateMethod(bar);
}
};
})();
来看另一个示例,我们可以看到一个使用这种模式实现的购物车。模块本身是完全自包含在一个被称为basketModule的全局变量中。模块中的basket数组是私有的,因此应用程序的其他部分无法直接读取它。它只与模块的闭包一起存在,所以能够访问它的方法都是那些能够访问其作用域的方法(即addItem()、getItem()等)。
var basketModule = (function () {
// 私有
var basket = [];
function doSomethingPrivate() {
//...
}
function doSomethingElsePrivate() {
//...
}
// 返回一个暴露出的公有对象
return {
// 添加item到购物车
addItem: function (values) {
basket.push(values);
},
// 获取购物车里的item数
getItemCount: function () {
return basket.length;
},
// 私有函数的公有形式别名
doSomething: doSomethingPrivate,
// 获取购物车里所有item的价格总值
getTotal: function () {
var itemCount = this.getItemCount(),
total = 0;
while (itemCount--) {
total += basket[itemCount].price;
}
return total;
}
};
})();
在该模块中,可能已经注意到返回了一个object。它会被自动赋值给basketModule,以便我们可以与它交互,如下所示:
// basketModule返回了一个拥有公用API的对象
basketModule.addItem({
item: "bread",
price: 0.5
});
basketModule.addItem({
item: "butter",
price: 0.3
});
//输出: 2
console.log(basketModule.getItemCount());
//输出: 0.8
console.log(basketModule.getTotal());
// 不过,下面的代码不会正常工作
// 输出:undefined
// 因为basket自身没有暴露在公有的API里
console.log(basketModule.basket);
// 下面的代码也不会正常工作,因为basket只存在于basketModule闭包的作用域里,而不是存在于返回的公有对象里
console.log(basket);
上述方法在basketModule内部都属于有效的命名空间设置。
请注意上面的 basket 模块中的作用域函数是如何包裹在所有函数的周围,然后调用并立即存储返回值。这有很多优点,包括:
• 只有我们的模块才能享有拥有私有函数的自由。因为它们不会暴露于页面的其余部分(只会暴露于我们输出的API),我们认为它们是真正的私有。
• 鉴于函数往往已声明并命名,在试图找到有哪些函数抛出异常时,这将使得在调试器中显示调用堆栈变得更容易。
• 正如 T.J.Crowder在过去所指出的,根据环境,它还可以让我们返回不同的函数。在过去,我曾看到开发人员使用它来执行UA测试,从而针对IE在他们的模块内提供一个代码路径,但我们现在可以很容易地选择特征检测来实现类似的目的。
9.2.3.1 引入混入
模式的这种变化演示了全局变量(如:jQuery、Underscore)如何作为参数传递给模块的匿名函数。这允许我们引入它们,并按照我们所希望的为它们取个本地别名。
// 全局模块
var myModule = (function (jQ, _) {
function privateMethod1() {
jQ(".container").html("test");
}
function privateMethod2() {
console.log(_.min([10, 5, 100, 2, 1000]));
}
return {
publicMethod: function () {
privateMethod1();
}
};
// 引入jQuery和Underscore
})(jQuery, _));
myModule.publicMethod();
9.2.3.2 引出
下一个变化允许我们声明全局变量,而不需实现它们,并可以同样地支持上一个示例中的全局引入的概念。
// 全局模块
var myModule = (function (){
// 模块对象
var module = {},
privateVariable = "Hello World";
function privateMethod() {
// ...
}
module.publicProperty = "Foobar";
module.publicMethod = function () {
console.log(privateVariable);
};
return module;
})();
9.2.3.3 工具包和特定框架的Module模式实现
Dojo。提供了一种和对象一起用的便利方法 dojo.setObject()。其第一个参数是用点号分割的字符串,如myObj.parent.child,它在parent对象中引用一个称为child的属性, parent对象是在myObj内部定义。我们可以使用setObject()设置子级的值(比如属性等),如果中间对象不存在的话,也可以通过点号分割将中间的字符作为中间对象进行创建。
例如,如果要将basket.core声明为store名称空间的对象,可以采用传统的方法来实现,如下所示:
var store = window.store || {};
if (!store["basket"]) {
store.basket = {};
}
if (!store.basket["core"]) {
store.basket.core = {};
}
store.basket.core = {
// ...剩余的逻辑
};
或者,使用Dojo 1.7(AMD兼容的版本)和上述方法,如下所示:
require(["dojo/_base/customStore"], function (store) {
//使用 dojo.setObject()
store.setObject("basket.core", (function () {
var basket = [];
function privateMethod() {
console.log(basket);
}
return {
publicMethod: function (){
privateMethod();
}
};
})());
});
欲了解更多有关 dojo.setObject()的信息,请参阅官方文档( http://dojotoolkit.org/reference-guide/1.7/dojo/setObject.html)。
ExtJS。对比那些使用Sencha ExtJS的人,你的运气会好一点,因为官方文档包含了一些示例(http:// www.sencha. com/learn/legacy/Tutorial:Application_Layout_ for_Beginners),演示了EXTJS框架下如何正确使用Module模式。
在这里,我们可以看到这样的一个示例:如何定义一个名称空间,然后填充一个包含私有和公有 API的模块。除了一些语义差异,它与如何在纯 JavaScript 中实现Module模式十分相近。
// 创建命名空间
Ext.namespace("myNameSpace");
// 创建应用程序
myNameSpace.app = function () {
// 这里不要访问DOM,因为元素还不存在
// 私有变量
var btn1,
privVar1 = 11;
// 私有函数
var btn1Handler = function (button, event) {
console.log("privVar1", privVar1);
console.log("this.btn1Text=" + this.btn1Text);
};
// 公有对象
return {
// 公有属性,例如要转化的字符
btn1Text: "Button 1",
// 公有方法
init: function () {
if (Ext.Ext2) {
btn1 = new Ext.Button({
renderTo: "btn1-ct",
text: this.btn1Text,
handler: btn1Handler
});
} else {
btn1 = new Ext.Button("btn1-ct", {
text: this.btn1Text,
handler: btn1Handler
});
}
}
};
}();
YUI。同样,在使用 YUI3构建应用程序时,我们也可以实现 Module模式。下面的示例在很大程度上基于由Eric Miraglia提出的原始YUI Module模式实现,但它又与纯JavaScript版本截然不同。
Y.namespace("store.basket") = (function () {
var myPrivateVar, myPrivateMethod;
// 私有变量:
myPrivateVar = "I can be accessed only within Y.store.basket.";
// 私有方法:
myPrivateMethod = function () {
Y.log("I can be accessed only from within YAHOO.store.basket");
}
return {
myPublicProperty: "I'm a public property.",
myPublicMethod: function () {
Y.log("I'm a public method.");
// 在basket里,可以访问到私有变量和方法
Y.log(myPrivateVar);
Y.log(myPrivateMethod());
// myPublicMethod的原始作用域是store,所以可以使用this来访问公有成员
Y.log(this.myPublicProperty);
}
};
})();
jQuery。有许多方式可以将非jQuery插件代码包装在Module模式中。如果模块之间有多个共性,Ben Cherry之前建议过一种实现,在模块模式内部模块定义附件使用函数包装器。
在下面的示例中,定义了library函数,它声明一个新库,并在创建新库(即模块)时将init函数自动绑定到document.ready。
function library(module) {
$(function () {
if (module.init) {
module.init();
}
});
return module;
}
var myLibrary = library(function () {
return {
init: function () {
// module implementation
// 模块实现
}
};
})();
9.2.3.4 优点
我们已经了解了单例模式如何有用,但为什么Module模式是一个好的选择呢?首先,相比真正封装的思想,它对于很多拥有面向对象背景的开发人员来说更加整洁,至少是从JavaScript的角度。
其次,它支持私有数据,因此,在Module模式中,代码的公有(public)部分能够接触私有部分,然而外界无法接触类的私有部分。
9.2.3.5 缺点
Module 模式的缺点是:由于我们访问公有和私有成员的方式不同,当我们想改变可见性时,实际上我们必须要修改每一个曾经使用过该成员的地方。
我们也无法访问那些之后在方法里添加的私有成员。也就是说,在很多情况下,如果正确使用,Module模式仍然是相当有用的,肯定可以改进应用程序的结构。
其他缺点包括:无法为私有成员创建自动化单元测试,bug需要修正补丁时会增加额外的复杂性。为私有方法打补丁是不可能的。相反,我们必须覆盖所有与有bug的私有方法进行交互的公有方法。另外开发人员也无法轻易地扩展私有方法,所以要记住,私有方法并不像它们最初显现出来的那么灵活。
欲进一步阅读有关Module模式的信息,请参阅 Ben Cherry更为深入的精彩文章(http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth)。
现在对Module模式应该更加熟悉了,让我们来看一个稍有改进的版本——Christian Heilmann的Revealing Module模式。
Revealing Module模式的产生是因为Heilmann很不满意这种状况:当他想从另一个方法调用一个公有方法或访问公有变量时,必须要重复主对象的名称。他也不喜欢使用Module模式时,必须要切换到对象字面量表示法来让某种方法变成公有方法。
他努力的结果就是创建了一个更新的模式,能够在私有范围内简单定义所有的函数和变量,并返回一个匿名对象,它拥有指向私有函数的指针,该函数是他希望展示为公有的方法。
有关如何使用Revealing Module模式的示例如下所示:
var myRevealingModule = function () {
var privateVar = "Ben Cherry",
publicVar = "Hey there!";
function privateFunction() {
console.log("Name:" + privateVar);
}
function publicSetName(strName) {
privateName = strName;
}
function publicGetName() {
privateFunction();
}
// 将暴露的公有指针指向到私有函数和属性上
return {
setName: publicSetName,
greeting: publicVar,
getName: publicGetName
};
}();
myRevealingModule.setName("Paul Kinlan");
如果你喜欢,该模式也可以用于展示拥有更具体命名方案的私有函数和属性:
var myRevealingModule = function () {
var privateCounter = 0;
function privateFunction() {
privateCounter++;
}
function publicFunction() {
publicIncrement();
}
function publicIncrement() {
privateFunction();
}
function publicGetCount() {
return privateCounter;
}
// 将暴露的公有指针指向到私有函数和属性上
return {
start: publicFunction,
increment: publicIncrement,
count: publicGetCount
};
}();
myRevealingModule.start();
该模式可以使脚本语法更加一致。在模块代码底部,它也会很容易指出哪些函数和变量可以被公开访问,从而改善可读性。
该模式的一个缺点是:如果一个私有函数引用一个公有函数,在需要打补丁时,公有函数是不能被覆盖的。这是因为私有函数将继续引用私有实现,该模式并不适用于公有成员,只适用于函数。
引用私有变量的公有对象成员也遵守无补丁规则。
正因为如此,采用Revealing Module模式创建的模块可能比那些采用原始Module模式创建的模块更加脆弱,所以在使用时应该特别小心。
Singleton(单例)模式被熟知的原因是因为它限制了类的实例化次数只能一次。从经典意义上来说,Singleton模式,在该实例不存在的情况下,可以通过一个方法创建一个类来实现创建类的新实例;如果实例已经存在,它会简单返回该对象的引用。Singleton不同于静态类(或对象),因为我们可以推迟它们的初始化,这通常是因为它们需要一些信息,而这些信息在初始化期间可能无法获得。对于没有察觉到之前的引用的代码,它们不会提供方便检索的方法。这是因为它既不是对象,也不是由一个 Singleton 返回的“类”;它是一个结构。思考一下闭包变量为何实际上并不是闭包,而提供闭包的函数作用域是闭包。在JavaScript中,Singleton充当共享资源命名空间,从全局命名空间中隔离出代码实现,从而为函数提供单一访问点。我们可以像如下这样实现一个Singleton:
var mySingleton = (function () {
// 实例保持了Singleton的一个引用
var instance;
function init() {
// Singleton
// 私有方法和变量
function privateMethod() {
console.log("I am private");
}
var privateVariable = "Im also private";
var privateRandomNumber = Math.random();
return {
// 公有方法和变量
publicMethod: function () {
console.log("The public can see me!");
},
publicProperty: "I am also public",
getRandomNumber: function () {
return privateRandomNumber;
}
};
};
return {
// 获取Singleton的实例,如果存在就返回,不存在就创建新实例
getInstance: function () {
if (!instance) {
instance = init();
}
return instance;
}
};
})();
var myBadSingleton = (function () {
// 实例保持了Singleton的一个引用
var instance;
function init() {
// Singleton
var privateRandomNumber = Math.random();
return {
getRandomNumber: function () {
return privateRandomNumber;
}
};
};
return {
// 每次都创建新实例
getInstance: function () {
instance = init();
return instance;
}
};
})();
var singleA = mySingleton.getInstance();
var singleB = mySingleton.getInstance();
console.log(singleA.getRandomNumber() === singleB.getRandomNumber()); // true
var badSingleA = myBadSingleton.getInstance();
var badSingleB = myBadSingleton.getInstance();
console.log(badSingleA.getRandomNumber() !== badSingleB.getRandomNumber()); // true
是什么使Singleton成为实例的全局访问入口(通常通过MySingleton.getinstance()),因为我们没有(至少在静态语言中)直接调用新的 MySingleton()。然而,这在JavaScript中是可能的。在“四人组”所著的书中,有关Singleton模式适用性的描述如下。
• 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。
• 该唯一的实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。
这些观点另外关联到一个场景,这里我们可能需要如下这样的代码:
mySingleton.getInstance = function () {
if (this._instance == null) {
if (isFoo()) {
this._instance = new FooSingleton();
} else {
this._instance = new BasicSingleton();
}
}
return this._instance;
};
在这里,getInstance变得有点像Factory(工厂)方法,当访问它时,我们不需要更新代码中的每个访问点。FooSingleton(上面)将是一个BasicSingleton的子类,并将实现相同的接口。
为何延迟执行对于Singleton很重要?
在C++中,Singleton负责隔绝动态初始化顺序的不可预知性,将控制权归还给程序员。
值得注意的是类的静态实例(对象)和Singleton之间的区别:当Singleton可以作为一个静态的实例实现时,它也可以延迟构建,直到需要使用静态实例时,无需使用资源或内存。
如果我们有一个可以直接被初始化的静态对象,需要确保执行代码的顺序总是相同的(例如:在初始化期间objCar需要objWheel的情况),当我们有大量的源文件时,它并不能伸缩。
Singleton 和静态对象都是有用的,但是我们不应当以同样的方式过度使用它们,也不应过度使用其他模式。
在实践中,当在系统中确实需要一个对象来协调其他对象时,Singleton 模式是很有用的。在这里,大家可以看到在这个上下文中模式的使用:
var SingletonTester = (function () {
// options:包含singleton所需配置信息的对象
// e.g var options = { name: "test", pointX: 5};
function Singleton( options ) {
// 如果未提供options,则设置为空对象
options = options || {};
// 为singleton设置一些属性
this.name = "SingletonTester";
this.pointX = options.pointX || 6;
this.pointY = options.pointY || 10;
}
// 实例持有者
var instance;
// 静态变量和方法的模拟
var _static = {
name: "SingletonTester",
// 获取实例的方法,返回singleton对象的singleton实例
getInstance: function( options ) {
if( instance === undefined ) {
instance = new Singleton( options );
}
return instance;
}
};
return _static;
})();
var singletonTest = SingletonTester.getInstance({
pointX: 5
});
// 记录pointX的输出以便验证
//输出: 5
console.log( singletonTest.pointX );
Singleton很有使用价值,通常当发现在JavaScript中需要它的时候,则表示我们可能需要重新评估我们的设计。
Singleton 的存在往往表明系统中的模块要么是系统紧密耦合,要么是其逻辑过于分散在代码库的多个部分。由于一系列的问题:从隐藏的依赖到创建多个实例的难度、底层依赖的难度等等,Singleton的测试会更加困难。
Miller Medeiros之前曾推荐过这篇优秀的文章(http://www.ibm.com/developerworks/ebservices/library/co-single/index.html),以进一步了解Singleton和它的各种问题。他还建议阅读有关这篇文章( http://misko.hevery.com/008/10/21/dependency-njection-myth-reference-passing/)的评论,包括讨论 Singleton 如何能够加强紧密耦合。我很高兴再次推荐这些内容,因为这两种内容都对这种模式提出了许多重要的问题。
Observer(观察者)是一种设计模式,其中,一个对象(称为subject)维持一系列依赖于它(观察者)的对象,将有关状态的任何变更自动通知给它们(见图9-3)。
当一个目标需要告诉观察者发生了什么有趣的事情,它会向观察者广播一个通知(可以包括与通知主题相关的特定数据)。
图9-3 Observer模式
当我们不再希望某个特定的观察者获得其注册目标发出的改变通知时,该目标可以将它从观察者列表中删除。
参考之前发布的设计模式定义通常是很有用的,它与语言无关,以便久而久之使其使用和优势变得更有意义。“四人组”所著书籍(《设计模式:可复用面向对象软件的基础》)中提供的Observer模式的定义是:
“一个或多个观察者对目标的状态感兴趣,它们通过将自己依附在目标对象上以便注册所感兴趣的内容。目标状态发生改变并且观察者可能对这些改变感兴趣,就会发送一个通知消息,调用每个观察者的更新方法。当观察者不再对目标状态感兴趣时,它们可以简单地将自己从中分离。”
现在我们可以扩展所学到的内容来使用以下组件实现Observer模式:
Subject(目标)
维护一系列的观察者,方便添加或删除观察者
Observer(观察者)
为那些在目标状态发生改变时需获得通知的对象提供一个更新接口
ConcreteSubject(具体目标)
状态发生改变时,向Observer发出通知,储存ConcreteObserver的状态
ConcreteObserver(具体观察者)
存储一个指向ConcreteSubject的引用,实现Observer的更新接口,以使自身状态
与目标的状态保持一致
首先,让我们来模拟一个目标可能拥有的一系列依赖Observer:
function ObserverList(){
this.observerList = [];
}
ObserverList.prototype.Add = function(obj){
return this.observerList.push(obj);
};
ObserverList.prototype.Empty = function(){
this.observerList = [];
};
ObserverList.prototype.Count = function(){
return this.observerList.length;
};
ObserverList.prototype.Get = function (index){
if (index > -1 && index <this.observerList.length){
return this.observerList[index];
}
};
ObserverList.prototype.Insert = function(obj, index){
var pointer = -1;
if (index === 0){
this.observerList.unshift(obj);
pointer = index;
} elseif (index === this.observerList.length){
this.observerList.push(obj);
pointer = index;
}
return pointer;
};
ObserverList.prototype.IndexOf = function(obj, startIndex){
var i = startIndex, pointer = -1;
while (i <this.observerList.length){
if (this.observerList[i] === obj){
pointer = i;
}
i++;
}
return pointer;
};
ObserverList.prototype.RemoveIndexAt() = function (index){
if (index === 0){
this.observerList.shift();
} else if(index === this.observerList.length - 1){
this.observerList.pop();
}
eLSe{
Tlmis observerList spLicecind
}
};
// 使用extension扩展对象
function extend(obj, extension){
for (var key in obj){
extension[key] = obj[key];
}
}
接下来,让我们来模拟目标(Subject)和在观察者列表上添加、删除或通知观察者的能力。
function Subject() {
this.observers = new ObserverList();
}
Subject.prototype.AddObserver = function(observer){
this.observers.Add(observer);
};
Subject.prototype.RemoveObserver = function (observer){
this.observers.RemoveIndexAt(this.observers.IndexOf(observer, 0));
};
Subject.prototype.Notify = function (context){
var observerCount = this.observers.Count();
for (var i = 0; i < observerCount; i++){
this.observers.Get(i).Update(context);
}
};
然后定义一个框架来创建新的 Observer。这里的 Update 功能将在后面的自定义行为部分进一步介绍。
// The Observer
function Observer(){
this.Update = function (){
// ...
};
}
在使用上述Observer组件的样例应用程序中,定义如下:
• 用于向页面添加新可见checkbox的按钮
• 控制checkbox,将充当一个目标,通知其他checkbox需要进行检查
• 用于添加新checkbox的容器
然后定义ConcreteSubject和ConcreteObserver处理程序,以便向页面添加新观察者,并实现更新界面。关于这些组件在示例上下文中的作用注释,请参阅下面的内容。
如下是HTML代码:
<button id="addNewObserver">Add New Observer checkbox</button>
<input id="mainCheckbox" type="checkbox"/>
<div id="observersContainer"></div>
如下是样例脚本:
// 引用DOM元素
var controlCheckbox = document.getElementById("mainCheckbox"),
addBtn = document.getElementById("addNewObserver"),
container = document.getElementById("observersContainer");
//具体目标 Concrete Subject
// 利用Subject扩展controlCheckbox
extend(new Subject(), controlCheckbox);
// 点击checkbox会触发通知到观察者上
controlCheckbox["onclick"] = new Function("controlCheckbox.Notify controlCheckbox.checked)");
addBtn["onclick"] = AddNewObserver;
//具体观察者 Concrete Observer
function AddNewObserver() {
// 创建需要添加的新checkbox
var check = document.createElement("input");
check.type = "checkbox";
// 利用Observer类扩展checkbox
extend(new Observer(), check);
// 重写自定义更新行为
check.Update = function (value) {
this.checked = value;
};
// 为主subject的观察者列表添加新的观察者
controlCheckbox.AddObserver(check);
// 将观察者附件到容器上
container.appendChild(check);
}
在本例中,我们了解了如何实现和使用Observer模式,包含目标(Subject)、观察者(Observer)、具体目标(ConcreteSubject)和具体观察者(ConcreteObserver)的概念。
通常在JavaScript里,注重Observer模式是很有用的,我们会发现它一般使用一个被称为Publish/Subscribe(发布/订阅)模式的变量来实现。虽然这些模式非常相似,但是它们之间的几点区别也是值得注意的。
Observer模式要求希望接收到主题通知的观察者(或对象)必须订阅内容改变的事件,如图9-4所示。
图9-4 Publish/Subscribe
Publish/Subscribe 模式使用了一个主题/事件通道,这个通道介于希望接收到通知(订阅者)的对象和激活事件的对象(发布者)之间。该事件系统允许代码定义应用程序的特定事件,这些事件可以传递自定义参数,自定义参数包含订阅者所需的值。
其目的是避免订阅者和发布者之间产生依赖关系。
这与 Observer 模式不同,因为它允许任何订阅者执行适当的事件处理程序来注册和接收发布者发出的通知。
下面这个示例说明了如果有 publish()、subscribe()和 unsubscribe()的功能实现,是如何使用Publish/Subscribe模式的:
// 非常简单的mail处理程序
// 接收到的消息数量
var mailCounter = 0;
// 初始化订阅,名称是inbox/newMessage
// 呈现消息预览
var subscriber1 = subscribe("inbox/newMessage", function (topic, data) {
// debug模式记录topic
console.log("A new message was received: ", topic);
// 使用从目标subject传递过来的data,一般呈现消息预览
$(".messageSender").html(data.sender);
$(".messagePreview").html(data.body);
});
// 另外一个订阅,使用同样的data数据用于不同的任务
// 通过发布者更新所接收消息的数量
var subscriber2 = subscribe("inbox/newMessage", function (topic, data) {
$('.newMessageCounter').html(mailCounter++);
});
publish("inbox/newMessage", [{
sender: "hello@google.com",
body: "Hey there! How are you doing today?"
}]);
// 之后可以通过unsubscribe来取消订阅
// unsubscribe( subscriber1, );
// unsubscribe( subscriber2 );
这里的中心思想是促进松散耦合。通过订阅另一个对象的特定任务或活动,当任务/活动发生时获得通知,而不是单个对象直接调用其他对象的方法。
Observer 模式和 Publish/Subscribe 模式鼓励我们努力思考应用程序不同部分之间的关系。它们也帮助我们识别包含直接关系的层,并且可以用目标集和观察者进行替换。实际上可以用于将应用程序分解为更小、更松散耦合的块,以改进代码管理和潜在的复用。
使用 Observer 模式背后的另一个动机是我们需要在哪里维护相关对象之间的一致性,而无需使类紧密耦合。例如,当一个对象需要能够通知其他对象,而无需在这些对象方面做假设时。
在使用任何一种模式时,动态关系可以在观察者和目标之间存在。这提供了很大的灵活性,当应用程序的不同部分紧密耦合时,这可不是很容易实现的。
虽然它可能不一直是所有问题的最佳解决方案,但这些模式仍是用于设计解耦性系统的最佳工具之一,应该视为所有JavaScript开发人员工具中的一个重要工具。
因此,这些模式的某些问题实际上源于它的主要好处。在 Publish/Subscribe 中,通过从订阅者中解耦发布者,它有时会很难保证应用程序的特定部分按照我们期望的运行。
例如,发布者可能会假设:一个或多个订阅者在监听它们。倘若我们假设订阅者需要记录或输出一些与应用程序处理有关的错误。如果订阅者执行日志崩溃了(或出于某种原因无法正常运行),由于系统的解耦特性,发布者就不会看到这一点。
这种模式的另一个缺点是:订阅者非常无视彼此的存在,并对变换发布者产生的成本视而不见。由于订阅者和发布者之间的动态关系,很难跟踪依赖更新。
Publish/Subscribe 非常适用于 JavaScript 生态系统,这主要是因为在其核心, ECMAScript实现是由事件驱动的。在浏览器环境下尤其如此,因为DOM将事件是作为脚本编程的主要交互API。
也就是说,在实现代码里,无论是ECMAScript还是DOM都不会提供核心对象或方法来创建自定义事件系统(或许除了 DOM3 CustomEvent 以外,它被绑定到DOM,因此一般是无用的)。
幸运的是,流行的 JavaScript库,比如Dojo、jQuery(自定义事件)和 YUI都拥有一些实用程序可以很容易实现Publish/Subscribe系统。如下是一些有关示例:
// Publish
// jQuery: $(obj).trigger("channel", [arg1, arg2, arg3]);
$( el ).trigger( "/login", [{username:"test", userData:"test"}] );
// Dojo: dojo.publish("channel", [arg1, arg2, arg3] );
dojo.publish( "/login", [{username:"test", userData:"test"}] );
// YUI: el.publish("channel", [arg1, arg2, arg3]);
el.publish( "/login", {username:"test", userData:"test"} );
// Subscribe
// jQuery: $(obj).on( "channel", [data], fn );
$( el ).on( "/login", function( event ){...} );
// Dojo: dojo.subscribe( "channel", fn);
var handle = dojo.subscribe( "/login", function(data){..} );
// YUI: el.on("channel", handler);
el.on( "/login", function( data ){...} );
// Unsubscribe
// jQuery: $(obj).off( "channel" );
$( el ).off( "/login" );
// Dojo: dojo.unsubscribe( handle );
dojo.unsubscribe( handle );
// YUI: el.detach("channel");
el.detach( "/login" );
对于那些希望使用采用纯 JavaScript(或其他库)的 Publish/Subscribe模式的人来说,AmplifyJS(http://amplifyjs.com/)包含了一个整洁、与库无关的实现,它可用于任何库或工具包。值得一看的类似语言有 Radio.js (http://radio.uxder.com/)、PubSubJS(https://github.com/mroderick/PubSubJS)、或Peter Higgins所写的Pure JS PubSub(https://github.com/phiggins42/bloody-jquery-plugins/blob/)。
jQuery开发人员更是有相当多的其他选择,可以选择使用众多完整实现中的一个,从 Peter Higgins的 jQuery插件到 Ben Alman在 GitHub上的优化过的 Pub/Sub jQuerygist。如下仅列出几个相关链接。
• Ben Alman的Pub/Subgist
https://gist.github.com/661855(recommended)(推荐)
• Rick Waldron上述所说的 jQuery核心风格
https://gist.github.com/705311
• Peter Higgins的插件
http://github.com/phiggins42/bloody-jquery-plugins/blob/master/pubsub.js.
• AppendTo在AmplifyJS里实现的Pub/Sub
http://amplifyjs.com
• Ben Truyman的 gist
https://gist.github.com/826794
所以我们现在能够正确了解有多少个纯JavaScript实现的Observer模式了,让我们来看一下在GitHub上发布的一个被称为 pubsubz(https:// github.com/ addyosmani/ pubsubz)的项目中一个极简版本的Publish/Subscribe I。它展示了订阅和发布的核心概念,以及取消订阅的概念。
我选择了在这个代码的基础上展示我们的示例,因为它非常接近我们所期望的JavaScript版经典Observer模式所包括的方法命名和实现方式。
9.5.4.1 Publish/Subscribe实现
var pubsub = {};
(function (q) {
var topics = {},
subUid = -1;
// 发布或广播事件,包含特定的topic名称和参数(比如传递的数据)
q.publish = function (topic, args) {
if (!topics[topic]) {
return false;
}
var subscribers = topics[topic],
len = subscribers ? subscribers.length : 0;
while (len--) {
subscribers[len].func(topic, args);
}
return this;
};
// 通过特定的名称和回调函数订阅事件,topic/event触发时执行事件
q.subscribe = function (topic, func) {
if (!topics[topic]) {
topics[topic] = [];
}
var token = (++subUid).toString();
topics[topic].push({
token: token,
func: func
});
return token;
};
//基于订阅上的标记引用,通过特定topic取消订阅
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 this;
};
}(pubsub));
9.5.4.2 使用上述实现
我们现在可以使用该实现来发布和订阅感兴趣的活动,如下所示(示例9-1):
示例9-1 使用上述实现
// 另一个简单的消息处理程序
// 简单的消息记录器记录所有通过订阅者接收到的主题(topic)和数据
var messageLogger = function ( topics, data ) {
console.log( "Logging: " + topics + ": " + data );
};
// 订阅者监听订阅的topic,一旦该topic广播一个通知,订阅者就调用回调函数
var subscription = pubsub.subscribe( "inbox/newMessage", messageLogger );
// 发布者负责发布程序感兴趣的topic或通知,例如:
pubsub.publish( "inbox/newMessage", "hello world!" );
// 或者
pubsub.publish( "inbox/newMessage", ["test", "a", "b", "c"] );
// 或者
pubsub.publish( "inbox/newMessage", {
sender: "hello@google.com",
body: "Hey again!"
});
// 如果订阅者不想被通知了,也可以取消订阅
// 一旦取消订阅,下面的代码执行后将不会记录消息,因为订阅者不再进行监听了
pubsub.publish( "inbox/newMessage", "Hello! are you still there?" );
9.5.4.3 用户界面通知
接下来,假设我们有一个负责显示实时股票信息的Web应用程序。
该应用程序有一个显示股票统计的网格和一个显示最后更新点的计数器。当数据模型改变时,应用程序需要更新网格和计数器。在这种情况下,目标(它将发布主题/通知)就是数据模型,观察者就是网格和计数器。
当观察者接收到Model(模型)自身已经改变的通知时,则可以相应地更新自己。
在我们的实现中,订阅者会监听newDataAvailable这个topic,以探测是否有新的股票信息。如果新通知发布到这个 topic,它将触发 gridUpdate 向包含股票信息的网格添加一个新行。它还将更新一个 last updated计数器来记录最后一次添加的数据(示例9-2)。
示例9-2 用户界面通知
//在newDataAvailable topic上创建一个订阅
var subscriber = pubsub.subscribe( "newDataAvailable", gridUpdate );
// 返回稍后界面上要用到的当前本地时间
getCurrentTime = function (){
var date = new Date(),
m = date.getMonth() + 1,
d = date.getDate(),
y = date.getFullYear(),
t = date.toLocaleTimeString().toLowerCase();
return (m + "/" + d + "/" + y + " " + t);
};
// 向网格组件上添加新数据行
function addGridRow( data ) {
// ui.grid.addRow( data );
console.log( "updated grid component with:" + data );
}
// 更新网格上的最新更新时间
function updateCounter( data ) {
// ui.grid.updateLastChanged( getCurrentTime() );
console.log( "data last updated at: " + getCurrentTime() + " with " + data);
}
// 使用传递给订阅者的数据data更新网格
gridUpdate = function( topic, data ){
if ( data !== "undefined" ) {
grid.addGridRow( data );
grid.updateCounter( data );
}
};
// 下面的代码描绘了数据层,一般应该使用ajax请求获取最新的数据后,告知程序有最新数据
//发布者更新gridUpdate topic来展示新数据项
pubsub.publish( "newDataAvailable", {
summary: "Apple made $5 billion",
identifier: "APPL",
stockPrice: 570.91
});
pubsub.publish( "newDataAvailable", {
summary: "Microsoft made $20 million",
identifier: "MSFT",
stockPrice: 30.85
});
9.5.4.4 使用Ben Alman的Pub/Sub实现解耦应用程序
在接下来的电影评级示例中,我们将使用Ben Alman在Publish/Subscribe模式上的jQuery 实现来展示我们如何解耦一个用户界面。需要注意的是,如何提交评级才会有新用户和评级数据同时发布通知的效果。
这是留给这些topic的订阅者来处理那些数据的。在我们的例子中,将新数据放入现有的数组中,然后使用Underscore库的.template()方法使用模板呈现它们。
如下是HTML/模板代码(示例9-3):
示例9-3 用于Pub/Sub的HTML/模板代码
<script id="userTemplate" type="text/html">
<li><%= name %></li>
</script>
<script id="ratingsTemplate" type="text/html">
<li><strong><%= title %></strong> was rated <%= rating %>/5</li>
</script>
<div id="container">
<div class="sampleForm">
<p>
<label for="twitter_handle">Twitter handle:</label>
<input type="text" id="twitter_handle" />
</p>
<p>
<label for="movie_seen">Name a movie you've seen this year:</label>
<input type="text" id="movie_seen" />
</p>
<p>
<label for="movie_rating">Rate the movie you saw:</label>
<select id="movie_rating">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5" selected>5</option>
</select>
</p>
<p>
<button id="add">Submit rating</button>
</p>
</div>
<div class="summaryTable">
<div id="users"><h3>Recent users</h3></div>
<div id="ratings"><h3>Recent movies rated</h3></div>
</div>
</div>
下面是JavaScript代码(示例9-4):
示例9-4 用于Pub/Sub的JavaScript代码
; (function ($) {
//订阅new user主题,提交评论的时候在用户列表上添加一个用户
$.subscribe("/new/user", function (e, data) {
var compiledTemplate;
if (data) {
compiledTemplate = _.template($("#userTemplate").html());
$('#users').append(compiledTemplate(data));
}
});
//订阅new rating主题,rating主题由title和rating组成。新rating添加到已有用户的rating列表上
$.subscribe("/new/rating", function (e, data) {
var compiledTemplate;
if (data) {
compiledTemplate = _.template($("#ratingsTemplate").html());
$("#ratings").append(compiledTemplate(data));
}
});
// 添加新用户处理程序
$("#add").on("click", function (e) {
e.preventDefault();
var strUser = $("#twitter_handle").val(),
strMovie = $("#movie_seen").val(),
strRating = $("#movie_rating").val();
// 通知程序,新用户有效
$.publish("/new/user", { name: strUser });
// 通知程序新rating评价有效
$.publish("/new/rating", { title: strMovie, rating: strRating });
});
})(jQuery);
9.5.4.5 解耦基于Ajax的jQuery应用程序
在最后一个示例中,我们将看一下如何使用 Pub/Sub解耦在早期开发过程中的代码,以此使我们省去一些可能繁琐的重构工作。
通常在侧重Ajax 技术的应用程序中,一旦我们收到了请求的响应,我们就想据此实现不只一个特定动作。我们可以简单地向成功回调中添加所有的post请求逻辑,但这种方法存在一些缺点。
由于函数/代码之间互相依赖的增加,高度耦合的应用程序有时会增加复用函数所需的工作量。这意味着,如果我们只是想一次性获取一个结果集,在回调中对post请求逻辑进行硬编码可能是行得通的,但是,当我们需要对相同的数据源(和不同的端行为)进一步地进行Ajax 调用,而没有多次重写部分代码,那就不那么合适了。我们可以从一开始就使用pub/sub来节省时间,而不必遍历调用相同数据源的每一层而后再对它们进行操作。
通过使用Observer,我们还可以将不同事件降至我们所要的任何粒度级别,并轻松地根据这些事件分离应用程序范围内的通知,而使用其他模式完成这项工作的优雅度较低。
请注意在下面的样例中,当用户表示他想进行搜索查询时,是如何发出一个 topic通知的,以及当请求返回并且有实际数据可用时,是如何发出另一个通知的。它让订阅者随后决定如何利用这些事件(或返回的数据)。它的好处是:如果我们愿意,我们可以有10个不同的订阅者以不同的方式使用返回的数据,但这对于Ajax层而言是无关紧要的。其唯一的责任是请求和返回数据,然后传递给任何想使用它的人。这种关注点分离能使代码的整个设计变得更加整洁。
下面是HTML/模板代码(示例9-5):
示例9-5 用于Ajax的HTML/模板
<form id="flickrSearch">
<input type="text" name="tag" id="query"/>
<input type="submit' name="submit' value="submit'/>
</form>
<div id="lastQuery"></div>
<div id="searchResults"></div>
<script id="resultTemplate" type="text/html">
<% _.each(items, function( item ){ %>
<li><p><img src="<%= item.media.m %>"/></p></li>
<% });%>
</script>
下面是JavaScript代码(示例9-6):
示例9-6 用于Ajax的JavaScript代码
; (function ($) {
// 预编译模板,并使用闭包缓存它
var resultTemplate = _.template($("#resultTemplate").html());
// 订阅新搜索tags主题
$.subscribe("/search/tags", function (e,tags) {
$("#searchResults")
.html("Searched for:" + tags + "");
});
// 订阅新搜索结果主题
$.subscribe("/search/resultSet", function (e,results) {
$("#searchResults").append(resultTemplate(results));
$("#searchResults").append(compiled_template(results));
});
// 提交搜索请求,并在/search/tags主题上发布tags
$("#flickrSearch").submit(function (e) {
e.preventDefault();
var tags = $(this).find("#query").val();
if (!tags) {
return;
}
$.publish("/search/tags", [$.trim(tags)]);
});
// 订阅发布的新tag,并且使用tag发起请求。一旦返回数据,将数据发布给应用程序的其他使用者
$.subscribe("/search/tags", function (e,tags) {
$.getJSON("http://api.flickr.com/services/feeds/photos_public.gne?jsoncallba ck=?", {
tags: tags,
tagmode: "any",
format: "json"
},
function (data) {
if (!data.items.length) {
return;
}
$.publish("/search/resultSet", data.items);
});
});
})(jaucry);
在应用程序设计中,Observer 模式在解耦多个不同脚本方面是非常有用的,如果你还没有使用它,我建议你了解一下这里提到的其中一个预先编写的实现,并试着使用一下。这是要入门了解的一个比较简单的设计模式,但同时也是最强大的设计模式之一。
在字典里,中介者是指“协助谈判和解决冲突的中立方”1。在本书设计模式里,中介者是一种行为设计模式,它允许我们公开一个统一的接口,系统的不同部分可以通过该接口进行通信。
注释:1 Wikipedia(http://en.wikipedia.org/wiki/Mediation); Dictionary.com(http://dictionary. reference.com/browse/mediator)
如果一个系统的各个组件之间看起来有太多的直接关系,也许是时候需要一个中心控制点了,以便各个组件可以通过这个中心控制点进行通信。Mediator 模式促进松散耦合的方式是:确保组件的交互是通过这个中心点来处理的,而不是通过显式地引用彼此。这种模式可以帮助我们解耦系统并提高组件的可重用性。
现实世界的一个例子是典型的机场交通控制系统。机场控制塔(中介者)处理飞机的起飞和降落,因为所有通信(监听到或发出的通知)都是从飞机到控制塔,而不是飞机和飞机直接相互通信的。中央控制系统是该系统成功的关键,而这才是中介者在软件设计中所担任的真正角色(图9-5)。
图9-5 Mediator模式
就实现而言,Mediator模式本质上是Observer模式的共享目标。它假设该系统中对象或模块之间的订阅和发布关系被牺牲掉了,从而维护中心联络点。
它也可能被认为是额外的或者是用于应用程序间的通知,如不同子系统之间的通信,这些子系统本身就很复杂,且可能希望通过发布/订阅关系实现内部组件间的解耦。
另一个例子是 DOM事件冒泡和事件委托。如果系统中所有的订阅针对的是文档document而不是单个node节点,则这个文档会有效地充当中介者。更高级别(level)的对象承担了向订阅者通知有关交互事件的责任,而不是绑定到单个节点的事件。
可以在下面找到Mediator模式的简单实现,暴露了publish()和subscribe()方法来使用:
var mediator = (function (){
// 存储可被广播或监听的topic
var topics = {};
// 订阅一个topic,提供一个回调函数,一旦topic被广播就执行该回调
var subscribe = function (topic, fn){
if (!topics[topic]){
topics[topic] = [];
}
topics[topic].push({ context: this, callback: fn });
returnthis;
};
// 发布/广播事件到程序的剩余部分
var publish = function (topic){
var args;
if (!topics[topic]){
return false;
}
args = Array.prototype.slice.call(arguments, 1);
for (var i = 0, l = topics[topic].length; i < l; i++) {
var subscription = topics[topic][i];
subscription.callback.apply(subscription.context, args);
}
return this;
};
return {
Publish: publish,
Subscribe: subscribe,
installTo: function (obj) {
obj.subscribe = subscribe;
obj.publish = publish;
}
};
})();
如果你对更高级的代码实现感兴趣,深入下去可以浏览到 Jack Lawson的优秀Mediator.js(http://thejacklawson.com/Mediator.js/)的简洁版。除了其他改进以外,这个版本支持 topic命名空间、订阅者删除和用于中介者的更强大的发布/订阅(Publish/Subscribe)系统。但如果你想跳过这些内容,则可以直接进入下一个示例继续阅读。1
注释:1 感谢Jack提供优秀代码注释,在本节中提供了莫大的帮助。
首先,让我们来实现订阅者的概念,可以考虑一个Mediator的topic注册实例。
通过生成对象实例,之后我们可以很容易地更新订阅者,而不需要注销并重新注册它们。订阅者可以写成构造函数,该函数接受三个参数:一个可被调用的函数fn、一个options对象和一个context(上下文)。
// 将context上下文传递给订阅者,默认上下文是window对象
(function (root){
function guidGenerator() { /*..*/ }
// 订阅者构造函数
function Subscriber(fn, options, context) {
if (!this instanceof Subscriber) {
retur nnew Subscriber(fn, context, options);
} else {
// guidGenerator()是一个函数,用于为订阅者生成GUID,以便之后很方便地引用它们。
// 为了简洁,跳过具体实现
this.id = guidGenerator();
this.fn = fn;
this.options = options;
this.context = context;
this.topic = null;
}
}
})();
Mediator中的topic持有了一组回调函数和子topic列表,一旦Mediator.Publish方法在 Mediator 实例上被调用时,这些回调函数就会被触发。它还包含用于操作数据列表的方法。
// 模拟Topic
// JavaScript允许我们使用Function对象作为原型的结合与新对象和构造函数一起调用
function Topic( namespace ){
if ( !this instanceof Topic ) {
return new Topic( namespace );
}else{
this.namespace = namespace || "";
this._callbacks = [];
this._topics = [];
this.stopped = false;
}
}
// 定义topic的prototype原型,包括添加订阅者和获取订阅者的方式
Topic.prototype = {
// 添加新订阅者
AddSubscriber: function( fn, options, context ){
var callback = new Subscriber( fn, options, context );
this._callbacks.push( callback );
callback.topic = this;
return callback;
},
...
我们的Topic实例作为一个参数传递给Mediator回调。然后可以StopPropagation()的简便方法来调用进一步的回调传播:
StopPropagation: function(){
this.stopped = true;
},
当给定GUID标识符时,我们也可以很容易获取现有的订阅者:
GetSubscriber: function( identifier ){
for(var x = 0, y = this._callbacks.length; x < y; x++ ){
if( this._callbacks[x].id == identifier || this._callbacks[x].fn == identifier ){
return this._callbacks[x];
}
}
for( var z in this._topics ){
if( this._topics.hasOwnProperty( z ) ){
var sub = this._topics[z].GetSubscriber( identifier );
if( sub !== undefined ){
return sub;
}
}
}
},
接下来,如果需要它们,我们可以提供简单方法来添加新topic、检查现有topic或者获取topic:
AddTopic: function( topic ){
this._topics[topic] = new Topic( (this.namespace ? this.namespace + ":" : "") + topic );
},
HasTopic: function( topic ){
return this._topics.hasOwnProperty( topic );
},
returnTopic: function( topic ){
Return this._topics[topic];
},
如果不再需要订阅者,我们可以显式地删除它们。以下代码将通过它的子主题递归删除一位订阅者:
RemoveSubscriber: function( identifier ){
if( !identifier ){
this._callbacks = [];
for( var z in this._topics ){
if( this._topics.hasOwnProperty(z) ){
this._topics[z].RemoveSubscriber( identifier );
}
}
}
for( var y = 0, x = this._callbacks.length; y < x; y++ ) {
if( this._callbacks[y].fn == identifier || this._callbacks[y].id == identifier ){
this._callbacks[y].topic = null;
this._callbacks.splice( y,1 );
x--; y--;
}
}
},
接下来,我们将通过子topic递归向订阅者发布(Publish)任意参数。
Publish: function( data ){
for( var y = 0, x = this._callbacks.length; y < x; y++ ) {
var callback = this._callbacks[y], l;
callback.fn.apply( callback.context, data );
l = this._callbacks.length;
if( l < x ){
y--;
x = l;
}
}
for( var x in this._topics ){
if( !this.stopped ){
if( this._topics.hasOwnProperty( x ) ){
this._topics[x].Publish( data );
}
}
}
this.stopped = false;
}
};
这里暴露了我们将主要与之交互的Mediator实例。在这里,完成了事件在topic上的注册和移除。
function Mediator() {
if (!this instanceof Mediator) {
return new Mediator();
} else {
this._topics = new Topic("");
}
};
对于更高级的使用场景,我们可以让Mediator支持用于inbox:messages:new:read等主题topic的命名空间。在接下来的示例中,GetTopic根据命名空间返回相应的主题实例。
Mediator.prototype = {
GetTopic: function( namespace ){
var topic = this._topics,
namespaceHierarchy = namespace.split( ":" );
if( namespace === "" ){
return topic;
}
if( namespaceHierarchy.length > 0 ){
for( var i = 0, j = namespaceHierarchy.length; i < j; i++ ){
if( !topic.HasTopic( namespaceHierarchy[i]) ){
topic.AddTopic( namespaceHierarchy[i] );
}
topic = topic.ReturnTopic( namespaceHierarchy[i] );
}
}
return topic;
},
在本小节中,我们定义了Mediator.Subscribe方法,它接受一个topic命名空间、一个可执行的fn函数、options,以及调用该函数的context上下文。如果topic不存在,则创建一个。
Subscribe: function( topiclName, fn, options, context ){
var options = options || {},
context = context || {},
topic = this.GetTopic( topicName ),
sub = topic.AddSubscriber( fn, options, context );
return sub;
},
继续下去,我们可以进一步定义用于访问特定订阅者或将订阅者从topic中递归删除的实用程序。
// 通过给定的订阅者ID/命名函数和topic命名空间返回一个订阅者
GetSubscriber: function( identifier, topic ){
return this.GetTopic( topic || "" ).GetSubscriber( identifier );
},
// 通过给定的订阅者ID或命名函数,从给定的topic命名空间递归删除订阅者
Remove: function( topicName, identifier ){
this.GetTopic( topicName ).RemoveSubscriber( identifier );
},
主要的Publish方法允许我们向所选择的topic命名空间任意发布数据。
Topic向下递归调用。例如,一个发往inbox:messages的帖子将被发至inbox:messages:new和inbox:messages:new:read。如下所示:
Mediator.Publish( "inbox:messages:new", [args] );
Publish: function( topicName ){
var args = Array.prototype.slice.call( arguments, 1),
topic = this.GetTopic( topicName );
args.push( topic );
this.GetTopic( topicName ).Publish( args );
}
};
最后,我们可以很容易地将Mediator作为一个对象附加到root上:
root.Mediator = Mediator;
Mediator.Topic = Topic;
Mediator.Subscriber = Subscriber;
// 记住,这里可以传递任何内容。这里我传递了window对象作为Mediator的附加对象,但也可以随时附加到其他对象上。
})( window );
通过使用上述的任一种实现(简单的和高级的实现),我们可以建立一个简单的聊天记录系统,如下所示。
如下是HTML代码:
<h1>Chat</h1>
<form id="chatForm">
<label for="fromBox">Your Name:</label>
<input id="fromBox" type="text"/>
<br />
<label for="toBox">Send to:</label>
<input id="toBox" type="text"/>
<br />
<label for="chatBox">Message:</label>
<input id="chatBox" type="text"/>
<button action="submit">Chat</button>
</form>
<div id="chatResult"></div>
如下是JavaScript代码:
$("#chatForm").on("submit", function (e) {
e.preventDefault();
// 从UI上获取chat的数据
var text = $("#chatBox").val(),
from = $("#fromBox").val();
to = $("#toBox").val();
// 将数据发布到newMessage主题上
mediator.publish("newMessage", { message: text, from: from, to: to });
});
// 将新消息附加到聊天结果记录上
function displayChat(data) {
var date = new Date(),
msg = data.from + " said \"" + data.message + "\" to " + data.to;
$("#chatResult")
.prepend("" + msg + " (" + date.toLocaleTimeString() + ")");
}
// 记录消息日志
function logChat(data) {
if (window.console) {
console.log(data);
}
}
// 通过mediator订阅新提交的newMessage主题
mediator.subscribe("newMessage", displayChat);
mediator.subscribe("newMessage", logChat);
// 如下代码仅在高级代码实现上可以使用
function amITalkingToMyself(data) {
return data.from === data.to;
}
function iAmClearlyCrazy(data) {
$("#chatResult").prepend("" + data.from + " is talking to himself.");
}
mediator.Subscribe(amITalkingToMyself, iAmClearlyCrazy);
Mediator 模式的最大好处是:它能够将系统中对象或组件之间所需的通信渠道从多对多减少到多对一。由于现在的解耦程度较高,添加新发布者和订阅者相对也容易多了。或许使用这种模式最大的缺点是:它会引入单一故障点。将 Mediator 放置于模块之间可以导致性能下降,因为它们总是间接地进行通信。由于松耦合的性质,很难通过仅关注广播来确定一个系统如何作出反应。
也就是说,自我提醒解耦的系统有很多其他的优点:如果模块之间直接相互通信,模块的改变(如另一个模块抛出一个异常)容易让应用程序的其余部分产生多米诺效应。这个问题对解耦的系统来说就不是个大问题。
最后,紧密耦合会引起各种各样的问题,这只是另一个替代方案,但如果正确地实现,它也能很好地工作。
开发人员通常想知道Mediator模式和Observer模式之间的差异是什么。无可否认地,它们之间有一些重叠,让我们重新参考“四人组”作出的解释:
在Observer模式中,不存在封装约束的单一对象。Observer和Subject (目标)必须合作才能维持约束。Communication(通信)模式由观察者和目标互连的方式所决定:单一目标通常有很多观察者,有时一个目标的观察者是另一个观察者的目标。
Mediator和Observer都能够促进松耦合;然而,Mediator模式通过限制对象严格通过Mediator进行通信来实现这一目的。Observer模式创建观察者对象,观察者对象向订阅它们的对象发布其感兴趣的事件。
我们将简单提一下 Facade 模式,但出于引用目的,一些开发人员可能也想知道Mediator 和 Facade 模式之间是否有相似点。它们都能够抽象出现有模块的功能,但是也有一些细微的差别。
Mediator模块在它被模块显式引用的地方汇集这些模块之间的通信。从某种意义上说,这是多方向的。另一方面,Facade模式仅是为模块或系统定义了一个较简单的接口,而没有添加任何额外的功能。系统中的其他模块不会直接关联外观,所以可以被视为单向的。
“四人组”称Prototype模式为一种基于现有对象模板,通过克隆方式创建对象的模式。
我们可以认为 Prototype 模式是基于原型继承的模式,可以在其中创建对象,作为其他对象的原型。prototype对象本身实际上是用作构造函数创建每个对象的蓝图。如果所用构造函数的原型包含一个名为 name 的属性(代码样例如下),那么由同样的构造函数创建的每个对象也会有同样的属性(见图9-6)。
图9-6 Prototype模式
如果查看现有(非 JavaScript)文献对该模式的定义,我们可能会再一次发现对类的引用。现实情况是,原型继承避免和类(Class)一起使用。理论上没有“定义”的对象,也没有核心的对象。我们仅是创建现有功能对象的拷贝。
使用Prototype模式的其中一个好处是,我们获得的是JavaScript其本身所具有的原型优势,而不是试图模仿其他语言的特性。与其他设计模式一起使用时,情况并非总是如此。
模式不仅是一种实现继承的简单方法,它也可以带来性能提升:在一个对象中定义一个函数,它们都是由引用创建(因此所有子对象都指向相同的函数),而不是创建它们自己的单份拷贝。
如同 ECMAScript5 标准所定义的,那些有趣的、真正的原型继承要求使用Object.create(我们在本节的前面曾看到过)。Object.create 创建一个对象,拥有指定原型和可选的属性(例如Object.create(prototype, optionalDescriptorObjects))。
在下面的示例中可以看到它的演示:
var myCar = {
name: "Ford Escort",
drive: function () {
console.log("Weeee. I'm driving!");
},
panic: function () {
console.log("Wait. How do you stop this thing?");
}
};
// 使用Object.create实例化一个新car
var yourCar = Object.create(myCar);
// 现在可以看到一个对象是另外一个对象的原型
console.log(yourCar.name);
Object.create还允许我们轻松实现差异继承等高级概念,通过差异继承,对象可以直接继承自其他对象。我们之前已经了解到,Object.create允许我们使用第二个提供的参数来初始化对象属性。例如:
var vehicle = {
getModel: function () {
console.log("The model of this vehicle is.." + this.model);
}
};
var car = Object.create(vehicle, {
"id": {
value: MY_GLOBAL.nextId(),
// writable:false, configurable:false 默认值
enumerable: true
},
"model": {
value: "Ford",
enumerable: true
}
});
在这里,可以使用对象字面量在Object.create的第二个参数上初始化属性,并且对象字面量采用的语法与Object.defineProperties和Object.defineProperty方法所使用的语法相似,我们之前已经了解过这些方法。
值得注意的是,在枚举对象的属性以及在hasOwnProperty()检查中包装循环内容时(Crockford推荐),原型关系可能会引起麻烦。
如果希望在不直接使用 Object.create 的情况下实现 Prototype 模式,我们可以按照上面的示例模拟该模式,如下所示:
var vehiclePrototype = {
init: function (carModel) {
this.model = carModel;
},
getModel: function () {
console.log("The model of this vehicle is.." + this.model);
}
};
function vehicle(model) {
function F() { };
F.prototype = vehiclePrototype;
var f = new F();
f.init(model);
return f;
}
var car = vehicle("Ford Escort");
car.getModel();
这个方案不允许用户以同样的方式定义只读属性(因为如果不小心, vehiclePrototype可能会被改变)。
最后一种可供选择的Prototype模式实现可以是这样的:
var beget = (function () {
function F() { }
return function (proto) {
F.prototype = proto;
return new F();
};
})();
我们可以从vehicle函数引出这个方法。请注意,这里的vehicle模仿了一个构造函数,因为Prototype模式不包含任何初始化的概念,而不仅是将对象链接至原型。
Command 模式旨在将方法调用、请求或操作封装到单一对象中,从而根据我们不同的请求对客户进行参数化和传递可供执行的方法调用。此外,这种模式将调用操作的对象与知道如何实现该操作的对象解耦,并在交换出具体类(对象)方面提供更大的整体灵活性。
用基于类的编程语言解释具体类是最恰当的,它们与抽象类的思想相关。一个抽象类定义一个接口,但不一定为它所有的成员函数提供实现。它作为一个基类,派生出其他类。实现缺失功能的派生类被称为一个具体的类(见图9-7)。
图9-7 Command模式
Command 模式背后的主要思想是:它为我们提供了一种分离职责的手段,这些职责包括从执行命令的任意地方发布命令以及将该职责转而委托给不同对象。
实施明智的、简单的命令对象将把action动作和调用该动作的对象绑定在一起。它们始终包括一个执行操作(如 run()或 execute())。所有具有相同接口的 Command对象可以根据需要轻松交换,这被认为是该模式的一个更大好处。
为了演示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 +")";
}
};
})();
看一下上面的代码,它可以通过直接访问对象轻松地调用我们的CarManager方法。我们认为这里没有任何错误,从技术上讲,它是完全有效的 JavaScript。然而,它在有些情况下可能是不利的。
例如,试想如果CarManager里的核心API改变了会怎么样。这将要求程序里所有直接访问这些方法的对象都需要进行修改。这可能被视为一个耦合层,它实际上最大程度地违反了松耦合对象的OOP方法论。而我们可以通过进一步抽象API来解决这个问题。
现在让我们来扩展CarManager,这样Command模式下的应用程序会产生如下结果:接受任意可以在CarManager对象上执行的命名方法,传递可以使用的任意数据,如CarModel(模型)和ID。
这是我们希望能够实现的内容:
CarManager.execute("buyVehicle", "Ford Escort", "453543");
按照这个结构,我们现在应该为CarManager.execute方法添加一个定义,如下所示:
CarManager.execute = function (name) {
return CarManager[name] && CarManager[name].apply(CarManager, [].slice.call(arguments, 1));
};
因此我们最终的示例调用看起来是这样的:
CarManager.execute("arrangeViewing", "Ferrari", "14523");
CarManager.execute("requestInfo", "Ford Mondeo", "54323");
CarManager.execute("requestInfo", "Ford Escort", "34232");
CarManager.execute("buyVehicle", "Ford Escort", "34232");
当创建外观时,向外界展示的外表可能掩盖了一个非常不同的现实。这是我们下一个要查看的模式名称的灵感来源——Facade 模式。Facade 模式为更大的代码体提供了一个方便的高层次接口,能够隐藏其底层的真实复杂性。可以把它想成是简化API来展示给其他开发人员,通常都是可以提高可用性(见图9-8)。
图9-8 Facade模式
Facade是一种结构型模式,在jQuery等JavaScript库中经常可以看到,尽管一个实现可能支持具有广泛行为的方法,但却只有一个“外观”或这些方法的有限抽象能够提供给公众使用。
这使我们可以直接与Facade交互,而不是与幕后子系统交互。每当使用jQuery的$(el).css()或$(el).animate()方法时,实际上我们是在使用Facade:一种更简单的公有接口,使我们不必手动在 jQuery核心中调用很多内部方法以便实现某些行为。这也避免了手动与DOM API交互并维护状态变量的需要。
jQuery核心方法应该被认为是中间抽象。对于开发人员来说,更直接的事是DOM API,外观可以使jQuery库很容易使用。
在我们学到的知识基础上,Facade模式既能简化类的接口,也能将这个类从使用它的代码中解耦。这使我们能够间接与子系统交互,这种方式相比直接访问子系统有时不易犯错误。Facade的优点包括易于使用和实现该模式时占用空间小。
让我们来看看运行中的模式。这是一个未优化的代码示例,但在这里,我们使用Facade来简化用于监听跨浏览器事件的接口。为此,创建一个可以用于某些代码的通用方法,该代码的任务是检查特性的存在,以便能够提供一个安全的、跨浏览器的兼容解决方案。
var addMyEvent = function (el, ev, fn) {
if (el.addEventListener) {
el.addEventListener(ev, fn, false);
} else if (el.attachEvent) {
el.attachEvent("on" + ev, fn);
} else {
el["on" + ev] = fn;
}
};
我们都很熟悉的jQuery的$(document).ready(..),采用了类似的方式。在内部,它实际上是使用了一个被称为bindReady()的方法,它是这样做的:
bindReady: function() {
...
if ( document.addEventListener ) {
// 使用便利的事件回调
document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
// 可靠的window.onload,始终可用
window.addEventListener( "load", jQuery.ready, false );
// 如果是IE事件模型
} else if ( document.attachEvent ) {
document.attachEvent( "onreadystatechange", DOMContentLoaded );
// 可靠的window.onload,始终可用
window.attachEvent( "onload", jQuery.ready );
...
这是Facade的另一个示例,其余部分仅仅使用了有限暴露的$(document).ready (..)接口,从而将更复杂的实现始终隐藏在视线之外。
但Facade不是必须单独使用的。它们也可以与其他模式集成,如Module模式。如下所示,Module模式的实例包含很多已经定义的私有方法。然后使用Facade提供一个更简单的API来访问这些方法:
var module = (function () {
var _private = {
i: 5,
get: function () {
console.log("current value:" + this.i);
},
set: function (val) {
this.i = val;
},
run: function () {
console.log("running");
},
jump: function () {
console.log("jumping");
}
};
return {
facade: function (args) {
_private.set(args.val);
_private.get();
if (args.run) {
_private.run();
}
}
};
}());
//输出: "running", 10
module.facade({ run: true, val: 10 });
在这个示例中,调用module.facade()实际上会在该模块中触发一系列的私有行为,但用户不会接触到。我们让façade变成一个不需要关注实现细节,而且更容易使用的一个特性。
Facade 也有一些缺点,但值得注意的一个问题是性能。也就是说,我们必须确定Facade提供给实现的抽象是否包含隐性成本,如果是的话,这种成本是否是合理的。回到 jQuery 库部分,我们大多数人都知道,getElementById("identifier")和$("#identifier")可以用于通过ID查询页面上的某个元素。
然而,你知道getElementById()本身的速度在高数量级下要快的多吗?看一下jsPerf测试,来查看每个浏览器级上的结果:http://jsperf. com/getelementbyid- vs- jquery-id。当然,现在我们必须要记住的是,jQuery(和 Sizzle,其选择器引擎)在幕后做了很多事情,以优化查询(一个jQuery对象,而不只是一个DOM节点被返回)。
这个特别的Facade给我们带来的挑战是:为了提供一种能够接受和解析多个查询类型的优雅选择器函数,其抽象会存在隐性成本。用户不需要访问 jQuery.getById ("identifier")或jQuery.getbyClass("identifier")等。也就是说,多年来性能的优劣已经在实践中检验过了,鉴于jQuery的成功,简单的Facade实际上能够为团队很好的效力。
当使用Facade模式时,要试着了解涉及的任何性能成本,并确认是否值得抽象。
Factory 模式是另一种创建型模式,涉及创建对象的概念。其分类不同于其他模式的地方在于它不显式地要求使用一个构造函数。而Factory可以提供一个通用的接口来创建对象,我们可以指定我们所希望创建的工厂对象的类型(见图9-9)。
图9-9 Factory模式
假设有一个UI工厂,我们要创建一个UI组件的类型。不需要直接使用new运算符或者通过另一个创建型构造函数创建这个组件,而是要求Factory对象创建一个新的组件。我们通知Factory需要什么类型的对象(如“按钮”、“面板”),它会进行实例化,然后将它返回给我们使用。
如果对象创建过程相对比较复杂,这种方法特别有用,例如,如果它强烈依赖于动态因素或应用程序配置的话。
可以在ExtJS等UI库中找到此模式的示例,其中创建对象或组件的方法也有可能被归入子类了。
下面这个示例构建在之前的代码片段之上,使用Constructor模式逻辑来定义汽车。它展示了如何使用Factory模式来实现vehicle工厂:
// Types.js –本例构造函数的存放文件
// 定义Car构造函数
function Car(options) {
// 默认值
this.doors = options.doors || 4;
this.state = options.state || "brand new";
this.color = options.color || "silver";
}
// 定义Truck构造函数
function Truck(options) {
this.state = options.state || "used";
this.wheelSize = options.wheelSize || "large";
this.color = options.color || "blue";
}
// FactoryExample.js
// 定义vehicle工厂的大体代码
function VehicleFactory() { }
// 定义该工厂factory的原型和试用工具,默认的vehicleClass是Car
VehicleFactory.prototype.vehicleClass = Car;
// 创建新Vehicle实例的工厂方法
VehicleFactory.prototype.createVehicle = function (options) {
if (options.vehicleType === "car") {
this.vehicleClass = Car;
} else {
this.vehicleClass = Truck;
}
return new this.vehicleClass(options);
};
// 创建生成汽车的工厂实例
var carFactory = new VehicleFactory();
var car = carFactory.createVehicle({
vehicleType: "car",
color: "yellow",
doors: 6});
// 测试汽车是由vehicleClass的原型prototype里的Car创建的
//输出: true
console.log(car instanceof Car);
//输出: Car对象,color: "yellow", doors: 6,state:"brand new"
console.log(car);
在方法 1中,我们修改了VehicleFactory实例来使用Truck类:
var movingTruck = carFactory.createVehicle({
vehicleType: "truck",
state: "like new",
color: "red",
wheelSize: "small"});
// 测试卡车是由vehicleClass的原型prototype里的Truck创建的
//输出: true
console.log(movingTruck instanceof Truck);
//输出: Truck对象,color:"red", state:"like new",wheelSize:"small"
console.log(movingTruck);
在方法2中,我们把VehicleFactory归入子类来创建一个构建Truck的工厂类:
function TruckFactory() { }
TruckFactory.prototype = new VehicleFactory();
TruckFactory.prototype.vehicleClass = Truck;
var truckFactory = new TruckFactory();
var myBigTruck = truckFactory.createVehicle({
state: "omg..so bad.",
color: "pink",
wheelSize: "so big"});
// 确认myBigTruck是由原型Truck创建的
//输出: true
console.log(myBigTruck instanceof Truck);
//输出:Truck对象,color: pink", wheelSize: "so big", state: "omg. so bad"
console.log(myBigTruck);
Factory模式应用于如下场景时是特别有用的:
• 当对象或组件设置涉及高复杂性时
• 当需要根据所在的不同环境轻松生成对象的不同实例时
• 当处理很多共享相同属性的小型对象或组件时
• 在编写只需要满足一个API契约(亦称鸭子类型)的其他对象的实例对象时。对于解耦是很有用的。
如果应用错误,这种模式会为应用程序带来大量不必要的复杂性。除非为创建对象提供一个接口是我们正在编写的库或框架的设计目标,否则我建议坚持使用显式构造函数,以避免不必要的开销。
由于对象创建的过程实际上是藏身接口之后抽象出来的,单元测试也可能带来问题,这取决于对象创建的过程有多复杂。
了解抽象工厂模式也是有用的,它用于封装一组具有共同目标的单个工厂。它能够将一组对象的实现细节从一般用法中分离出来。
应当使用抽象工厂模式的情况是:一个系统必须独立于它所创建的对象的生成方式,或它需要与多种对象类型一起工作。
既简单又容易理解的示例是车辆工厂,它定义了获取或注册车辆类型的方法。抽象工厂可以命名为AbstractVehicleFactory。抽象工厂将允许对像car或truck这样的车辆类型进行定义,具体工厂只需要实现履行车辆契约的类(如Vehicle.prototype.drive和Vehicle.prototype.breakDown)。
var AbstractVehicleFactory = (function () {
// 存储车辆类型
var types = {};
return {
getVehicle: function ( type, customizations ) {
var Vehicle = types[type];
return (Vehicle) ? return new Vehicle(customizations) : null;
},
registerVehicle: function ( type, Vehicle ) {
var proto = Vehicle.prototype;
// 只注册实现车辆契约的类
if ( proto.drive && proto.breakDown ) {
types[type] = Vehicle;
}
return AbstractVehicleFactory;
}
};
})();
// 用法:
AbstractVehicleFactory.registerVehicle( "car", Car );
AbstractVehicleFactory.registerVehicle( "truck", Truck );
// 基于抽象车辆类型实例化一个新car对象
var car = AbstractVehicleFactory.getVehicle( "car" , {
color: "lime green",
state: "like new" } );
// 同理实例化一个新truck对象
var truck = AbstractVehicleFactory.getVehicle( "truck" , {
wheelSize: "medium",
color: "neon yellow" } );
在C++和Lisp等传统编程语言中,Mixin是可以轻松被一个子类或一组子类继承功能的类,目的是函数复用。
对于不熟悉子类化的开发人员来说,在深入研究Mixin和Decorator之前,将阅读初学者内容。
子类化这个术语是指针对一个新对象,从一个基础或超类对象中继承相关的属性。在传统的面向对象编程中,类B是从另外一个类A扩展得来。这里我们认为A是一个超类,B是A的一个子类。因此,B的所有实例从A处继承了相关方法。但是B仍然能够定义自己的方法,包括那些A最初所定义方法的重写。
A中的一个方法,在B里已经被重写了,那么B还需要调用A中的这个方法吗,我们称此为方法链。B需要调用构造函数A(超类)吗,我们称此为构造函数链。
为了演示子类化,首先需要一个可以创建自己新实例的基本对象。让我们围绕一个人的概念来模拟子类化。
var Person = function (firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.gender = "male";
};
下一步,指定一个新类(对象),它是现有Person对象的一个子类。想象一下,在继承Person超类上的属性同时,我们需要在SuperHero上添加另外不同的属性。由于超级英雄与平常人具有很多共同的特征(如姓名、性别),希望这应该能够充分说明子类化是如何工作的。
// Person的新实例很容易像如下这样创建:
var clark = new Person("Clark", "Kent");
// 为超人(Superhero)定义一个子类构造函数
var Superhero = function (firstName, lastName, powers) {
// 调用超类的构造函数,然后使用.call()方法进行调用从而进行初始化
Person.call(this, firstName, lastName);
// 最后,保存powers,在正常Person里找不到的特性数组
this.powers = powers;
};
SuperHero.prototype = Object.create(Person.prototype);
var superman = new Superhero("Clark", "Kent", ["flight", "heat-vision"]);
console.log(superman);
// 输出Person属性和powers
Superhero构造函数创建一个源于Person的对象。这种类型的对象拥有在链中比它们靠上对象的属性,如果我们已经在Person对象中设置默认值,Superhero就能够重写所有继承的值,并且其对象本身也可以拥有特定的值。
在JavaScript中,我们可以将继承Mixin看作为一种通过扩展收集功能的方式。我们定义的每个新对象都有一个原型,可以从中继承更多属性。原型可以继承于其他对象的原型,但更重要的是,它可以为任意数量的对象实例定义属性。可以利用这一点来促进函数复用(见图9-10)。
图9-10 Mixin
Mixin 允许对象通过较低的复杂性借用(或继承)功能。由于该模式非常适用于JavaScript的对象原型,它为我们提供了一种相当灵活的方式,从不只一个Mixin中分享功能,但实际上很多功能是通过多重继承获得的。
它们可以被视为具有可以在很多其他对象原型中轻松共享属性和方法的对象。想象一下,我们在标准对象字面量中定义一个包含实用函数的Mixin,如下所示:
var myMixins = {
moveUp: function () {
console.log("move up");
},
moveDown: function () {
console.log("move down");
},
stop: function () {
console.log("stop! in the name of love!");
}
};
然后我们可以使用 Underscore.js 的_.extend()方法等辅助器轻松地扩展现有构造器函数的原型,以将上述行为包含进来:
// carAnimator构造函数的大体代码
function carAnimator() {
this.moveLeft = function () {
console.log("move left");
};
}
// personAnimator构造函数的大体代码
function personAnimator() {
this.moveRandomly = function () { /*..*/ };
}
// 使用Mixin扩展2个构造函数
_.extend(carAnimator.prototype, myMixins);
_.extend(personAnimator.prototype, myMixins);
// 创建carAnimator的新实例
var myAnimator = new carAnimator();
myAnimator.moveLeft();
myAnimator.moveDown();
myAnimator.stop();
// 输出:
// move left
// move down
// stop! in the name of love!
正如我们所看到的,这允许我们以通用方式轻松“混入”对象构造函数。
在下一个示例中,我们有两个构造函数:Car 和 Mixin。我们要做的是扩充(扩展的另一种说法)Car,以便它可以继承Mixin中定义的特定方法,即driveForward()和driveBackward()。这次,我们不会使用Underscore.js。
本示例将演示如何扩展构造函数,不需要对我们可能拥有的每个构造函数都重复这个过程而将功能包含进来。
// 定义简单的Car构造函数
var Car = function (settings) {
this.model = settings.model || "no model provided";
this.color = settings.color || "no colour provided";
};
// Mixin
var Mixin = function () { };
Mixin.prototype = {
driveForward: function () {
console.log("drive forward");
},
driveBackward: function () {
console.log("drive backward");
},
driveSideways: function () {
console.log("drive sideways");
}
};
// 通过一个方法将现有对象扩展到另外一个对象上
function augment(receivingClass, givingClass) {
// 只提供特定的方法
if (arguments[2]) {
for (var i = 2, len = arguments.length; i < len; i++) {
receivingClass.prototype[arguments[i]] = givingClass. prototype [arguments[i]];
}
}
// 提供所有方法
else {
for (var methodName in givingClass.prototype) {
// 确保接收类不包含所处理方法的同名方法
if (!Object.hasOwnProperty(receivingClass.prototype, methodName)) {
receivingClass.prototype[methodName] = givingClass.prototype[methodName];
}
// 另一方式:
// if ( !receivingClass.prototype[methodName] ) {
// receivingClass.prototype[methodName] = givingClass.prototype[methodName];
// }
}
}
}
// 给Car构造函数增加"driveForward"和"driveBackward"两个方法
augment(Car, Mixin, "driveForward", "driveBackward");
// 创建一个新Car
var myCar = new Car({
model: "Ford Escort",
color: "blue"
});
// 测试确保新增方法可用
myCar.driveForward();
myCar.driveBackward();
// 输出:
// drive forward
// drive backward
// 也可以通过不声明特定方法名的形式,将Mixin的所有方法都添加到Car里
augment(Car, Mixin);
var mySportsCar = new Car({
model: "Porsche",
color: "red"
});
mySportsCar.driveSideways();
// 输出:
// drive sideways
优点和缺点
Mixin 有助于减少系统中的重复功能及增加函数复用。当一个应用程序可能需要在各种对象实例中共享行为时,我们可以通过在 Mixin 中维持这种共享功能并专注于仅实现系统中真正不同的功能,来轻松避免任何重复。
也就是说,有关Mixin的缺点是稍有争议的。有些开发人员认为将功能注入对象原型中是一种很糟糕的想法,因为它会导致原型污染和函数起源方面的不确定性。在大型系统中,可能就会有这种情况。
我认为,强大的文档有助于将与混入函数来源有关的困惑减至最低,但对于每一种模式,如果在实现期间多加注意,一切应该会很顺利。
Decorator 是一种结构型设计模式,旨在促进代码复用。与 Mixin 相类似,它们可以被认为是另一个可行的对象子类化的替代方案。
通常,Decorator提供了将行为动态添加至系统的现有类的能力。其想法是,装饰本身对于类原有的基本功能来说并不是必要的;否则,它就可以被合并到超类本身了。
装饰者可以用于修改现有的系统,希望在系统中为对象添加额外的功能,而不需要大量修改使用它们的底层代码。开发人员使用它们的一个共同原因是,应用程序可能包含需要大量不同类型对象的功能。想象一下,如果必须为一个JavaScript游戏定义数百种不同的对象构造函数会怎么样(见图9-11)。
图9-11 Decorator模式
对象构造函数可以代表不同的玩家类型,每个类型都有不同的功能。魔戒游戏可能需要Hobbit、Elf、Orc、Wizard、Mountain Giant、Stone Giant等构造函数,甚至有可能数以百计。如果我们把功能作为因素计算,可以想象必须为每个能力类型组合创建子类——如:HobbitWithRing、HobbitWithSword、HobbitWithRingAndSword等等。当计算越来越多的不同能力时,这并不是很实用,当然也是不可控的。
Decorator 模式并不严重依赖于创建对象的方式,而是关注扩展其额外功能。我们使用了一个单一的基本对象并逐步添加提供额外功能的 decorator 对象,而不是仅仅依赖于原型继承。这个想法是:向基本对象添加(装饰)属性或方法,而不是进行子类化,因此它较为精简。
在JavaScript中向对象添加新属性是一个非常简单的过程,所以带着这种想法,可以实现一个非常简单的decorator,如下所示(示例9-7和示例9-8):
示例9-7 使用新功能装饰构造函数
// 车辆vehicle构造函数
function vehicle(vehicleType) {
// 默认值
this.vehicleType = vehicleType || "car";
this.model = "default";
this.license = "00000-000";
}
// 测试基本的vehicle实例
var testInstance = new vehicle("car");
console.log(testInstance);
// 输出:
// vehicle: car, model:default, license: 00000-000
// 创建一个vehicle实例进行装饰
var truck = new vehicle("truck");
// 给truck装饰新的功能
truck.setModel = function (modelName) {
this.model = modelName;
};
truck.setColor = function (color) {
this.color = color;
};
// 测试赋值操作是否正常工作
truck.setModel("CAT");
truck.setColor("blue");
console.log(truck);
// 输出:
// vehicle:truck, model:CAT, color: blue
// 下面的代码,展示vehicle依然是不被改变的
var secondInstance = new vehicle("car");
console.log(secondInstance);
// 输出:
// vehicle: car, model:default, license: 00000-000
这种类型的简单实现是可行的,但它并不能真正证明装饰者所提供的所有优势。为此,首先要查阅一下改编的咖啡示例,该示例来自Freeman、Sierra和Bates所著的一本名为《深入浅出设计模式》书籍,它围绕的是模拟购买苹果笔记本。
示例9-8 使用多个Decorator装饰对象
// 被装饰的对象构造函数
function MacBook() {
this.cost = function () { return 997; };
this.screenSize = function () { return 11.6; };
}
// Decorator 1
function Memory(macbook) {
var v = macbook.cost();
macbook.cost = function () {
return v + 75;
};
}
// Decorator 2
function Engraving(macbook) {
var v = macbook.cost();
macbook.cost = function () {
return v + 200;
};
}
// Decorator 3
function Insurance(macbook) {
var v = macbook.cost();
macbook.cost = function () {
return v + 250;
};
}
var mb = new MacBook();
Memory(mb);
Engraving(mb);
Insurance(mb);
//输出: 1522
console.log(mb.cost());
//输出: 11.6
console.log(mb.screenSize());
在这个示例中,Decorator 重写 MacBook()超类对象的.cost()函数来返回 MacBook的当前价格加上特定的升级价格。
我们认为装饰作为并没有重写原始Macbook对象的构造函数方法(如screenSize()),为Macbook定义的其他属性也一样,依然保持不变并完好无损。
实际上在前面的示例中没有已定义的接口,从创建者移动到接收者时,我们转移了确保一个对象符合接口要求的职责。
现在,我们要查看Dustin Diaz和Ross Harmes所著的《JavaScript设计模式》(PJDP)一书中提出的装饰者变体。
不像早些时候的一些示例,Diaz 和 Harmes 更关注如何在其他编程语言(如 Java或C++)中使用“接口”的概念实现装饰者,我们稍后将对其进行更详细地定义。
这个 Decorator 模式的特殊变体是用于引用目的。如果你发现它过于复杂,我建议选择前面介绍的较简单实现。
9.12.1.1 接口
PJDP将Decorator模式描述为一种用于在相同接口的其他对象内部透明地包装对象的模式。接口应该是对象定义方法的一种方式,但是,它实际上并不直接指定如何实现这些方法。
接口还可以定义接收哪些参数,但这些都是可选的。
那么,我们为什么要在JavaScript中使用接口呢?其想法是:它们可以自我记录,并能促进可复用性。理论上,通过确保实现类保持和接口相同的改变,接口可以使代码变得更加稳定。
下面是使用鸭子类型在JavaScript中实现接口的一个示例,这种方法帮助确定一个对象是否是基于其实现方法的构造函数/对象的实例。
// 用事先定义好的接口构造函数创建接口,该函数将接口名称和方法名称作为参数
// 在reminder示例中,summary()和placeOrder()描绘的功能,接口应该支持
var reminder = new Interface("List", ["summary", "placeOrder"]);
var properties = {
name: "Remember to buy the milk",
date: "05/06/2016",
actions: {
summary: function () {
return "Remember to buy the milk, we are almost out!";
},
placeOrder: function () {
return "Ordering milk from your local grocery store";
}
}
};
// 创建构造函数实现上述属性和方法
function Todo(config) {
// 为了支持这些功能,接口示例需要检查这些功能
Interface.ensureImplements(config.actions, reminder);
this.name = config.name;
this.methods = config.actions;
}
// 创建Todo构造函数的新实例
var todoItem = Todo(properties);
// 最后测试确保新增加的功能可用
console.log(todoItem.methods.summary());
console.log(todoItem.methods.placeOrder());
// 输出:
// Remember to buy the milk, we are almost out!
// Ordering milk from your local grocery store
在这个示例中, Interface.ensureImplements 提供了严格的功能检查。在这里(https://gist.github.com/1057989)可以找到它的代码以及Interface构造函数的代码。
接口的最大问题是,在JavaScript中没有为它们提供内置支持,试图模仿可能不太合适的另外一种语言特性是有风险的。可以在不花费大量性能成本的情况下使用享元接口,但我们将继续看一下使用相同概念的抽象装饰者。
9.12.1.2 抽象Decorator(抽象装饰者)
为了演示该版本Decorator模式的结构,假设我们有一个超类,再次模拟Macbook,以及模拟一个商店允许我们“装饰”苹果笔记本并收取增强功能的额外费用。
增强功能可以包括将内存升级到 4GB 或 8GB、雕刻、Parallels 或外壳。如果为每个增强选项组合使用单个子类来模拟它,看起来可能就是这样的:
var Macbook = function () {
//...
};
var MacbookWith4GBRam = function () { },
MacbookWith8GBRam = function () { },
MacbookWith4GBRamAndEngraving = function () { },
MacbookWith8GBRamAndEngraving = function () { },
MacbookWith8GBRamAndParallels = function () { },
MacbookWith4GBRamAndParallels = function () { },
MacbookWith8GBRamAndParallelsAndCase = function () { },
MacbookWith4GBRamAndParallelsAndCase = function () { },
MacbookWith8GBRamAndParallelsAndCaseAndInsurance = function () { },
MacbookWith4GBRamAndParallelsAndCaseAndInsurance = function () { };
…等等。
这将是一个不切实际的解决方案,因为每个可用的增强功能组合都需要一个新的子类。为了让事情变得简单点,而不需维护大量的子类,让我们来看看可以如何使用装饰者来更好地解决这个问题。
我们只需创建五个新的装饰者类,而不是需要之前看到的所有组合。在这些增强类上调用的方法将被传递给Macbook类。
在接下来的示例中,装饰者会透明地包装它们的组件,由于使用了相同的接口,它们可以进行相互交换。
如下是我们将为Macbook定义的接口:
var Macbook = new Interface("Macbook",
["addEngraving",
"addParallels",
"add4GBRam",
"add8GBRam",
"addCase]);
// Macbook Pro可能需要如下这样来描述:
var MacbookPro = function () {
// 实现Macbook
};
MacbookPro.prototype = {
addEngraving: function () {
},
addParallels: function () {
},
add4GBRam: function () {
},
add8GBRam: function () {
},
addCase: function () {
},
getPrice: function () {
// 基本价格
return 900.00;
}
};
为了便于我们添加后期需要的更多选项,我们定义了一个具有默认方法的抽象装饰者类来实现 Macbook 接口,其余的选项则划入子类。抽象装饰者确保我们可以装饰出一个独立的,而且多个装饰者在不同组合下都需要的基类(还记得前面的示例吗?),而不需要为每一个可能的组合都派生子类。
// Macbook装饰者抽象装饰者类
var MacbookDecorator = function (macbook) {
Interface.ensureImplements(macbook, Macbook);
this.macbook = macbook;
};
MacbookDecorator.prototype = {
addEngraving: function () {
return this.macbook.addEngraving();
},
addParallels: function () {
return this.macbook.addParallels();
},
add4GBRam: function () {
return this.macbook.add4GBRam();
},
add8GBRam: function () {
returnthis.macbook.add8GBRam();
},
addCase: function () {
return this.macbook.addCase();
},
getPrice: function () {
return this.macbook.getPrice();
}
};
上面的示例演示的是:Macbook decorator接受一个对象作为组件。它使用了我们前面定义的Macbook接口,针对每个方法,在组件上会调用相同的方法。我们现在可以仅通过使用MacbookDecorator创建选项类;简单调用超类构造函数,必要时可以重写任何方法。
var CaseDecorator = function (macbook) {
// 接下来调用超类的构造函数
this.superclass.constructor(macbook);
};
// 扩展超类
extend(CaseDecorator, MacbookDecorator);
CaseDecorator.prototype.addCase = function () {
return this.macbook.addCase() + "Adding case to macbook";
};
CaseDecorator.prototype.getPrice = function () {
return this.macbook.getPrice() + 45.00;
};
正如我们可以看到的,其中的大部分内容都是很容易实现的。我们所做的是重写需要装饰的addCase()和getPrice()方法,首先执行该组件的原有方法,然后加上额外的内容(文本或价格)。到目前为止本节已展示了很多的信息了,让我们试着将所有内容整合到一个示例中,希望能够加强所学到的内容。
// 实例化macbook
var myMacbookPro = new MacbookPro();
//输出: 900.00
console.log(myMacbookPro.getPrice());
// 装饰macbook
myMacbookPro = new CaseDecorator(myMacbookPro);
// 返回的将是945.00
console.log(myMacbookPro.getPrice());
由于装饰者可以动态地修改对象,因此它们是一种用于改变现有系统的完美模式。有时候,为对象创建装饰者比维护每个对象类型的单个子类要简单一些。可以让可能需要大量子类对象的应用程序的维护变得更加简单。
与我们已经涉及的其他模式一样,也有一些使用jQuery实现的装饰者模式的示例。jQuery.extend()允许我们在运行时或者在随后一个点上动态地将两个或两个以上的对象(和它们的属性)一起扩展(或合并)为一个单一对象。
在这种情况下,一个目标对象可以用新功能来装饰,而不会在源/超类对象中破坏或重写现有的方法(虽然这是可以做到的)。
在接下来的示例中定义三个对象:defaults、options和settings。该任务的目的是为了装饰defaults对象,将options中的额外功能附加到defaults上。我们必须首先使defaults保持未接触状态,并且保持稍后可以访问其属性或函数的能力;然后,给 defaults赋予使用装饰属性和函数的能力,这些装饰属性和函数是从options里获取的:
var decoratorApp = decoratorApp || {};
// 定义要使用的对象
decoratorApp = {
defaults: {
validate: false,
limit: 5,
name: "foo",
welcome: function () {
console.log("welcome!");
}
},
options:{
validate: true,
name: "bar",
helloWorld: function () {
console.log("hello world");
}
},
settings: {},
printObj: function (obj) {
var arr = [],
next;
$.each(obj, function (key, val) {
next = key + ": ";
next += $.isPlainObject(val) ? printObj(val) : val;
arr.push(next);
});
return "{ " + arr.join(", ") + " }";
}
};
// 合并defaults和options,没有显式修改defaults
decoratorApp.settings = $.extend({}, decoratorApp.defaults, decoratorApp. options);
// 这里所做的就是装饰可以访问defaults属性和功能的方式(options也一样),defaults本身未作改变
$("#log")
.append(decoratorApp.printObj(decoratorApp.settings) +
+decoratorApp.printObj(decoratorApp.options) +
+decoratorApp.printObj(decoratorApp.defaults));
// settings -- { validate: true, limit: 5, name: bar,
// welcome: function (){ console.log( "welcome!" ); },
// helloWorld: function (){ console.log("hello!"); } }
// options -- { validate: true, name: bar,
helloWorld: function (){ console.log("hello!"); } }
// defaults -- { validate: false, limit: 5, name: foo,
welcome: function (){ console.log("welcome!"); } }
开发人员喜欢使用这种模式,因为它使用时可以是透明的,并且也是相当灵活的:正如我们所看到的,对象可以被新行为包装或“装饰”,然后可以继续被使用,而不必担心被修改的基本对象。在一个更广泛的上下文中,这种模式也使我们不必依靠大量的子类来获得同样的好处。
但是在实现该模式时,也有一些缺陷是我们应该要注意的。如果管理不当,它会极大地复杂化应用程序架构,因为它向我们的命名空间引入了很多小型但类似的对象。让人担心的是,除了对象变得难以管理,其他不熟悉这个模式的开发人员可能难以理解为什么使用它。
大量的评论或模式研究应该有助于解决后者的问题,但是,只要我们继续把握住在应用程序中使用装饰者的广度,在这两方面就应该可以做得很好。
Flyweight 模式是一种经典的结构型解决方案,用于优化重复、缓慢及数据共享效率较低的代码。它旨在通过与相关的对象共享尽可能多的数据来减少应用程序中内存的使用(如:应用程序配置、状态等,见图9-12)。
图9-12 Flyweight模式
该模式最早是由Paul Calder和Mark Linton于 1990年构思出来,它以拳击重量级别命名,它包括重量不到112磅的拳手。Flyweight这个名字是源自这一重量级别,因为它所指的是:模式旨在帮助我们实现的轻量级(内存占用)。
在实践中,Flyweight 数据共享会涉及获取多个对象使用的若干相似对象或数据结构,以及将这些数据放到一个单一的外部对象中。我们可以将该对象传递给依赖这些数据的对象,而不是在每一个对象都存储相同的数据。
Flyweight 模式的应用方式有两种。第一种是用于数据层,处理内存中保存的大量相似对象的共享数据。
第二种是用于 DOM 层,Flyweight 可以用作中央事件管理器,来避免将事件处理程序附加到父容器中的每个子元素上,而是将事件处理程序附加到这个父容器上。
鉴于数据层是Flyweight模式最常使用的地方,我们首先要对它进行了解。
对于该应用程序,还有一些经典 Flyweight 模式的概念我们需要注意。在 Flyweight模式中,有个有关两个状态的概念——内部和外部。对象中的内部方法可能需要内部信息,没有内部信息,它们就绝对无法正常运行。但外部信息是可以被删除的或是可以存储在外部的。
具有相同内部数据的对象可以被替换为一个由 factory 方法创建的单一共享对象。这使我们可以极大减少存储隐式数据的总数量。
这么做的好处是,我们能够密切关注已经被实例化的对象,这样新副本就只需要创建与现有对象不同的部分就可以了。
我们使用管理器来处理外部状态。如何实现管理器是不固定的,但有一种方法就是让管理器对象包含一个外部状态的中央数据库以及这些外部状态所属的享元对象。
由于近年来 Flyweight 模式还没有在 JavaScript 中大量使用,很多给我们带来启发的相关实现都是来自Java和C++。
享元模式的首次代码实现就是我所写的 JavaScript 实现,该实现基于维基百科Flyweight模式的Java示例(http://en.wikipedia.org/wiki/Flyweight_pattern)。
在这个实现中我们将利用三种类型的Flyweight组件,它们是:
Flyweight(享元)
描述一个接口,通过这个接口flyweight可以接受并作用于外部状态。
Concrete flyweight(具体享元)
实现Flyweight接口,并存储内部状态。Concrete Flyweight对象必须是可共享的,并能够控制外部状态。
Flyweight factory(享元工厂)
创建并管理flyweight对象。确保合理地共享flyweight,并将它们当作一组对象进行管理,并且如果我们需要单个实例时,可以查询这些对象。如果该对象已经存在则直接返回,否则,创建新对象并返回。
它们与实现中的下列定义相对应:
• CoffeeOrder:享元
• CoffeeFlavor:具体享元
• CoffeeOrderContext:辅助器
• CoffeeFlavorFactory:享元工厂
• testFlyweight:享元的应用
鸭子补丁“实现”
鸭子补丁(Duck punching)使我们无需修改运行时源,就可以扩展一种语言或解决方案的功能。由于下一个解决方案要求使用 Java关键字(implements)来实现接口,并且无法在原生JavaScript中找到,所以让我们首先对它进行鸭子补丁。
Function.prototype.implementsFor 作用于一个对象构造函数,并将接受一个父类(函数)或对象,或者使用普通继承(函数)或虚拟继承(对象)来继承它。
//在JS里模拟纯虚拟继承 implement
Function.prototype.implementsFor = function (parentClassOrObject) {
if (parentClassOrObject.constructor === Function)
{
// 正常继承
this.prototype = new parentClassOrObject();
this.prototype.constructor = this;
this.prototype.parent = parentClassOrObject.prototype;
}
else {
// 纯虚拟继承
this.prototype = parentClassOrObject;
this.prototype.constructor = this;
this.prototype.parent = parentClassOrObject;
}
return this;
};
通过使一个函数显式地继承一个接口,可以用它来为缺少的 implements 关键字打上补丁。在下面的代码里,CoffeeFlavor实现了CoffeeOrder接口,且必须包含它的接口方法,以便将功能的实现赋值给对象。
// 享元对象
var CoffeeOrder = {
// 接口
serveCoffee: function (context) { },
getFlavor: function () { }
};
// 实现CoffeeOrder的具体享元对象
function CoffeeFlavor(newFlavor) {
var flavor = newFlavor;
// 如果已经为某一功能定义了接口,则实现该功能
if (typeofthis.getFlavor === "function") {
this.getFlavor = function () {
return flavor;
};
}
if (typeofthis.serveCoffee === "function") {
this.serveCoffee = function (context) {
console.log("Serving Coffee flavor "
+ flavor
+ " to table number "
+ context.getTable());
};
}
}
// 为CoffeeOrder实现接口
CoffeeFlavor.implementsFor(CoffeeOrder);
// 处理coffee订单的table数
function CoffeeOrderContext(tableNumber) {
return {
getTable: function () {
return tableNumber;
}
};
}
// 享元工厂对象
function CoffeeFlavorFactory() {
var flavors = [],
return {
getCoffeeFlavor: function (flavorName) {
var flavor = flavors[flavorName];
if (flavor === undefined) {
flavor = new CoffeeFlavor(flavorName);
flavors.pushc [flavorName],flavor]);
}
return flavor;
},
getTotalCoffeeFlavorsMade: function () {
return flavors.length;
}
}
};
// 样例用法:
// testFlyweight()
function testFlyweight() {
// 已订购的flavor.
var flavors = new CoffeeFlavor(),
// 订单table
tables = new CoffeeOrderContext(),
// 订单数量
ordersMade = 0,
//TheCoffeeFlavorFactory 实例
flavorFactory;
function takeOrders(flavorIn, table) {
flavors[ordersMade] = flavorFactory.getCoffeeFlavor(flavorIn);
tables[ordersMade++] = new CoffeeOrderContext(table);
}
flavorFactory = new CoffeeFlavorFactory();
takeOrders("Cappuccino", 2);
takeOrders("Cappuccino", 2);
takeOrders("Frappe", 1);
takeOrders("Frappe", 1);
takeOrders("Xpresso", 1);
takeOrders("Frappe", 897);
takeOrders("Cappuccino", 97);
takeOrders("Cappuccino", 97);
takeOrders("Frappe", 3);
takeOrders("Xpresso", 3);
takeOrders("Cappuccino", 3);
takeOrders("Xpresso", 96);
takeOrders("Frappe", 552);
takeOrders("Cappuccino", 121);
takeOrders("Xpresso", 121);
for (var i = 0; i < ordersMade; ++i) {
flavors[i].serveCoffee(tables[i]);
}
console.log(" ");
console.log("total CoffeeFlavor objects made: " + flavorFactory.
getTotalCoffeeFlavorsMade());
}
接下来,通过实现一个系统来管理图书馆中的所有书籍,让我们来继续了解一下享元。每本书的重要元数据可以被分解成如下形式:
• ID
• Title
• Author
• Genre
• Page count
• Publisher ID
• ISBN
我们还将需要使用以下属性来跟踪哪些成员已借出了哪些书籍,借书日期以及预计返还的日期。
• checkoutDate
• checkoutMember
• dueReturnDate
• availability
因此每本书在使用享元模式进行优化之前,都会按如下方式表示:
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 () {
returnthis.ISBN;
},
// 鉴于篇幅,其他属性就暂不列出了
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);
}
};
刚开始对于少量书籍可能是行得通的,但是,当图书馆扩大到拥有一个更大的库存,并且每本书都有多个版本和副本时,就会发现随着时间的推移,管理系统运行得越来越慢。使用数以千计的书籍对象可能会淹没可用内存,但可以使用享元模式优化系统来改善这个问题。
现在可以将数据分成内部和外部状态,如下所示:与书籍对象(title、author 等)相关的数据是内部状态,而借出数据(checkoutMember、dueReturnDate 等)是外部状态。实际上这意味着,每个书籍属性组合只需要有一个Book对象。它仍然要处理相当多的对象,但比以前处理的对象明显减少了。
下面书籍元数据组合的单个实例将在指定书名的书籍副本之间共享。
// 享元优化版本
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;
};
正如我们可以看到的,外部状态已被删除。图书馆借出有关的所有事情都将转移给管理器,由于对象数据现在已被分割,可以使用工厂进行实例化。
现在让我们来定义一个基本的工厂。首先,必须要检查一下指定书名的书是否已在系统内部创建。如果已经创建,则返回它;如果没有,就会创建并存储这本新书,以便以后可以访问它。这确保我们仅为每一个特定的内部数据块创建一个拷贝:
// 书籍工厂单例
var BookFactory = (function () {
var existingBooks = {}, existingBook;
return {
createBook: function (title, author, genre, pageCount, publisherID, ISBN) {
// 如果书籍之前已经创建,则找出并返回它
// !!强制返回布尔值
existingBook = existingBooks[ISBN];
if (!!existingBook) {
return existingBook;
} else {
// 如果没找到,则创建一个该书的新实例,并保存
var book = new Book(title, author, genre, pageCount, publisherID, ISBN);
existingBooks[ISBN] = book;
return book;
}
}
};
});
接下来,我们需要存储从Book对象中删除的状态。幸运的是,可以使用管理器(我们会将它定义为一个单例)来封装它们。一个Book对象和借书成员的组合将被称为书籍记录。管理器会将它们存储起来,它还包括在Book类享元优化期间我们排除的与借出有关的逻辑。
// 书籍记录管理器单例
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);
}
};
});
这些代码修改的结果是,从Book类中提取的所有数据,现在被存储在BookManager单例(BookDatabase)的属性中,这比我们以前使用大量对象时的效率要高很多。现在与书籍出借相关的方法在这里成为了基础,因为它们处理的是外部数据,而不是内部数据。
这个过程给我们的最终解决方案上增加了一点复杂性,但与它所解决的性能问题相比,这只是一个小问题。它具有数据智能性,如果有30本完全相同的书,我们现在只需要存储它一次。同时,每个函数都占用内存。通过使用Flyweight模式,这些函数在一个地方(在管理器上)存在,而不是在每个对象上存在,从而节约更多的内存。
文档对象模型(DOM)支持两种方式让对象检测事件:自上而下(事件捕捉)和自下而上(事件冒泡)。
在事件捕捉中,事件首先被最外层的元素捕捉,然后传播到最里面的元素。在事件冒泡中,事件被捕捉并传递给最里面的元素,然后传播到外部元素。
在这个上下文中描述享元的最好比喻之一是由Gary Chisholm编写的,它是类似这样的:
试着用池塘的方式思考一下享元。一条鱼张开它的嘴(事件),气泡升到表面(冒泡),当气泡到达表面(动作)时,一只坐在顶部的苍蝇飞走了。在本例中,我们可以很容易地把鱼张开嘴转换成点一个按钮,气泡转换成冒泡效应,苍蝇飞走可转换成运行一些功能。
引入冒泡用于处理这些情况:一个单一的事件(如一次点击)可能是由DOM层级的不同级别所定义的多个事件处理程序进行处理。上述事情发生时,事件冒泡先执行为最低层级特定元素定义的事件处理程序。此后,事件在冒泡到更高级元素之前,先冒泡到包含的这些元素上。
享元可以用来进一步调整事件冒泡过程,正如我们即将要看到的(示例9-9)。
在第一个实际示例中,假设一个文档中有一些相似的元素,在用户对它们执行用户动作(如:点击、鼠标悬停)时执行同样相似的行为。
通常在构建我们自己的accordion 组件、菜单或其他基于列表的小部件时,我们要做的就是将一个点击事件绑定至父容器(如$('ul li a').on(..))中的每个链接元素上。其实不需将点击绑定至多个元素,我们就可以很容易地将享元附加到容器的顶部,它可以监听来自下面的事件。然后这些事情可以使用逻辑进行处理,逻辑与否简单取决于要求是否简单或复杂。
由于之前经常提到的组件类型的每个部分都有相同的重复标记(如accordion的每一节代码),有很大的机会是:被点击的每个元素的行为都和附近其他带有同名样式(class)元素的行为非常相似。利用这些信息,我们将使用享元来构建一个基本的accordion。
在 jQuery 用户将初始化点击绑定到一个容器 div 的同时,这里使用了一个stateManager命名空间来封装我们的享元逻辑。为了确保页面上没有其他相似逻辑处理程序附加在div容器上,刚开始就应用unbind事件。
现在要确定容器中的哪个子元素被点击,我们利用一个target检查,它提供了一个对被点击元素的引用,和父元素无关。然后,我们利用此信息来处理单击事件,而不是在页面加载时将事件绑定至特定的子元素上。
示例9-9 集中事件处理
如下是HTML代码:
<div id="container">
<div class="toggle" href="#">More Info (Address)
<span class="info">
This is more information
</span></div>
<div class="toggle" href="#">Even More Info (Map)
<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代码:
var stateManager = {
fly: function () {
var self = this;
$("#container").unbind().on("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");
}
};
这里的好处在于,我们将很多独立的动作转变成一个共享的动作(可能会节省内存)。
在第二个示例中,我们可以通过使用具有jQuery的享元模式进一步提高性能。
James Padolsey之前写了一篇名为《76 bytes for faster jQuery》的文章,文中他提醒我们:每次jQuery触发一个回调,无论何种类型(过滤器、每个、事件处理程序),我们都能够通过this关键字访问函数的上下文(DOM元素与它相关)。
可惜的是,我们中的很多人都已经习惯了在$()或jQuery()中包装this这个想法,这意味着每次构建jQuery的新实例都不是必要的。而不是像如下这样做:(示例9-10)示例9-10 使用Flyweight进行性能优化
$("div").on("click", function () {
console.log("You clicked: " + $(this).attr("id"));
});
// 我们需要避免使用DOM元素创建jQuery对象(像上面的代码那样),直接像下面这样使用DOM元素即可:
$("div").on("click", function () {
console.log("You clicked:" + this.id);
});
James希望在下列上下文中使用jQuery的jQuery.text;但是,他不同意的观点是:在每个迭代循环里创建新的jQuery对象。
$("a").map(function () {
return $(this).text();
});
在冗余的包装方面(这里可能是使用 jQuery 实用方法的情况下),最好使用jQuery.methodName(如:jQuery.text),而不是 jQuery.fn.methodName(如:jQuery. fn.text)。其中,methodName代表一个实用程序,例如each()或text。这样做不需要在每次调用我们的函数时,都调用更高一级的抽象或创建一个新的 jQuery 对象,因为jQuery.methodName是库本身在底层抽象所使用的方法,以助力 jQuery. fn. methodName。
因为不是所有的jQuery方法都有相应的单节点函数,所以Padolsey想出了jQuery. single工具这一概念。
这里的想法是:单一的jQuery对象被创建,用于每次对jQuery.single的调用(实际上意味着只有一个jQuery对象被创建)。可以在下面找到它的实现,由于我们是将多个可能对象的数据合并到一个更加集中的单一结构中,这在技术上讲也是享元。
jQuery.single = (function (o) {
var collection = jQuery([1]);
return function (element) {
// 将元素赋值给集合:
collection[0] = element;
// 返回集合:
return collection;
};
});
使用链接的示例如下所示:
$("div").on("click", function () {
var html = jQuery.single(this).next().html();
console.log(html);
});
虽然我们可能相信,简单缓存jQuery代码可能会提供相等的性能受益, Padolsey称仍然值得使用$.single,并且它可以表现的更好。这并不是说不需要使用任何缓存,只是要注意这种方法是对我们有帮助的。要进一步了解$.single方面的细节,我建议大家阅读Padolsey的完整文章。