JavaScript-开发高级教程-三-

155 阅读16分钟

JavaScript 开发高级教程(三)

原文:Pro JavaScript Development

协议:CC BY-NC-SA 4.0

四、提升 JavaScript 性能

在这一章中,我们将看看如何通过编码技巧、技术、最佳实践和现代 API 来提高 JavaScript 应用的性能。我从整体上看待性能,这意味着我考虑了整个应用的生命周期。这意味着,我认为,应用的感知性能可以通过更快、更高效的 JavaScript 文件加载,以及更高效的调用、选择器,甚至通过 CSS 和 HTML 优化来提高,而不是仅仅关注执行的代码行数。然而,在这一章中,我们将特别关注如何利用 JavaScript 代码和文件来提高网站和应用的性能。如果你想了解更多关于提高页面渲染性能的信息,可以看看 Addy Osmani 关于这个主题的优秀案例研究,题为“通过 http://bit.ly/gone_60 以每秒 60 帧的速度消失”。

改善页面加载时间

在我们开始修改任何 JavaScript 代码以提高应用的性能之前,我们需要查看浏览器与 JavaScript 代码的第一次接触,这是通过 HTML <script>标签加载的引用。我们在此阶段所做的更改将确保您的代码快速有效地加载,这意味着您的代码可以更快地执行,并提高您的应用的感知响应能力。

HTML 标签顺序很重要

当浏览器遇到一个<script>标签时,在大多数情况下,它会停止呈现页面,直到它设法读取并解析该脚本的内容,以防该脚本包含一个document.write()方法调用,这意味着在该点上页面的呈现发生变化。因此,将所有的<script>标签移到 HTML 中的</body>标签之前。这样,您的整个页面将在脚本加载和解析之前呈现,从而提高页面的感知性能。

您 JavaScript 文件的 GZip 编码交付

