八、编程和设计模式

现在,您已经了解了 JavaScript 的面向对象特性,例如原型和继承,并且已经看到了一些使用浏览器对象的实际例子,让我们继续前进,或者更确切地说,向上前进一个层次。让我们来看看 JavaScript 使用的一些常见模式。首先,让我们定义什么是模式。简而言之,模式是解决常见问题的好方法。

有时候,当你面临一个新的编程问题时,你可能马上意识到你已经解决了另一个类似的问题。在这种情况下,隔离这类问题并寻找共同的解决方案是值得的。模式是一类问题的经过验证的可重用解决方案(或解决方案的方法)。有时候一个模式只不过是一个想法或者一个名字,但是有时候仅仅使用一个名字可以帮助你更清楚地思考一个问题。此外,当与团队中的其他开发人员一起工作时,当每个人在讨论问题或解决方案时使用相同的术语时,沟通会容易得多。

有时可能会有这样的情况,你的问题很独特,不适合一个已知的模式。仅仅为了使用模式而盲目应用模式不是一个好主意。实际上,不使用一种模式(如果你不能想出一种新的模式)比试图改变你的问题以便它适合现有的解决方案要好。

本章讨论两种类型的模式:

  • 编码模式——这些大多是特定于 JavaScript 的最佳实践

  • 设计模式——这些是独立于语言的模式,由“四人帮”的书推广开来

编码模式

本章的第一部分讨论了一些反映 JavaScript 独特特性的模式。有些模式旨在帮助您组织代码(如名称空间模式),有些模式与提高性能有关(如惰性定义和初始化时分支),有些模式弥补了私有范围属性等缺失的功能。本节讨论的模式包括:

  • 分离行为

  • 命名空间

  • 初始分支

  • 懒惰定义

  • 配置对象

  • 私有变量和方法

  • 特权方法

  • 私有函数作为公共方法

  • 可自我执行的功能

  • 链接

  • JSON 的作用

分离行为

如前所述,网页的三个组成部分是:

  • 内容

  • 演示文稿

  • 行为(JavaScript)

含量

HTML 是网页的内容;实际的文本。应该使用最少量的能够充分描述内容语义的 HTML 标签来标记内容。例如,如果您正在处理导航菜单,使用<ul><li>可能是一个好主意,因为导航菜单基本上是一个链接列表。

您的内容(HTML)应该没有任何格式元素。视觉格式属于表示层,应该通过使用 CSS(级联样式表)来实现。这意味着:

  • 如果可能的话,不应该使用 HTML 标签的 style属性。

  • <font>这样的表现性 HTML 标签根本不应该使用。

  • 标签应该根据它们的语义来使用,而不是因为默认情况下浏览器如何呈现它们。例如,开发人员有时使用<div>标签,其中<p>更合适。用<strong><em>代替<b><i>也是有利的,因为后者描述的是视觉呈现而不是意义。

演示

一个很好的方法是重置或者取消所有浏览器的默认设置。比如使用雅虎的 reset.css!UI 库。这样,浏览器的默认呈现不会分散你有意识地思考要使用的正确语义标签。

行为

页面的第三个组成部分是行为。行为应该与内容和表现分开。行为通常通过使用隔离到<script>标签的 JavaScript 来添加,最好包含在外部文件中。这意味着不使用任何内联属性,如 onclick, onmouseover等。除此之外,你可以使用你在上一章已经看到的 addEventListener/attachEvent方法。

将行为与内容分开的最佳策略是:

  • 最小化<script>标签的数量

  • 避免内联事件处理程序

  • 不要使用 CSS 表达式

  • 当用户禁用 JavaScript 时,动态添加没有任何用途的标记

  • 在内容的最后,当您准备关闭<body>标签时,插入一个单独的外部.js文件

分离行为示例

假设您在一个页面上有一个搜索表单,并且希望用 JavaScript 验证该表单。因此,您继续保持表单标签不受任何 JavaScript 的影响,然后,在关闭</body>标签之前,您插入一个链接到外部文件的<script>标签。

<body>
<form id="myform" method="post" action="server.php">
<fieldset>
<legend>Search</legend>
<input
name="search"
id="search"
type="text"
/>
<input type="submit" />
</fieldset>
</form>
<script type="text/javascript" src="behaviors.js"></script>
</body>

