作者 Joseph Zimmerman
本文是介绍 JavaScript 采用的常见设计模式系列文章的首篇文章。 设计模式是一些可靠的编程方式,有助于保证您的代码更加易于维护、扩展及分离,所有设计模式在创建大型 JavaScript 应用程序(特别是在大群体中)时均不可或缺。
单例模式是指在您要确保始终只创建一个对象实例时使用的设计模式。 在面向对象的经典编程语言中,创建单例模式背后的概念多少有点让人难以理解,因为其中包含一个同时具有静态及非静态属性和方法的类。 但本文主要讨论 JavaScript,因此,由于 JavaScript 是一种不包含真正类的动态语言,因此 JavaScript 版本的单例模式极其简单。
为什么需要采用单例模式?
在我开始介绍实施细节之前,我需要探讨一下为什么单例模式对于应用程序非常有用。 它能够确保您只有一个对象实例能够实际派上用场。 在服务器端语言中,您可能会使用单例处理数据库连接,这是由于为一个请求创建多个数据库连接纯粹是一种资源浪费。 同样,在前端 JavaScript 中,您可能会希望将负责处理所有 AJAX 请求的某个对象设置为单例。 规则非常简单: 如果每次创建新实例时,实例的功能均完全相同,那么将其设置为单例。
但是,这并不是采用单例的唯一原因。 至少在 JavaScript 中,单例可让您保证命名空间对象和函数井然有序,防止它们与全局命名空间混淆,您可能明白,这是一种可怕的想法,特别是在使用第三方代码的情况下。 使用命名空间单例模式也被称为模块设计模式。
展示单例模式
要创建单例,您只需创建一个对象文字。
var Singleton = {
prop: 1,
another_prop: 'value',
method: function() {…},
another_method: function() {…}
};
您还可以创建具有私有属性和方法的单例,但由于其涉及使用封闭函数和自调用匿名函数,因而稍微有些难以理解。 函数内部声明了一些局部函数和/或变量。 然后,创建并返回一个对象文字,其中包含一些引用您在更大的函数范围内声明的变量和函数的方法。 紧随函数声明放置 () 即可立即执行外部函数,并将所得的对象文字分配给变量。 如果这些介绍让您感到困惑,那么请看下面的代码,随后我将会做出进一步的说明。
var Singleton = (function() {
var private_property = 0,
private_method = function () {
console.log('This is private');
}
return {
prop: 1,
another_prop: 'value',
method: function() {
private_method();
return private_property;
},
another_method: function() {…}
}
}());
关键在于,当通过某个变量所在函数前方的 Var 声明该变量时,该变量只能在函数内部通过该函数内声明的各函数(例如对象文字内的函数)进行访问。 return 语句可帮助我们回到在外部函数自行执行后分配给单例的对象文字。
单例模式命名空间
在 JavaScript 中,命名空间化通过将对象作为另一对象的属性添加来完成,因此深度为一个或多个图层。 这对于将代码组合成逻辑片段非常有用。 虽然我认为 YUI JavaScript 库在一定程度上命名空间层次过多,但总体而言仍可算作将嵌套命名空间限制在只有几个或更少图层的最佳实践。 以下代码是一个命名空间示例。
var Namespace = {
Util: {
util_method1: function() {…},
util_method2: function() {…}
},
Ajax: {
ajax_method: function() {…}
},
some_method: function() {…}
};
// Here's what it looks like when it's used
Namespace.Util.util_method1();
Namespace.Ajax.ajax_method();
Namespace.some_method();
如前所述,使用命名空间保证全局变量数量最低。 您甚至可以将整个应用程序连接到单一对象命名空间命名的应用程序(如果您有这项特权的话)。 如果您想继续了解有关单例设计模式及其命名空间适用性的更多信息,请继续学习,在我的个人博客上查看文章"JavaScript 设计模式:单例模式"。
如果您在通读单例模式一节后,认为“嗨,这很简单”,那么不要着急,我还有一些更加复杂的模式要讨论,其中一个就是组合模式。 组合,顾名思义是指用包含多个部件的对象创建单一实体。 这个单一实体将用作所有这些部件的访问点,虽然这大大简化了操作,但也可能具有相当的欺骗性,因为没有哪种隐性方式明确表明该组合包含多少部件。
组合结构
我们最好使用例证解说组合。 在图 1 中,您可以看到两种不同类型的对象: 容器和库是组合,图像是叶片。 组合可承载子项,但一般不会实施更多行为。 叶片包含绝大多数行为,但不能承载子项,至少在传统的组合示例中不可以。
另一个示例,我本人百分百确定您之前见到过组合模式,但从未真正进行深入思考。 计算机文件结构是组合模式的一个实例。 如果您删除某个文件夹,也将删除该文件夹的所有内容,是吗? 这实质上就是组合模式运行原理。 您可以调用结构树上较高层次的组合对象,消息将沿这一层次结构向下传输。
组合编码示例
此示例创建图片库,将其作为组合模式示例。 只有三个层次: 专辑、库和图像。 专辑和库将作为组合,图像是叶片,如图 1 所示。这是一种比组合本身需求更加明确的结构,但对于本示例而言,将这些层次仅限制为组合或叶片很有意义。 标准组合不会限制哪些结构层次可以具有叶片,也不会限制叶片数量。
要开始操作,应首先创建用于专辑和库的 GalleryComposite“类”。 请注意,我正在使用 jQuery 执行 DOM 操作以简化过程。
var GalleryComposite = function (heading, id) {
this.children = [];
this.element = $('<div id="' + id + '" class="composite-gallery"></div>')
.append('<h2>' + heading + '</h2>');
}
GalleryComposite.prototype = {
add: function (child) {
this.children.push(child);
this.element.append(child.getElement());
},
remove: function (child) {
for (var node, i = 0; node = this.getChild(i); i++) {
if (node == child) {
this.children.splice(i, 1);
this.element.detach(child.getElement());
return true;
}
if (node.remove(child)) {
return true;
}
}
return false;
},
getChild: function (i) {
return this.children[i];
},
hide: function () {
for (var node, i = 0; node = this.getChild(i); i++) {
node.hide();
}
this.element.hide(0);
},
show: function () {
for (var node, i = 0; node = this.getChild(i); i++) {
node.show();
}
this.element.show(0);
},
getElement: function () {
return this.element;
}
}
这个位置有点棘手,能否允许我再更多的解释一下? 我们同时使用 add
, remove
, 和getChild
方法构建这一组合。 本示例不会实际使用 remove
和 getChild
,但它们对于创建动态组合非常有用。 hide
, show
, 和getElement
方法则用来操纵 DOM。 该组合旨在作为库的 表示在页面上向用户展示。 该组合可通过 hide
和 show
控制这些库元素。 如果在专辑上调用 hide
,则整个专辑将消失,或者您也可以只在单一图像上调用它,这样只有该图像会消失。
现在,创建一个 GalleryImage
类。 请注意,它使用的方法与 GalleryComposite
完全相同。 换句话说,它们实现同一接口,不同的是该图像是叶片,因此不会实际对子项相关方法执行任何操作,就像不具有任何子项一样。 必须使用同一接口运行该组合,因为组合元素不知道自身添加的是另一个组合元素还是叶片,因此如果尝试在其子项上调用这些方法,则需要运行完全正常,没有任何错误。
var GalleryImage = function (src, id) {
this.children = [];
this.element = $('<img />')
.attr('id', id)
.attr('src', src);
}
GalleryImage.prototype = {
// Due to this being a leaf, it doesn't use these methods,
// but must implement them to count as implementing the
// Composite interface
add: function () { },
remove: function () { },
getChild: function () { },
hide: function () {
this.element.hide(0);
},
show: function () {
this.element.show(0);
},
getElement: function () {
return this.element;
}
}
鉴于您已经构建了对象原型,您现已能够进行使用。 从下面您可以看到实际构建图像库的代码。
var container = new GalleryComposite('', 'allgalleries');
var gallery1 = new GalleryComposite('Gallery 1', 'gallery1');
var gallery2 = new GalleryComposite('Gallery 2', 'gallery2');
var image1 = new GalleryImage('image1.jpg', 'img1');
var image2 = new GalleryImage('image2.jpg', 'img2');
var image3 = new GalleryImage('image3.jpg', 'img3');
var image4 = new GalleryImage('image4.jpg', 'img4');
gallery1.add(image1);
gallery1.add(image2);
gallery2.add(image3);
gallery2.add(image4);
container.add(gallery1);
container.add(gallery2);
// Make sure to add the top container to the body,
// otherwise it'll never show up.
container.getElement().appendTo('body');
container.show();
这就是该组合的全部代码! 如果要查看该库的现场演示,您可以访问我博客上的演示页。 您还可以阅读我博客上的 "JavaScript 设计模式:组合模式"一文更加深入地了解这一模式的更多信息。
外观模式是本文介绍的最后一种设计模式,它仅仅是一种简化复杂界面的函数或另一段代码。 其实,该模式相当普遍,这时有人可能会说,大多数函数实际上都是为了实现这一目的。 外观模式的目标在于将大型逻辑片段简化为一个简单的函数调用操作。
外观模式示例
您或许一直在使用外观模式,却并不认为自己使用了任何设计模式。 您使用的几乎任何编程语言的图像库均在一定程度上使用外观模式,因为它们的普遍用途在于使复杂的事务变得简单。
让我们来看一个 jQuery 示例,jQuery 使用一个函数 (jquery()
) 执行多项操作;它可以查询 DOM、创建元素,也可以只是用来将 DOM 元素转换为 jQuery 对象。 如果您只是想了解 DOM 查询,简单看一下用于创建该功能的代码行数量,那么您可能会对自己说,“我很庆幸自己不必写这些代码”,因为代码实在太长太复杂。 为方便您使用,他们已成功使用外观模式将数百行复杂代码转换为一个简单的函数调用。
结束语
外观模式很容易理解,但如果您有兴趣,可以阅读我的个人博客 JavaScript 设计模式:外观模式一文了解更多信息。
下一步学习方向
在 JavaScript 设计模式系列文章的这一部分中,我介绍了单例模式、组合模式和外观模式,随后在此系列文章的第二部分中,我将向您介绍另外 3 种设计模式,其中包含的一些概念较此处涉及的概念更加复杂。 如果您没有足够的耐心,我已经在自己的个人博客上撰写了一个有关 JavaScript 设计模式的系列文章,其中还包括几种我并不打算在此系列文章中介绍的设计模式。 您可以在我的博客上查找该系列的所有文章。