简单的服务器设置可以确保您的 JavaScript 代码文件以及 HTML、CSS 和任何其他基于文本的文件以最有效的方式发送到浏览器,方法是在发送之前压缩或“压缩”数据,并在数据到达浏览器时解压缩或“解压缩”,从而减少网络上传输的数据,以更快的速度到达浏览器。此设置称为 gzip 编码,只要您能够控制服务器的设置,几乎可以在任何 web 服务器上启用:

  • 对于托管在 Apache web 服务器上的网站,安装并配置 mod_deflate 模块( http://httpd.apache.org/docs/2.2/mod/mod_deflate.html )以启用 gzip 压缩。
  • 对于托管在微软 IIS 7 上的站点,打开 IIS 管理器程序并选择功能视图。打开压缩选项并选中复选框,为静态和/或动态内容类型启用 gzip 压缩。静态内容每次都产生相同的输出,例如 CSS 或平面 HTML 文件,而动态内容使用应用服务器端代码产生不同的输出。
  • 对于 Node.js 上的站点主机和使用 Express 框架( http://expressjs.com ),只需在应用代码中尽早使用 Express 对象的compress()方法。接下来的所有回答都将被 gzip 编码。

每个请求的动态 gzip 编码过程会消耗服务器上的额外资源和 CPU 时间。如果您使用的服务器的性能受到您的关注,您可以提前压缩您的 JavaScript 和其他基于文本的文件。只要确保使用额外的 HTTP 头Content-Encoding: gzip来提供这些预压缩的文件,就可以获得相同的性能提升,而不会对服务器的性能造成任何损失。

缩小、混淆和编译

JavaScript 文件越小,通过网络下载到浏览器的速度就越快,浏览器读取和解析它的速度也就越快。因此,我们需要尽我们所能来确保我们的代码尽可能的小,我们通过三个过程来实现,即缩小、混淆和编译。

缩小是指从 JavaScript 中删除所有空格和换行符,以产生更小的文件大小,但仍然包含开发人员编写的完全相同的代码语句。因为 JavaScript 的执行基于特定的关键字和语句,而不是它所包含的空白字符,所以我们可以安全地删除这些以减小文件大小。

模糊处理是一种更高级的代码优化形式,它查看变量和函数名,并确定哪些是全局可访问的,哪些仅限于特定范围。任何全局变量和函数名称都保持不变,但是那些具有有限范围的名称被重命名为更短的名称,从而显著减少这些名称在代码文件中占用的空间。通过确保以正确的方式在正确的位置替换名称,代码将继续像混淆发生之前一样运行。代码中的全局变量和函数越少,这是一个很好的实践,因为它减少了你的代码和其他代码之间相互干扰的机会,你的混淆代码就变得越小。

编译是一个更高级的过程,它从整体上研究您的代码,并试图简化、减少代码语句,以及将代码语句组合成具有相同行为的其他语句。尽管可用于执行这种特定类型优化的工具较少,但当与缩小和模糊处理结合使用时,它在产生最小文件大小方面是最有效的。

让我们以清单 4-1 中的函数为例,分别尝试使用缩小、混淆和编译来减小它的大小。这个清单中的代码有 205 字节,我们需要知道它,以便发现我们得到的文件有多小。

清单 4-1。要缩小、混淆和编译的函数

var add = function() {

var total = 0,

index = 0,

length = arguments.length;

for (; index < length; index++) {

total  = total + arguments[index];

}

return total;

};

使用 JSMin 进行代码精简

道格·克罗克福德的 JSMin 工具,可通过 http://bit.ly/js_min 获得,如图 4-1 所示,写于 2001 年,目的是缩小 JavaScript 文件以减小文件大小,从而缩短下载时间。它被认为是第一个可用的此类工具,它基本上删除了文件中所有不必要的空白和回车。最初只作为 MS-DOS 命令行工具提供,现在它可以作为大多数使用 Node.js 应用框架的操作系统的命令行工具,这要感谢开发人员 Peteris Krumins,他将它移植到了这个应用平台上(通过 http://bit.ly/node_jsmin 下载)。

A978-1-4302-6269-5_4_Fig1_HTML.jpg

图 4-1。

The JSMin homepage describes how the process of minification works under the hood

在你的计算机上安装 Node.js 之后(从 http://nodejs.org ,在你的命令行上执行下面的命令来安装 JSMin 以便在你的计算机上的任何文件夹中使用。Mac 和 Linux 用户可能需要在命令前加上sudo来以足够的权限执行命令:

npm install -g jsmin

缩小文件就像指定输出文件名一样简单,前面是命令行工具的–o选项,后面是要缩小的文件名,如下所示:

jsmin –o Listing4-1.min.js Listing4-1.js

通过 JSMin 运行清单 4-1 中的代码,产生了清单 4-2 中所示的代码,它重 136 字节,文件大小减少了 33.6%。

清单 4-2。用 JSMin 缩小后的清单 4-1 中的代码

var add=function(){var total=0,index=0,length=arguments.length;for(;index<length;index++){total=total+arguments[index];}

return total;};

使用丑陋的 JS 进行代码混淆

几年后出现的比 JSMin 更先进的工具是 UglifyJS,其主页通过 http://bit.ly/uglifyjs_home 在线,可以在图 4-2 中看到。可以通过 http://bit.ly/uglify_js 作为独立工具下载。现在,在它的第二个版本中,它缩小并混淆了您的 JavaScript 代码,以减小文件大小。它可以直接从项目主页运行,也可以使用 Node.js 通过命令行工具运行。安装 Node.js 后,在命令行上执行以下命令,安装 UglifyJS,以便在计算机上的任何文件夹中使用。Mac 和 Linux 用户应该知道在这之前加上sudo,通常用于他们系统的全局操作:

npm install uglify-js –g

A978-1-4302-6269-5_4_Fig2_HTML.jpg

图 4-2。

Code can be obfuscated directly from the UglifyJS homepage

通过 UglifyJS 运行清单 4-1 中的代码会产生下面清单 4-3 中所示的代码,它的重量为 88 字节,减少了 57%。

清单 4-3。清单 4-1 中的代码在用丑陋的 JS 混淆后

var add=function(){for(var r=0,n=0,a=arguments.length;a>n;n++)r+=arguments[n]

return r}

使用 Google Closure 编译器进行代码编译

我们在第三章看了一下谷歌的 Closure 编译器,把它作为检查你的代码错误的工具,但是它的主要目的实际上是缩小、混淆和编译你的代码到一个比你的原始文件小得多的文件中。在我看来,这是目前最有效的 JavaScript 压缩工具。

要使用闭包编译器优化您的代码,尝试通过 http://bit.ly/closure_compile 将您的代码复制并粘贴到在线表单中,如图 4-3 所示,然后按下编译按钮。优化后的代码将出现在页面右侧的“已编译代码”选项卡中。这个框的上方会出现一个链接,允许您直接下载编译后的代码作为文件。如果您喜欢独立运行编译器,而不是通过 web 浏览器,那么可以使用 Node.js 的包在命令行上运行该工具。

A978-1-4302-6269-5_4_Fig3_HTML.jpg

图 4-3。

JavaScript code may be compiled through the Google Closure Compiler online service

通过 Google Closure Compiler 运行清单 4-1 中的代码产生了清单 4-4 中所示的代码,它只有 88 个字节,减少了 57 %,比任何通过 JSMin 缩小得到的结果都好,实际上与通过 UglifyJS 混淆得到的结果相同。

清单 4-4。用 Google Closure 编译器编译后的清单 4-1 中的代码

var add=function(){for(var a=0,b=0,c=arguments.length;b<c;b++)a+=arguments[b];return a};

对于更大的代码块,这种方法甚至可以产生比丑陋的 JS 更大的改进。这是因为,与缩小和混淆工具不同,Google Closure Compiler 实际上运行并研究代码,不断寻找功能与原始代码完全相同的最佳最终代码。这与缩小和混淆形成对比,缩小和混淆使用一种静态分析形式,其中代码不运行,只是简单地按原样观察。

在大多数情况下,Google Closure Compiler 会产生最好的结果和最小的输出文件大小,我建议在您的应用中采用它来显著减小 JavaScript 文件的大小,使它们更快地加载,并为您的最终用户带来更好的体验。

避免全局变量以获得更好的压缩

代码中的任何全局变量或函数名将在缩小、混淆或编译后保持其名称,因此代码的行为不受这些压缩操作的影响。因此,您拥有的全局变量或函数越少,您的代码就越小,因为这些名称可以在压缩期间用更短的名称重写。避免无意中创建全局变量的一个好的编码技术是将你的整个代码或者部分代码放在一个匿名的、自执行的函数闭包块中,这将为变量和函数名定义创建一个新的非全局范围,如清单 4-5 所示。这些变量名中实际上需要是全局变量的任何一个都可以在匿名函数之外显式列出,剩下的所有变量都可以更有效地压缩。

清单 4-5。用匿名自执行函数闭包最小化全局变量

// Define a global variable

var myGlobalVariable;

// Create a self-executing, anonymous (unnamed) function to wrap around your code

(function() {

// Your code, that before was global, goes here with a new, non-global scope,

// making it easier to generate smaller compressed files via minification,

// obfuscation, or compilation

// Define a local variable

var myLocalVariable = "Local";

// Set the global variable to a string

myGlobalVariable = "Global";

// The open-close bracket combination here executes the function straight away

}());

按需延迟加载 JavaScript 文件

当使用 HTML 文件中的文件引用正常加载 JavaScript 文件时,浏览器会阻止下载和解析页面的其余部分,直到脚本下载并执行完毕。这并不理想,尤其是如果有大量代码要下载的话。有一种方法可以使用 JavaScript 本身来克服这个性能瓶颈,但是您必须小心。脚本通常会阻止浏览器并行加载脚本,以避免竞争情况,即一段代码在另一段代码之前完成,从而导致执行顺序错误。如果您打算以这种方式加载脚本,那么您需要一种技术来关联一个 JavaScript 代码块,以便在文件完成下载后执行,从而防止这种竞争情况的发生。

您可以通过 JavaScript 动态创建一个新的<script>标记,设置它的src属性指向要异步加载的文件的位置,从而为 JavaScript 文件创建一个非阻塞请求。通过将一个函数连接到标签的onload方法,我们可以执行依赖于这个被加载的脚本的特定代码。清单 4-6 显示了一个函数,它将这种行为封装在一个简单的函数中,该函数的参数分别是一个要加载的脚本文件位置和一个文件加载后要执行的函数。

清单 4-6。在不阻塞浏览器的情况下按需加载 JavaScript 文件

function loadScript(src, onLoad) {

var scriptTag = document.createElement("script");

scriptTag.src = src;

if (typeof onLoad === "function") {

scriptTag.onload = onLoad;

scripTag.onreadystatechange = function() {

if (scriptTag.readyState === 4) {

onLoad();

}

}

}

document.body.appendChild(scriptTag);

}

清单 4-6 中的loadScript方法可以在你的代码中的任何地方使用,如下所示:

// Loads my-script.js, then outputs "script loaded and available" when complete

loadScript("my-script.js", function() {

alert("script loaded and available!");

});

// Loads my-script.js, which is self-contained and does not require extra code to be

// execute once it has loaded. The second parameter is therefore not passed in.

loadScript("my-script.js");

我们将在第六章中进一步探讨这个主题,在那一章中,我将介绍 RequireJS 库,它允许 JavaScript 文件的延迟加载和使用标准化格式对代码文件依赖关系的简单管理。

优化文档对象操作

大多数网站和应用性能低下的一个最大原因是通过 JavaScript 对 HTML 页面元素的低效访问。因为任何 web 浏览器中的 JavaScript 引擎都是独立于其呈现引擎的,所以通过 JavaScript 获取对页面元素的引用涉及到从一个引擎跳到另一个引擎,浏览器充当两者之间的中介。为了提高性能,我们需要减少这种跳跃发生的次数。在这一节中,我概述了一些技术来帮助避免 JavaScript 对 HTML 页面上的元素进行不必要的访问。

最小化对页面元素的访问

减少对页面元素的访问是相当简单的,因为一旦 JavaScript 中有了对页面元素的引用,你只需将该引用存储在一个变量中,并在整个代码中引用该变量,而不是返回页面再次获取相同的引用,如清单 4-7 所示。

清单 4-7。将 DOM 元素引用存储在变量中以供将来访问

var header = document.getElementById("header"),

nav = document.getElementById("nav");

header.className += " " + nav.className;

如果您需要访问位于同一父元素中的多个页面元素,只获取对父元素的引用,并使用 JavaScript 从该引用中定位子元素,这将通过访问父元素的单个请求在页面中实现,如清单 4-8 所示。避免简单地获取对整个页面的引用,并将其用作页面的整个 DOM 树所占用的内存,再加上 JavaScript 需要额外的时间来定位您希望在树中进一步访问的元素,这实际上可能会对您的应用产生负面的性能影响。

清单 4-8。通过引用单个父元素来访问子 DOM 元素

var wrapper = document.getElementById("wrapper"),

header = wrapper.getElementsByTagName("header")[0],

nav = wrapper.getElementsByTagName("nav")[0];

header.className += " " + nav.className;

如果你需要动态地创建和添加 DOM 元素到页面,在添加新的元素到页面之前,应用所有的属性并设置所有必要的属性,如清单 4-9 所示。这样,浏览器就不需要一直访问动态 HTML 页面来进行更改。

清单 4-9。在将新元素添加到动态页面之前对其进行 DOM 更改

var list = document.createElement("ul"),

listItem = document.createElement("li");

// Perform all the DOM manipulation possible within JavaScript first

listItem.innerHTML = "I am a list item";

list.appendChild(listItem);

// Finally, add the element to the page when you are sure you no longer need to alter it

// before display

document.body.appendChild(list);

尽可能关闭现有元素

动态创建 DOM 元素在网站和应用中是一个相当常见的需求,但是每次使用标准的document.createElement()方法创建一个元素并以类似于另一个现有元素的方式配置它时,您都会付出性能代价。为了提高性能,复制现有的元素而不是从头开始创建新的元素,如清单 4-10 所示。如果您正在创建多个具有相似属性的元素,那么创建一个实例,然后使用 DOM 元素的cloneNode()方法来复制元素及其相关属性。

清单 4-10。复制现有元素以提高性能

var list1 = document.createElement("ul"),

list2,

listItem1 = document.createElement("li"),

listItem2,

listItem3;

listItem1.className = "list-item";

listItem1.innerHTML = "I am a list item";

// The cloneNode method duplicates the element efficiently. Setting the optional parameter

// to 'true' also copies across any child elements and properties. Leaving this property out

// copies just the individual element itself

listItem2 = listItem1.cloneNode(true);

listItem3 = listItem1.cloneNode(true);

// Add the three list items to the unordered list element

list1.appendChild(listItem1);

list1.appendChild(listItem2);

list1.appendChild(listItem3);

// Duplicate the entire unordered list

list2 = list1.cloneNode(true);

// Add the two identical unordered list elements to the live page

document.body.appendChild(list1);

document.body.appendChild(list2);

利用离线 DOM

DOM 规范的一个经常被忽视的方面是文档片段,或者离线 DOM,它是 DOM 的一个轻量级版本,用于创建和操作小的元素树结构,以便以后添加到动态页面中,如清单 4-11 所示。使用这种技术操作元素比使用动态页面 DOM 有更好的性能。

清单 4-11。利用离线 DOM 避免访问在线 DOM

// Create a DocumentFragment object as an offline DOM structure, disconnected from the live DOM

var offlineDOM = document.createDocumentFragment(),

// Create elements for adding to the page dynamically

header = document.createElement("header"),

nav = document.createElement("nav");

// Add each element to the offline DOM

offlineDOM.appendChild(header);

offlineDOM.appendChild(nav);

// Add a copy of the offline DOM to the live page

document.body.appendChild(offlineDOM);

使用 CSS 而不是 JavaScript 来操作页面样式

CSS 样式属性可以通过 DOM 使用元素的style属性直接操作。更新影响其布局的页面元素的样式属性会导致浏览器中发生回流,如清单 4-12 所示,它计算了对页面上该元素和其他元素所做的更改的效果。回流需要时间来完成,因此按顺序操作多个样式属性会导致不必要的回流次数。

清单 4-12。演示直接更新 DOM 元素样式属性时浏览器回流

var nav = document.getElementsByTagName("nav");

nav.style.backgroundColor = "#000"; // Causes a reflow in the browser

nav.style.color = "#fff"; // Causes a reflow

nav.style.opacity = 0.5; // Causes a reflow

这个问题有两个解决方案。第一种是通过 JavaScript 将 CSS 类应用于页面元素,而不是单独的样式。这会导致所有的 CSS 规则同时应用到元素上,导致只发生一次回流,如清单 4-13 所示。这样做还有一个额外的逻辑好处,就是将所有的视觉和布局规则保存在一个专门用于此任务的 CSS 文件中,而不是用一些通过 CSS 应用的样式和一些通过 JavaScript 应用的样式来搅浑水。

清单 4-13。将 CSS 类应用于 DOM 元素以减少浏览器回流

var nav = document.getElementsByTagName("nav");

nav.className += " selected"; // The CSS class name "selected" contains multiple style settings

第二个解决方案是,如果第一个在您的特定环境中无法实现,那么在对其他样式属性进行更改之前,将display样式属性设置为none。这将从页面流中直观地移除元素,导致浏览器回流,但这意味着当元素不在页面流中时,任何其他样式属性的更改都不会导致回流。一旦做了更改,元素应该通过设置它的display属性为block或任何其他可接受的值返回到页面流,如清单 4-14 所示。

清单 4-14。通过隐藏元素同时改变它们的样式属性来减少浏览器重排

var nav = document.getElementsByTagName("nav");

nav.style.display = "none"; // Causes a browser reflow, hiding the element from display

nav.style.backgroundColor = "#000"; // Causes no reflow since the element is hidden

nav.style.color = "#fff"; // Causes no reflow

nav.style.opacity = 0.5; // Causes no reflow

nav.style.display = "block"; // Causes a browser reflow, bringing the element back on display

提高 DOM 事件性能

自从 JavaScript 出现以来,我们一直在编写代码,将网站和应用上的功能与用户操作联系起来。当用户提交表单、点击链接或加载页面时,我们希望能够拦截这些动作,并通过 JavaScript 改善页面体验。我们需要这些动作快速,并且不中断浏览器中其余页面的行为。附加或处理不当的事件会导致性能问题,尽管幸运的是,通过适当的事件委托和框架,我们可以将事件处理的性能影响降至最低。

将事件委托给父元素

DOM 事件从它们第一次被触发的元素冒泡到文档结构的最顶层。这意味着,例如,当用户点击一个链接时,JavaScript 为链接元素触发一个click事件,然后是父元素上的一个click事件,依此类推,对于 DOM 树中的每个元素,直到树结构的最顶端,最后到达根<html>元素。这被称为事件的泡沫阶段。在此之前,有一个捕获阶段,在这个阶段中,事件从 DOM 树的顶部向下触发到元素。当使用元素的addEventListener()方法设置事件处理程序时,您需要提供事件名称、事件在元素上发生时将执行的处理程序函数,以及最后的第三个参数,一个布尔型truefalse值,它分别指示您希望在事件生命周期的捕获阶段还是冒泡阶段触发事件。

在提高应用的性能时,我们可以利用冒泡阶段,因为这意味着我们只需要添加一个事件处理程序,通过将该处理程序应用于用户所操作的元素的父元素来处理多个元素上的事件。然后,您可以根据事件发生的元素的属性来分配要发生的动作,如清单 4-15 所示,其中单个事件处理程序处理在许多子元素上触发的事件。这些被称为事件委托,因为它们成为基于事件和页面元素的属性委托动作的逻辑块。假设此清单在 HTML 页面的上下文中运行,该页面的一部分包含以下标记:

<ul id="list">

<li class="list-item"><a href="/" class="list-item-link">Home</a></li>

<li class="list-item"><a href="/news" class="list-item-link">News</a></li>

<li class="list-item"><a href="/events" class="list-item-link">Events</a></li>

</ul>

清单 4-15。链接列表上的事件委托

// Get a reference to the list element surrounding all the links we wish to

// assign the event handler to

var list = document.getElementById("list");

// Define a function to execute when either the link or an element within the link

// is executed

function onClick(evt) {

// Get a reference to the actual element clicked on using the event's 'target' property

var clickedElem = evt.target,

tagNameSought = "A";

// Check to see if the element clicked on is of the type we are looking for, in this

// case if it is an <a> tag

if (clickedElem && clickedElem.tagName === tagNameSought) {

// If it is, we get the link's 'href' value and open this in a new window

window.open(clickedElem.href);

}

}

// Assign the event handler to the list item, the parent surrounding all the links. Adding

// one event handler is faster than assigning an event handler to each of the individual

// links. The third parameter is set to 'false', indicating that events should be handled

// in the bubble phase of the event lifecycle, from the element the event occurred on, up the

// tree to the list item itself

list.addEventListener("click", onClick, false);

通过利用事件委托,您可以确保在页面加载时只有少数事件可以绑定到页面元素,从而减少 DOM 访问并提高性能。这有助于实现减少最终用户能够与页面交互所需时间的总体目标。

用框架处理速射事件

某些事件可能会非常快速地连续触发,例如当页面被主动调整大小时的浏览器resize事件,当鼠标或触摸屏被主动使用时的mousemovetouchmove事件,或者当页面被滚动时的scroll事件;这些事件可能每隔几毫秒就会发生一次。将事件处理程序直接连接到这些执行大量代码或潜在计算密集型操作的事件可能会导致性能问题。如果在另一个事件触发时正在执行事件处理程序代码,则函数调用被堆叠;只有当第一个事件的代码完成时,第二个事件的代码才能开始执行。如果许多事件快速连续发生,浏览器很快就会在负载下挣扎,导致用户界面更新的延迟,从而导致无响应、糟糕的用户体验。

对于这些类型的快速触发事件,请调整您的代码,以便事件处理函数只需将事件的当前值保存到一个变量中。这意味着对于每个mousemovetouchmoveresizescroll事件,事件处理程序只是将鼠标位置、触摸位置、浏览器宽度和高度以及滚动位置存储在变量中。这不会导致任何性能问题。将计算量大的代码移到一个单独的函数中,该函数在不太频繁的计时器或时间间隔上触发,运行代码并使用存储在变量中的值,而不是直接从事件处理程序中触发。清单 4-16 展示了这个被称为事件框架的原则。

清单 4-16。提高性能的事件框架

// Create variables to store the scroll position of the page

var scrollTopPosition = 0,

scrollLeftPosition = 0,

body = document.body,

header = document.getElementById("header");

// Create an event handler function that does nothing more than store the current

// scroll position

function onScroll() {

scrollTopPosition = body.scrollTop;

scrollLeftPosition = body.scrollLeft;

}

// Add a function to write the current scroll position to the header element of the page

function writeScrollPosition () {

header.innerHTML = scrollTopPosition + "px, " + scrollLeftPosition + "px";

}

// Connect the event to the handler function as usual

document.addEventListener("scroll", onScroll, false);

// Execute the writeScrollPosition function once every 500 ms rather than every time the

// scroll event fires, improving application performance

window.setInterval(writeScrollPosition, 500);

尽可能避免将计算密集型事件处理函数直接分配给可能连续快速触发的事件。请改用事件框架来提高事件处理的性能。

提高功能性能

提高 JavaScript 应用的性能很大程度上是为了提高执行代码的效率。函数是应用效率的主要领域,其中执行的每一行都关系到代码的速度和性能。因此,减少执行的行数是游戏的名字,我们可以通过一种叫做记忆化的技术来实现。

用记忆存储以前的函数返回值

当涉及到减少应用中执行的代码行数时,我们需要确保在相同的函数以相同的参数被执行两次的情况下,我们将第一次执行的结果存储在一个变量中,用于代替对相同函数的第二次调用。清单 4-17 显示了一个计算任意数的数学阶乘的函数,这个函数在一个应用中可能会被调用很多次。

清单 4-17。函数计算一个数的阶乘

// getFactorial calculates the factorial of a number, i.e. the multiplication of each number

// from 1 up to the supplied number. The factorial of 3, for example, is (1 * 2 * 3) = 6

function getFactorial(num) {

var result = 1,

index = 1;

for (; index <= num; index++) {

result *= index;

}

return result;

}

// Example usage

alert(getFactorial(3)); // = (1 * 2 * 3) =  6

alert(getFactorial(4)); // = (1 * 2 * 3 * 4) = 24

alert(getFactorial(5)); // = (1 * 2 * 3 * 4 * 5) = 120

一旦我们调用了清单 4-17 中的函数来计算一个数的阶乘,将结果存储在一个变量中以便在我们的代码中使用以避免再次执行整个函数将是有益的。然而,理想情况下,我们需要一种方法来自动完成这项工作,因为在一个大型应用中,我们可能不知道某个特定的函数之前是否被调用过,也不知道我们是否提供了相同的输入。这就是 memoizer 的概念的由来,如果一个函数可以包含一个存储机制来保存以前执行特定输入的结果,那么它可以调用其存储来返回以前执行的函数输出,而不是再次重新执行整个函数,从而提高其性能。清单 4-18 展示了我们如何在计算阶乘的函数中增加存储,使用一个对象文字,并根据提供给函数的输入设置属性名。

清单 4-18。记忆清单 4-17 中的函数以提高它的性能

function getFactorial(num) {

var result = 1,

index = 1;

if (!getFactorial.storage) {

getFactorial.storage = {};

} else if (getFactorial.storage[num]) {

return getFactorial.storage[num];

}

for (; index <= num; index++) {

result *= index;

}

getFactorial.storage[num] = result;

return result;

}

// Example usage

alert(getFactorial(50)); // Executes the whole function

alert(getFactorial(50)); // Returns a stored value. Avoids full function execution,

// boosts performance

记忆技术可以产生显著的效果,通过大的因素改善复杂函数的性能;然而,要使它真正有价值,我们需要一种方法来将记忆应用到任何函数,而不必每次都手动添加它。清单 4-19 展示了如何构建一个实用函数,它允许你为任何函数增加存储结果值的能力,在可能的情况下从函数的内部存储属性自动返回函数的结果以提高性能。

清单 4-19。一个通用的 memoizer 函数,可与任何函数一起使用以提高其性能

// memoize() expects a function as an input and returns the same function

// with storage capabilities added

function memoize(fn) {

return function() {

var propertyName;

// Add a memory object property to this function, if it does not exist

fn.storage = fn.storage || {};

// Create a property name to use to store and retrieve function results within

// the 'storage' object literal. This should be based on a combination of

// all the arguments passed to the function to ensure it is unique based

// on all possible combinations of inputs.

// We borrow the 'join' method from the Array type as 'arguments' isn't a

// proper array type and doesn't contain this method.

propertyName = Array.prototype.join.call(arguments, "|");

// Does the key exist in the memory object?

if (propertyName in fn.storage) {

// If it does, then return the associated value to avoid re-execution of

// the full function

return fn.storage[propertyName];

} else {

// If it doesn't, execute the associated function then save the result

// to the memory object

fn.storage[propertyName] = fn.apply(this, arguments);

// Return the newly saved value, the result of the function's execution

return fn.storage[propertyName];

}

}

};

清单 4-20 中的代码展示了如何将清单 4-19 中的memorize()函数应用于清单 4-17 中的原始getFactorial()函数。

清单 4-20。将泛型记忆应用于函数

function getFactorial(num) {

var result = 1,

index = 1;

for (; index <= num; index++) {

result *= index;

}

return result;

}

// Add the generic memoize capability to the function

var getFactorialMemoized = memoize(getFactorial);

// Example usage

alert(getFactorialMemoized(50)); // Executes the whole function

alert(getFactorialMemoized(50)); // Returns a stored value. Avoids full function execution,

// boosts performance

对基于给定输入返回特定输出的 JavaScript 函数采用记忆化实践,您会发现应用的性能显著提高,尤其是对于计算密集型函数。

使用正则表达式进行更快的字符串操作

正则表达式提供了一种有用、强大和高效的方法来执行字符串定位、操作和模式匹配——比任何其他方法都快,这解释了为什么许多编程语言的开发人员从 20 世纪 60 年代首次在软件产品中使用它们以来就一直在使用它们!

在 JavaScript 中,正则表达式可以用两种不同的方式定义,使用对象构造函数或通过文字表达式,如清单 4-21 所示。

清单 4-21。在 JavaScript 中定义正则表达式

// Define a regular expression through JavaScript's RegExp constructor, where the expression

// is passed in the first parameter as a string, and any modifiers are passed as a string to

// the second parameter

var caps1 = new RegExp("[A-Z]", "g");

// Define a regular expression literal, where the expression is delimited by slashes (/) and

// any modifiers follow immediately after

var caps2 = /[A-Z]/g;

RegExp构造函数获取一个字符串并将其转换成正则表达式,而文字形式无需任何额外处理即可使用;这使得文字形式成为创建正则表达式的最快方式。因此,您应该避免使用RegExp,除非您绝对需要动态生成正则表达式。

正则表达式语法涉及使用特殊字符和字符序列来表达特定的含义,并描述应该如何处理表达式。表 4-1 总结了正则表达式中特殊字符的一些常见用法。

表 4-1。

Characters commonly used in regular expressions

| 特性 | 描述 | | --- | --- | | `[exp]` | 字符序列周围的方括号(`[]`)通知正则表达式处理器匹配括号内的任何字符。例如,`[ABC]`匹配任意一个字符`A`、`B`或`C`。 | | `[^exp]` | 将`^`字符放在方括号内将匹配除方括号内列出的字符之外的任何字符。例如,`[^ABC]`匹配除了字符`A`、`B`或`C`之外的任何内容。 | | `[exp1-exp2]` | 使用`-`字符表示表达式应该匹配从第一个指定字符到最后一个字符的序列。例如,`[A-Z]`匹配从`A`到`Z`的任何字符,包括这两个数字,`[0-9]`匹配`0`到`9`之间的任何数字,包括这两个数字。 | | `(exp)` | 在字符序列周围使用标准括号表示表达式应该与指定顺序的字符序列完全匹配。例如,`(great)`仅精确匹配字符序列`great`。 | | `(exp1|exp2)` | 在括号中使用管道字符`|`表示正则表达式应该与提供的表达式相匹配。例如,`(great|escape)`匹配字符序列`great`或字符序列`escape`。 | | `exp+` | 仅当表达式包含一次或多次时,在表达式后使用的`+`字符才匹配。例如,`A+`只有在字符`A`出现一次或多次时才匹配。 | | `exp*` | 如果表达式出现了零次或多次,在表达式后使用`*`字符进行匹配,这意味着它可能出现也可能不出现,这对于匹配表达式的可选部分很有用。例如,`A*` matches 是字符 A 出现一次或多次,或者根本不出现。 | | `exp?` | 如果表达式出现了 0 次或 1 次,则在表达式后使用的`?`字符匹配。例如,`A?`仅在字符`A`出现零次或一次(不超过一次)时匹配。 | | `\s` | 匹配空白字符,即空格、制表符、回车符、换行符、制表符或换页符。例如,如果表达式包含一个`A`字符,后跟一个空白字符,再后跟一个`B`字符,则`A\sB`匹配。 | | `\S` | 匹配除空白字符以外的所有内容。 | | `\d` | 匹配一个数字,`0`到`9`。 | | `\D` | 匹配除数字以外的所有内容。 | | `\w` | 匹配单词字符,即字母。 | | `\W` | 匹配非单词字符的所有内容。 |

正则表达式修饰符是一个定义正则表达式使用方式的选项。有三种可能的值,可以单独使用来应用单个选项,也可以一起应用多个选项,如表 4-2 所示。

表 4-2。

Regular expression modifiers

| 修饰语 | 描述 | | --- | --- | | `g` | 应用正则表达式,查找正在比较的字符串中的所有匹配项,而不只是返回第一个匹配项 | | `i` | 应用表达式,不考虑文本字符串的字母大小写 | | `m` | 将表达式应用于字符串中的多行文本,而不仅仅是第一行 |

正则表达式通常应用于字符串,以定位或替换其中的子字符串。有三种String类型的方法可以使用正则表达式— match()replace()search()match()方法查找与正则表达式匹配的子串,并将它们作为字符串数组返回,replace()方法查找相同的子串,然后用传递给该方法的另一个字符串替换它们,而search()方法仅定位正则表达式找到的子串的第一个实例,并将该子串在整个字符串中的位置作为数字索引返回。每种方法的例子如清单 4-22 所示。

清单 4-22。用于字符串查找子字符串的正则表达式方法

// This regular expression locates all capital letters from A to M, inclusive. The 'g' modifier

// indicates that the expression shouldn't stop when it reaches the first match, but continue to

// search through the rest of the applied string

var regEx = /[A-M]/g,

string = "The Great Escape",

match,

search,

replace;

// match() returns an array of the characters in the string found by the regular expression

match = string.match(regEx); // = ["G", "E"]

// search() returns the index of the first located character - the 'g' modifier in the regular

// expression is ignored

search = string.search(regEx); // = 4

// replace() switches out any located characters matched by the regular expression with the

// value in the second parameter

replace = string.replace(regEx, "_"); // = "The _reat _scape"

string replace()方法实际上比大多数开发人员认为的要强大得多,尤其是在与正则表达式一起使用时。第二个参数中给出的特定字符序列能够动态地将定位文本中的文本添加到替换文本中,如表 4-3 所示。

表 4-3。

Special characters for use in the second parameter of the JavaScript string replace() method

| 字符序列 | 意义 | | --- | --- | | `$$` | 用单个`$`字符替换找到的子字符串。比如:`"Hello World".replace(/o/g, "$$"); // "Hell$ W$rld"` | | `$&` | 用第一个参数中给定的子字符串替换找到的子字符串。例如:`"Hello World".replace(/o/g, "$&"); // "Hello World"` | | `$`` | 用定位的子字符串之前的文本替换定位的子字符串。例如:`"Hello World".replace(/o/g, "$`"); // "HellHell WHello Wrld"` | | `$'` | 用定位的子字符串后面的文本替换定位的子字符串。例如:`"Hello World".replace(/o/g, "$'"); // "Hell World Wrldrld"` | | `$1, $2, etc.` | 当第一个参数包含一个用括号将表达式分组的正则表达式时,这种表示法允许您提取在特定表达式中找到的子串。例如:`"Hello World".replace(/(o)(\s)/g, "$1$1$2"); // "Helloo World"` |

关于 string replace()方法的另一个鲜为人知的事实是,第二个参数可以作为函数而不是字符串值传递。在这种情况下,对于原始字符串中的每个子字符串匹配,该函数执行一次,将匹配的子字符串传递给该函数。然后使用函数的返回值,并代入替换后的字符串,如清单 4-23 所示。

清单 4-23。使用函数作为字符串replace()方法调用的第二个参数

// Initialize a value to use as a counter

var count = 0;

// Define a function to be executed on each matched substring, where the supplied parameter

// is the matched substring itself

function replaceWithCount(value) {

// Increment the counter

count = count + 1;

// Return to the replaced string the passed in value, with the current value of the

// counter appended to it

return value + count;

}

// Example usage

alert("Hello World".replace(/o/g, replaceWithCount)); // Hello1 Wo2rld

alert("Hello World".replace(/\s/g, replaceWithCount)); // Hello 3World

正则表达式可能变得非常复杂和强大,可能需要很多年才能理解和掌握。我在这里只讨论了最基本的内容;如果你想更深入地了解这个奇妙的世界,你可以在 Mozilla 的开发者网络上通过 http://bit.ly/reg_exps 找到关于 JavaScript 在线正则表达式使用的非常全面的参考资料。

更快地使用阵列

处理大型数据数组可能会降低代码的速度,因为您发现需要创建、访问、排序或循环访问它们,这通常涉及访问或操作数组中的每个单独的项。因此,数组越大,速度就越慢,你应该更加重视确保你的代码尽可能的高效。但是,有一些处理大型数据数组的提示和技巧,我将在本节中详细解释。

快速阵列创建

在 JavaScript 中有两种创建数组的方法,如下所示。JavaScript 创建和初始化数组的最快方法是后者,因为前者需要一个额外的步骤来获取Array类型的构造函数并对其执行new关键字:

var myArray = new Array();

var myArray = [];

快速数组循环

大多数 JavaScript 代码都充满了循环。通常,您需要处理数组或遍历对象文字来对存储在其中的数据执行计算或操作。众所周知,在 JavaScript 中循环数据是一项非常慢的任务,尤其是在较旧的浏览器中。清单 4-24 中的代码展示了一个典型的 JavaScript for循环,以及一个类似的,但是更加高效的版本。

清单 4-24。两种类型的 for 循环,一种比另一种更有效

var myArray = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];

// The most common type of loop

for (var index = 0; index < myArray.length; index++) {

// On every iteration through the loop, the value of myArray.length must

// be recomputed to ensure it has not changed since the last iteration

// - this is slow

}

// A similar but much faster version of the same loop

for (var index = 0, length = myArray.length; index < length; index++) {

// The value of myArray.length is computed once and stored in a variable.

// the value is read back from the variable on each iteration instead of being

// recomputed - much faster!

}

您可以使用两个 JavaScript 命令来管理循环:

  • break停止当前循环的执行,继续执行循环后面的代码。
  • continue停止循环的当前迭代,进入下一次迭代。

如果您已经找到了要寻找的值,您可以使用这些命令有效地停止循环迭代,或者如果某些代码块与循环的当前迭代无关,则跳过这些代码块的执行。清单 4-25 中的代码显示了这两个命令的例子。

清单 4-25。使用中断和继续命令来缩短循环的迭代次数

var myArray = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];

for (var index = 0, length = myArray.length; index < length; index++) {

if (myArray[index] < 50) {

// Ignore any values in the array below 50

// continue executes the next iteration immediately, ignoring any other code

// within the loop

continue;

}

if (myArray[index] == 90) {

// Ignore any values in the array above 90

// break stops the loop from iterating immediately, ignoring any other code

// no other iterations will be performed in the loop

break;

}

}

循环大量数据的最快方法是反向 while 循环。在这种技术中,我们使用一个while循环来代替一个for循环,并从数组的最后一个元素开始向下计数,直到到达第一个元素。这种方法比前面提到的for循环更快的原因是,在围绕for循环的每次迭代中,JavaScript 解释器必须运行一次比较,例如index < length,以知道何时停止循环。在while循环的情况下,当传递给while循环的参数为 falsy 值时,循环将停止运行,当其值为0时就会发生这种情况。正是因为这个原因,我们向下计数到数组的第一个索引0,因为它不需要执行更复杂的比较,所以这种类型的循环是最快的。清单 4-26 演示了反向 while 循环。

清单 4-26。反向 while 循环

var daysOfWeek = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],