behaviors.js中,您将一个事件监听器附加到提交事件。在监听器中,检查文本输入字段是否留空,如果是,则停止提交表单。以下是 behaviors.js的完整内容。它假设您已经通过上一章末尾的练习创建了您的 myevent实用程序:

// init
myevent.addListener('myform', 'submit', function(e){
// no need to propagate further
e = myevent.getEvent(e);
myevent.stopPropagation(e);
// validate
var el = document.getElementById('search');
if (!el.value) { // too bad, field is empty
myevent.preventDefault(e); // prevent the form submission
alert('Please enter a search string');
}
});

命名空间

应该避免使用全局变量,以降低变量命名冲突的可能性。最小化全局变量数量的一种方法是对变量和函数进行命名。想法很简单:只创建一个全局对象,所有其他变量和函数都成为该对象的属性。

作为命名空间的对象

让我们创建一个名为 MYAPP: 的全局对象

// global namespace
var MYAPP = MYAPP || {};

现在,您可以将它作为 MYAPP对象的一个 event属性,而不是一个全局 myevent实用程序(来自上一章)。

// sub-object
MYAPP.event = {};

将这些方法添加到 event实用程序中与通常的方法基本相同:

// object together with the method declarations
MYAPP.event = {
addListener: function(el, type, fn) {
// .. do the thing
},
removeListener: function(el, type, fn) {
// ...
},
getEvent: function(e) {
// ...
}
// ... other methods or properties
};

命名空间构造函数

使用命名空间并不妨碍您创建构造函数。下面是如何让一个 DOM 实用程序拥有一个 Element构造器,让你更容易地创建 DOM 元素。

MYAPP.dom = {};
MYAPP.dom.Element = function(type, prop){
var tmp = document.createElement(type);
for (var i in prop) {
tmp.setAttribute(i, prop[i]);
}
return tmp;
}

同样,如果需要,可以使用 Text构造函数来创建文本节点:

MYAPP.dom.Text = function(txt){
return document.createTextNode(txt);
}

使用构造函数在页面底部创建链接:

var el1 = new MYAPP.dom.Element(
'a',
{href:'http://phpied.com'}
);
var el2 = new MYAPP.dom.Text('click me');
el1.appendChild(el2);
document.body.appendChild(el1);

一个命名空间()方法

一些库,比如 YUI,实现了一个名字空间实用方法,让你的生活变得更容易,这样你可以做一些事情,比如:

MYAPP.namespace('dom.style');

而不是更冗长的:

MYAPP.dom = {};
MYAPP.dom.style = {};

以下是如何创建这样的 namespace()方法。首先,通过使用句点()拆分输入字符串来创建数组。)作为分隔符。然后,对于新数组中的每个元素,如果一个属性还不存在,就给全局对象添加一个属性。

var MYAPP = {};
MYAPP.namespace = function(name){
var parts = name.split('.');
var current = MYAPP;
for (var i in parts) {
if (!current[parts[i]]) {
current[parts[i]] = {};
}
current = current[parts[i]];
}
}

测试新方法:

MYAPP.namespace('event');
MYAPP.namespace('dom.style');

上面的结果和你做的一样:

var MYAPP = {
event: {},
dom: {
style: {}
}
}

初始时间分支

在前一章中,您看到不同的浏览器对于相同或相似的功能通常有不同的实现。在这种情况下,您需要根据当前执行脚本的浏览器所支持的内容来分支代码。根据您的程序,这种分支可能发生得太频繁,结果会降低脚本的执行速度。

您可以通过在初始化期间(脚本加载时)而不是在运行时分支代码的某些部分来缓解这个问题。基于动态定义函数的能力,您可以根据浏览器使用不同的主体来分支和定义相同的函数。让我们看看如何。

首先,让我们为 event实用程序定义一个名称空间和占位符方法。

var MYAPP = {};
MYAPP.event = {
addListener: null,
removeListener: null
};

此时,添加或移除侦听器的方法尚未实现。基于特征嗅探的结果,这些方法可以有不同的定义。

if (typeof window.addEventListener === 'function') {
MYAPP.event.addListener = function(el, type, fn) {
el.addEventListener(type, fn, false);
};
MYAPP.event.removeListener = function(el, type, fn) {
el.removeEventListener(type, fn, false);
};
} else if (typeof document.attachEvent === 'function'){ // IE
MYAPP.event.addListener = function(el, type, fn) {
el.attachEvent('on' + type, fn);
};
MYAPP.event.removeListener = function(el, type, fn) {
el.detachEvent('on' + type, fn);
};
} else { // older browsers
MYAPP.event.addListener = function(el, type, fn) {
el['on' + type] = fn;
};
MYAPP.event.removeListener = function(el, type, fn) {
el['on' + type] = null;
};
};

