JavaScript 设计模式:深入了解有效的设计
今天,我们将戴上计算机科学的帽子,学习一些常见的设计模式。设计模式为开发人员提供了以可重用且优雅的方式解决技术问题的方法。有兴趣成为一名更好的 JavaScript 开发人员吗?然后继续阅读。
重新发布的教程
每隔几周,我们就会重新访问网站历史上一些读者最喜欢的帖子。本教程首次发布于 2012 年 7 月。
简介
可靠的设计模式是可维护的软件应用程序的基本构建块。如果您曾经参加过技术面试,您一定会喜欢被问到这些问题。在本教程中,我们将介绍一些您今天就可以开始使用的模式。
立即学习“Java免费学习笔记(深入)”;
什么是设计模式?
设计模式是可重用的软件解决方案
简单地说,设计模式是针对软件开发过程中经常出现的特定类型问题的可重用的软件解决方案。经过多年的软件开发实践,专家们已经找到了解决类似问题的方法。这些解决方案已被封装到设计模式中。所以:
- 模式是经过验证的软件开发问题解决方案
- 模式是可扩展的,因为它们通常是结构化的并且具有您应该遵循的规则
- 模式可以重复使用来解决类似的问题
我们将在本教程中进一步介绍一些设计模式的示例。
设计模式的类型
在软件开发中,设计模式通常分为几类。我们将在本教程中介绍三个最重要的内容。下面简要解释一下:
- 创建模式侧重于创建对象或类的方法。这听起来可能很简单(在某些情况下确实如此),但大型应用程序需要控制对象创建过程。
- 结构设计模式侧重于管理对象之间关系的方法,以便以可扩展的方式构建应用程序。结构模式的一个关键方面是确保应用程序某一部分的更改不会影响所有其他部分。
- 行为模式侧重于对象之间的通信。
阅读这些简短说明后您可能仍有疑问。这是很自然的,一旦我们深入研究下面的一些设计模式,事情就会变得清晰起来。所以请继续阅读!
关于 JavaScript 中的类的注释
在阅读设计模式时,您经常会看到对类和对象的引用。这可能会令人困惑,因为 JavaScript 并不真正具有“类”的构造;更正确的术语是“数据类型”。
JavaScript 中的数据类型
JavaScript 是一种面向对象的语言,其中对象以原型继承的概念从其他对象继承。可以通过定义所谓的构造函数来创建数据类型,如下所示:
function Person(config) { this.name = config.name; this.age = config.age; } Person.prototype.getAge = function() { return this.age; }; var tilo = new Person({name:"Tilo", age:23 }); console.log(tilo.getAge());
请注意在 Person 数据类型上定义方法时使用 prototype。由于多个 Person 对象将引用相同的原型,因此这允许 getAge() 方法由 Person 数据类型的所有实例共享,而不是为每个实例重新定义它。此外,任何继承自 Person 的数据类型都可以访问 getAge() 方法。
处理隐私
JavaScript 中的另一个常见问题是不存在真正意义上的私有变量。然而,我们可以使用闭包来模拟隐私。考虑以下代码片段:
var retinaMacbook = (function() { //Private variables var RAM, addRAM; RAM = 4; //Private method addRAM = function (additionalRAM) { RAM += additionalRAM; }; return { //Public variables and methods USB: undefined, insertUSB: function (device) { this.USB = device; }, removeUSB: function () { var device = this.USB; this.USB = undefined; return device; } }; })();
在上面的示例中,我们创建了一个 retinaMacbook 对象,具有公共和私有变量和方法。这就是我们使用它的方式:
retinaMacbook.insertUSB("myUSB"); console.log(retinaMacbook.USB); //logs out "myUSB" console.log(retinaMacbook.RAM) //logs out undefined
我们可以使用 JavaScript 中的函数和闭包做更多事情,但我们不会在本教程中详细介绍所有内容。有了关于 JavaScript 数据类型和隐私的这一小课,我们就可以继续学习设计模式。
创意设计模式
有许多不同类型的创作设计模式,但我们将在本教程中介绍其中两种:构建器和原型。我发现这些内容的使用频率足以引起人们的关注。
构建器模式
构建器模式经常在 Web 开发中使用,您之前可能已经使用过它,但没有意识到。简单来说,这个模式可以定义如下:
应用构建器模式允许我们仅通过指定对象的类型和内容来构造对象。我们不必显式创建该对象。
例如,您可能已经在 jQuery 中执行过无数次了:
var myDiv = $('<div id="myDiv">This is a div.</div>'); //myDiv now represents a jQuery object referencing a DOM node. var someText = $('<p/>'); //someText is a jQuery object referencing an HTMLParagraphElement var input = $('<input />');
看一下上面的三个例子。在第一个中,我们传入了包含一些内容的
元素。在第二个中,我们传入了一个空的标签。在最后一个中,我们传入了 元素。这三个的结果都是相同的:我们返回了一个引用 DOM 节点的 jQuery 对象。
$变量采用了jQuery中的Builder模式。在每个示例中,我们都返回了一个 jQuery DOM 对象,并且可以访问 jQuery 库提供的所有方法,但我们从未显式调用 document.createElement。 JS 库在幕后处理了所有这些事情。
想象一下,如果我们必须显式创建 DOM 元素并向其中插入内容,那将需要耗费多少工作!通过利用构建器模式,我们能够专注于对象的类型和内容,而不是显式创建它。
原型模式
前面,我们介绍了如何通过函数在 JavaScript 中定义数据类型,以及如何向对象的 prototype 添加方法。原型模式允许对象通过原型从其他对象继承。
原型模式是通过克隆基于现有对象的模板来创建对象的模式。
这是在 JavaScript 中实现继承的一种简单而自然的方式。例如:
var Person = { numFeet: 2, numHeads: 1, numHands:2 }; //Object.create takes its first argument and applies it to the prototype of your new object. var tilo = Object.create(Person); console.log(tilo.numHeads); //outputs 1 tilo.numHeads = 2; console.log(tilo.numHeads) //outputs 2
Person 对象中的属性(和方法)应用于 tilo 对象的原型。如果我们希望它们不同,我们可以重新定义 tilo 对象的属性。
在上面的例子中,我们使用了 Object.create()。但是,Internet Explorer 8 不支持较新的方法。在这些情况下,我们可以模拟它的行为:
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();
此方法唯一的缺点是不能指定只读属性,而可以在使用 Object.create() 时指定。尽管如此,原型模式展示了对象如何从其他对象继承。
结构设计模式
在弄清楚系统应该如何工作时,结构设计模式非常有用。它们使我们的应用程序能够轻松扩展并保持可维护性。我们将研究本组中的以下模式:复合模式和外观模式。
复合模式
复合模式是您之前可能使用过但没有意识到的另一种模式。
复合模式表示可以像对待组中的单个对象一样对待一组对象。
那么这是什么意思呢?好吧,考虑一下 jQuery 中的这个示例(大多数 JS 库都有与此等效的示例):
$('.myList').addClass('selected'); $('#myItem').addClass('selected'); //dont do this on large tables, it's just an example. $("#dataTable tbody tr").on("click", function(event){ alert($(this).text()); }); $('#myButton').on("click", function(event) { alert("Clicked."); });
无论我们处理的是单个 DOM 元素还是 DOM 元素数组,大多数 JavaScript 库都提供一致的 API。在第一个示例中,我们可以将 selected 类添加到 .myList 选择器选取的所有项目中,但在处理单个 DOM 元素 #myItem 时,我们可以使用相同的方法。同样,我们可以使用 on() 方法在多个节点上附加事件处理程序,或者通过相同的 API 在单个节点上附加事件处理程序。
通过利用复合模式,jQuery(和许多其他库)为我们提供了一个简化的 API。
复合模式有时也会引起问题。在 JavaScript 等松散类型语言中,了解我们正在处理单个元素还是多个元素通常会很有帮助。由于复合模式对两者使用相同的 API,因此我们经常会误认为其中一个,并最终出现意想不到的错误。某些库(例如 YUI3)提供两种单独的获取元素的方法(Y.one() 与 Y.all())。
外观模式
这是我们认为理所当然的另一个常见模式。事实上,这是我最喜欢的之一,因为它很简单,而且我已经看到它被到处使用来帮助解决浏览器不一致的问题。以下是外观模式的含义:
外观模式为用户提供了一个简单的界面,同时隐藏了其底层的复杂性。
外观模式几乎总能提高软件的可用性。再次以 jQuery 为例,该库中比较流行的方法之一是 ready() 方法:
$(document).ready(function() { //all your code goes here... });
ready() 方法实际上实现了一个门面。如果您查看源代码,您会发现以下内容:
ready: (function() { ... //Mozilla, Opera, and Webkit if (document.addEventListener) { document.addEventListener("DOMContentLoaded", idempotent_fn, false); ... } //IE event model else if (document.attachEvent) { // ensure firing before onload; maybe late but safe also for iframes document.attachEvent("onreadystatechange", idempotent_fn); // A fallback to window.onload, that will always work window.attachEvent("onload", idempotent_fn); ... } })
在底层, ready() 方法并不那么简单。 jQuery 规范了浏览器的不一致,以确保在适当的时间触发 ready()。但是,作为开发人员,您会看到一个简单的界面。
大多数外观模式示例都遵循这一原则。在实现时,我们通常依赖于底层的条件语句,但将其作为一个简单的界面呈现给用户。实现此模式的其他方法包括 animate() 和 css()。你能想到为什么这些会使用外观模式吗?
行为设计模式
任何面向对象的软件系统都会在对象之间进行通信。不组织这种沟通可能会导致难以发现和修复的错误。行为设计模式规定了组织对象之间通信的不同方法。在本节中,我们将研究观察者模式和中介者模式。
观察者模式
观察者模式是我们将要经历的两种行为模式中的第一种。它是这样说的:
在观察者模式中,主题可以拥有对其生命周期感兴趣的观察者列表。每当主题做了一些有趣的事情时,它都会向其观察者发送通知。如果观察者不再有兴趣听主题,则主题可以将其从列表中删除。
听起来很简单,对吧?我们需要三种方法来描述这种模式:
- publish(data):当主题有通知要发出时调用。某些数据可以通过此方法传递。
- subscribe(observer):由主题调用以将观察者添加到其观察者列表中。
- unsubscribe(observer):由主题调用,从其观察者列表中删除观察者。
事实证明,大多数现代 JavaScript 库都支持这三种方法作为其自定义事件基础结构的一部分。通常,有一个 on() 或 attach() 方法,一个 trigger() 或 fire() 方法,以及一个 off() 或 detach()方法。考虑以下代码片段:
//We just create an association between the jQuery events methods
//and those prescribed by the Observer Pattern but you don't have to. var o = $( {} ); $.subscribe = o.on.bind(o); $.unsubscribe = o.off.bind(o); $.publish = o.trigger.bind(o); // Usage document.on( 'tweetsReceived', function(tweets) { //perform some actions, then fire an event $.publish('tweetsShow', tweets); }); //We can subscribe to this event and then fire our own event. $.subscribe( 'tweetsShow', function() { //display the tweets somehow .. //publish an action after they are shown. $.publish('tweetsDisplayed); }); $.subscribe('tweetsDisplayed, function() { ... });
观察者模式是实现起来比较简单的模式之一,但它非常强大。 JavaScript 非常适合采用这种模式,因为它本质上是基于事件的。下次开发 Web 应用程序时,请考虑开发彼此松散耦合的模块,并采用观察者模式作为通信方式。如果涉及太多主体和观察者,观察者模式可能会出现问题。这可能会发生在大型系统中,我们研究的下一个模式将尝试解决这个问题。
调解者模式
我们要讨论的最后一个模式是中介者模式。它与观察者模式类似,但有一些显着的差异。
中介者模式提倡使用单个共享主题来处理与多个对象的通信。所有对象都通过中介者相互通信。
现实世界中一个很好的类比是空中交通塔,它负责处理机场和航班之间的通信。在软件开发领域,当系统变得过于复杂时,通常会使用中介者模式。通过放置中介,可以通过单个对象来处理通信,而不是让多个对象相互通信。从这个意义上说,中介者模式可以用来替代实现观察者模式的系统。
在这个要点中,Addy Osmani 提供了中介者模式的简化实现。让我们谈谈如何使用它。想象一下,您有一个 Web 应用程序,允许用户单击专辑并播放其中的音乐。您可以像这样设置中介者:
$('#album').on('click', function(e) { e.preventDefault(); var albumId = $(this).id(); mediator.publish("playAlbum", albumId); }); var playAlbum = function(id) { … mediator.publish("albumStartedPlaying", {songList: [..], currentSong: "Without You"}); }; var logAlbumPlayed = function(id) { //Log the album in the backend }; var updateUserInterface = function(album) { //Update UI to reflect what's being played }; //Mediator subscriptions mediator.subscribe("playAlbum", playAlbum); mediator.subscribe("playAlbum", logAlbumPlayed); mediator.subscribe("albumStartedPlaying", updateUserInterface);
此模式相对于观察者模式的好处是单个对象负责通信,而在观察者模式中,多个对象可以相互监听和订阅。
在观察者模式中,没有封装约束的单个对象。相反,观察者和主体必须合作来维持约束。通信模式由观察者和主体互连的方式决定:一个主体通常有许多观察者,有时一个主体的观察者是另一个观察者的主体。
结论
过去已经有人成功应用过它。
设计模式的伟大之处在于,过去已经有人成功地应用过它。有许多开源代码可以在 JavaScript 中实现各种模式。作为开发人员,我们需要了解现有的模式以及何时应用它们。我希望本教程可以帮助您在回答这些问题上更进一步。
补充阅读
本文的大部分内容可以在 Addy Osmani 所著的优秀的《学习 JavaScript 设计模式》一书中找到。这是一本根据知识共享许可免费发布的在线图书。本书广泛涵盖了许多不同模式的理论和实现,包括普通 JavaScript 和各种 JS 库。我鼓励您在开始下一个项目时将其作为参考。
JavaScript 设计模式:深入了解有效的设计的详细内容,更多请关注红帽云邮其它相关文章!