len = daysOfWeek.length,

// The start index of the loop is the end item of the array

index = len,

daysOfWeekInReverse = [];

// Decrement the index each time through the loop, just after it's been compared in the while

// loop parameter. When the index is 0, the while loop will stop executing.

while(index--) {

daysOfWeekInReverse.push(daysOfWeek[index]);

}

// Because of the decrement in the while loop, at the end of the code, the value of index will

// be -1

alert(index); // -1

我觉得我应该强调一下,虽然这是最快的方法,但是对于大型数组来说,我们谈论的是几分之一秒的速度,所以本质上,这更像是一种教育,而不是编码技巧。

避免在循环中创建函数

为了更有效地使用数组,应该注意在循环中创建函数的陷阱。每次创建一个函数时,机器上的内存都是为该函数保留的,并用表示该函数的对象数据填充。因此,遍历 100 个项目并在每次循环中创建一个相同的函数将导致在内存中创建 100 个独立但相同的函数。这个原则在清单 4-27 中用一个更小的七项数组进行了演示,其中一个函数在循环的每一次迭代中被添加到一个结果对象文本中,这个结果对象文本可以用来反转原始数组项中的字符串。

清单 4-27。在循环中创建函数

var daysOfWeek = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],

index = 0,

length = daysOfWeek.length,