在这个脚本执行之后,您已经以依赖于浏览器的方式定义了 addListener()removeListener()方法。现在,每次您调用其中一个方法时,它都不会再进行任何功能嗅探,因此运行速度会更快,因为它做的工作更少。

嗅探特性时要注意的一点是,在检查一个特性后不要假设太多。在上面的例子中,这个规则被破坏了,因为代码只检查 add*支持,但是定义了 add*remove*方法。在这种情况下,假设在下一个版本的浏览器中,如果 IE 决定实现 addEventListener(),它也将实现 removeEventListener(),这可能是安全的。但是想象一下,如果 IE 实现了 stopPropagation()而不是 preventDefault()并且您没有单独检查这些,会发生什么。你已经假设因为 addEventListener()没有定义,浏览器就是 IE,用你对 IE 工作原理的了解来编写你的代码。请记住,你所有的知识都是基于 IE 今天的工作方式,但不一定是它明天的工作方式。因此,为了避免在新浏览器版本发布时对代码进行多次重写,最好单独检查您打算使用的功能,不要一概而论某个浏览器支持什么。

懒惰定义

惰性定义模式与之前的初始分支模式非常相似。不同的是,分支只在第一次调用函数时发生。当函数被调用时,它用最好的实现重新定义自己。与初始化时分支 if发生一次不同,在加载过程中,它可能根本不会发生——在函数从未被调用的情况下。惰性定义也使初始化过程变得更简单,因为没有初始化时的分支工作要做。

让我们看一个例子,通过定义一个 addListener()函数来说明这一点。该函数首先用一个通用体定义。它检查第一次调用浏览器时浏览器支持哪些功能,然后使用最合适的实现重新定义自己。在第一次调用结束时,函数调用自己,以便执行实际的事件附加。下一次你调用同一个函数时,它将被新的主体定义,并准备使用,所以不需要进一步的分支。

var MYAPP = {};
MYAPP.myevent = {
addListener: function(el, type, fn){
if (typeof el.addEventListener === 'function') {
MYAPP.myevent.addListener = function(el, type, fn) {
el.addEventListener(type, fn, false);
};
} else if (typeof el.attachEvent === 'function'){
MYAPP.myevent.addListener = function(el, type, fn) {
el.attachEvent('on' + type, fn);
};
} else {
MYAPP.myevent.addListener = function(el, type, fn) {
el['on' + type] = fn;
};
}
MYAPP.myevent.addListener(el, type, fn);
}
};

配置对象

当您有一个接受大量参数的函数或方法时,这种模式非常有用。多少构成“很多”由你决定,但一般来说,一个参数超过三个的函数调用起来可能不太方便,因为你要记住参数的顺序,有些参数是可选的就更不方便了。

您可以使用一个参数并使其成为一个对象,而不是有许多参数。对象的属性是实际参数。这特别适合于传递配置参数,因为这些参数往往很多,而且大多是可选的(带有智能默认值)。使用单个对象而不是多个参数的好处是:

  • 顺序不重要

  • 您可以轻松跳过不想设置的参数

  • 如果未来的需求需要,它使得函数签名很容易扩展

  • 它使代码更易读,因为 config 对象的属性及其名称都存在于调用代码中

假设您有一个用于创建输入按钮的 Button构造函数。它接受要放入按钮内部的文本(T2 标签的 value属性)和按钮的 type的可选参数。

// a constructor that creates buttons
var MYAPP = {};
MYAPP.dom = {};
MYAPP.dom.Button = function(text, type) {
var b = document.createElement('input');
b.type = type || 'submit';
b.value = text;
return b;
}

使用构造函数很简单;你只要给它一根绳子。然后,您可以将新按钮附加到文档正文中:

document.body.appendChild(new MYAPP.dom.Button('puuush'));

这一切都很好,工作正常,但是您决定您也希望能够设置按钮的一些样式属性,例如颜色和字体。你可能会得到这样一个定义:

MYAPP.dom.Button = function(text, type, color, border, font) {
// ....
}

现在使用构造函数可能会变得有些不方便,例如当您想要设置第三个和第五个参数,而不是第二个或第四个参数时:

new MYAPP.dom.Button('puuush', null, 'white', null, 'Arial, Verdana, sans-serif');

更好的方法是对所有设置使用一个配置对象参数。函数定义可以类似于:

MYAPP.dom.Button = function(text, conf) {
var type = conf.type || 'submit';
var font = conf.font || 'Verdana';
// ...
}

使用构造函数:

var config = {
font: 'Arial, Verdana, sans-serif',
color: 'white'
};
new MYAPP.dom.Button('puuush', config);

另一个用法示例:

document.body.appendChild(
new MYAPP.dom.Button('dude', {color: 'red'})
);

如您所见,只设置选定的参数并切换它们的顺序很容易。此外,它更友好,当您在调用方法时看到参数的名称时,代码更容易理解。

私有属性和方法

JavaScript 没有访问修饰符的概念,后者设置对象中属性的特权。古典语言通常有访问修饰语,如:

  • 公共—对象的所有用户都可以访问这些属性(或方法)

  • 私有—只有对象本身可以访问这些属性

  • 受保护—只有继承相关对象的对象才能访问这些属性

JavaScript 没有特殊的语法来表示私有属性,但是正如第 3 章中所讨论的,您可以在构造函数内部使用局部变量和方法,并实现相同级别的保护。

继续 Button构造函数的例子,您可以有一个包含所有默认值的局部变量 styles和一个局部 setStyle()函数。这些对于构造函数之外的代码是不可见的。以下是 Button如何利用本地私有财产:

var MYAPP = {};
MYAPP.dom = {};
MYAPP.dom.Button = function(text, conf) {
var styles = {
font: 'Verdana',
border: '1px solid black',
color: 'black',
background: 'grey'
};
function setStyles() {
for (var i in styles) {
b.style[i] = conf[i] || styles[i];
}
}
conf = conf || {};
var b = document.createElement('input');
b.type = conf['type'] || 'submit';
b.value = text;
setStyles();
return b;
};

在这个实现中, styles是私有属性, setStyle()是私有方法。构造函数在内部使用它们(并且它们可以访问构造函数内部的任何内容),但是它们不可用于函数外部的代码。

特权方法

特权方法(这个术语是道格拉斯·克洛克福特创造的)是可以访问私有方法或属性的普通公共方法。它们可以像一座桥梁一样,让一些私有功能变得可访问,但是以一种受控的方式,用一种特权方法包装。

继续前面的例子,你可以创建一个返回 stylesgetDefaults()方法。这样, Button构造函数外的代码可以看到默认样式,但不能修改它们。在这种情况下 getDefaults()将是一种特权方法。

私有函数作为公共方法

假设您已经定义了一个绝对需要保持完整的函数,因此您将其设为私有。但是您也希望提供对同一函数的访问,以便外部代码也能从中受益。在这种情况下,您可以将私有函数分配给公共可用的属性。

让我们将 _setStyle()_getStyle()定义为私有功能,然后将它们分配给公共的 setStyle()getStyle():

var MYAPP = {};
MYAPP.dom = (function(){
var _setStyle = function(el, prop, value) {
console.log('setStyle');
};
var _getStyle = function(el, prop) {
console.log('getStyle');
};
return {
setStyle: _setStyle,
getStyle: _getStyle,
yetAnother: _setStyle
};
})()

现在如果调用 MYAPP.dom.setStyle(),会调用私有的 _setStyle()函数。您也可以从外部覆盖 setStyle():

MYAPP.dom.setStyle = function(){alert('b')};

现在的结果将是:

  • MYAPP.dom.setStyle指向新功能

  • MYAPP.dom.yetAnother仍然指向 _setStyle()

  • 当任何其他内部代码依赖于它按预期工作时_setStyle()总是可用的,而不管外部代码

自动执行功能

另一种有助于保持全局命名空间干净的有用模式是将代码包装在一个匿名函数中,并立即执行该函数。这样,函数中的任何变量都是局部变量(只要使用 var语句),并且在函数返回时被销毁,如果它们不是闭包的一部分的话。这种模式在第三章中有更详细的讨论。

(function(){
// code goes here...
})()

这种模式特别适用于脚本加载时执行的一次性初始化任务。

可以扩展自执行模式来创建和返回对象。如果这些对象的创建比较复杂,并且涉及一些初始化工作,那么您可以在自执行函数的第一部分和 return单个对象中完成,该对象可以访问顶部的任何私有属性并从中受益:

var MYAPP = {};
MYAPP.dom = function(){
// initialization code...
function _private(){
// ... body
}
return {
getStyle: function(el, prop) {
console.log('getStyle');
_private();
},
setStyle: function(el, prop, value) {
console.log('setStyle');
}
};
}();

链接

链接是一种模式,它允许您在一行调用方法,就像方法是链中的链接一样。当调用几个相关的方法时,这可能非常方便。基本上,您可以根据上一个方法的结果调用下一个方法,而不使用中间变量。

假设您已经创建了一个构造函数来帮助您处理 DOM 元素。创建一个新的<span>并将其添加到<body>的代码如下所示:

var obj = new MYAPP.dom.Element('span');
obj.setText('hello');
obj.setStyle('color', 'red');
obj.setStyle('font', 'Verdana');
document.body.appendChild(obj);

如您所知,构造函数返回它们创建的 this对象。您可以使您的方法如 setText()setStyle()也返回 this,这将允许您对前一个返回的实例调用下一个方法。这样,您可以链接方法调用:

var obj = new MYAPP.dom.Element('span');
obj.setText('hello')
.setStyle('color', 'red')
.setStyle('font', 'Verdana');
document.body.appendChild(obj);

如果您不打算在新元素添加到树中之后使用 obj变量,那么您可能甚至不需要它,因此代码可能如下所示:

document.body.appendChild(
new MYAPP.dom.Element('span')
.setText('hello')
.setStyle('color', 'red')
.setStyle('font', 'Verdana')
);

jQuery 大量使用链接模式;这可能是这个受欢迎的图书馆最容易识别的特征之一。

让我们用几句关于 JSON 的话来结束本章的编码模式部分。JSON 在技术上不是一种编码模式,但是可以说使用 JSON 是一种有用的模式。

JSON 是一种流行的轻量级数据交换格式。当使用 XMLHttpRequest()从服务器检索数据时,它通常比 XML 更受欢迎。JSON 代表 JavaScript 对象符号,除了它非常方便之外,没有什么特别有趣的。JSON 格式由使用对象和数组文字定义的数据组成。下面是一个 JSON 字符串的例子,您的服务器可以在 XHR 请求后用它来响应。

{
'name': 'Stoyan',
'family': 'Stefanov',
'books': ['phpBB2', 'phpBB UG', 'PEAR']
}

这种情况的一个 XML 等价物类似于:

<?xml version="1.1" encoding="iso-8859-1"?>
<response>
<name>Stoyan</name>
<family>Stefanov</family>
<books>
<book>phpBB2</book>
<book>phpBB UG</book>
<book>PEAR</book>
</books>
</response>

首先,您可以看到 JSON 在字节数方面有多轻。但是主要的好处不是更小的字节大小,而是在 JavaScript 中使用 JSON 非常容易。假设您发出了一个 XHR 请求,并在 XHR 对象的 responseText属性中收到了一个 JSON 字符串。只需使用 eval():,就可以将这一串数据转换成一个工作的 JavaScript 对象

var obj = eval( '(' + xhr.responseText + ')' );

现在您可以访问 obj中的数据作为对象属性:

alert(obj.name); // Stoyan
alert(obj.books[2]); // PEAR

eval()的问题是它不安全,所以最好使用一点点http://json.org/提供的 JavaScript 库来解析 JSON 数据。从 JSON 字符串创建对象仍然很简单:

var obj = JSON.parse(xhr.responseText);

由于其简单性,JSON 作为一种独立于语言的数据交换格式很快变得流行起来,您可以使用自己喜欢的语言在服务器端轻松生成 JSON。例如,在 PHP 中,有函数 json_encode()json_decode()可以让您将 PHP 数组或对象序列化为 JSON 字符串,反之亦然。

设计模式

本章的第二部分介绍了一种 JavaScript 方法,该方法是由名为设计模式:可重用面向对象软件的元素一书引入的设计模式的子集,这是一本有影响力的书,通常被称为四书或“四人帮”,或 GoF (以其四位作者命名)。 GoF 书中讨论的模式分为三组:

  • 处理对象如何被创建的创造模式**(实例化)

  • 结构模式描述不同的对象如何组成以提供新的功能

  • 行为模式,描述对象之间交流的方式