daysObj = {},

dayOfWeek;

// Loop through each day of the week

for (; index < length; index++) {

dayOfWeek = daysOfWeek[index];

// Add a property to the daysObj object literal for each day of the week, adding

// a function that reverses the name of the day of the week to each

daysObj[dayOfWeek] = {

name: dayOfWeek,

getReverseName: function() {

return this.name.split("").reverse().join("");

}

};

}

为了避免在每个循环中创建一个函数,只需在循环前创建并定义一个函数,并在循环中引用它,如清单 4-28 所示。

清单 4-28。在循环迭代中使用单个函数

var daysOfWeek = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],

index = 0,

length = daysOfWeek.length,

daysObj = {},

dayOfWeek;

// Define a single function to be used within any iteration of the loop

function getReverseName() {

// When called, the 'this' keyword will refer to the context in which it was called,

// namely the property in the daysObj object literal which it was called on

return this.name.split("").reverse().join("");

}

for (; index < length; index++) {

dayOfWeek = daysOfWeek[index];

daysObj[dayOfWeek] = {

name: dayOfWeek,

// Simply refer to the existing function here, rather than creating a new function

getReverseName: getReverseName

};

}

将密集型任务转移给 Web 工作器

您可能已经敏锐地意识到,JavaScript 只在浏览器的单个线程中运行。它对异步函数调用的处理意味着,例如,当对服务器进行 Ajax 调用时,整个用户界面不会锁定。然而,创建一个在每次迭代中进行大量处理的循环,你很快就会发现这是多么的受限,在某些情况下整个用户界面都会被锁住。