四书共有 23 个图案,自该书出版后,更多的图案被识别出来。讨论所有这些都超出了本书的范围,因此本章的剩余部分将只演示其中的四个,以及在 JavaScript 中实现这四个的示例。请记住,模式更多的是关于接口和关系,而不是实现。一旦理解了设计模式,实现它通常并不难,尤其是在动态语言中,比如 JavaScript。

本章其余部分讨论的模式是:

  • 一个

  • 工厂

  • 装饰者

  • 观察者

单胎

Singleton 是一种创造性的设计模式,这意味着它的重点是创建对象。当您想要确保给定的种类或类只有一个对象时,这很有用。在经典语言中,这意味着一个类的实例只被创建一次,任何随后创建同一类的新对象的尝试都将返回原始实例。

在 JavaScript 中,因为没有类,所以单例是默认的也是最自然的模式。每个对象都是一个单独的对象。如果你不复制它,也不把它作为另一个对象的原型,它将仍然是同类对象中唯一的一个。

JavaScript 中单例最基本的实现是对象文字:

var single = {};

单例 2

如果你想使用类一样的语法并且仍然实现单例,事情会变得更有趣。假设您有一个名为 Logger()的构造函数,并且您希望能够执行类似于:的操作

var my_log = new Logger();
my_log.log('some event');
// ... 1000 lines of code later ...
var other_log = new Logger();
other_log.log('some new event');
alert(other_log === my_log); // true

其思想是,虽然您使用 new,但只需要创建一个实例,然后在连续的调用中返回这个实例。

全局变量

一种方法是使用全局变量来存储单个实例。您的构造函数可能如下所示:

function Logger() {
if (typeof global_log === "undefined") {
global_log = this;
}
return global_log;
}

使用此构造函数会得到预期的结果:

var a = new Logger();
var b = new Logger();
alert(a === b); // true

缺点当然是使用了全局变量。它可以在任何时候被覆盖,即使是意外的,并且您会丢失实例。相反,您的全局变量覆盖其他人的全局变量也是可能的。

施工方的属性

众所周知,函数是对象,它们有属性。您可以将单个实例分配给构造函数的属性。

function Logger() {
if (typeof Logger.single_instance === "undefined") {
Logger.single_instance = this;
}
return Logger.single_instance;
}

如果写 var a = new Logger(), a会指向新创建的 Logger.single_instance属性。随后的呼叫 var b = new Logger()将导致 b指向相同的 Logger.single_instance房产,这正是您想要的。

这种方法当然解决了全局命名空间问题,因为没有创建全局变量。唯一的缺点是 Logger构造函数的属性是公开可见的,所以可以随时覆盖。在这种情况下,单个实例可能会丢失或修改。

在私人财产中

覆盖公共可见属性问题的解决方案是不使用公共属性,而是使用私有属性。您已经知道如何用闭包保护变量,因此作为练习,您可以将这种方法实现到单例模式中。

工厂

工厂是另一种创造性的设计模式,因为它处理创建对象。当您有类似类型的对象,并且您事先不知道要使用哪个对象时,工厂非常有用。根据用户输入或其他标准,您的代码可以随时确定所需的对象类型。

假设您有三个不同的构造函数来实现类似的功能。他们创建的对象都有一个网址,但使用它做不同的事情。一个创建文本 DOM 节点;第二个创建链接,第三个创建图像。

var MYAPP = {};
MYAPP.dom = {};
MYAPP.dom.Text = function() {
this.insert = function(where) {
var txt = document.createTextNode(this.url);
where.appendChild(txt);
};
};
MYAPP.dom.Link = function() {
this.insert = function(where) {
var link = document.createElement('a');
link.href = this.url;
link.appendChild(document.createTextNode(this.url));
where.appendChild(link);
};
};
MYAPP.dom.Image = function() {
this.insert = function(where) {
var im = document.createElement('img');
im.src = this.url;
where.appendChild(im);
};
};

使用三种不同构造函数的方式完全相同:设置 url属性,然后调用 insert()方法。

var o = new MYAPP.dom.Image();
o.url = 'http://images.packtpub.cimg/PacktLogoSmall.png';
o.insert(document.body);
var o = new MYAPP.dom.Text();
o.url = 'http://images.packtpub.cimg/PacktLogoSmall.png';
o.insert(document.body);
var o = new MYAPP.dom.Link();
o.url = 'http://images.packtpub.cimg/PacktLogoSmall.png';
o.insert(document.body);