Web workers 是 Google 以 Gears 的名字创建的 W3C 标准化版本,它解决了这个问题,允许您旋转一个新的线程来执行特定的密集代码,避免了原来的线程锁定浏览器,本质上是在后台运行代码。如果您从操作系统的角度熟悉线程,您会很高兴听到 web worker 创建的每个新线程实际上都在操作系统中启动了一个全新的线程,这意味着它与原始线程完全不同,不涉及虚拟化。事实上,每个线程都与原始浏览器代码截然不同,它甚至不能访问 DOM 中的页面元素,也不能访问页面上的任何全局变量。为了在 web 工作线程中使用变量或对象,必须将其显式传递给它。所有 Web 浏览器的最新版本都支持 Web workers,尤其是从版本 10 开始的 Internet Explorer。

创建一个 web worker 线程如清单 4-29 所示,所有需要传递给 worker 构造函数的是包含在 Worker 线程中运行的代码的 JavaScript 文件的位置。这会创建 worker 对象并初始化它,但它还不会在 worker 线程中运行代码。这是为了让您有时间在创建 worker 之后、代码实际运行之前配置它。

清单 4-29。创建 web worker 是一项简单的任务

var workerThread = new Worker("filename.js");

创建一个 worker 固然很好,但在大多数情况下,您会希望将一些密集的代码卸载到一个新的 worker 线程,并明确表示希望从该线程取回代码的输出,以便在原始浏览器代码中使用。web worker 规范定义了两个事件,messageerror,当消息从 worker 回传或者 worker 内部发生错误时,分别调用这两个事件。可以使用 worker thread 对象的postMessage方法将消息发送到 worker thread,并且首先通过向它发送一条消息来启动 worker 运行,该消息中包含以这种方式触发它行动所需的任何输入数据,如清单 4-30 所示。

清单 4-30。配置 web 工作线程以侦听从工作线程发布的消息

//Create the worker thread

var workerThread = new Worker("filename.js");

// Start listening for messages posted from the code in the thread

workerThread.addEventListener("message", function(e) {

// The object e passed into the event handler contains the

// posted message in its data property

alert(e.data);

}, false);

// Run the code in the worker thread

workerThread.postMessage("");

在工作线程文件本身中,您使用self.addEventListener监听收到的消息,并可以使用self.postMessage将消息发送回调用浏览器脚本,从而完成两个脚本之间的通信循环。

如果一个 worker 已经完成了有用的工作,需要被关闭,可以从浏览器端或者在 worker 内部完成。在浏览器中,调用 worker 对象的terminate()方法会立即停止 worker 中的代码执行,无论它当时处于什么状态,它的线程都会立即终止。在工作线程本身中,对self.close()方法的调用将停止工作线程的运行并终止其线程,向调用浏览器脚本发回一条close消息。

使用 Web Worker 处理图像数据

让我们使用一个 web worker 来做一些繁重的图像处理,否则会阻塞标准浏览器脚本的用户界面。我们将从页面上的图像中获取原始图像数据,处理像素以创建同一图像的变体,然后在处理完成后替换原始图像。

为了从页面上的图像中提取原始像素数据,我们需要获取该图像并在 HTML5 <canvas>元素中绘制它,该元素用于在页面上的某个区域中绘制像素数据,我们可以使用 JavaScript 动态创建该区域。然后,我们可以获取这些原始像素数据并进行处理,使用 web worker 来避免在处理过程中锁定页面。这个 worker 将创建一组新的图像像素数据,我们可以将这些数据绘制到同一个<canvas>元素上,在将该元素添加到页面之前,用新处理的图像替换原始图像。我们将在第八章中更详细地介绍 HTML5 <canvas>元素,在那里我将解释如何使用这个强大而简单的新浏览器插件来构建简单的游戏。

清单 4-31。Web 工作器的图像处理

<!doctype html>

<html>

<head>

<meta charset="utf-8">

<title>Listing 4-31</title>

</head>

<body>

<img id="image" src="Listing4-31.jpg" alt="">

<script src="Listing4-32.js"></script>

</body>

</html>

清单 4-31 中的代码显示了一个简单的 HTML 页面,包含一个由imageid引用的图像,允许 JavaScript 轻松定位这个 DOM 元素。然后页面加载清单 4-32 中包含的 JavaScript 代码,如下所示。

清单 4-32。JavaScript 文件来启动图像处理

// Create a <canvas> element dynamically in JavaScript and get a reference to its

// 2d drawing context

var canvas = document.createElement("canvas"),

context = canvas.getContext("2d"),

// Get a reference to the image on the page

img = document.getElementById("image");

// Define a function to process the image data

function processImage() {

// Store the image width and height to avoid looking them up each time

var imgWidth = img.width,

imgHeight = img.height,

// Define a new web worker, using the code from the 'Listing4-33.js' file

workerThread = new Worker("Listing4-33.js");

// Set the new <canvas> element's dimensions to match that of the image

canvas.width = imgWidth;

canvas.height = imgHeight;

// Copy the image to the canvas, starting in the top-left corner

context.drawImage(img, 0, 0, imgWidth, imgHeight);

// Define the code to execute once a message is received from the web worker, which

// will be fired once the image data has been processed

workerThread.addEventListener("message", function(e) {

// Get the image data sent in the message from the event's data property

var imageData = e.data;

// Push the new image pixel data to the canvas, starting in the top-left corner

context.putImageData(imageData, 0, 0);

// Now add the resulting <canvas> element to the page. By performing all the necessary

// canvas actions before it's added to the page, we avoid the need for the browser to

// repaint the canvas element as we added and then replaced the image displayed on it

document.body.appendChild(canvas);

}, false);

// Kick off the web worker, sending it the raw image data displayed on the canvas

workerThread.postMessage(context.getImageData(0,0, imgWidth, imgHeight));

}

// Execute the processImage function once the image has finished loading

img.addEventListener("load", processImage, false);

清单 4-32 中的代码获取了一个对页面上图像的引用,一旦它被加载,就决定了它的宽度和高度,用来创建一个类似大小的<canvas>元素。然后提取原始图像像素数据,并使用清单 4-33 中的代码创建一个 web worker 对象。使用工作线程对象的postMessage()方法,像素数据被发送到工作线程,工作线程处理数据。一旦处理完成,worker 调用它自己的self.postMessage()方法,该方法是通过调用脚本的message事件监听器接收的,在将元素最终添加到页面之前,返回的、经过处理的图像数据被绘制到<canvas>元素上。

清单 4-33。Web worker 处理图像—反转其颜色

// Call the invertImage method when this worker receives a message from the calling script.

// The 'self' object contains the only methods a web worker can access apart from those it

// defines and creates itself

self.addEventListener("message", invertImage, false);

// Define a function to take an image and invert it, pixel by pixel, using its raw data

function invertImage(e) {

// The 'data' property of the 'message' event contains the pixel data passed from

// the calling script

var message = e.data,

// The 'data' property of the message passed contains the raw image pixel data

imagePixels = message.data,

x = 0,

len = imagePixels.length;

// Loop through each pixel, inverting its value within the original pixel data array.

// Pixel data is arranged in groups of 4 values, representing the red, green, blue, and

// opacity values of each visible screen pixel. We therefore loop through in jumps of 4

// on each iteration

for (; x < len; x += 4) {

// To invert a pixel's value, subtract it from the maximum possible value, which is 255

imagePixels[x] = 255 - imagePixels[x];

imagePixels[x + 1] = 255 - imagePixels[x + 1];

imagePixels[x + 2] = 255 - imagePixels[x + 2];

}

// Finally, post a message containing the updated pixel data back to the calling script

self.postMessage(message);

}

该图像处理操作的结果如图 4-4 所示,原始图像在左边,由 web worker 处理并绘制成<canvas>元素的反转图像在右边。

A978-1-4302-6269-5_4_Fig4_HTML.jpg

图 4-4。

Using a web worker and <canvas> to invert an image

基本性能测量

在这一章中,我们已经研究了很多提高 JavaScript 应用性能的技术,但是由于没有测量性能的方法,我们只能依靠自己的感觉来判断性能是否有所提高。

测量一段代码执行速度的最简单的方法是通过测量计算机时钟在代码执行开始和结束之间的差异来计时,如清单 4-34 所示。

清单 4-34。测量一个函数执行的时间

// Define variables to calculate the time taken to execute the function

var startTime,

endTime,

duration;

// Function to execute which we wish to measure

function doSomething() {

var index = 0,

length = 10000000,

counter = 0;

for (; index < length; index++) {

counter += index;

}

}

// Set the initial time to be the current date/time at this exact point, just before execution

// of the function

startTime = new Date();

// Execute the function

doSomething();

// Set the end time to be the current date/time just after execution

endTime = new Date();

// The time taken is the end time minus the first time, with both represented in milliseconds,

// the most precise measurement we have with JavaScript times

duration = endTime.getTime() - startTime.getTime();

alert(duration); // Took ∼700 ms on my machine

因为 JavaScript date 对象只表示低至毫秒级别的时间,所以使用这种技术我们无法获得任何更精确的度量。在第十四章的中,我们将看到console.time(),一种更精确的 JavaScript 代码测量形式,使用大多数浏览器内置的开发工具。

摘要

在这一章中,我们研究了可以用来提高 JavaScript 代码性能的技巧和技术,从脚本文件的初始加载,到处理 DOM 元素和事件,再到处理数组和字符串以提高性能的最佳方式。最后,我们讨论了如何将密集型代码任务卸载到单独的操作系统线程,以便在后台执行大量繁重操作的同时保持用户界面的响应,以及一种测量 JavaScript 代码性能的简单方法。关于 JavaScript 性能的主题,可能还有成千上万的技巧可以写,但是在大多数情况下,实现本章中详细介绍的技巧将会显著提高应用的性能。

在下一章中,我们将探讨 JavaScript 设计模式,解决某些代码问题的常用方法,以及如何组织您的代码,使其易于您和其他开发人员理解。

五、设计模式:创造型

在这一章和接下来的三章中,我将解释大规模 JavaScript 应用的设计模式和代码架构模式的原则,这些原则将使您的代码保持合理的组织和易于理解,使维护和添加代码的工作更加简单。通过实现这些章节中的一种或多种模式和技术,你会发现你的许多代码文件看起来彼此非常相似,这使得在同一个项目中一起工作的许多开发人员很快就熟悉了。事实上,如果您选择在多个项目中采用这些技术中的任何一种,您很可能会发现相同的模式和架构习惯用法适用于所有项目,这使得新开发人员更容易掌握不同项目的速度,并使这些开发人员能够专注于编写优秀的代码。

使用设计模式的秘诀是将它们视为编程工具箱中的工具,每种工具都有特定的用途。首先熟悉可用的模式以及何时使用每种模式,这些章节将有助于做到这一点,然后再尝试将它们应用到您的代码中——应用错误的工具会导致不必要的麻烦和浪费时间。除非您是一名经验丰富的 JavaScript 开发人员,否则您将在一开始就没有特定设计模式的情况下开始为您的应用编写代码,随着代码的增长,您会发现您需要对其进行更改,以便使进一步的开发更易于管理,并为代码库中的文件提供一些结构和熟悉度。这个过程通常被称为重构,通常在开发的这个阶段,您会考虑将特定的设计或架构模式应用到您的代码中,以简化未来的开发。对于那些坚持以特定模式开始新项目的人,或者那些坚持在一开始就使用特定的预建 JavaScript 框架的人,要保持警惕,因为除非他们是经验丰富的专业人士,否则这就相当于在确定需要工具解决的问题之前选择了一个新的、闪亮的工具。

每章都涵盖了您应该熟悉的设计模式和架构模式。研究每个模式并理解它是如何使用的,然后随着时间的推移,您将开始识别代码中需要应用的特定模式,以提高代码的可维护性,并且在某些情况下提高代码的效率。

什么是设计模式?

设计模式是经过试验和测试的、经过验证的编程和结构化代码的方式,因此它易于理解、易于维护,并且易于扩展,因为它有利于清晰性,为开发人员消除了不必要的复杂性,并且分离了大型代码库的不同部分之间的连接。它们是你编程工具箱中的工具。

设计模式最初是在 1994 年出版的一本名为《设计模式:可重用面向对象软件的元素》的书中介绍的,该书由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人组合写。他们最初的例子是用 C++和 Smalltalk 编程语言编写的,但是他们描述的大多数模式的原则适用于任何语言,包括 JavaScript。作者描述了 23 种不同的设计模式,分为三个不同的类别:创造、结构和行为。在这一章和接下来的章节中,我们将讨论那些最适用于 JavaScript 编程的模式,跳过那些不相关的,包括一些原著中没有的同样适用于 JavaScript 的现代模式。虽然许多关于设计模式的教程一遍又一遍地重复使用了许多相同的例子,但是我为这里介绍的每种模式都创建了原始的例子,这些例子通常比您可能找到的其他例子更好地解决了现实世界中的编码问题。

创造性设计模式

创造性设计模式描述了一个为您创建对象的“类”或方法,而不是您自己直接创建它们。这一抽象层为您和您的代码提供了更大的灵活性来决定哪种对象或哪种类型的对象最适合您的特定情况和需求。在这里,我将向你介绍五种在你的代码中可能有用的创造模式,每种模式都有例子。

工厂模式

工厂设计模式允许您创建一个对象,而无需指定用于创建它的“类”。当我们在前面的章节中讨论“类”时,我们已经直接使用了new JavaScript 关键字来创建一个特定“类”或子类的实例;使用工厂模式,对象创建过程是抽象的,允许相对复杂的对象创建过程隐藏在一个简单的接口后面,该接口不需要new关键字。这种抽象意味着底层的“类”类型和创建它们的方法可以在任何时候被完全替换,而不需要为其他开发人员更改“类”创建的接口——如果您知道将来可能需要进行大量的更改,但是您不想在大量代码文件中重写您的“类”实例化代码,这是一个理想的选择。

清单 5-1 显示了工厂模式的一个例子,它基于依赖于工厂方法输入参数的许多不同的“类”来实例化对象。

清单 5-1。工厂设计模式

// Define the factory that will make form field objects for us using the most appropriate

// "class" depending on the inputs

var FormFieldFactory = {

// The makeField method takes two options:

// - type, which defines the type of form field object to create, e.g. text, email,

//    or button

// - displayText, which defines either the placeholder text for the form field, or the

//    text to display on the button, depending on the type

makeField: function(options) {

var options = options || {},

type = options.type || "text",

displayText = options.displayText || "",

field;

// Create an object instance using the most appropriate "class" based on the

// supplied input type

switch (type) {

case "text":

field = new TextField(displayText);

break;

case "email":

field = new EmailField(displayText);

break;

case "button":

field = new ButtonField(displayText);

break;

// If in doubt, use the TextField "class"

default:

field = new TextField(displayText);

break;

}

return field;

}

};

// Define the TextField "class" to be used for creating <input type="text"> form elements

function TextField(displayText) {

this.displayText = displayText;

}

// The getElement method will create a DOM element using the supplied placeholder text value

TextField.prototype.getElement = function() {

var textField = document.createElement("input");

textField.setAttribute("type", "text");

textField.setAttribute("placeholder", this.displayText);

return textField;

};

// Define the EmailField "class" to be used for creating <input type="email"> form elements

function EmailField(displayText) {

this.displayText = displayText;

}

// The getElement method will create a DOM element using the supplied placeholder text value

EmailField.prototype.getElement = function() {

var emailField = document.createElement("input");

emailField.setAttribute("type", "email");

emailField.setAttribute("placeholder", this.displayText);

return emailField;

};

// Define the ButtonField "class" to be used for creating <button> form elements

function ButtonField(displayText) {

this.displayText = displayText;

}

// The getElement method will create a DOM element using the supplied button text value

ButtonField.prototype.getElement = function() {

var button = document.createElement("button");

button.setAttribute("type", "submit");

button.innerHTML = this.displayText;

return button;

};

清单 5-2 展示了如何在应用中使用清单 5-1 中创建的工厂。

清单 5-2。正在使用的工厂设计模式

// Use the factory to create a text input form field, an email form field, and a submit button.

// Note how we do not need to know about the underlying "classes" or their specific inputs to

// create the form fields - the FormFieldFactory abstracts this away

var textField = FormFieldFactory.makeField({

type: "text",

displayText: "Enter the first line of your address"

}),