假设你的程序事先不知道需要哪种类型的对象。例如,用户在运行时通过点击按钮来决定。如果 type包含所需的对象类型,您可能需要使用一个 if或一个 switch,并执行如下操作:

var o;
if (type === 'Image') {
o = new MYAPP.dom.Image();
}
if (type === 'Link') {
o = new MYAPP.dom.Link();
}
if (type === 'Text') {
o = new MYAPP.dom.Text();
}
o.url = 'http://...'
o.insert();

这很好,但是如果您有很多构造函数,代码可能会变得太长。此外,如果您正在创建一个库或框架,您可能甚至不知道构造函数的确切名称。在这种情况下,让工厂函数负责创建动态确定类型的对象是很有用的。

让我们给 MYAPP.dom实用程序添加一个工厂方法:

MYAPP.dom.factory = function(type) {
return new MYAPP.dom[type];
}

现在你可以用更简单的代替三个 ifs:

var o = MYAPP.dom.factory(type);
o.url = 'http://...'
o.insert();

上面的示例 factory()方法很简单,但是在实际场景中,您可能想要对 type值进行一些验证,并可选地进行一些所有对象类型共有的设置工作。

装饰工

装饰器设计模式是结构模式;它与对象是如何创建的没有太大关系,而是它们的功能是如何扩展的。您可以拥有一个基本对象和一个不同装饰器对象的池,提供额外的功能,而不是使用继承,在继承中您以线性方式(父子孙)进行扩展。你的程序可以挑选它想要的装饰者,并按顺序排列。对于不同的程序,您可能有不同的需求集,并从同一个池中挑选不同的装饰者。看看装饰模式的使用部分是如何实现的:

var obj = {
function: doSomething(){
console.log('sure, asap');
},
// ...
};
obj = obj.getDecorator('deco1');
obj = obj.getDecorator('deco13');
obj = obj.getDecorator('deco5');
obj.doSomething();

你可以看到如何从一个有 doSomething()方法的简单对象开始。然后,您可以挑选一些您身边的装饰器对象(通过名称来标识)。所有装饰者都提供一个 doSomething()方法,该方法首先调用前一个装饰者的相同方法,然后继续自己的代码。每次你添加一个装饰器,你就用它的改进版本覆盖基础 obj。最后,当你添加完装修师后,你调用 doSomething()。因此,所有装饰者的所有 doSomething()方法都是按顺序执行的。我们来看一个例子。

装饰圣诞树

让我们用一个例子来说明装饰模式:装饰圣诞树。你从 decorate()方法开始。

var tree = {};
tree.decorate = function() {
alert('Make sure the tree won\'t fall');
};

现在让我们实现一个 getDecorator()方法,该方法将用于添加额外的装饰器。装饰器将作为构造函数实现,它们都将从基础 tree对象继承。

tree.getDecorator = function(deco){
tree[deco].prototype = this;
return new tree[deco];
};

现在让我们创建第一个装饰器 RedBalls(),作为 tree的一个属性(为了保持全局名称空间更干净)。 RedBall对象也提供了一个 decorate()方法,但是他们确保首先调用他们父母的 decorate()

tree.RedBalls = function() {
this.decorate = function() {
this.RedBalls.prototype.decorate();
alert('Put on some red balls');
}
};

同样,增加一个 BlueBalls()Angel()装饰者:

tree.BlueBalls = function() {
this.decorate = function() {
this.BlueBalls.prototype.decorate();
alert('Add blue balls');
}
};
tree.Angel = function() {
this.decorate = function() {
this.Angel.prototype.decorate();
alert('An angel on the top');
}
};

现在,让我们将所有装饰器添加到基础对象中:

tree = tree.getDecorator('BlueBalls');
tree = tree.getDecorator('Angel');
tree = tree.getDecorator('RedBalls');

最后,运行 decorate()方法:

tree.decorate();

此单次呼叫会产生以下警报(按此顺序):

  • 确保树不会倒下

  • 加入蓝色小球

  • 顶上的天使

  • 放点红球

正如您所看到的,这个功能允许您拥有任意多的装饰者,并以您喜欢的任何方式选择和组合它们。

观察者

观察者模式(也称为订阅者-发布者模式)是一种行为模式,这意味着它处理不同对象之间的交互和通信方式。当实现观察者模式时,您有以下对象:

  • 一个或多个发布者对象,当他们做一些重要的事情时,这些发布者对象会宣布

  • 调谐到一个或多个发布者的一个或多个订阅者,听发布者宣布什么,然后适当地行动