emailField = FormFieldFactory.makeField({

type: "email",

displayText: "Enter your email address"

}),

buttonField = FormFieldFactory.makeField({

type: "button",

displayText: "Submit"

});

// Wait for the browser's "load" event to fire, then add the DOM elements represented by the

// three newly created objects to the current page

window.addEventListener("load", function() {

var bodyElement = document.body;

// Use the getElement() method of each object to get a reference to its DOM element for

// adding to the page

bodyElement.appendChild(textField.getElement());

bodyElement.appendChild(emailField.getElement());

bodyElement.appendChild(buttonField.getElement());

}, false);

当您希望通过屏蔽创建特定对象的更复杂操作来简化其余代码中这些对象的创建时,最好使用工厂模式。要在线阅读更多关于工厂模式的详细信息,请查看以下资源:

抽象工厂模式

抽象工厂模式将我们刚刚看到的工厂模式向前推进了一步,允许根据共同的用途或主题一起创建多个工厂,如果您的应用需要的话,创建一个额外的抽象层。清单 5-3 中的代码演示了这种模式,通过将两个工厂视为一个新工厂类型的实例来扩展了清单 5-1 中的例子,从这两个工厂中它们隐藏了相似的行为。

清单 5-3。抽象工厂设计模式

// Define a base factory "class" for creating form fields, from which other, more specialised

// form field creation factory "classes" will be inherited.

function FormFieldFactory() {

// Define a list of supported field types to be applied to all inherited form field

// factory classes

this.availableTypes = {

TEXT: "text",

EMAIL: "email",

BUTTON: "button"

};

}

FormFieldFactory.prototype = {

// Define a makeField() method which will be overwritten by sub classes using polymorphism.

// This method should therefore not be called directly from within this parent "class" so

// we'll throw an error if it is

makeField: function() {

throw new Error("This method should not be called directly.");

}

};

// Define a factory "class", inherited from the base factory, for creating HTML5 form fields.

// Read more about the differences in these form fields from HTML4 at

//http://bit.ly/html5_webforms

function Html5FormFieldFactory() {}

Html5FormFieldFactory.prototype = new FormFieldFactory();

// Override the makeField() method with code specific for this factory

Html5FormFieldFactory.prototype.makeField = function(options) {

var options = options || {},

type = options.type || this.availableTypes.TEXT,

displayText = options.displayText || "",

field;

// Select the most appropriate field type based on the provided options

switch (type) {

case this.availableTypes.TEXT:

field = new Html5TextField(displayText);

break;

case this.availableTypes.EMAIL:

field = new Html5EmailField(displayText);

break;

case this.availableTypes.BUTTON:

field = new ButtonField(displayText);

break;

default:

throw new Error("Invalid field type specified: " + type);

}

return field;

};

// Define a factory "class", also inherited from the same base factory, for creating

// older-style HTML4 form fields

function Html4FormFieldFactory() {}

Html4FormFieldFactory.prototype = new FormFieldFactory();

// Override the makeField() method with code specific for this factory

Html4FormFieldFactory.prototype.makeField = function(options) {

var options = options || {},

type = options.type || this.availableTypes.TEXT,

displayText = options.displayText || "",

field;

switch (type) {

case this.availableTypes.TEXT:

case this.availableTypes.EMAIL:

field = new Html4TextField(displayText);

break;

case this.availableTypes.BUTTON:

field = new ButtonField(displayText);

break;

default:

throw new Error("Invalid field type specified: " + type);

}

return field;

};

// Define the form field "classes" to be used for creating HTML5 and HTML4 form elements

function Html5TextField(displayText) {

this.displayText = displayText || "";

}

Html5TextField.prototype.getElement = function() {

var textField = document.createElement("input");

textField.setAttribute("type", "text");

textField.setAttribute("placeholder", this.displayText);

return textField;

};

// Since the placeholder attribute isn't supported in HTML4, we'll instead create and return a

// <div> element containing the text field and an associated <label> containing the

// placeholder text

function Html4TextField(displayText) {

this.displayText = displayText || "";

}

Html4TextField.prototype.getElement = function() {

var wrapper = document.createElement("div"),

textField = document.createElement("input"),

textFieldId = "text-field-" + Math.floor(Math.random() * 999),

label = document.createElement("label"),

labelText = document.createTextNode(this.displayText);

textField.setAttribute("type", "text");

textField.setAttribute("id", textFieldId);

// Associate the <label> with the <input> using the label 'for' attribute and the input 'id'

label.setAttribute("for", textFieldId);

label.appendChild(labelText);

wrapper.appendChild(textField);

wrapper.appendChild(label);

return wrapper;

};

function Html5EmailField(displayText) {

this.displayText = displayText;

}

Html5EmailField.prototype.getElement = function() {

var emailField = document.createElement("input");

emailField.setAttribute("type", "email");

emailField.setAttribute("placeholder", this.displayText);

return emailField;

};

// We define the button form element to be identical for both HTML5 and HTML4 form field types,

// so no need for two separate "classes". If we ever needed to create a different HTML5 version

// in future, we'd only need to update the relevant factory "class" with the change, and the

// rest of the code in our full application will adapt accordingly

function ButtonField(displayText) {

this.displayText = displayText;

}

ButtonField.prototype.getElement = function() {

var button = document.createElement("button");

button.setAttribute("type", "submit");

button.innerHTML = this.displayText;

return button;

};

我们可以使用清单 5-3 中的抽象工厂,如清单 5-4 所示,根据运行代码的浏览器的支持,产生正确类型的表单域。

清单 5-4。正在使用的抽象工厂设计模式

// Establish if the browser supports HTML5, and select the appropriate form field factory

var supportsHtml5FormFields = (function() {

// This self-executing function attempts to create a HTML5 form field type:

// <input type="email">

var field = document.createElement("input");

field.setAttribute("type", "email");

// If the new form field returns the corrent field type then it was created correctly

// and is a browser that supports HTML5\. If not, the browser is HTML4-only

return field.type === "email";

}()),

// Use the value returned previously to select the appropriate field field creation factory

// "class" and create an instance of it

formFieldFactory = supportsHtml5FormFields ? new Html5FormFieldFactory() : new Html4FormFieldFactory(),

// Use the factory to create a text input form field, an email form field, and a submit

// button, which will now use the most appropriate field type and attributes for the

// current browser

textField = formFieldFactory.makeField({

type: "text",

displayText: "Enter the first line of your address"

}),

emailField = formFieldFactory.makeField({

type: "email",

displayText: "Enter your email address"

}),

// Notice how we can harness the availableTypes property containing the list of supported

// field types from the factory "class" instead of using a hard-coded text string for the

// form field type. This is preferred, just as variables are preferable over

// hard-coded values.

buttonField = formFieldFactory.makeField({

type: formFieldFactory.availableTypes.BUTTON,

displayText: "Submit"

});

// Wait for the browser's "load" event to fire, then add the DOM elements represented by the

// three newly created objects to the current page

window.addEventListener("load", function() {

var bodyElement = document.body;

// Use the getElement() method of each object to get a reference to its DOM element for .

// adding to the page

bodyElement.appendChild(textField.getElement());

bodyElement.appendChild(emailField.getElement());

bodyElement.appendChild(buttonField.getElement());

}, false);

当您需要根据现有代码中多个“类”之间的共享目的或共同主题,在它们之外创建一个额外的抽象层时,最好使用抽象工厂模式,以便降低应用其余部分的开发复杂性。要在线阅读有关抽象工厂模式的更多详细信息,请查看以下资源:

构建器模式

像我们到目前为止看到的工厂和抽象工厂模式一样,构建器模式抽象了对象的创建。在这种模式中,我们只需要提供我们希望创建的对象的内容和类型,以及决定使用哪个“类”来创建它的过程,由构建器抽象出来。我们有效地构造了一个完整的对象,方法是将它的创建分成一系列更小的步骤,最后调用一个“构建”结果对象的操作,将它返回给调用代码。一个构建器可能包含相当数量的代码,所有这些代码都是为了让开发人员尽可能轻松地创建对象。

清单 5-5 展示了构建器模式,它定义了一个构建器来创建简单的 HTML 表单,该表单包含任意数量和类型的表单字段,可以按任意顺序添加,也可以随时添加;一旦添加完所有字段,使用getForm()“build”方法,仅在需要时创建并返回<form>元素。

清单 5-5。构建器模式

// Define a builder "class" for constructing simple forms which can be configured according to

// the end developer's needs. The end developer will instantiate the builder and add fields to

// the form as needed throughout the course of their application, finally calling a method to

// return a <form> element containing all the fields added

function FormBuilder() {}

FormBuilder.prototype = {

// Define a property for storing fields created

fields: [],

// Define a method for adding fields to the form instance

addField: function(type, displayText) {

var field;

// Use the supplied form field type and display text to instantiate the relevant form

// field "class"

switch (type) {

case "text":

field = new TextField(displayText);

break;

case "email":

field = new EmailField(displayText);

break;

case "button":

field = new ButtonField(displayText);

break;

default:

throw new Error("Invalid field type specified: " + type);

}

// Add the created field object to the storage array

this.fields.push(field);

},

// Define a method for returning the resulting <form> element, containing the fields added

// using the addField method

getForm: function() {

// Create a new <form> element

var form = document.createElement("form"),

index = 0,

numFields = this.fields.length,

field;

// Loop through each field in the fields property, getting the DOM element from each and

// adding it to the <form> element

for (; index < numFields; index++) {

field = this.fields[index];

form.appendChild(field.getElement());

}

// Return the populated <form> element

return form;

}

};

// Define the underlying form field "classes", as in Listing 5-1

function TextField(displayText) {

this.displayText = displayText || "";

}

TextField.prototype.getElement = function() {

var textField = document.createElement("input");

textField.setAttribute("type", "text");

textField.setAttribute("placeholder", this.displayText);

return textField;

};

function EmailField(displayText) {

this.displayText = displayText || "";

}

EmailField.prototype.getElement = function() {

var emailField = document.createElement("input");

emailField.setAttribute("type", "email");

emailField.setAttribute("placeholder", this.displayText);

return emailField;

};