观察者模式对于前一章中讨论的浏览器事件来说听起来很熟悉,这是正确的,因为浏览器事件是这种模式的一个示例应用。浏览器是发布者:它宣布一个事件(如 onclick)已经发生的事实。订阅(监听)此类事件的事件侦听器函数将在事件发生时得到通知。浏览器-发布者向所有订阅者发送一个事件对象,但是在您的自定义实现中,您不必使用事件对象,您可以发送您认为合适的任何类型的数据。

观察者模式有两个子类型:推和拉。推送是发布者负责通知每个订阅者的地方,而拉取是订阅者监视发布者状态变化的地方。

让我们看一下推送模型的一个示例实现。让我们将与观察者相关的代码保存到一个单独的对象中,然后将这个对象用作一个混合对象,将其功能添加到任何其他决定成为发布者的对象中。这样,任何对象都可以成为发布者,任何函数对象都可以成为订阅者。 observer对象将具有以下属性和方法:

  • 只是回调函数的 subscribers数组

  • addSubscriber()removeSubscriber()添加和移除 subscribers数组的方法

  • 一种 publish()方法,获取数据并调用所有用户,将数据传递给他们

  • 一种 make()方法,通过将上述所有方法添加到其中,获取任何对象并将其转化为发布者

这里是 observer mixin 对象,它包含所有与订阅相关的方法,可以用来将任何对象转换成发布者。

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);
}
}
},
make: function(o) { // turns an object into a publisher
for(var i in this) {
o[i] = this[i];
o.subscribers = [];
}
}
};

现在让我们创建一些发布者。发布者可以是任何对象;它唯一的职责就是在重要的事情发生时调用 publish()方法。这里有一个 blogger对象,每当一个新的博客帖子准备好了,它就调用 publish()

var blogger = {
writeBlogPost: function() {
var content = 'Today is ' + new Date();
this.publish(content);
}
};

另一个对象可能是《洛杉矶时报》报纸,当一份新的报纸发行时,它会呼叫 publish()

var la_times = {
newIssue: function() {
var paper = 'Martians have landed on Earth!';
this.publish(paper);
}
};

将这些简单的对象转化为发布者非常容易:

observer.make(blogger);
observer.make(la_times);

现在让我们有两个简单的对象 jackjill:

var jack = {
read: function(what) {
console.log('I just read that ' + what)
}
};
var jill = {
gossip: function(what) {
console.log('You didn\'t hear it from me, but ' + what)
}
};

jackjill可以订阅 blogger对象,方法是在发布内容时提供它们想要调用的回调方法。

blogger.addSubscriber(jack.read);
blogger.addSubscriber(jill.gossip);

blogger写新帖子时,现在会发生什么?结果是 jackjill得到通知:

>>> blogger.writeBlogPost();
I just read that Today is Sun Apr 06 2008 00:43:54 GMT-0700 (Pacific Daylight Time)
You didn't hear it from me, but Today is Sun Apr 06 2008 00:43:54 GMT-0700 (Pacific Daylight Time)

jill随时可能决定取消她的订阅。然后在撰写另一篇博文时,不再通知退订对象:

>>> blogger.removeSubscriber(jill.gossip);
>>> blogger.writeBlogPost();
I just read that Today is Sun Apr 06 2008 00:44:37 GMT-0700 (Pacific Daylight Time)

jill可以决定订阅 LA Times ,作为对象可以是很多出版商的订阅者。

>>> la_times.addSubscriber(jill.gossip);

然后当 LA Times 发布新一期时, jill得到通知, jill.gossip()执行。

>>> la_times.newIssue();

你没有从我这里听到,但是火星人已经登陆地球了!

总结

在最后一章中,您学习了一些常见的 JavaScript 编码模式,并学习了如何使您的程序更干净、更快、更好地与其他程序和库一起工作。然后,您看到了对四本书中一些设计模式的讨论和示例实现。您可以看到 JavaScript 是一种功能齐全的动态面向对象编程语言,用动态语言实现经典模式非常容易。总的来说,模式是一个很大的话题,你可以和本书的作者一起在网站JSPatterns.com上进一步讨论 JavaScript 模式。

您现在有足够的知识,能够使用面向对象编程的概念创建可扩展和可重用的高质量 JavaScript 应用和库。一路顺风!