function ButtonField(displayText) {

this.displayText = displayText || "";

}

ButtonField.prototype.getElement = function() {

var button = document.createElement("button");

button.setAttribute("type", "submit");

button.innerHTML = this.displayText;

return button;

};

然后,清单 5-5 中的表单生成器可以用于清单 5-6 所示的应用中,在该应用中,许多字段被添加到表单中,而不必直接实例化任何表单或字段“类”,也不必手动创建任何 DOM 元素。然后使用getForm()方法“构建”最终的对象,返回它以在调用代码中使用。

清单 5-6。正在使用的生成器模式

// Instantiate the form builder

var formBuilder = new FormBuilder(),

form;

// Add fields in any order and at any time required in the application - only the type and

// content is required, the actual object creation is abstracted away in the builder

formBuilder.addField("text", "Enter the first line of your address");

formBuilder.addField("email", "Enter your email address");

formBuilder.addField("button", "Submit");

// When the final form is required, call the builder's getForm method to return a <form> element

// containing all the fields

form = formBuilder.getForm();

window.addEventListener("load", function() {

document.body.appendChild(form);

}, false);

当您需要通过一系列较小的步骤在代码中创建一个大对象,并根据应用的要求在特定的点返回创建的对象时,最适合使用生成器模式。要在线阅读关于构建器模式的更多详细信息,请查阅以下资源:

原型模式

原型模式通过使用原型继承克隆现有对象来创建新对象。阅读完《??》第一章后,你会很熟悉这一点,因为原型继承是 JavaScript 创建时所围绕的继承类型,可以使用现有对象的prototype属性来实现,就像我们在 JavaScript 中创建“类”时看到的那样,或者通过使用 ECMAScript 5 的Object.create()方法来实现,这是首选方法,但仍然需要更好的 web 浏览器支持才能独占使用。清单 5-7 展示了使用第一种技术的原型模式,而清单 5-8 展示了使用第二种技术的模式。

清单 5-7。使用 prototype 关键字的原型模式

var textField,

emailField;

// Define a Field "class" to be used for creating <input> form elements

function Field(type, displayText) {

this.type = type || "";

this.displayText = displayText || "";

}

// Use the prototype property to adopt the Prototype pattern of defining methods that will be

// applied to any object instantiated from this "class"

Field.prototype = {

getElement: function() {

var field = document.createElement("input");

field.setAttribute("type", this.type);

field.setAttribute("placeholder", this.displayText);

return field;

}

};

// Create two object instances, both of which receive the getElement method from the prototype

textField = new Field("text", "Enter the first line of your address");

emailField = new Field("email", "Enter your email address");

// Add the elements stored in these objects to the current page once loaded

window.addEventListener("load", function() {

var bodyElement = document.body;

bodyElement.appendChild(textField.getElement());

bodyElement.appendChild(emailField.getElement());

}, false);

清单 5-8。使用 ECMAScript 5 的原型模式

// Define a base object with two properties, type and displayText, and a getElement() method

// which creates a HTML <input> element, configuring it using the values from the two properties

var field = {

type: "",

displayText: "",

getElement: function() {

var field = document.createElement("input");

field.setAttribute("type", this.type);

field.setAttribute("placeholder", this.displayText);

return field;

}

},

// Create a new object based upon the base object, using ECMAScript 5's Object.create()

// method to clone the original object and apply values to the two properties type and

// displayText, in order to create an object capable of creating a <input type="text">

// element when the object's getElement() method is called

textField = Object.create(field, {

// The second parameter of Object.create() allows values from the first parameter to be

// overwritten using the format described in Chapter 1

'type': {

value: "text",

enumerable: true

},

'displayText':{

value: 'Enter the first line of your address',

enumerable: true

}

}),

// Create another new object based upon the base object, using different property values in

// order to allow the creation of a <input type="email"> element when the object's

// getElement() method is called

emailField = Object.create(field, {

'type': {

value: "email",

enumerable: true

},

'displayText':{

value: 'Enter your email address',

enumerable: true

}

});

// Call the getElement() method of both objects, appending the created <input> DOM elements to

// the current page once loaded

window.addEventListener("load", function() {

var bodyElement = document.body;

bodyElement.appendChild(textField.getElement());

bodyElement.appendChild(emailField.getElement());

}, false);

当您想要动态地创建新对象作为现有对象的克隆,或者基于“类”模板创建对象时,最好使用原型模式。要在线阅读关于原型模式的更多详细信息,请查看以下资源:

单一模式

当应用于 JavaScript 时,singleton 模式定义了只有一个实例的对象的创建。最简单的形式是,单例可以是一个简单的对象文字,封装了特定的相关行为,如清单 5-9 所示。

清单 5-9。单一模式

// Group related properties and methods together into a single object literal, which

// we call a Singleton

var element = {

// Create an array for storage of page element references

allElements: [],

// Get an element reference by its ID and store it

get: function(id) {

var elem = document.getElementById(id);

this.allElements.push(elem);

return elem;

},

// Create a new element of a given type, and store it

create: function(type) {

var elem = document.createElement(type);

this.allElements.push(elem);

return elem;

},

// Return all stored elements

getAllElements: function() {

return this.allElements;

}

},

// Get and store a reference to a page element with ID of "header"

header = element.get("header"),

// Create a new <input> element

input = element.create("input"),

// Contains id="header", and new <input> elements

allElements = element.getAllElements();

// Check to see how many elements are stored

alert(allElements.length); // 2

但是,在某些情况下,您可能希望执行一些初始化代码,作为创建单例的一部分。对于这些,使用一个自执行函数,如清单 5-10 所示,并使用return关键字来显示你希望对代码的其余部分可用的对象结构。当我在下一章讲述模块模式时,我将进一步研究以这种方式使用自执行函数。

清单 5-10。具有自执行功能的单例模式

// Define a singleton containing cookie-related methods. Initialization code is achieved by

// using a self-executing function closure, which allows code to be executed at creation which

// is then unavailable publicly to the rest of the application

var cookie = (function() {

// Cookies are stored in the document.cookie string, separated by semi-colons (;)

var allCookies = document.cookie.split(";"),

cookies = {},

cookiesIndex = 0,

cookiesLength = allCookies.length,

cookie;

// Loop through all cookies, adding them to the "cookies" object, using the cookie names

// as the property names

for (; cookiesIndex < cookiesLength; cookiesIndex++) {

cookie = allCookies[cookiesIndex].split("=");

cookies[unescape(cookie[0])] = unescape(cookie[1]);

}

// Returning methods here will make them available to the global "cookie" variable defined

// at the top of this code listing

return {

// Create a function to get a cookie value by name

get: function(name) {

return cookies[name] || "";

},

// Create a function to add a new session cookie

set: function(name, value) {

// Add the new cookie to the "cookies" object as well as the document.cookie string

cookies[name] = value;

document.cookie = escape(name) + "=" + escape(value);

}

};

}());

// Set a cookie using the "set" method exposed through the "cookie" singleton

cookie.set("userID", "1234567890");

// Check that the cookie was set correctly

alert(cookie.get("userID")); // 1234567890

许多开发人员使用这种单例模式将相关代码封装和分组到一个层次结构中,称为命名空间,这在 Java 等其他编程语言中很流行。通过像这样将所有内容保存在一个全局变量中,可以降低与应用中使用的任何第三方代码冲突的风险。看一下清单 5-11,它显示了一个基本的命名空间结构,用于在命名的部分中将代码联系在一起,以减少开发人员的困惑,简化维护和开发,使代码更容易阅读和理解。

清单 5-11。使用单例模式的命名空间

// Use an object literal to create a hierarchy of grouped properties and methods,

// known as a "namespace"

var myProject = {

data: {

// Each nested property represents a new, deeper level in the namespace hierarchy

ajax: {

// Create a method to send an Ajax GET request

get: function(url, callback) {

var xhr = new XMLHttpRequest(),

STATE_LOADED = 4,

STATUS_OK = 200;

xhr.onreadystatechange = function() {

if (xhr.readyState !== STATE_LOADED) {

return;

}

if (xhr.status === STATUS_OK) {

callback(xhr.responseText);

}

};

xhr.open("GET", url);

xhr.send();

}

}

}

};

// Add to the namespace after creation using dot notation

myProject.data.cookies = {

// Create a method for reading a cookie value by name

get: function(name) {

var output = "",

escapedName = escape(name),

start = document.cookie.indexOf(escapedName + "="),

end = document.cookie.indexOf(";", start);

end = end === -1 ? (document.cookie.length - 1) : end;

if (start > = 0) {

output = document.cookie.substring(start + escapedName.length + 1, end);

}

return unescape(output);

},

// Create a method for setting a cookie name/value pair

set: function(name, value) {

document.cookie = escape(name) + "=" + escape(value);

}

};

// Execute methods directly through the "namespace" hierarchy using dot notation

myProject.data.ajax.get("/", function(response) {

alert("Received the following response: " + response);

});

// Note how using the hierarchy adds clarity to the final method call

myProject.data.cookies.set("userID", "1234567890");

myProject.data.cookies.set("name", "Den Odell");

// Read back the cookie valus set previously

alert(myProject.data.cookies.get("userID")); // 1234567890

alert(myProject.data.cookies.get("name")); // Den Odell

当您需要创建一个在整个代码中使用的对象的单个实例时,或者需要命名您的代码时,最好使用 singleton 模式,通过在单个全局对象下定义的层次结构将代码划分为命名的部分。要在线阅读关于单例模式的更多细节,请查阅以下资源:

摘要

在这一章中,我介绍了设计模式的概念,并展示了如何使用创造性的设计模式在您自己的 JavaScript 应用中抽象出对象创建。设计模式是 JavaScript 开发的瑞士军刀中的工具,但是像所有工具一样,您需要知道何时何地最好地使用它们。熟悉本章中的模式,并确保在认识到代码中需要设计模式之前,不要使用它。

在下一章,我将继续关注结构化设计模式,您可以在 JavaScript 代码中将对象组合成更大、更结构化的形式。