JavaScript-技巧高级教程-全-

JavaScript 技巧高级教程(全)

原文:Pro JavaScript Techniques

协议:CC BY-NC-SA 4.0

一、JavaScript 专业技术

欢迎来到专业 JavaScript 技术。这本书概述了 JavaScript 的现状,特别是当它应用于职业程序员时。谁是职业程序员?一个对 JavaScript(可能还有其他几种语言)有着坚实基础的人。您对 JavaScript 的广度和深度感兴趣。您想了解文档对象模型(DOM)等典型特性,但也想了解客户端上模型-视图-控制器(MVC) 的所有这些内容。更新的 API,新的特性和功能,以及代码的创造性应用是你在这里寻找的。

这是这本书的第二版。自 2006 年第一版问世以来,情况发生了很大变化。当时,JavaScript 正在经历一个有点痛苦的转变,从一种玩具脚本语言转变为一种对多种不同任务都有用且有效的语言。如果你愿意,那是 JavaScript 的青春期。现在,JavaScript 正处于另一个转变的末期:继续这个比喻,从青春期到成年期。JavaScript 的使用几乎无处不在,85%到 95%的网站,取决于你相信谁的统计,在他们的主页上有一些 JavaScript。许多人认为 JavaScript 是世界上最流行的编程语言(就经常使用它的人数而言)。但是比单纯的使用更重要的是有效性和能力。

JavaScript 已经从一种玩具语言(图像翻转!状态栏文本操作!)到一个有效但有限的工具(想想客户端表单验证),到它目前作为一种功能广泛的编程语言的地位,不再局限于仅仅是浏览器。程序员正在编写提供 MVC 功能的 JavaScript 工具,这一直是服务器的领域,以及复杂的数据可视化、模板库等等。这个清单还在继续。在过去,设计人员需要依赖. NET 或 Java Swing 客户端来为服务器端数据提供功能全面、丰富的接口,现在我们可以通过浏览器用 JavaScript 实现该应用。而且,使用 Node.js,我们有了 JavaScript 自己版本的虚拟机,一个可以运行任何数量的不同应用的可执行文件,所有这些都是用 JavaScript 编写的,不需要浏览器。

这一章将描述我们是如何来到这里的,以及我们要去哪里。它将着眼于促成 JavaScript 革命的浏览器技术(和流行度)的各种改进。JavaScript 本身的状态需要检查,因为我们想在看我们要去哪里之前知道我们在哪里。然后,当我们检查接下来的章节时,您将会看到职业 JavaScript 程序员需要知道什么才能名副其实。

我们是怎么到这里的?

在这本书的第一版,谷歌 Chrome 和 Mozilla Firefox 是相对较新的产品。Internet Explorer 6 和 7 独领风骚,第 8 版越来越受欢迎。几个因素共同推动了 JavaScript 的发展。

JavaScript 的大部分时间都依赖于浏览器。浏览器是 JavaScript 的运行时环境,程序员对 JavaScript 功能的访问高度依赖于访问该程序员网站的浏览器的品牌、型号和版本。到了 2005 年中期,90 年代的浏览器大战被 Internet Explorer 轻松拿下,浏览器开发陷入停滞。两种浏览器挑战了这种状况:Mozilla Firefox 和 Google Chrome。Firefox 是最早的网络浏览器之一 Netscape 的后代。Chrome 有谷歌的支持,这足以让它成为一个即时玩家。

但是这两种浏览器都做出了一些促进 JavaScript 革命的设计决策。第一个决定是支持万维网联盟实现各种标准。无论是处理 DOM、事件处理还是 Ajax ,Chrome 和 Firefox 通常都遵循规范并尽可能好地实现它。对于程序员来说,这意味着我们不必为 Firefox 和 Chrome 编写单独的代码。我们已经习惯于为 IE 和其他东西编写单独的代码,所以拥有分支代码本身并不新鲜。但是确保分支不会过于复杂是一种令人欣慰的解脱。

说到标准,Firefox 和 Chrome 也和欧洲计算机制造商协会(ECMA,现在称为 Ecma)一起做了很多工作。Ecma 是监管 JavaScript 的标准机构。(从技术角度来说,Ecma 负责 ECMAScript 标准,因为 JavaScript 是 Oracle 的商标,而且……嗯,我们其实并不关心这些细节,不是吗?我们将使用 JavaScript 来指代这种语言,使用 ECMAScript 来指代 JavaScript 实现所遵循的规范。ECMAScript 标准就像 IE 开发一样衰落了。随着真正的浏览器竞争的兴起,ECMAScript 标准再次被采用。ECMAScript 第 5 版(2009) 编纂了许多在十年(!)自上一版本标准以来。随着 2011 年 5.1 版本的推出,这个团队本身也充满了活力。未来是可以预见的,目前正在对标准的第 6 版和第 7 版进行大量的工作。

值得表扬的是,Chrome 也推动了 JavaScript 的更新。Chrome JavaScript 引擎名为 V8 ,是 Chrome 在 2008 年首次亮相时非常重要的一部分。Chrome 团队构建了一个比大多数 JavaScript 引擎更快的引擎,并在后续版本中保持了这一目标。事实上,V8 引擎令人印象深刻,它成为了 Node.js 的核心,这是一个独立于浏览器的 JavaScript 解释器。Node 最初是作为一个使用 JavaScript 作为主要应用语言的服务器,现在已经成为一个灵活的平台,可以运行任意数量的基于 JavaScript 的应用。

回到 Chrome:谷歌引入浏览器领域的另一项重大创新是常青树应用的概念。Chrome 的默认设置是自动为你更新浏览器,而不是下载一个单独的浏览器安装进行更新。虽然这种方法有时在公司界是一种痛苦,但对非公司的消费者来说却是一大福音。).如果你使用 Chrome(最近几年还使用了 Firefox),你的浏览器就是最新的,无需你做任何努力。虽然微软在通过 Windows Update 推送安全更新方面已经做了很长时间,但它并没有向 Internet Explorer 引入新功能,除非它们与新版本的 Windows 相结合。换句话说,IE 的更新来得很慢。Chrome 和 Firefox 总是拥有最新和最棒的功能,并且非常安全。

随着谷歌加紧开发 Chrome 的功能,其他浏览器制造商也在迎头赶上。有时这是以更愚蠢的方式出现的,比如 Firefox 采用了 Chrome 的版本编号。但这也导致了 Mozilla 和微软对 JavaScript 引擎冷眼相看。在过去的几年里,这两家浏览器制造商都大幅改进了他们的 JS 引擎,Chrome 的领先优势虽然强大,但不再是不可逾越的。

最后,微软已经(大部分)放弃了其经典的“拥抱和扩展”哲学,至少在 JavaScript 方面。在 IE 第 9 版中,微软实现了万维网联盟(W3C)事件处理,并标准化了它的 DOM 接口和 Ajax API。对于 JavaScript 的大多数标准特性,我们不再需要实现同一个代码的两个版本!(当然,遗留浏览器的遗留代码仍然是一个问题……)

这似乎是一剂灵丹妙药。JavaScript 比以往任何时候都要快。为各种不同的浏览器编写代码更容易。标准文档既描述了现实世界,又为未来的特性提供了有用的路线图。我们的大多数浏览器都是最新的。那么我们现在需要担心的是什么,未来的路在何方?

现代 JavaScript

使用 JavaScript 开发严肃的应用从未如此简单。我们已经与过去的糟糕时代彻底决裂,过去的时代是为多种浏览器分别编写代码,糟糕的标准没有得到很好的实现,缓慢的 JavaScript 引擎常常是事后才想到的。让我们看看现代 JavaScript 环境的状态。具体来说,我们将关注两个领域:现代浏览器和现代工具包。

现代 JavaScript 依赖于现代浏览器的理念。什么是现代浏览器 ?不同的组织用不同的方式描述它。谷歌表示,他们的应用支持当前和以前的主要浏览器版本。(有趣的是,据我们所知,Gmail 仍然可以在 IE9 上工作!)在一篇有趣的文章中,英国广播公司(BBC)网站的幕后人员透露,他们将定义为支持以下功能的现代浏览器:

  1. document.querySelector() / document.querySelectorAll()
  2. window.addEventListener()
  3. 存储 API ( localStoragesessionStorage)

jQuery 可能是网络上最流行的 JavaScript 库,它将其版本分为 1.x 系列和 2.x 系列,1 . x 系列支持 IE 6 及更高版本,2 . x 系列支持 IE 9 及更高版本等“现代”浏览器。毫无疑问,这是现代和古代的分界线。另外两大浏览器是常青树。虽然 Safari 和 Opera 不是常青树,但它们的更新速度比 IE 快,市场份额也不如 IE。

那么,现代浏览器的界限在哪里?唉,边界似乎徘徊在 Internet Explorer 版本 9 到 11 之间。但 IE 8 绝对是浏览器历史上的遥远一面。它不支持 ECMAScript 5 的大多数功能。它不包括用于 W3C 事件处理的 API。这个清单还在继续。所以当我们讨论现代浏览器时,我们至少会提到 Internet Explorer 9。我们的报道不会尽力支持古老的浏览器。在相关和简单的地方,我们会指出旧版本 IE 的 polyfills,但一般来说,我们的底线是 Internet Explorer 9。

图书馆的崛起

除了现代浏览器之外,我们还需要讨论 JavaScript 当前环境的另一个重要方面:库。在过去的 8 年里,JavaScript 库的数量和种类呈爆炸式增长。JavaScript 的 GitHub 库超过 80 万个;其中,近 900 颗恒星超过 1000 颗。JavaScript 库生态系统最初只是一些实用函数的集合,现在已经演变(有些混乱)成一个广阔的前景。

作为 JavaScript 开发人员,这对我们有什么影响?当然,有“库作为扩展”的模型,其中库提供额外的功能。想想像 Backbone 和 Angular 这样的 MVC 库(我们将在后面的章节中讨论),或者像 d3 或 Highcharts 这样的数据可视化库。但是 JavaScript 处于一个有趣的位置,因为库也可以为一些浏览器上的标准特性提供一个层次接口,而在其他浏览器上却不是。

长期以来,JavaScript 中可变实现特性的标准例子是事件处理。Internet Explorer 有自己的事件处理 API。其他浏览器通常遵循 W3C 的 API。各种库为事件处理提供了统一的实现,包括了两个世界的精华。其中一些库是独立的,但成功的库也规范了 Ajax、DOM 和许多其他跨浏览器实现的功能。

这些库中最流行的是 jQuery 。从一开始,jQuery 就是使用新 JavaScript 特性的首选库,而不用担心浏览器对这些特性的支持。因此,不使用 IE 或 W3C 的事件处理,您可以简单地使用 jQuery 的.on()函数,该函数包装了方差,提供了一个统一的接口。其他几个库也提供了类似的功能:Dojo、Ext JS、Prototype、YUI、MooTools 等等。这些工具包库旨在为开发人员标准化 API。

标准化不仅仅是提供简单的分支代码。这些库经常改善有缺陷的实现。一个函数的官方 API,版本之间可能变化不大,但是会有 bugs 有时这些错误会被修复,有时不会,有时修复会引入新的错误。在图书馆可以修复或解决这些问题的地方,他们做到了。例如,jQuery 1.11 包含六个以上的事件处理 API 问题修复程序。

一些库(特别是 jQuery)也对某些功能提供了新的或不同的解释。jQuery 选择器函数是这个库的核心,早于现在标准的querySelector()querySelectorAll()函数,它是在 JavaScript 中包含这些函数的驱动力。其他库提供对功能的访问,尽管底层实现非常不同。在本书的后面,我们将研究 Ajax 新的跨源资源共享(CORS )协议,该协议允许 Ajax 向最初服务页面的服务器之外的服务器发出请求。一些库已经实现了一个使用 CORS 的版本,但是在需要的地方退回到 JSON 和 padding (JSON-P)。

由于它们的实用性,一些库已经成为专业 JavaScript 程序员的标准开发工具包的一部分。它们的特性可能还没有被标准化为 JavaScript,但是它们是知识和功能的积累,这使得快速实现设计变得更加容易。然而,最近几年,你可以通过询问 jQuery(或另一个库)是否真的是在现代浏览器上开发所必需的,来获得相当多的点击量。考虑 BBC 的要求;如果有这三种方法,您肯定可以实现很大程度上类似 jQuery 的功能。但是 jQuery 还包括一个简化但扩展的 DOM 接口,它可以处理各种不同边缘情况下的错误,如果您需要支持 IE 8 或更早版本,jQuery 是您的主要选择。因此,专业的 JavaScript 程序员必须考虑项目的需求,并考虑是否值得冒险重新发明 jQuery(或另一个类似的库)提供的轮子。

不止是关于移动设备的说明

在早期的 JavaScript 和 web 开发书籍中,你会看到一个章节,也许是整个第,讲述如何处理移动浏览。移动浏览 在总浏览量中所占的份额很小,而且市场非常分散,似乎只有专家才会对移动开发感兴趣。现在不是这样的了。从这本书的第一版开始,移动网页浏览已经爆炸了,它是一个与桌面开发非常不同的野兽。考虑一些统计数据:根据各种来源,移动浏览占所有浏览的 20%到 30%。当你读到这篇文章时,它很可能代表了更多,因为自 iPhone 问世以来,它一直在增长。说到这里,超过 40%的移动浏览是通过 iOS Safari 完成的,尽管 Android 的浏览器和 Chrome for Android 正在取得进展(可能已经超过 Safari,取决于你相信谁的统计数据)。iOS 上的 Safari 和桌面上的 Safari 不一样,安卓 Chrome 对桌面 Chrome,手机火狐对桌面火狐也是一样。移动是主流。

移动设备上的浏览器带来了一系列新的挑战和机遇。移动设备通常比台式机更受限制(尽管这是另一个正在迅速缩小的差距)。相反,移动设备提供了新的功能(滑动事件、更精确的地理定位等)和新的交互习惯(用手代替鼠标,滑动滚动)。根据您的开发需求,您可能需要构建一个在移动和桌面上都好看的应用,或者为移动平台重新实现现有功能。

在过去的几年里,JavaScript 领域发生了巨大的变化。尽管 API 有一些标准化,但也有许多新的挑战。这对我们这些职业 JavaScript 程序员有什么影响?

我们将何去何从?

我们应该为自己制定一些标准。我们已经设定了一个:IE9 作为现代浏览器体验的基础。其他浏览器是常青树,不用担心。那么手机呢?虽然这个问题很复杂,但一般来说,iOS 6 和 Android 4.1 将成为我们的底线。移动计算比台式机更新得更快更频繁,因此我们对使用这些更新版本的移动操作系统充满信心。

也就是说,让我们暂时离题,讨论一下你的受众,而不是浏览器版本、操作系统或平台。虽然我们可以整天引用统计数据,但有价值的统计数据告诉你的是你的听众,而不是一般的听众。也许你正在为你的雇主设计,他已经标准化了 IE 10。或者你对应用的想法很大程度上依赖于只有 Chrome 提供的功能。或者可能甚至没有桌面版本,但你的目标是推出 iPads 和 Android 平板电脑。考虑你的目标受众。这本书被写得具有广泛的适用性,你的应用也可能如此。但是花时间担心之前提到的平板电脑应用的 IE9 漏洞是愚蠢的,不是吗?现在,回到我们的标准。

对于截图和测试,这本书一般会倾向于谷歌 Chrome。偶尔,我们会在 Firefox 或 Internet Explorer 上演示相关的代码。对于开发者来说,Chrome 是黄金标准——不一定是用户友好,但肯定是向程序员展示的信息。在后面的章节中,我们将研究各种可用的开发工具,不仅研究 Chrome,还研究 Firefox(有和没有 Firebug)和 IE。

作为标准库,我们会参考 jQuery。当然,还有很多选择,但是 jQuery 胜出有两个原因:首先,它是 web 上最流行的通用 JavaScript 库。其次,至少其中一位作者(John Resig)与 jQuery 有一点渊源,这使得另一位作者(约翰·帕克斯顿)不得不承认使用 jQuery 的重要性。在更新这本书的过程中,我们用 jQuery 的功能库替换了以前版本中的许多技术。在这些情况下,我们不愿意重新发明轮子。根据需要,我们将引用适当的 jQuery 功能。当然,我们也会讨论新的令人兴奋的技术!

在 JavaScript 自身发展的推动下,JavaScript IDEs 在过去几年中有了显著的更新。可能性不胜枚举,但有几个应用值得注意。John Resig 在他的开发环境中使用了高度定制的 vim 版本。约翰·帕克斯顿稍微懒一点,他选择使用 JetBrains 的优秀的 WebStorm ( http://www.jetbrains.com/webstorm/)作为他的 IDE。Adobe 提供开源、免费的支架 IDE ( http://brackets.io/),目前版本为 1.3。Eclipse 也是可用的,许多人已经报告了通过定制 SublimeText 或 Emacs 来完成他们的投标的积极结果。像往常一样,用你觉得最舒服的。

还有其他工具可以帮助 JavaScript 开发。我们将在本书的后面专门用一章来介绍它们,而不是在这里一一列举。这意味着这是一个很好的时间来给出一个未来的轮廓。

接下来

从第二章开始,我们将看看 JavaScript 语言的最新和最伟大之处。这意味着查看新的特性,比如那些通过Object类型可用的特性,但是也要重新检查一些旧的概念,比如引用、函数、作用域和闭包。我们将把所有这些都放在特性、功能和对象的标题下,但是它涵盖的内容不止这些。

第三章讨论了创建可重用代码。第二章跳过了 JavaScript 最大的新特性之一,即Object.create()方法,以及它对面向对象 JavaScript 代码的影响。因此,在这一章中,我们将花时间了解用 JavaScript 实现的Object.create()、函数构造函数、原型和面向对象的概念。

已经花了两章开发代码,我们应该开始考虑如何管理它。第四章向你展示了用于调试 JavaScript 代码的工具。我们首先研究浏览器及其开发工具。

第五章开始讨论 JavaScript 功能的一些常用领域。这里我们看一下文档对象模型。自上一版以来,DOM API 的复杂性增加了,但并没有真正变得更简单。但是有一些我们应该熟悉的新特性。

在第六章中,我们试图掌握事件。这里的大新闻是遵循 W3C 风格的事件 API 的标准化。这让我们有机会远离实用程序库,最终深入到事件 API 中,而不用担心浏览器之间的巨大差异。

JavaScript 的第一个非玩具应用之一是客户端表单验证。令人惊讶的是,浏览器制造商花了十多年的时间才想到除了捕获提交事件之外,还要增加表单验证的功能。当查看第七章中的 JavaScript 和表单验证时,我们会发现 HTML 和 JavaScript 都提供了一套全新的表单验证功能。

每个用 JavaScript 开发的人都花了一些时间介绍 Ajax 。随着跨源资源共享(CORS)的引入,Ajax 功能终于克服了最愚蠢的限制。

像 Yeoman、Bower、Git 和 Grunt 这样的命令行工具包含在 Web 制作工具中。这些工具将向我们展示如何快速添加所有需要的文件和文件夹。这样我们才能专注于发展。

第十章包括和检测。使用在前一章中获得的知识,我们现在开始看看是什么使 Angular 工作,以及如何实现单元测试和端到端测试。

最后,第十一章讨论了 JavaScript 的未来。ECMAScript 6 或多或少会在本书出版时被解决。ECMAScript 7 正在积极开发中。除了 JavaScript 的基本发展方向之外,我们还将看看您现在可以使用哪些特性。

摘要

这一章我们花了很多时间讨论 JavaScript 的一切:平台、历史、ide 等等。我们相信历史影响着现在。我们想要解释我们在哪里,以及我们是如何到达这里的,来帮助你理解为什么 JavaScript 是现在的样子。当然,我们计划用这本书的大部分来讨论 JavaScript 是如何工作的,特别是对于专业程序员来说。我们强烈认为,这本书涵盖了每个专业 JavaScript 程序员都应该熟悉的技术和 API。所以事不宜迟…

二、特性、函数和对象

对象是 JavaScript 的基本单位。事实上,JavaScript 中的一切都是对象,并且在面向对象的层次上进行交互。为了构建这种可靠的面向对象语言,JavaScript 包含了一系列特性,使其在基础和功能上都独一无二。

本章涵盖了 JavaScript 语言的一些最重要的方面,比如引用、作用域、闭包和上下文。这些不一定是语言的基石,但优雅的拱门,既支持又完善了 JavaScript。我们将深入研究将对象作为数据结构使用的工具。接下来深入探讨面向对象 JavaScript 的本质,包括对类和原型的讨论。最后,这一章探索了面向对象 JavaScript 的使用,包括对象的行为和如何创建新的对象。如果认真对待,这很可能是本书最重要的一章,因为它将彻底改变你看待 JavaScript 语言的方式。

语言特征

JavaScript 有许多特性,这些特性对于语言的发展至关重要。很少有其他语言像它一样。我们发现这些特性的结合恰到好处,造就了一种看似强大的语言。

参考值和数值

JavaScript 变量以两种方式保存数据:拷贝和引用。任何原始值都被复制到变量中。原语 是字符串、数字、布尔、空和未定义。原语最重要的特征是它们通过被赋值、复制、传递给函数以及从函数返回。

JavaScript 的其余部分依赖于引用。任何不包含上述原语值的变量都包含一个对对象的引用。一个引用是一个指向一个对象(或者数组,或者日期,或者你所拥有的东西)在内存中的位置的指针。实际的对象(数组、日期或其他)被称为引用对象 。这是一个非常强大的特性,在许多语言中都有。它允许某些效率:两个(或更多!)变量没有自己的对象副本;它们只是指同一个对象。通过一个引用对 referent 的更新反映在另一个引用中。通过维护对象的引用集,JavaScript 为您提供了更大的灵活性。清单 2-1 中显示了一个例子,其中两个变量指向同一个对象,通过一个引用对对象内容的修改反映在另一个引用中。

清单 2-1 。多个变量引用一个对象的例子

// Set obj to an empty object
// (Using {} is shorter than 'new Object()')
var obj = {};

// objRef now refers to the other object
var refToObj = obj;

// Modify a property in the original object
obj.oneProperty = true;

// We now see that the change is represented in both variables
// (Since they both refer to the same object)
console.log( obj.oneProperty === refToObj.oneProperty );

// This change goes both ways, since obj and refToObj are both references
refToObj.anotherProperty = 1;
console.log( obj.anotherProperty === refToObj.anotherProperty );

对象有两个特性:属性和方法。这些通常被统称为对象的成员 。属性包含对象的数据。属性可以是原语,也可以是对象本身。方法是作用于对象数据的函数。在 JavaScript 的一些讨论中,方法包含在属性集中。但是这种区别通常是有用的。

自修改 对象在 JavaScript 中非常少见。让我们看一个发生这种情况的常见例子。数组对象能够使用push方法向自身添加额外的项目。因为在数组对象的核心,值被存储为对象属性,结果是类似于清单 2-1 中所示的情况,其中一个对象被全局修改(导致多个变量的内容同时被改变)。这种情况的一个例子可以在清单 2-2 中找到。

清单 2-2 。自修改对象的示例

// Create an array of items
// (Similar to 2-1, using [] is shorter than 'new Array()')
var items = [ 'one', 'two', 'three' ];

// Create a reference to the array of items
var itemsRef = items;

// Add an item to the original array
items.push( 'four' );

// The length of each array should be the same,
// since they both point to the same array object
console.log( items.length == itemsRef.length );

重要的是要记住,引用只指向被引用对象,而不是另一个引用。例如,在 Perl 中,可以有一个指向另一个变量的引用点,该变量也是一个引用。然而,在 JavaScript 中,它沿着引用链向下遍历,只指向核心对象。这种情况的一个例子可以在清单 2-3 中看到,其中物理对象被改变,但是引用继续指向旧对象。

清单 2-3 。在保持完整性的同时更改对象的引用

// Set items to an array (object) of strings
var items = [ 'one', 'two', 'three' ];
// Set itemsRef to a reference to items
var itemsRef = items;

// Set items to equal a new object
items = [ 'new', 'array' ];

// items and itemsRef now point to different objects.
// items points to [ 'new', 'array' ]
// itemsRef points to [ 'one', 'two', 'three' ]
console.log( items !== itemsRef );

最后,让我们看一个奇怪的实例,您可能认为它会涉及引用,但实际上不会。当执行字符串连接时,结果总是一个新的字符串对象,而不是原始字符串的修改版本。因为字符串(像数字和布尔值)是原语,所以它们实际上不是引用对象,包含它们的变量也不是引用。这可以在清单 2-4 中看到。

清单 2-4 。对象修改示例产生一个新对象,而不是自修改对象

// Set item equal to a new string object
var item = 'test';

// itemRef now refers to the same string object
var itemRef = item;

// Concatenate some new text onto the string object
// NOTE: This creates a new object and does not modify
// the original object.
item += 'ing';

// The values of item and itemRef are NOT equal, as a whole
// new string object has been created
console.log( item != itemRef );

字符串通常特别容易混淆,因为它们的行为类似于对象。您可以通过调用new String来创建字符串的实例。字符串有类似于length的属性。字符串也有类似indexOftoUpperCase的方法。但是当与变量或函数交互时,字符串是非常原始的。

如果你是新手,推荐信可能是一个很难理解的话题。尽管如此,理解引用是如何工作的对于编写好的、干净的 JavaScript 代码是至关重要的。在接下来的几节中,我们将会看到一些特性,它们不一定是新的或者令人兴奋的,但是对于编写好的、干净的代码来说是很重要的。

范围

范围是 JavaScript 的一个棘手特性。大多数编程语言都有某种形式的作用域;不同之处在于该范围的持续时间。JavaScript 中只有两个作用域:函数作用域和全局作用域。这看似简单。函数有自己的作用域,但是块(比如whileiffor语句)没有。如果您来自块范围的语言,这可能看起来很奇怪。清单 2-5 展示了一个函数作用域代码含义的例子。

清单 2-5 。JavaScript 中变量作用域如何工作的例子

// Set a global variable, foo, equal to test
var foo = 'test';

// Within an if block
if ( true ) {
    // Set foo equal to 'new test'
    // NOTE: This still belongs to the global scope!
    var foo = 'new test';
}

// As we can see here, as foo is now equal to 'new test'
console.log( foo === 'new test' );

// Create a function that will modify the variable foo
function test() {
    var foo = 'old test';
}

// However, when called, 'foo' remains within the scope
// of the function
test();

// Which is confirmed, as foo is still equal to 'new test'
console.log( foo === 'new test' );

你会注意到在清单 2-5 的中,变量在全局范围内。在基于浏览器的 JavaScript 中,所有全局范围的变量实际上都是作为window对象的属性可见的。在其他环境中,会有一个全局上下文,所有全局范围的变量都属于这个上下文。

在清单 2-6 中,在test()函数的范围内,一个值被赋给变量foo。然而,在清单 2-6 中没有任何地方是变量的实际声明范围(使用var foo)。当foo变量没有被明确限定作用域时,它将被全局定义,即使它只打算在函数的上下文中使用。

清单 2-6 。隐式全局范围变量声明示例

// A function in which the value of foo is set
function test() {
    foo = 'test';
}

// Call the function to set the value of foo
test();

// We see that foo is now globally scoped
console.log( window.foo === 'test' );

JavaScript 的作用域通常是混乱的来源。如果您来自块范围的语言,这种混淆可能会导致意外的全局变量,如下所示。通常,关键字的不精确用法会加剧这种混淆。为了简单起见,专业的 JavaScript 程序员应该总是用var初始化变量,不管作用域是什么。这样,您的变量将具有您期望的范围,并且您可以避免意外的全局变量。

在函数中声明变量时,要注意提升的问题。函数中声明的任何变量都将其声明(而不是初始化时使用的值)提升到作用域的顶部。JavaScript 这样做是为了确保变量名在整个范围内都可用。

特别是当我们将范围与上下文和闭包的概念结合起来时,JavaScript 显示出它是一种强大的脚本语言。

语境

你的代码总是有某种形式的上下文(代码运行的范围)。上下文可能是一个强大的工具,对于面向对象的代码来说是必不可少的。这是其他语言的一个共同特征,但是 JavaScript 通常有一个微妙的不同之处。

您通过变量this访问上下文,该变量将始终引用代码在其中运行的上下文。回想一下,全局对象实际上是window对象的属性。这意味着即使在全球范围内,this仍将指代一个对象。清单 2-7 展示了一些使用上下文的简单例子。

清单 2-7 。在上下文中使用函数,然后将上下文切换到另一个变量的示例

function setFoo(fooInput) {
    this.foo = fooInput;
}

var foo = 5;
console.log( 'foo at the window level is set to: ' + foo ); 

var obj = {
    foo : 10
};

console.log( 'foo inside of obj is set to: ' + obj.foo );

// This will change window-level foo
setFoo( 15 );
console.log( 'foo at the window level is now set to: ' + foo );

// This will change the foo inside the object
obj.setFoo = setFoo;
obj.setFoo( 20 );
console.log( 'foo inside of obj is now set to: ' + obj.foo );

在清单 2-7 中,我们的setFoo函数看起来有点奇怪。我们通常不在通用的效用函数中使用this。知道我们最终将把setFoo附加到obj上,我们使用了this,这样我们可以访问obj的上下文。然而,这种方法并不是绝对必要的。JavaScript 有两种方法,允许您在任意指定的上下文中运行函数。清单 2-8 显示了两种方法,callapply,可以用来实现这一点。

清单 2-8 。改变函数上下文的例子

// A simple function that sets the color style of its context
function changeColor( color ) {
    this.style.color = color;
}

// Calling it on the window object, which fails, since it doesn't
// have a style object
changeColor('white' );

// Create a new div element, which will have a style object
var main = document.createElement('div');

// Set its color to black, using the call method
// The call method sets the context with the first argument
// and passes all the other arguments as arguments to the function
changeColor.call( main, 'black' );

//Check results using console.log
//The output should say 'black'
console.log(main.style.color);

// A function that sets the color on the body element
function setBodyColor() {
    // The apply method sets the context to the body element
    // with the first argument, and the second argument is an array
    // of arguments that gets passed to the function
    changeColor.apply( document.body, arguments );
}

// Set the background color of the body to black

setBodyColor('black' );

虽然上下文的用处可能不会马上显现出来,但是当我们很快看到面向对象时,它会变得更加清晰。

关闭

闭包 是一种手段,在父函数已经终止后,内部函数可以通过它来引用外部封闭函数中的变量。总之,这是技术上的定义。也许将闭包与上下文联系起来更有用。到目前为止,当我们定义了一个对象字面量,这个对象是开放的,可以修改。我们已经看到,我们可以随时向对象添加属性和函数。但是如果我们想要一个锁定的上下文呢?将值“保存”为默认值的上下文。没有我们提供的 API 就无法访问的上下文怎么办?这就是闭包所提供的:一个只能以我们选择的方式访问的上下文。

这个话题可以很强大,也很复杂。我们强烈推荐参考本节末尾提到的站点,因为它们有一些关于闭包的优秀信息。

让我们从查看两个简单的闭包的例子开始,如清单 2-9 所示。

清单 2-9 。闭包如何提高代码清晰度的两个例子

// Find the element with an ID of 'main'
var obj = document.getElementById('main');

// Change its border styling
obj.style.border = '1px solid red';

// Initialize a callback that will occur in one second
setTimeout(function(){
    // Which will hide the object
    obj.style.display = 'none';
}, 1000);

// A generic function for displaying a delayed alert message
function delayedAlert( msg, time ) {
    // Initialize an enclosed callback
    setTimeout(function(){
        // Which utilizes the msg passed in from the enclosing function
        console.log( msg );
    }, time );
}
// Call the delayedAlert function with two arguments
delayedAlert('Welcome!', 2000 );

setTimeout的第一个函数调用显示了一个新的 JavaScript 开发人员经常遇到问题的实例。在新开发人员的程序中,这样的代码并不少见:

setTimeout('otherFunction()', 1000);

以至...

setTimeout('otherFunction(' + num + ',' + num2 + ')', 1000);

在这两个例子中,被调用的函数都表示为字符串。当您准备将代码投入生产时,这可能会导致缩小过程出现问题。通过使用闭包,您可以按照最初的意图调用函数、使用变量和传递参数。

使用闭包的概念,完全有可能绕过这些混乱的代码。清单 2-9 中的第一个例子很简单;有一个setTimeout回调在第一次调用后 1000 毫秒被调用,但仍然引用obj变量(它被全局定义为 ID 为main的元素)。定义的第二个函数delayedAlert展示了对发生的setTimeout混乱的解决方案,以及在函数范围内使用闭包的能力。

您应该会发现,当在代码中使用这样的简单闭包时,您所编写的内容会更加清晰,而不是变成一碗语法汤。

让我们看看闭包可能带来的有趣的副作用。在一些函数式编程语言中,有一个概念叫做匹配 ,这是一种为函数预先填充多个参数的方法,可以创建一个新的、更简单的函数。清单 2-10 有一个简单的 currying 示例,创建一个新函数,将一个参数预填充到另一个函数中。

清单 2-10 。使用闭包的函数 Currying 示例

// A function that generates a new function for adding numbers
function addGenerator( num ) {

    // Return a simple function for adding two numbers
    // with the first number borrowed from the generator
    return function( toAdd ) {
        return num + toAdd
    };

}
// addFive now contains a function that takes one argument,
// adds five to it, and returns the resulting number
var addFive = addGenerator( 5 );

// We can see here that the result of the addFive function is 9,
// when passed an argument of 4
console.log( addFive( 4 ) == 9 );

闭包可以解决另一个常见的 JavaScript 编码问题。新的 JavaScript 开发人员经常会不小心在全局范围内留下很多额外的变量。这通常被认为是不好的做法,因为这些额外的变量可能会悄悄地干扰其他库,导致混乱的问题发生。使用一个自执行的匿名函数,你可以隐藏所有正常的全局变量,不让其他代码看到,如清单 2-11 所示。

清单 2-11 。使用匿名函数隐藏全局变量的例子

// Create a new anonymous function, to use as a wrapper
(function(){
    // The variable that would normally be global
    var msg = 'Thanks for visiting! ';

    // Binding a new function to a global object
    window.onload = function(){
        // Which uses the 'hidden' variable
        console.log( msg );
    };

// Close off the anonymous function and execute it
})();

最后,让我们看看闭包会出现的一个问题。记住闭包允许你引用存在于父函数中的变量。但是,它不提供变量在创建时的值;它提供父函数中变量的最后一个值。在一个for循环中,你最常看到这种情况。有一个变量被用作迭代器(i)。在for循环中,新的函数正在被创建,它们利用闭包再次引用迭代器。问题是,当调用新的 closured 函数时,它们将引用迭代器的最后一个值(即数组中的最后一个位置),而不是您所期望的值。清单 2-12 展示了一个使用匿名函数诱导作用域的例子,创建一个期望闭包可能存在的实例。

清单 2-12 。使用匿名函数归纳创建多个闭包所需的范围的示例-使用函数

// An element with an ID of main
var obj = document.getElementById('main');

// An array of items to bind to
var items = ['click', 'keypress' ];

// Iterate through each of the items
for ( var i = 0; i < items.length; i++ ) {
    // Use a self-executed anonymous function to induce scope
    (function(){
        // Remember the value within this scope
       // Each 'item' is unique.
      //Not relying on variables created in the parent context.
        var item = items[i];
        // Bind a function to the element
        obj['on' + item ] = function() {
            // item refers to a parent variable that has been successfully
            // scoped within the context of this for loop
            console.log('Thanks for your ' + item );
        };
    })();
}

我们将在面向对象代码的部分回到闭包,在那里它们将帮助我们实现私有属性。

闭包的概念不是一个容易理解的概念;我们花费了大量的时间和精力来真正理解闭包是多么强大。幸运的是,有一些很好的参考资料解释了闭包在 JavaScript 中是如何工作的:Richard Cornford 在http://jibbering.com/faq/faq_notes/closures.html写的“JavaScript Closures”,以及 Mozilla 开发者网络的另一个解释https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

函数重载和类型检查

其他面向对象语言的一个共同特征是能够根据传入参数的类型或数量来重载函数以执行不同的行为。虽然这种能力不是 JavaScript 的语言特性,但是我们可以使用现有的能力来实现函数的重载。

我们的重载函数需要知道两件事:传入了多少个参数,以及传递了什么类型的参数。让我们先来看看所提供的参数数量。

在 JavaScript 的每个函数中,都有一个名为arguments的上下文变量,它充当一个类似数组的对象,包含传递给函数的所有参数。arguments对象不是真正的数组;它不与 Array 共享一个原型,也没有像pushindexOf那样的数组处理函数。它有位置数组访问(例如,arguments[2]返回第三个参数),还有一个length属性。在清单 2-13 中有两个这样的例子。

清单 2-13 。JavaScript 中函数重载的两个例子

// A simple function for sending a message
function sendMessage( msg, obj ) {
    // If both a message and an object are provided
    if ( arguments.length === 2 ) {
        // Send the message to the object
        // (Assumes that obj has a log property!)
        obj.log( msg );
    } else {
        // Otherwise, assume that only a message was provided
        // So just display the default error message
        console.log( msg );
    }
}

// Both of these function calls work
sendMessage( 'Hello, World!' );
sendMessage( 'How are you?', console );

您可能想知道是否有一种方法可以让arguments对象使用数组的全部函数。用arguments本身是不可能的,但是可以创建一个arguments的副本,这是一个数组。通过从数组原型中调用slice方法,我们可以快速地将arguments对象复制到一个数组中,如清单 2-14 中的所示。

清单 2-14 。将参数转换为数组

function aFunction(x, y, z) {
    var argsArray = Array.prototype.slice.call( arguments, 0 );
    console.log( 'The last argument is: ' + argsArray.pop() );
}

// Will output 'The last argument is 3'.
aFunction( 1, 2, 3 );

我们将很快了解更多关于prototype房产的信息。目前,只要说原型允许我们以静态方式访问对象方法就足够了。

如果消息没有被定义会怎样?我们不仅需要检查一个论点的存在,还需要检查它的不存在。我们可以利用这样一个事实,即任何没有提供的参数都有一个值undefined。清单 2-15 显示了一个简单的函数,如果没有提供特定的参数,它会显示一条错误消息并提供一条默认消息。(注意,我们必须在这里使用typeof,因为否则,带有文字字符串“undefined”的参数将指示错误。)

清单 2-15 。显示错误消息和默认消息

function displayError( msg ) {
    // Check and make sure that msg is not undefined
    if ( typeof msg === 'undefined' ) {
        // If it is, set a default message
        msg = 'An error occurred.';
    }

    // Display the message
    console.log( msg );
}

displayError();

使用typeof语句有助于将我们引入类型检查的主题。因为 JavaScript 是一种动态类型语言,所以这被证明是一个非常有用和重要的主题。有许多方法可以检查变量的类型;我们要看两个特别有用的。

检查对象类型的第一种方法是使用明显的typeof操作符。这个工具为我们提供了一个字符串名称,代表变量内容的类型。这种方法的一个例子可以在清单 2-16 中看到。

清单 2-16 。使用typeof确定对象类型的示例

var num = '50';
var arr = 'apples,oranges,pears';

// Check to see if our number is actually a string
if ( typeof num === 'string' ) {
    // If it is, then parse a number out of it
    num = parseInt( num );
}

// Check to see if our array is actually a string
if ( typeof arr == 'string' ) {
    // If that's the case, make an array, splitting on commas
    arr = arr.split( ',' );
}

typeof的优点是你不必知道测试变量的实际类型。除了对于 object 或 Array 类型的变量,或者像 User 这样的自定义对象,typeof只返回“Object ”,这使得很难区分具体的对象类型。接下来的两种确定变量类型的方法要求您针对特定的现有类型进行测试。

检查对象类型的第二种方法是使用instanceof操作符。这个操作符检查左操作数和右操作数的构造函数,这听起来可能比实际要复杂一点!看一下清单 2-17 ,展示了一个使用instanceof 的例子。

清单 2-17 。使用instanceof的例子

var today = new Date();
var re = /[a-z]+/i;

// These don't give us enough details
console.log('typeof today: ' + typeof today);
console.log('typeof re: ' + typeof re);

// Let's find out if the variables are of a more specific type
if (today instanceof Date) {
    console.log('today is an instance of a Date.');
}

if (re instanceof RegExp) {
    console.log( 're is an instance of a RegExp object.' ); 
}

在下一章,当我们看面向对象的 JavaScript 时,我们将讨论Object.isPrototypeOf()函数,它也有助于类型确定。

类型检查变量和验证参数数组的长度本质上是简单的概念,但可以用来提供复杂的方法,这些方法可以适应并为开发人员和代码用户提供更好的体验。当你需要特定的类型检查时(这是数组吗?是约会吗?特定类型的自定义对象?),我们建议创建一个自定义函数来确定类型。许多框架都有确定数组、日期等的便利函数。将这些代码封装到一个函数中,可以确保您有且只有一个地方可以检查该特定类型,而不是让检查代码分散在整个代码库中。

新对象工具

JavaScript 语言中最激动人心的发展之一是管理对象工具的扩展。正如我们将看到的,这些工具可以用于对象文字(更像数据结构)和对象实例。

目标

对象是 JavaScript 的基础。事实上,语言中的一切都是对象。这种语言的大部分力量来源于这个事实。在最基本的层面上,对象是作为属性的集合而存在的,就像你在其他语言中看到的散列结构一样。清单 2-18 展示了用一组属性创建一个对象的两个基本例子。

清单 2-18 。创建简单对象和设置属性的两个示例

// Creates a new Object object and stores it in 'obj'
var obj = new Object();

// Set some properties of the object to different values
obj.val = 5;
obj.click = function(){
    console.log('hello');
};

// Here is some equivalent code, using the {...} shorthand
// along with key-value pairs for defining properties
var obj = {

    // Set the property names and values using key/value pairs
    val: 5,
    click: function(){
        console.log('hello');
    }
};

事实上,除此之外,对象并没有太多的意义。然而,事情变得棘手的地方在于新对象的创建,尤其是那些继承了其他对象属性的对象。

修改对象

JavaScript 现在有三种方法可以帮助您控制对象是否可以被修改。我们将从最小到最大的限制尺度来看待它们。

默认情况下,JavaScript 中的对象可以随时修改。通过使用Object.preventExtensions(),你可以防止新的属性被添加到对象中。发生这种情况时,可以使用所有当前属性,但不能添加新属性。试图添加一个新属性将导致一个TypeError——或者会无声地失败;在严格模式下运行时,您更有可能看到该错误。清单 2-19 显示了一个例子。

清单 2-19 。使用Object.preventExtensions()的例子

// Creates a new object and stores it in 'obj'
var obj = {};

// Creates a new Object object using preventExtensions
var obj2 = Object.preventExtensions(obj);

// Generates TypeError when trying to define a new property
function makeTypeError(){
'use strict';

//Generates TypeError when trying to define a new property
Object.defineProperty(obj2, 'greeting',{value: 'Hello World'});
}

makeTypeError();

使用Object.seal(),你可以限制一个对象的能力,类似于你使用Object.preventExtensions()所做的。然而,与我们前面的例子不同,属性不能被删除或转换成访问器(getter 方法)。试图删除或添加属性也会导致TypeError。可以更新现有的可写属性,而不会导致错误。清单 2-20 显示了一个例子。

清单 2-20 。使用Object.seal()的例子

// Creates a new object and uses object.seal to restrict it
var obj = {};
obj.greeting = 'Welcome';
Object.seal(obj);

//Updating the existing writable property
//Cannot convert existing property to accessor, throws TypeErrors
obj.greeting = 'Hello World';
Object.defineProperty(obj, 'greeting', {get:function(){return 'Hello World'; } });

// Cannot delete property, fails silently
delete obj.greeting;

function makeTypeError(){
  'use strict';

  //Generates TypeError when trying to delete a property
  delete obj.greeting;

  //Can still update property
  obj.greeting = 'Welcome';
  console.log(obj.greeting);
}

makeTypeError();

在清单 2-21 中展示的Object.freeze(),是三种方法中最具限制性的。一旦被使用,对象就被认为是不可变的。不能添加、删除或更新属性。任何尝试都会导致TypeError。如果属性本身是一个对象,那么它是可以更新的。这叫做浅冻结。为了使对象完全不可变,其值包含对象的所有属性也必须被冻结。

清单 2-21 。使用Object.freeze()的例子

//Creates a new object with two properties. Second property is an object
var obj = {
  greeting: "Welcome",
  innerObj: {}
};

//Freeezes our obj
Object.freeze(obj);

//silently fails
obj.greeting = 'Hello World';

//innerObj can still be updated
obj.innerObj.greeting = 'Hello World';
console.log('obj.innerObj.greeting = ' + obj.innerObj.greeting);

//Cannot convert existing property to accessor
//Throws TypeError
Object.defineProperty(obj, 'greeting', {get:function(){return 'Hello World'; } });

// Cannot delete property, fails silently
delete obj.greeting;

function makeTypeError(){
  'use strict';
}

//Generates TypeError when trying to delete a property
delete obj.greeting;

//Freeze inner object
Object.freeze(obj.innerObj);

//innerObj is now frozen. Fails silently
obj.innerObj.greeting = 'Worked so far...';

function makeTypeError(){
      'use strict';
     //all attempts will throw TypeErrors

     delete obj.greeting;
     obj.innerObj.greeting = 'Worked so far...';
     obj.greeting = "Welcome";

   };

   makeTypeError();

通过理解如何控制对象的可变性,您可以创建一定程度的一致性。例如,如果您有一个名为User的对象,您可以确定基于它的每个新对象都将与第一个对象具有相同的属性。任何可以在运行时添加的属性都将失败。

摘要

理解本章概述的概念的重要性不能低估。本章的前半部分让您很好地理解 JavaScript 的行为以及如何最好地使用它,这是全面掌握如何专业地使用 JavaScript 的起点。简单地理解对象的行为、引用的处理和范围的确定无疑会改变您编写 JavaScript 代码的方式。

基于这些技能,高级技术为我们提供了解决 JavaScript 问题的额外方法。理解范围和上下文导致了闭包的使用。研究如何在 JavaScript 中确定类型,使我们能够将函数重载添加到一种语言中,而这种语言本身并没有函数重载功能。然后我们花时间研究 JavaScript 中的一个基本类型:对象。对象类型中的各种新特性允许我们更好地控制我们创建的对象文字。这自然会导致下一章,我们开始构建自己的面向对象的 JavaScript。

三、创建可重用代码

在上一章的介绍中,我们讨论了作为 JavaScript 基本单位的对象。讨论完 JavaScript 对象字面量后,我们将用本章的大部分时间来研究这些对象如何与面向对象编程交互。在这里,JavaScript 存在于经典编程和 JavaScript 自身近乎独特的能力之间的紧张状态中。

从将代码组织成对象开始,我们将看看管理代码的其他模式。我们希望确保不污染全局名称空间,或者(过度)依赖全局变量。这意味着我们将从名称空间的讨论开始,但名称空间只是冰山一角,一些更新的调用模式可以帮助我们正确地保护我们的代码:模块,以及稍后立即调用的函数表达式 (IIFEs 或“iffies)。

一旦我们在单个文件中很好地组织了我们的代码,看看可用于管理多个 JavaScript 文件的工具是有意义的。当然,对于我们可能使用的一些库,我们可以依赖内容交付网络。但是我们也应该考虑加载我们自己的 JavaScript 文件的最佳方式,以免我们最终得到的 HTML 文件包含一个又一个脚本标签。

面向对象的 JavaScript

JavaScript 是一种原型语言,而不是经典语言。让我们先把这一点说清楚。Java 是一种经典语言,因为 Java 中的一切都需要一个类。另一方面,在 JavaScript 中,一切都有原型;因此它是原型。但是,正如道格拉斯·克洛克福特和其他人所说,它的原型性质是“矛盾的”。像一些不情愿的超级英雄一样,JavaScript 有时不想从其他编程语言的人群中脱颖而出,让它的能力大放异彩。好吧,让我们给它一个斗篷,看看会发生什么!

首先,我们重申一下,JavaScript 不是经典语言。有许多书籍、博客文章、指南、幻灯片和库试图在 JavaScript 上强加基于类的语言结构。我们欢迎您深入研究它们,但请记住,这样做时,尽管意图良好,但它们的作者是在试图将一个方钉钉进一个圆孔。我们在这里不打算这样做。本章不会讨论如何让 JavaScript 像 Java 一样运行。相反,我们将关注 JavaScript 与面向对象理论中概述的功能的交集,以及它如何有时达不到预期,有时又超出预期。

最终,我们为什么要使用面向对象编程?它提供了允许简化代码重用的使用模式,消除了重复劳动。此外,面向对象风格的编程有助于我们更深入地思考我们正在处理的代码。它提供了一个轮廓,一张地图,我们可以按照它来成功地实现。但这不是唯一的地图。JavaScript 的原型是一种相似但不同的方式来达到我们的目的。

先从原型本身说起。JavaScript 中的每种类型(对象、函数、日期等等)都有一个原型。ECMAScript 标准 规定这个属性是隐藏的,称为[[Prototype]]。到目前为止,您可以通过两种方式之一访问该属性:非标准的__proto__属性和prototype属性。起初,暴露__proto__并不可靠地跨浏览器可用,即使可用也不总是以相同的方式实现。[脚注:令人震惊的是,浏览器会以不同的方式实现 JavaScript 的关键部分!]与 ECMAScript 6(即将在您身边的浏览器中推出!),__proto__将成为类型的正式属性,并将可用于任何符合的实现。但未来还不是现在。

还可以访问某些类型的prototype属性。所有核心 JavaScript 类型(日期、字符串、数组等等)都有一个公共的prototype属性。任何从函数构造函数创建的 JavaScript 类型也有一个公共的prototype属性。但是这些类型的实例,不管是字符串、日期还是其他,都有而不是属性。这是因为prototype属性在实例上不可用。我们也不会在这里使用prototype属性,因为我们不会使用函数作为构造函数。我们将使用对象作为构造函数。

没错;我们将使用一个对象文字作为其他对象的基础。如果这听起来很像类和实例,那么有一些相似之处,但是,正如你所料,也有一些不同之处。考虑一个人对象,如清单 3-1 中的所示。

清单 3-1 。一个人对象

var Person = {
    firstName : 'John',
    lastName : 'Connolly',
    birthDate : new Date('1964-09-05'),
    gender: 'male',
    getAge : function() {
        var today = new Date();
        var diff = today.getTime() - this.birthDate.getTime();
        var year = 1000 * 60 * 60 * 24 * 365.25;
        return Math.floor(diff / year);
    }
};

这里没什么特别的:一个人有名字、姓氏、性别、出生日期和计算年龄的方法。这个人是一个字面上的对象,不是我们认为的类。但是我们想把这个人当作一个类来使用。我们想创造更多的符合这个人提出的结构的物体。为了保持无类 JavaScript 和有类的面向对象语言之间的区别,我们将 Person 称为一种类型(类似于 Date、Array 和 RegExp 都是类型的方式)。我们想要创建 Person 类型的实例:为此,我们可以使用Object.create ( 清单 3-2 )。

清单 3-2 。创造人

var Person = {
    firstName : 'John',
    lastName : 'Connolly',
    birthDate : new Date( '1964-09-05' ),
    gender : 'male',
    getAge : function () {
        var today = new Date();
        var diff = today.getTime() - this.birthDate.getTime();
        var year = 1000 * 60 * 60 * 24 * 365.25;
        return Math.floor( diff / year );
    },

    toString : function () {
        return this.firstName + ' ' + this.lastName + ' is a ' + this.getAge() +
            ' year-old ' + this.gender;
    }
};

var bob = Object.create( Person );
bob.firstName = 'Bob';
bob.lastName = 'Sabatelli';
bob.birthDate = new Date( '1969-06-07' );
console.log( bob.toString() );

已经从 Person 对象创建了一个实例。我们将 Person 的一个实例存储在变量bob中。没有课。但是在我们创建的 Person 对象和 Person 类型之间有一个链接。这个链接在[[Prototype]]属性 y 之上。如果你运行的是足够现代的浏览器(在我写这篇文章的时候,这个可以在 IE11、Firefox 27 和 Chrome 33 上运行),你可以在开发者工具中打开控制台,查看bob上的__proto__属性。您会注意到它指向 Person 对象。事实上,你可以通过检查bob.__proto__ === Person来测试这一点。

在 ECMAScript 5 中,Object.create被添加到 JavaScript 中,表面上是为了简化和阐明对象之间的关系,特别是哪些对象通过它们的原型相关联。但是在这样做的时候,它允许一个简单的,一步到位的创建对象之间的关系。这种关系非常类似于类和实例的面向对象思想。但是因为 JavaScript 没有类,所以我们只有相互之间有关系的对象。

这种关系通常被称为原型链。在 JavaScript 中,原型链是解析对象成员值的两个地方之一。也就是说,当你引用foo.barfoo[bar]时,JavaScript 引擎在两个潜在的地方查找bar的值:在foo本身上,或者在foo的原型链上。

在他关于面向对象 JavaScript 的三篇文章中,Kyle Simpson 就我们应该如何看待这个过程提出了一个优雅的观点。不要把bob与人的关系看作是实例与类的关系,或者孩子与父母的关系,我们应该把它看作是行为委托的一种情况。bob对象有自己的firstNamelastName,但它没有任何getAge功能。委托给某人。通过使用Object.create 建立委托关系。原型链是这种委托的机制,允许我们将行为委托给链上更远的东西。从bob的角度来看,当我们连续调用Object.create时,功能会累积,在附加功能上分层。

顺便说一下,你可能会担心你的浏览器不支持 ECMAScript 5,或者至少没有它的Object.create 版本。这不是问题;Object.create可以很容易地在任何带有 JavaScript 引擎的浏览器中填充,如清单 3-3 中的所示。

清单 3-3 。多孔填料

if ( typeof Object.create !== 'function' ) {
    Object.create = function ( o ) {
        function F() {
        }
        F.prototype = o;
        return new F();
    };
}

最后,有些人不喜欢不断使用Object.create来创建对象。如果是这样的话,考虑一下对清单 3-4 中的 Person 对象的快速修改,它提供了一个生成更多 Person 的工厂方法。

清单 3-4 。使用工厂方法的 Person 对象

var Person = {
    firstName : 'John',
    lastName : 'Connolly',
    birthDate : new Date( '1964-09-05' ),
    gender : 'male',
    getAge : function () {
        var today = new Date();
        var diff = today.getTime() - this.birthDate.getTime();
        var year = 1000 * 60 * 60 * 24 * 365.25;
        return Math.floor( diff / year );
    },

    toString : function () {
        return this.firstName + ' ' + this.lastName + ' is a ' + this.getAge() +
            ' year-old ' + this.gender;
    },

    extend : function ( config ) {
        var tmp = Object.create( this );
        for ( var key in config ) {
            if ( config.hasOwnProperty( key ) ) {
                tmp[key] = config[key];
            }
        }
        return tmp;
    }
};

var bob = Person.extend( {
    firstName : 'Bob',
    lastName : 'Sabatelli',
    birthDate : new Date( '1969-06-07' )
} );

console.log( bob.toString() );

这里,extend函数封装了对Object.create的调用。当extend被调用时,它在内部调用Object.create。大概,extend是通过一个传入的配置对象调用的,这是一个相当典型的 JavaScript 使用模式。通过循环遍历tmp中的属性,extend函数还确保只有已经存在于tmp对象上的config的属性被扩展到新创建的tmp对象上。一旦我们将属性从config复制到tmp,我们就可以返回tmp,一个人的实例。

既然我们已经看到了在 JavaScript 中建立对象间关系的新风格,让我们看看它如何影响 JavaScript 与典型的面向对象概念的交互。

继承

到目前为止,最大的问题是继承。面向对象代码的主要目的是通过从一般的父类到更具体的子类来重用功能。我们已经看到,用Object.create 很容易在两个对象之间建立关系。我们可以简单地扩展这种用法来创建我们喜欢的任何类型的继承层次。(好吧,不管我们喜欢哪种单一继承层次结构。Object.create不允许多重继承。)记住我们是在委托行为的想法;当我们用Object.create创建子类时,它们将自己的一些行为委托给原型链上更高的类型。用Object.create继承更倾向于自底向上,而不是典型的自顶向下的面向对象风格。

继承其实很简单:使用Object.create。具体来说,使用Object.create在“父”类型和“子”类型之间创建一个关系。子类型可以添加功能、删除功能或覆盖现有功能。用一个参数调用Object.create,无论你决定哪个对象是你的“父”类型,返回值都是你决定的“子”类型。然后重复清单 3-4 中的模式,并使用extend方法(或重用Object.create!)来创建那个子类型(清单 3-5 )的实例。

清单 3-5 。人是老师的父母

var Person = {
    firstName : 'John',
    lastName : 'Connolly',
    birthDate : new Date( '1964-09-05' ),
    gender : 'male',
    getAge : function () {
        var today = new Date();
        var diff = today.getTime() - this.birthDate.getTime();
        var year = 1000 * 60 * 60 * 24 * 365.25;
        return Math.floor( diff / year );
    },

    toString : function () {
        return this.firstName + ' ' + this.lastName + ' is a ' + this.getAge() +
            ' year-old ' + this.gender; 
    },

    extend : function ( config ) {
        var tmp = Object.create( this );
        for ( var key in config ) {
            if ( config.hasOwnProperty( key ) ) {
                tmp[key] = config[key];
            }
        }
        return tmp;
    }
};

var Teacher = Person.extend( {
    job : 'teacher',
    subject : 'English Literature',
    yearsExp : 5,
    toString : function () {
        return this.firstName + ' ' + this.lastName + ' is a ' + this.getAge() +
            ' year-old ' + this.gender + ' ' + this.subject + ' teacher.';
    }
} );

var patty = Teacher.extend( {
    firstName : 'Patricia',
    lastName : 'Hannon',
    subject: 'chemistry',
    yearsExp : 20,
    gender : 'female'
} );

console.log( patty.toString() );

Object.create在老师的[[Prototype]]和人的[[Prototype]]之间建立了联系。如果您有前面提到的现代浏览器之一,您应该能够查看 Teacher 的__proto__属性,并看到它指向 Person。

在第二章的中,我们谈到了instanceof作为一种发现一个对象是否是一个类型的实例的方法。instanceof操作员不会在这里工作。它依赖于显式的prototype属性来跟踪对象与类型的关系。更简单地说,instanceof的右边操作数必须是一个函数(尽管很可能是一个函数构造函数)。左边的操作数必须是从函数构造函数创建的(虽然不一定是右边的函数构造函数)。那么我们如何判断一个对象是否是一个类型的实例呢?进入isPrototypeOf功能。

可以在任何对象上调用isPrototypeOf函数。它出现在所有 JavaScript 对象上,很像toString。在完成该类型角色的对象上调用它(到目前为止,在我们的例子中是 Person 或 Teacher),并向它传递完成实例角色的对象的参数(bobpatty)。因此,Teacher.isPrototypeOf(patty)将返回真实,如你所料。清单 3-6 提供了查看教师、人员、bobpatty的组合以及isPrototypeOf调用的代码。

清单 3-6isPrototypeOf()功能

var Person = {
    firstName : 'John',
    lastName : 'Connolly',
    birthDate : new Date( '1964-09-05' ),
    gender : 'male',
    getAge : function () {
        var today = new Date();
        var diff = today.getTime() - this.birthDate.getTime();
        var year = 1000 * 60 * 60 * 24 * 365.25;
        return Math.floor( diff / year );
    },

    toString : function () {
        return this.firstName + ' ' + this.lastName + ' is a ' + this.getAge() +
            ' year-old ' + this.gender;
    },

    extend : function ( config ) {
        var tmp = Object.create( this );
        for ( var key in config ) {
            if ( config.hasOwnProperty( key ) ) {
                tmp[key] = config[key];
            }
        }
        return tmp;
    }
};

var Teacher = Person.extend( {
    job : 'teacher',
    subject : 'English Literature',
    yearsExp : 5,
    toString : function () {
        return this.firstName + ' ' + this.lastName + ' is a ' + this.getAge() +
            ' year-old ' + this.gender + ' ' + this.subject + ' teacher.';
    }
} );

var bob = Person.extend( {
    firstName : 'Bob',
    lastName : 'Sabatelli',
    birthDate : new Date( '1969-06-07' )
} );

var patty = Teacher.extend( {
    firstName : 'Patricia',
    lastName : 'Hannon',
    subject: 'chemistry',
    yearsExp : 20,
    gender : 'female'
} );

console.log( 'Is bob an instance of Person? ' + Person.isPrototypeOf(bob) );          // true
console.log( 'Is bob an instance of Teacher? ' + Teacher.isPrototypeOf( bob ) );      // false
console.log( 'Is patty an instance of Teacher? ' + Teacher.isPrototypeOf( patty ) );  // true
console.log( 'Is patty an instance of Person? ' + Person.isPrototypeOf( patty ) );    // true

有一个伴随函数给isPrototypeOf;它名叫getPrototypeOf 。被称为Object.getPrototypeOf(obj),它返回一个对类型的引用,该类型是当前对象的基础。如前所述,您还可以查看(目前是非标准的,但很快将成为标准的)__proto__属性来获得相同的信息(清单 3-7 )。

清单 3-7 。getPrototypeOf

console.log( 'The prototype of bob is Person' + Object.getPrototypeOf( bob ) );

访问被覆盖的方法呢?当然,在子对象中重写父对象的方法是可能的。这种能力没有什么特别的,任何面向对象的系统都应该有这种能力。但是在大多数面向对象的系统中,被覆盖的方法可以通过类似于super的属性或访问器访问父方法。也就是说,当您正在重写一个方法时,您通常可以通过一个特殊的关键字调用您正在重写的方法。

我们这里没有那个。JavaScript 的基于原型的面向对象代码根本没有super()特性。一般来说,有三种方法可以解决这个问题。首先,您可以编写一些代码来重新实现super。这将涉及遍历原型链,可能用getPrototypeOf,在继承链中找到拥有你正在重写的方法的前一版本的对象。(请记住,您并不总是覆盖父类中的某些内容;它可能是来自“祖父”类的东西,或者是原型链上更远的东西。)那么您将需要某种方法来访问该方法,并使用传递给覆盖方法的相同参数集来调用它。这当然是可能的,但是它往往是丑陋的,同时也是非常低效的。

作为第二个解决方案,你可以显式调用父方法,如清单 3-8 所示。

清单 3-8 。再现功能的效果

var Person = {
    firstName : 'John',
    lastName : 'Connolly',
    birthDate : new Date( '1964-09-05' ),
    gender : 'male',
    getAge : function () {
        var today = new Date();
        var diff = today.getTime() - this.birthDate.getTime();
        var year = 1000 * 60 * 60 * 24 * 365.25;
        return Math.floor( diff / year );
    },

    toString : function () {
        return this.firstName + ' ' + this.lastName + ' is a ' + this.getAge() +
            ' year-old ' + this.gender; 
    },

    extend : function ( config ) {
        var tmp = Object.create( this );
        for ( var key in config ) {
            if ( config.hasOwnProperty( key ) ) {
                tmp[key] = config[key];
            }
        }
        return tmp;
    }
};

var Teacher = Person.extend( {
    job : 'teacher',
    subject : 'English Literature',
    yearsExp : 5,
    toString : function () {
        var originalStr = Person.toString.call(this);
        return originalStr + ' ' + this.subject + ' teacher.';
    }
} );

var patty = Teacher.extend( {
    firstName : 'Patricia',
    lastName : 'Hannon',
    subject: 'chemistry',
    yearsExp : 20,
    gender : 'female'
} );

console.log( patty.toString() );

请特别注意教师中的toString方法。您会注意到,教师的toString函数显式调用了个人的toString函数。许多面向对象的设计者会认为我们不应该硬编码人和老师之间的关系。但是作为达到目的的一种简单方法,这样做确实可以快速、灵活、有效地解决问题。另一方面,它不便于携带。这种方法只适用于与父对象有某种关系的对象。

第三种可能性是,我们根本不用担心我们是否有super。是的,JavaScript 这种语言缺乏super特性,这在许多其他面向对象语言中都存在。但是这个特性并不是面向对象代码的全部。也许在将来,JavaScript 会有一个带有适当功能的super关键字。(实际上,我们知道在 ECMAScript 6 中,对象有一个super属性。)但就目前而言,没有它我们也能过得相当好。

成员可见性

在面向对象的代码中,我们经常希望控制对象数据的可见性。我们的大多数成员,无论是函数还是属性,都是公共的,与 JavaScript 的实现保持一致。但是如果我们需要私有函数或者私有属性呢?JavaScript 没有简单直接的可见性修饰符(如“私有”、“受保护”或“公共”)来控制谁可以访问属性的成员。但是你可以有私人会员的效果。此外,你可以通过道格拉斯·克洛克福特所谓的特权函数为这些私有成员提供特殊的访问权限。

回想一下,JavaScript 只有两个作用域:全局作用域和当前执行函数的作用域。在前一章中,我们利用闭包利用了这一点,闭包是实现私有成员特权访问的关键部分。它是这样工作的:在构建对象的函数中使用var创建私有成员。(那些私有成员是函数还是属性由你决定。)在同一个作用域内,创建一个函数;它将隐含对私有数据的访问,因为函数和私有数据都属于同一个范围。将这个新函数添加到对象本身,使函数(而不是私有数据)成为公共的。因为该函数来自相同的作用域,所以它仍然可以间接访问该数据。详见清单 3-9 。

清单 3-9 。私人成员

var Person = {
    firstName : 'John',
    lastName : 'Connolly',
    birthDate : new Date( '1964-09-05' ),
    gender : 'male',
    getAge : function () {
        var today = new Date();
        var diff = today.getTime() - this.birthDate.getTime();
        var year = 1000 * 60 * 60 * 24 * 365.25;
        return Math.floor( diff / year );
    },

    toString : function () {
        return this.firstName + ' ' + this.lastName + ' is a ' + this.getAge() +
            ' year-old ' + this.gender;
    },

    extend : function ( config ) {
        var tmp = Object.create( this );

        for ( var key in config ) {
            if ( config.hasOwnProperty( key ) ) {
                tmp[key] = config[key];
            }
        }

        // When was this object created?
        var creationTime = new Date();

        // An accessor, at the moment, it's private
        var getCreationTime = function() {
            return creationTime;
        };

        tmp.getCreationTime = getCreationTime; 
        return tmp;
    }
};

var Teacher = Person.extend( {
    job : 'teacher',
    subject : 'English Literature',
    yearsExp : 5,
    toString : function () {
        var originalStr = Person.toString.call(this);
        return originalStr + ' ' + this.subject + ' teacher.';
    }
} );

var patty = Teacher.extend( {
    firstName : 'Patricia',
    lastName : 'Hannon',
    subject: 'chemistry',
    yearsExp : 20,
    gender : 'female'
} );

console.log( patty.toString() ); 
console.log( 'The Teacher object was created at %s', patty.getCreationTime() );

如您所见,creationTime变量是extend函数的局部变量。它在该功能之外不可用。如果你在控制台上用console.dir检查人,你不会看到creationTime被列为人的公共属性。最初,getCreationTime也是如此。它是在与creationTime相同的作用域中创建的函数,因此该函数可以访问creationTime。使用简单赋值,我们将getCreationTime附加到我们返回的对象实例上。现在,getCreationTime是一个公共方法,可以特权访问creationTime中的私有数据。

一个小警告:这不是最有效的模式。每当您创建 Person 的实例或它的任何子类型时,您将创建一个全新的函数,它可以访问创建 Person 实例的调用extend的执行上下文。相比之下,当我们使用Object.create时,我们的公共函数是对我们传递给Object.create的类型的引用。在我们这里处理的小范围内,特权函数并不是特别低效。但是,如果您添加更多的特权方法,它们都将保留对该执行上下文的引用,并且都将是该特权方法的自己的实例。内存成本会迅速增加。谨慎使用特权方法,将它们留给需要严格访问控制的数据。否则,就接受 JavaScript 中的大多数数据都是公共的这一观点吧。

面向对象 JavaScript 的未来

我们忽略了 ECMAScript 6 给面向对象的 JavaScript 带来了一些变化。这些变化中最重要的是引入了一个有效的关键字classclass关键字将用于定义 JavaScript 类型(不是类,因为 JavaScript 仍然没有类!).它还将包括使用关键字extends创建继承关系的附带条件。最后,当在一个子类型中重写函数时,ECMAScript 6 留出super关键字来引用原型链上的函数版本。

然而,所有这些都是语法上的糖。当这些结构被 JavaScript 引擎分解后,它们被揭示为函数构造函数的使用。这些新特性实际上并没有建立新的功能:它们只是引入了一种其他面向对象语言的程序员更容易接受的习惯用法。更糟糕的是,他们继续掩盖 JavaScript 的一些最佳特性,试图让它符合其他语言对“真正的”面向对象语言应该是什么样子的概念。有时,JavaScript 似乎仍然有点羞于在使用其强大功能之前穿上斗篷和紧身衣。

封装 JavaScript

从面向对象的 JavaScript 向外发展,我们应该考虑如何组织我们的代码以便广泛重用。我们需要一套工具来正确封装我们的代码,防止意外使用全局上下文,以及使我们的代码可重用和可再分发的方法。让我们按顺序解决各种需求。

名称空间

到目前为止,我们已经声明了我们的类型(以及更早的函数和变量)是全局上下文的一部分。我们没有明确地这样做,但是由于我们没有将这些对象和变量声明为任何其他上下文的一部分。我们希望将函数、变量、对象、类型等封装到一个单独的上下文中,以便不依赖于全局上下文。为此,我们将(最初)依赖于名称空间。

名称空间并不是 JavaScript 独有的,但是,就像 JavaScript 中的许多东西一样,它们与您可能期望的有些不同。命名空间为变量和函数提供了上下文。当然,名称空间本身很可能是全局的。这是两害相权取其轻的方法。我们可以有一个属于窗口的变量,然后各种数据和功能属于那个变量,而不是有许多属于窗口的变量和函数。实现 很简单:使用一个对象文字来封装你想要从全局上下文中隐藏的代码(清单 3-10 )。

清单 3-10 。命名空间

// Namespaces example

var FOO = {};

// Add a variable
FOO.x = 10;

// Add a function
FOO.addEmUp = function(x, y) {
    return x + y; 
};

名称空间最好用作封装无关代码的专用解决方案。如果我们试图在所有的代码中使用名称空间,随着它们增加越来越多的功能和数据,名称空间会很快变得难以使用。您可能会尝试在名称空间中设置名称空间,模拟包与 Java 一起工作的方式。Ext JS 库 很好地使用了这种技术,值得一提。但是他们也花了很多时间考虑如何组织他们的功能,什么代码属于什么名称空间或子名称空间,等等。大量使用名称空间是有代价的。

此外,名称空间名称是硬编码的 : FOO,在前面提到的库 Ext JS 中是Ext,在同样流行的 YUI 库中是YAHOO。这些名称空间实际上是这些库的保留字。如果两个或更多的库使用同一个名称空间(就像 jQuery 的使用$作为名称空间一样),会发生什么呢?潜在的冲突。JQuery 已经添加了显式代码来处理这种可能性,如果它到来的话。虽然这个问题在您自己的代码中不太可能出现,但这是一个必须考虑的可能性。在团队环境中尤其如此,在团队环境中,多个程序员可以访问名称空间,这增加了意外覆盖或删除另一个程序员的名称空间的可能性。

模块模式

我们有一些工具来改进我们使用名称空间的方式。我们可以使用模块模式,它将命名空间的生成封装在一个函数中。这允许多种改进,包括为名称空间包含的函数和数据建立基线,在生成器函数中使用私有变量,这可能使某些功能的实现更容易,以及简单地让函数生成名称空间,这意味着我们可以让 JavaScript 在运行时而不是编译时动态生成部分或全部名称空间。

模块可以根据您的喜好简单或复杂。清单 3-11 提供了一个非常简单的创建模块的例子。

清单 3-11 。创建一个模块

function getModule() {
    // Namespaces example
    var FOO = {};

    // Add a variable
    FOO.x = 10;

    // Add a function
    FOO.addEmUp = function ( x, y ) {
        return x + y;
    };

    return FOO;
}

var myNamespace = getModule();

我们已经将名称空间代码封装在一个函数中。因此,当我们最初设置FOO对象时,它是getModule函数的私有对象。然后,我们可以将FOO返回给任何调用getModule的人,他们可以按照自己认为合适的方式使用封装的结构,包括随意命名。

这种模式的另一个优点是,我们可以再次利用我们的朋友闭包来设置只对名称空间私有的数据。如果我们的名称空间,我们封装的代码,需要有内部数据或内部函数,我们可以添加它们而不用担心公开它们(清单 3-12 )。

清单 3-12 。带有私有数据的模块

function getModule() {
    // Namespaces example
    var FOO = {};

    // Add a variable
    FOO.x = 10;

    // Add a function
    FOO.addEmUp = function ( x, y ) {
        return x + y;
    };

    // A private variable
    var events = [];

    FOO.addEvent = function(eventName, target, fn) {
        events.push({eventName: eventName, target: target, fn: fn});
    };

    FOO.listEvents = function(eventName) {
        return events.filter(function(evtObj) {
            return evtObj.eventName === eventName
        });
    };

    return FOO;
}

var myNamespace = getModule();

在这个例子中,我们实现了一个公共接口,用于添加某种带有addEvents的事件跟踪。稍后,我们可能希望通过listEvents按事件名称取回事件引用。但是实际的events集合是私有的,由我们提供给它的公共 API 管理,但是对直接访问是隐藏的。

像名称空间一样,模块也有两害相权取其轻的问题。我们用一个全局变量交换了我们的名称空间,换来了一个全局函数getModule。如果我们可以完全控制在全局名称空间中结束的内容,而不必使用全局范围的对象或函数来这样做,这不是很好吗?幸运的是,我们即将看到一种工具可以帮助我们做到这一点。

立即调用的函数表达式

如果我们想避免污染全局名称空间,函数是合乎逻辑的解决方案。函数在运行时会创建自己的执行上下文,它从属于全局名称空间,但与全局名称空间相隔离。当函数完成运行时,执行上下文可用于垃圾收集,并且专用于它的资源可以被回收。但是我们所有的函数要么是全局的,要么是名称空间的一部分,而名称空间本身就是全局的。我们希望有一个可以立即执行的函数,不需要命名,也不需要成为全局或其他名称空间或上下文的一部分。然后,在这个函数中,我们可以构建我们需要的模块。我们可以返回这样一个对象,导出它,并使它可用,但我们不需要一个公共函数来占用生成它的资源。这就是立即调用的函数表达式(life)背后的思想。

到目前为止,我们使用的所有函数都是函数声明。无论我们将它们定义为function funcName { ... }还是var funcName = function() { ... },我们都是在声明函数,将它们的用法留待以后使用。我们是否可以创建一个函数表达式,这个函数可以一次性创建并执行?答案是肯定的,但是这样做需要一定程度的语法勇气。

我们如何执行函数?通常,对于一个已命名的函数,我们打印函数的名称,然后在后面附加一些括号,表明我们想要执行与该名称相关的代码。我们不能对函数定义本身做同样的事情。结果将是一个语法错误,显然不是我们想要的。

但是我们可以将函数声明放在一组括号内,这是对解析器的一个提示,表明这不是一个语句,而是一个表达式。圆括号中不能包含语句,只能包含作为表达式计算的代码,这是我们希望从函数中得到的。我们还需要一点语法来实现这一点,另一组括号,通常在函数声明本身的末尾。清单 3-13 将阐明完整的语法。

清单 3-13 。立即调用的函数表达式

// A regular function
function foo() {
    console.log( 'Called foo!' );
}

// Function assignment
var bar = function () {
    console.log( 'Called bar!' );
};

// Function expression
(function () {
    console.log( 'This function was invoked immediately!' )
})();

// Alternate syntax
(function () {
    console.log( 'This function was ALSO invoked immediately!' )
}());

将前两个函数(函数声明)与后两个函数(函数表达式)进行比较。表达式用圆括号括起来,以便“表达式化”(或者,如果您愿意的话:“反声明”),然后使用第二组圆括号来调用表达式。俏皮!

顺便说一下,有各种各样的 JavaScript 语法粒子会导致 IIFEs:函数作为逻辑求值的组件,一元运算符作为函数声明的前缀,等等。“牛仔”本·阿尔曼关于生命的文章(http://benalman.com/news/2010/11/immediately-invoked-function-expression/)包含了关于有效语法的极好的细节,并深入到生命是如何工作的以及它们是如何形成的。

既然我们知道如何创造生活,我们该如何利用它呢?生命有许多应用,但我们在这里关心的是模块的生成。我们能把一个生命的结果捕捉到一个变量中吗?当然可以!所以我们可以将我们的模块生成器包装在一个生命中,并让它返回模块(清单 3-14 )。

清单 3-14 。生命模块生成器

var myModule = (function () {
    // A private variable
    var events = [];

    return {
        x : 10,
        addEmUp : function ( x, y ) {
            return x + y;
        },
        addEvent : function ( eventName, target, fn ) {
            events.push( {eventName : eventName, target : target, fn : fn} );
        },
        listEvents : function ( eventName ) {
            return events.filter( function ( evtObj ) {
                return evtObj.eventName === eventName
            } );
        }
    };

})();

在最后一个例子中,我们做了一些改动。首先,也是最简单的,我们现在在myModule而不是myNamespace中捕获我们工厂生活的输出。第二,我们不是创建一个对象然后返回它,而是直接返回对象。这简化了我们的代码,减少了为我们最终不会使用的对象保留空间。

IIFE 模式开辟了许多新的可能性,包括根据需要使用库或其他工具。函数表达式末尾的括号与常规函数调用中的括号相同。因此,我们可以将争论传递到我们的生活中,并在生活中使用它们。想象一下可以使用 jQuery 功能的生活(清单 3-15 )。

清单 3-15 。给生命传递论据

// Here, the $ refers to jQuery and jQuery only for the entire
// scope of the module
var myModule = (function ($) {
    // A private variable
    var events = [];

    return {
        x : 10,
        addEmUp : function ( x, y ) {
            return x + y;
        },
        addEvent : function ( eventName, target, fn ) {
            events.push( {eventName : eventName, target : target, fn : fn} );
            $( target ).on( eventName, fn );
        },
        listEvents : function ( eventName ) {
            return events.filter( function ( evtObj ) {
                return evtObj.eventName === eventName
            } );
        }
    };

})(jQuery); // Assumes that we had included jQuery earlier

我们将 jQuery 传递到我们的生活中,然后在整个生活中将其称为$。在内部,它在addEvent函数中被用来向 DOM 添加一个事件处理程序。(不用担心语法没有意义;这不是例子的核心!)

基于这段代码,您可能会想象一个系统,在这个系统中,由 IIFEs 生成的模块相互对话,来回传递参数并使用库,所有这些都不需要在全局级别上进行交互。事实上,这也是下一章的内容之一。

摘要

本章开始时摆在我们面前的问题是代码管理问题。我们如何按照良好的面向对象准则编写代码,以及如何封装代码以实现可重用性?在前一种情况下,我们专注于 JavaScript 的原型性质,用它来生成类似于类和实例的东西,但带有独特的 JavaScript 旋转。这种实现比试图强迫 JavaScript 像 C#或 Java 那样运行要简单得多。对于后一个需求,我们通过各种解决方案来封装我们的代码:名称空间、模块和立即调用的函数表达式。最终,这三者的结合为我们提供了最少使用全局环境的最佳解决方案。

四、调试 JavaScript 代码

有时候,并不是代码的编写,而是对代码的管理,让我们陷入困境,回到我们最喜欢的视频游戏。为什么它能在这台机器上工作,而不是在那台机器上?什么叫两倍等于()不好,三倍等于(=)好?为什么运行测试如此麻烦?我应该如何打包这些代码以供分发?我们被问题困扰,被与我们正在编写的代码没有直接关系的问题分心。

当然,我们不应该忽视这些问题。我们希望编写最高质量的代码,当我们做不到时,我们希望获得易于使用的调试工具。我们需要良好的测试覆盖率,无论是现在还是未来的重构。我们应该考虑我们的代码将如何分布。这就是本章的全部内容。

我们将从如何解决代码问题开始。我们希望成为完美的程序员,第一次就把所有东西都写对。但是我们都知道这在现实世界中是不会发生的。所以先从调试工具说起。

调试工具

所有现代浏览器都有某种形式的开发者工具包。即使是落后的 Internet Explorer 8 也有一个基本的调试器,尽管你需要管理员权限来安装它。我们现在所拥有的与使用各种alert()语句或者偶尔将 DOM 元素作为我们唯一依靠的开发时代相去甚远。

一般来说,开发人员的工具包会有以下实用程序:

  • 控制台:我们的应用的 JavaScript 便笺簿和日志位置的组合。
  • 调试器:长久以来困扰 JavaScript 开发人员的工具。
  • 一个 DOM 检查器:我们的大部分工作都集中在操作 DOM 上,右键选择 View Source 不会削减它。检查器应该反映 DOM 的当前状态(而不是原始源)。大多数 DOM 检查器都有一个基于树的视图,可以通过在检查器或页面中单击来选择 DOM 元素。
  • 一个网络分析器:告诉我请求了什么文件,实际上找到了哪些文件,以及下载它们花了多长时间。
  • 分析器:这些通常有些粗糙,但是它们比将一个调用包装在对new Date().getTime()的两个调用中要好。

还有一些扩展可以添加到浏览器中,为您提供超出浏览器内置功能的额外调试功能。例如,Postman ( http://getpostman.com)是 Chrome 的一个扩展,它可以让你创建任何 HTTP 请求并查看响应。另一个流行的扩展是 Firebug ( http://getfirebug.com),这是一个开源项目,它为 Firefox 添加了所有的开发工具,并且也可以拥有自己的一组扩展。

在本章中,我们将把通用工具集称为开发人员工具或开发人员工具包,除非讨论特定的浏览器工具集。

控制台

作为开发人员,控制台是我们花费大量时间的地方。控制台界面模仿了大多数应用上熟悉的日志级别:debuginfowarnerrorlog。通常,我们第一次遇到它是作为代码中alert()语句的替代,尤其是在调试的时候。在一些老版本的 IE 上,只支持log,但从 IE 11 开始,五个功能都支持。此外,控制台有一个dir()函数 ,它将为您提供一个递归的、基于树的对象接口。万一控制台不在您选择的平台上,尝试将清单 4-1 作为多项填充。

清单 4-1 。控制台聚合填充

if (!window.console) {
  window.console = {
    log : alert
  }
}

(显然,这只是log函数的多填充 。如果您要使用其他人,您必须单独添加他们。)

各个级别的输出变化很小。在 Chrome 或 Firefox 上,console.error包含了一个自动堆栈跟踪。其他浏览器(和原生 Firefox)只是添加一个图标并改变文本颜色来区分不同的级别。也许使用不同功能级别的主要原因是它们可以在所有三种主要浏览器上被过滤掉。清单 4-2 提供了一些测试代码 ,随后是来自各大浏览器的屏幕截图:Chrome、Firefox 和 Internet Explorer ( 图 4-1 到 4-3 )。

清单 4-2 。控制台级别

console.log( 'A basic log message.' );
console.debug( 'Debug level.' );
console.info( 'Info level.' );
console.warn( 'Warn level.' );
console.error( 'Error level (possibly with a stacktrace).' );

var person = {
  name : 'John Connelly',
  age : 56,
  title : 'Teacher',
  toString: function() {
    return this.name + ' is a ' + this.age + '-year-old ' + this.title + '.';
  }
};

console.log( 'A person: ' );
console.dir( person );

console.log( 'Person object (implicit call to toString()): ' + person );
console.log( 'Person object as argument, similar to console.dir: ', person );

9781430263913_Fig04-01.jpg

图 4-1 。在 Chrome 40.0 中查看的测试代码

9781430263913_Fig04-02.jpg

。在 Firefox 35.0.1 中查看的测试代码

9781430263913_Fig04-03.jpg

图 4-3 。在 Internet Explorer 11.0 中查看测试代码

利用控制台功能

那么,使用这些控制台功能的最佳方式是什么呢?与 JavaScript 的许多特性一样,关键是一致性。您和您的团队应该在使用模式上达成一致,记住几件事:首先也是最重要的是,在您部署代码让全世界看到之前,应该删除所有的控制台语句。生产代码不需要包含控制台语句,并且移除控制台函数的调用非常容易(正如您将在本章后面看到的)。还要记住,调试,我们很快就会看到,可以取代一次性需求的日志记录。一般来说,使用控制台日志记录来获取关于应用状态的信息:它启动了吗?它能找到数据吗?各种复杂的物体是什么样子的?等等。您的日志记录将为您提供应用生命周期的编年史,以及应用变化状态的视图。如果您的应用是一条高速公路,良好的日志记录就相当于一种里程标——进度的指示,以及当问题不可避免地出现时从哪里开始搜索的通用指示器。

控制台也不仅仅是一个日志记录工具。这是一个 JavaScript 便笺本。控制台以单行模式启动,您可以逐行输入 JavaScript。如果你想输入多行代码,你可以切换到多行模式(通过 Firefox 和 IE 中的图标启用;在 Chrome 中,只需用 Shift+Enter 结束你的行。在单行模式下,您可以输入各种 JavaScript 语句,通过点击 Tab 或右箭头键享受自动完成。控制台还包括一个简单的历史记录,通过它,您可以使用上下箭头键向前和向后移动。控制台维护状态,因此在前面一行(或多行模式的运行)中定义的变量会一直存在,直到您重新加载页面。

这最后一个特征值得进一步研究。控制台拥有 JavaScript 解释器的全部当前状态。这是难以置信的强大。你加载 jQuery 了吗?然后你可以根据它的 API 输入命令。想在页面末尾检查变量的状态?或者也许你需要看看一个特定的动画是怎么回事?控制台是你的朋友。您可以调用函数、检查变量、操作 DOM 等等。想象一下,您输入的任何命令都被添加到刚刚完成的脚本中,并且可以访问它的所有状态。

控制台还有一个扩展的命令行 API。最初是由 Firebug 的优秀人员创建的,它的一些元素也已经移植到了其他浏览器上。现在 Chrome 和原生 Firefox 支持,但 Internet Explorer 不支持。这个 API 有许多有用的应用,我们全心全意地推荐在https://getfirebug.com/wiki/index.php/Command_Line_API查看细节。以下是一些亮点:

  • debug( functionName ):functionName 被调用时,调试器会在函数中的第一行代码前自动启动。
  • undebug( 函数名 ):停止调试已命名的函数。
  • include( url ):将远程脚本拉入页面。如果你想引入另一个调试库,或者不同地操作 DOM 的东西,或者诸如此类的东西,这非常方便。
  • monitor( functionName ):打开对命名函数的所有调用的日志记录;不影响console.*调用,而是为函数的每次调用插入一个对console.log的自定义调用。这将记录函数名、它的参数以及它们的值。
  • unmonitor( 函数名 ):关闭通过monitor()启用的所有函数调用的日志记录。
  • profile([ 标题 ]):打开 JavaScript profiler 您可以为这个概要文件传入一个可选的标题。
  • profileEnd():结束当前运行的配置文件并打印一份报告,可能带有调用配置文件中指定的标题。
  • getEventListeners(element):获取所提供元素的事件监听器。

多亏了控制台,我们开发人员有了一个全功能的工具来与我们的代码进行交互。我们可以记录应用状态的快照,并且一旦它完成加载,我们就可以与之交互。控制台也将在我们的下一个工具调试器中占据显著位置。

调试器

多年来,对 JavaScript 的一个打击是它不可能是一种“真正的”语言,因为它缺乏像调试器这样的工具。快进到现在,调试器是所有开发人员工具包的标准装备。所有当前的浏览器都有一个开发工具,可以让你检查你的应用和调试你的工作。让我们看看这些工具是如何工作的,从调试器开始。

调试器背后的想法很简单:作为开发人员,您需要暂停应用的执行并检查其当前状态。尽管我们可以通过明智地应用console.log语句来完成后一部分,但是如果没有调试器,我们就无法处理前一部分。暂停应用后,我们需要使用一些工具。我们需要一种方法来告诉调试器激活。在代码本身中,我们可以添加简单的语句debugger;来激活该行的调试器。如前所述,我们还可以从控制台调用debug命令,向它传递一个函数的名称,该函数在被调用时将启动调试器。但是选择调试器何时启动的最简单的方法是设置一个断点。

断点允许我们将 JavaScript 代码运行到某一点,然后将应用冻结在那里。当我们到达断点时,我们可以开始了解应用的当前状态。从这里我们可以看到变量的内容,这些变量的范围等等。此外,我们有一个导航菜单,其中至少包括四个选项:单步执行当前函数(深入堆栈一层),单步退出当前函数(运行当前堆栈框架直到完成,并在框架返回的点继续调试),单步执行当前函数(无需首先深入函数)和继续执行(运行直到完成或下一个断点)。

DOM 检查器

许多 JavaScript 应用对 DOM 的状态做了大量的更改——事实上,这些更改如此之多,以至于在加载页面后立即引用实际的 HTML 源代码通常是没有用的。DOM inspector 反映了 DOM 的当前状态(而不是页面加载时 DOM 的状态)。每当 DOM 发生变化时,它应该动态地即时更新。开发人员工具已经将 DOM 检查器作为一个标准特性。

网络分析仪

自从这本书的前一版以来,Ajax 已经从 JavaScript 的一个奇异特性变成了专业 JavaScript 程序员锦囊妙计中的一个标准工具。调试工具花了一段时间才跟上。现在,开发人员工具提供了几种跟踪 Ajax 请求的方法。一般来说,您应该能够在控制台或网络分析器上获得关于 Ajax 请求的信息。后者有更详细的接口。您应该能够对特定类型的请求进行排序(XHR/Ajax、脚本、图像、HTML 等等)。每个请求都应该有自己的条目,这些条目通常会提供关于请求状态(响应代码和响应消息)、请求去往何处(完整的 URL)、交换了多少数据以及请求花费了多长时间的信息。深入到单个请求,您可以看到请求和响应头、已处理数据的预览以及数据的原始视图(取决于数据类型)。例如,如果您的应用请求 JSON 格式的数据,网络分析器将告诉您原始数据(一个普通的字符串),并且可能通过 JSON 格式化程序传递该字符串,因此它可以向您显示请求的最终结果。图 4-4 显示的是 Chrome 40.0 中的网络分析仪,图 4-5 显示的是 Firefox 35.0.1 中的。

9781430263913_Fig04-04.jpg

图 4-4 。Chrome 40.0 中的网络分析仪

9781430263913_Fig04-05.jpg

图 4-5 。火狐 35.0.1 中的网络分析器

使用堆分析器和时间线,您可以检测桌面和移动设备上的内存泄漏。首先让我们看看时间线。

时间线

当您第一次注意到页面变慢时,时间线可以快速帮助您了解随着时间的推移您使用了多少内存。时间表中的功能在所有现代浏览器中都非常相似,所以为了简短起见,我们将重点放在 Chrome 上。

转到时间线面板并选中内存复选框。在那里,你可以点击左侧的录制按钮。这将开始记录应用的内存消耗。在记录时,以暴露内存泄漏的方式使用应用。停止记录,图表将会显示你在一段时间内使用了多少内存。

如果您发现随着时间的推移,您的应用正在使用内存,并且垃圾收集的水平从未下降,那么您有一个内存泄漏。

配置文件 ??

如果您发现确实存在内存泄漏,下一步就是查看分析器,并尝试了解发生了什么。

理解内存在浏览器中是如何工作的,以及它是如何被清理或垃圾收集的是很有帮助的。垃圾收集是在浏览器中自动处理的。这是浏览器查看所有已创建对象的过程。不再被引用的对象被删除,内存被回收。

现在所有的浏览器都内置了分析工具。这将让您看到随着时间的推移哪些对象使用了更多的内存。

图 4-6 显示了 Chrome 40.0 中的 Profiles 面板。

9781430263913_Fig04-06.jpg

图 4-6 。Chrome 40.0 中的配置文件面板

图 4-7 显示了 Firefox 35.0.1 中的等效面板,性能选项卡。

9781430263913_Fig04-07.jpg

图 4-7 。Firefox 35.0.1 中的个人资料面板

使用 profiler 是类似的,因为您需要浏览器来记录运行中的应用。在这种情况下,您拍摄了所谓的快照。Gmail 团队建议按以下顺序参加三次考试:

  1. 拍一张快照。
  2. 在您认为泄漏来自的地方执行操作。
  3. 拍摄第二张快照。
  4. 执行相同的操作。
  5. 拍第三张快照。
  6. 在快照 3 的摘要视图中筛选快照 1 和 2 中的对象。

此时,您可以开始看到所有仍在周围并可能占用内存的对象。现在,您应该能够看到哪些引用是剩余的,并处理掉它们。

那么什么是参考呢?通常,当一个对象有一个值是另一个对象的属性时,就会发生引用。清单 4-3 显示了一个例子。

清单 4-3 。创建对象引用

var myObject = {};
myObject.property = document.createElement('div');
mainDiv.appendChild(myObject.property);

这里的myObject.property 现在引用了新创建的div对象。appendChild方法可以毫无问题地使用它。如果在某个时候从 DOM 中删除了那个divmyObject仍然会有一个对div的引用,并且不会被垃圾收集。当对象不再持有引用时,它们会被自动垃圾回收。

移除引用的一种方法是使用delete关键字,如清单 4-4 所示。

清单 4-4 。删除对象引用

delete myObject.property;

摘要

正如你所看到的,现代浏览器有工具给你一个环境,帮助你完全理解你的应用。如果您确实看到了可以改进的地方,时间线可以显示一段时间内使用了多少内存。调试器可以帮助您在任何给定时间看到变量的值。使用 profiler 可以帮助您看到哪里正在泄漏内存,以及如何修复它。

五、文档对象模型

使用文档对象模型(DOM ) 是专业 JavaScript 程序员工具箱的一个关键组件。全面理解 DOM 脚本不仅有利于我们构建应用的范围,而且有利于这些应用的质量。像 JavaScript 的大多数特性一样,DOM 有一段曲折的历史。但是有了现代浏览器,不引人注目地操纵 DOM 并与之交互比以往任何时候都容易。了解如何使用这项技术以及如何最好地运用它,可以让您在开发下一个 web 应用时有一个良好的开端。

在这一章中,我们将讨论一些与 DOM 相关的主题。对于不熟悉 DOM 的读者,我们将从基础开始,浏览所有重要的概念。对于那些已经熟悉 DOM 的人,我们提供了一些很酷的技术,我们相信你会喜欢并开始在自己的网页中使用它们。

DOM 也处于十字路口。从历史上看,因为 DOM 接口更新与浏览器或 JavaScript 更新不同步,所以浏览器和 DOM 支持之间存在脱节。错误的实现加剧了这种脱节。流行的库如 jQuery 和 Dojo 就是为了解决这些问题而出现的。但是随着现代浏览器的出现,DOM 已经规范化,界面也有了很大的改善。我们需要解决的问题是,是使用库来帮助我们访问 DOM,还是用标准的 DOM 接口做所有的事情。

文档对象模型简介

最初,DOM 是作为一种在浏览器中表示 HTML 文档的部分的方式而创建的。使用 JavaScript,开发人员可以查看表单、锚点、图像和页面的其他组件,但不必查看整个页面。这有时被称为“遗留 DOM”或 0 级 DOM。最终,DOM 变成了一个接口,由 W3C 监管。从一开始,DOM 就已经成为 HTML 文档和 XML 文档的官方接口。它不一定是最快、最轻或最容易使用的接口,但它是最普遍的,在大多数编程语言(如 Java、Perl、PHP、Ruby、Python,当然还有 JavaScript)中都有实现。当我们使用 DOM 接口时,请记住,您学到的几乎所有东西都可以应用于 HTML 和 XML,尽管大多数时候我们会专门提到 HTML。

万维网联盟负责监督 DOM 规范。由于各种历史原因,DOM 规范的版本被标识为 DOM 级别 n 。目前的规范(截至发布时)是 DOM Level 4。这有时会令人困惑,因为 DOM 树本身可以有层次。我们将努力把 DOM 规范的版本称为 DOM 级别(用大写的 L ),然后用小写的 L 表示 DOM 树级别。

首先,我们应该快速讨论一下 HTML 文档的结构。因为这是一本关于 JavaScript 的书,而不是 HTML,所以我们将关注 HTML 文档对 JavaScript 的影响。让我们提出几个简单的原则:

  1. 我们的 HTML 文档应该以 HTML 5 doctype 开始。它们很简单:<!DOCTYPE html>。包含 doctype 可以防止浏览器陷入古怪模式,在这种模式下,浏览器的行为不太一致。
  2. 比起脚本块或内嵌脚本,我们更喜欢通过<script>标签包含单独的 JavaScript 文件。这使得开发(将 JavaScript 从 HTML 中分离出来)和管理更加容易。在极少数情况下,脚本块比包含文件更有意义。但是一般来说,更喜欢包含的文件。
  3. 我们的脚本标签应该出现在 HTML 文档的底部,紧接在结束的</body>标签之前。

第三项需要一些解释。通过将我们的<script>包含在页面底部,我们获得了几个优势。我们的 HTML 的大部分(如果不是全部的话)应该已经被加载了(以及相关的文件:图像、音频、视频、CSS 等等)。为什么这很重要?处理 JavaScript 代码会锁定页面的呈现!当 JavaScript 代码被解析时,浏览器不能呈现其他页面元素(有时当 JavaScript 代码正在运行时!).因此,只要有可能,我们应该等到最后一刻才加载 JavaScript 代码。此外,在移动或慢速连接场景中,获取和加载 JavaScript 可能比在桌面浏览器上慢。页面其余部分的早期加载意味着你的用户不会看到一个除了旋转器什么都没有的空白页面。这里的原则是,用户应该能够尽快看到页面已经加载的反馈。

没错。那么这个理想的 HTML 页面是什么样子的呢?查看清单 5-1 中的。

清单 5-1 。一个样本 HTML 文件

<!DOCTYPE html>
<html>
<head>
  <title>Introduction to the DOM</title>
</head>
<body>
<h1>Introduction to the DOM</h1>

<p id="intro" class="test">There are a number of reasons why the DOM is awesome; here are some:</p>
<ul id="items">
  <li id="everywhere">It can be found everywhere.</li>
  <li class="test">It’s easy to use.</li>
  <li class="test">It can help you to find what you want, really quickly.</li>
</ul>
<script src="01-sample.js"></script>
</body>
</html>

有时,在我们的例子中,我们会有一个内联脚本块。如果是这样,它将根据其功能显示在页面中。如果它的位置与其功能无关,我们将在页面底部放置脚本块,就像我们的脚本包含的一样。

DOM 结构

HTML 文档的结构在 DOM 中表示为一棵可导航的树。所有使用的术语都类似于系谱树(父母、子女、兄弟姐妹等等)。出于我们的目的,树的主干是文档节点,也称为文档元素。该元素包含指向其子节点的指针,反过来,每个子节点又包含指向其父节点、兄弟节点和子节点的指针。

DOM 使用特定的术语来指代 HTML 树中的不同对象。几乎 DOM 树中的所有东西都是节点:HTML 元素是节点,元素中的文本是节点,注释是节点,DOCTYPE 是节点,甚至属性也是节点!显然,我们需要能够区分这些节点,因此每个节点都有一个节点类型属性,适当地称为nodeType ( 表 5-1 )。我们可以查询这个属性来判断我们正在查看的是哪种类型的节点。如果你得到一个节点的引用,它将是一个节点类型的实例,实现所有的方法并拥有该类型的所有属性。

表 5-1 。节点类型及其常量值

|

节点名

|

节点类型值

|
| --- | --- |
| 元素 _ 节点 | one |
| 属性 _ 注释(已弃用) | Two |
| 正文 _ 节点 | three |
| CDATA_SECTION_NODE(已弃用) | four |
| 实体 _ 引用 _ 节点(已弃用) | five |
| 实体节点(已弃用) | six |
| 处理 _ 指令 _ 节点 | seven |
| 评论 _ 节点 | eight |
| 文档 _ 节点 | nine |
| 文档类型节点 | Ten |
| 文档 _ 片段 _ 节点 | Eleven |
| 符号 _ 节点(已弃用) | Twelve |

标记为 deprecated 的节点类型将被取代并可能被删除,但这种可能性很小。他们可能仍然工作,因为他们已经使用了几年了。

正如您在表中看到的,节点有各种专门化,它们对应于 DOM 规范中的接口。特别感兴趣的是文档、元素、属性和文本。其中每个都有自己的实现类型:分别是文档、元素、属性和文本。

Image 注意属性是一个特例。在 DOM 级别 1、2 和 3 下,Attr 接口实现了节点接口。对于 DOM Level 4 来说,这不再是真的。值得庆幸的是,这更多的是一种常识性的改变。更多细节可以在属性部分找到。

一般来说,该文档关注的是将 HTML 文档作为一个整体来管理。文档中的每个标签都是一个元素,它本身被特化为特定的 HTML 元素类型(例如,HTMLLIElementHTMLFormElement)。元素的属性被表示为 Attr 的实例。元素中的任何纯文本都是文本节点,由文本类型表示。这些子类型不是从 Node 继承的所有类型,但它们是我们最有可能与之交互的类型。

给定我们的清单,让我们看看它的结构:从<!DOCTYPE html></html>的整个文档就是文档。doctype 本身就是 doctype 类型的一个实例。<html>...</html>元素是我们的首要元素。它包含了<head><body>标签的子元素。再深入一点,我们可以看到<body>中的<p>元素有两个属性,idclass。同一个<p>元素的内容只有一个文本节点。在各种 DOM 类型的实例之间的关系中,文档的层次结构是重复的。我们应该更详细地研究这些关系。

DOM 关系

让我们检查一个非常简单的文档片段,以显示节点之间的各种关系:

<p><strong>Hello</strong> how are you doing?</p>

这个代码片段的每个部分都分解成一个 DOM 节点,每个节点的指针都指向它的直接亲属(父节点、子节点、兄弟节点)。如果你要完全描绘出存在的关系,它看起来会像图 5-1 。代码片段的每个部分(圆形框表示元素,常规框表示文本节点)都与其可用的引用一起显示。

9781430263913_Fig05-01.jpg

图 5-1 。节点之间的关系

每个 DOM 节点都包含一个指针集合,可以用来引用它的亲戚。您将使用这些指针来学习如何在 DOM 中导航。所有可用的指针都显示在图 5-2 中。每个 DOM 节点上都有这些属性,它们都是指向另一个节点或其子类的指针。唯一的例外是childNodes (当前节点的所有子节点的集合)。当然,如果这些关系中有一个未定义,属性的值将为空(想象一个<img>标签,它既没有定义firstChild也没有定义lastChild)。

9781430263913_Fig05-02.jpg

图 5-2 。使用指针导航 DOM 树

只需使用不同的指针,就可以导航到页面上的任何元素或文本块。回想一下清单 5-1 ,它展示了一个典型的 HTML 页面。之前,我们从 JavaScript 类型的角度来看。现在我们将从 DOM 的角度来看它。

在示例文档中,文档节点是<html>元素。在 JavaScript 中访问这个元素很简单:document.documentElement直接引用<html>元素。根节点拥有用于导航的所有指针,就像任何其他 DOM 节点一样。使用这些指针,您可以开始浏览整个文档,导航到您想要的任何元素。例如,要获得<h1>元素,您可以使用以下代码:

// Does not work!
document.documentElement.firstChild.nextSibling.firstChild

但是我们遇到了一个主要障碍:DOM 指针可以指向文本节点和元素。我们的 JavaScript 语句实际上并没有指向<h1>元素;而是指向<title>元素。为什么会这样?这是因为 XML 中最棘手和最有争议的一个方面:空白。如果你注意到清单 5-1 中的,在<html><head>元素之间实际上有一条结束线,这被认为是空白,这意味着实际上首先有一个文本节点,而不是<head>元素。从中我们可以学到四点:

  • 当试图只使用指针浏览 DOM 时,编写漂亮、干净的 HTML 标记实际上会使事情变得非常混乱。
  • 只使用 DOM 指针来导航文档可能非常冗长且不切实际。
  • 事实上,DOM 指针显然非常脆弱,因为它们将 JavaScript 逻辑与 HTML 过于紧密地联系在一起。
  • 通常,您不需要直接访问文本节点,只需要访问它们周围的元素。

这就引出了一个问题:有没有更好的方法来查找文档中的元素?有,有!更准确的说:有!我们有两种主要的方法来访问页面中的元素。一方面,我们可以继续相对访问,有时称为 DOM 遍历。出于刚才列出的原因,我们将避免在一般 DOM 访问中使用这种方法。不过,当我们对访问特定元素有了更好的处理时,我们将在后面重新讨论 DOM 遍历。相反,我们将走第二条路,关注现代 DOM 接口提供的各种元素检索功能。

访问 DOM 元素

所有现代的 DOM 实现都包含几种方法,可以很容易地在页面中找到元素。将这些方法与一些定制函数结合使用,可以使 DOM 导航体验更加流畅。首先,让我们看看如何访问单个元素:

  • document.getElementById('everywhere'):这个方法只能在文档对象上运行,它在所有的中查找 ID 等于的所有元素。这是一个非常强大的功能,也是立即访问元素的最快方法。

getElementById方法使用提供的 ID 返回对 HTML 元素的引用,否则返回 null。返回的对象特别是元素类型的一个实例。我们将很快讨论如何处理这个元素。

Image 注意 getElementById与您想象的 HTML 文档一样工作:它浏览所有元素并找到一个具有指定值的属性id的元素。但是,如果您加载一个远程 XML 文档并使用getElementById(或者使用除 JavaScript 之外的任何语言的 DOM 实现),默认情况下它不使用id属性。这是故意的;XML 文档必须明确指定id属性是什么,通常使用 XML 定义或模式。

让我们继续参观元素访问函数。接下来的两个函数提供了对元素集合的访问:

  • getElementsByTagName('li'):这个方法可以在任何元素上运行,查找所有标签名为li的派生元素,并将其作为活动的NodeList返回(这几乎与数组相同)。
  • getElementsByClassName(' 测试 '):类似于getElementsByTag 名字,这个方法可以从元素的任何实例运行。它返回一个匹配元素的 live HTMLCollection

这两个函数允许我们一次访问多个元素。暂且抛开返回类型的区别,返回的集合是 live 。这意味着,如果修改了 DOM,并且这些修改将包含在集合中(或者将从集合中移除元素),那么集合将自动更新这些更改。很厉害!

奇怪的是,这两个函数相似的方法返回两种不同的类型。首先,让我们考虑简单的部分:两种类型都有类似数组的位置访问。也就是说,对于以下内容:

var lis = document.getElementsByTagName('li');

您可以通过lis[1]访问lis集合中的第二个列表项。两个集合都有一个length属性,它告诉您集合中有多少项。它们还有一个item方法,将访问的位置作为参数,并返回该位置的元素。item方法是一种按位置访问元素的函数方式。最后,两个集合都没有更高阶的数组方法,比如pushpopmapfilter

如果你想在你的HTMLCollectionNodeList上使用数组方法,你总是可以使用它们,如清单 5-2 所示。

清单 5-2NodeList s/ HTMLCollection s 上的数组函数

// A simple filtering function
// An Element's nodeName property is always the name of the underlying tag.
function filterForListItems(el) {
    return el.nodeName === 'LI';
}

var testElements = document.getElementsByClassName( 'test' );
console.log( 'There are ' + testElements.length + ' elements in testElements.');

// Generating an array from the elements gathered from testElements
// based on whether they pass the filtering proccess set up by filterForListItems
var liElements = Array.prototype.filter.call(testElements, filterForListItems);
console.log( 'There are ' +  liElements.length + ' elements in liElements.');

返回类型中方法之间的差异是由浏览器中 DOM 实现的不确定性造成的。在将来,两者都应该返回HTMLCollection实例,但是现在还不是时候。因为NodeList s 和HTMLCollection s 的访问模式实际上是相同的,我们不必太关心哪个方法返回哪个类型。

当使用getElementsByClassNamegetElementsByTagName时,值得记住的是它们不仅属于文档实例,也属于元素实例。当从文档中调用时,它们将对整个文档进行搜索。考虑到您的<head>部分将被搜索到<li>标签,或者您将在那里寻找具有类foo的元素。可以想象,这有点低效。想象你正在你的房子里寻找你的钥匙。你可能不会在冰箱里或浴室里寻找,因为它们不太可能是你忘记带钥匙的地方。所以你会在卧室、客厅、入口通道等地方寻找。尽可能将搜索范围限制在适当的包含元素上。看一下清单 5-3 ,它得到与清单 5-2 相同的结果,但是将它的范围限制在一个特定的父元素。

清单 5-3 。限制搜索范围

var ul = document.getElementById( 'items' );
var liElements = ul.getElementsByClassName( 'test' );
console.log( 'There are ' +  liElements.length + ' elements in liElements.');

Image 注意 document.getElementByIdgetElementsByClassNamegetElementsByTagName不同,在元素类型的实例上不可用。在文档或文档类型的实例上只有可用。**

这三种方法在所有现代浏览器中都可用,对于定位特定元素非常有帮助。回到之前我们试图寻找<h1>元素的例子,我们现在可以做以下事情:

document.getElementsByTagName('h1')[0];

这段代码保证能够工作,并且总是返回文档中的第一个<h1>元素。

通过 CSS 选择器寻找元素

作为一名 web 开发人员,您已经知道选择 HTML 元素的另一种方法:CSS 选择器。CSS 选择器是用于将 CSS 样式应用于一组元素的表达式。随着 CSS 标准的每次修订(1、2 和 3,有时也分别称为 CSS 1 级、2 级或 3 级),选择器规范中添加了更多的功能,因此开发人员可以更容易地找到他们需要的确切元素。浏览器有时提供 CSS 2 和 CSS 选择器的完整实现很慢,所以你可能不知道它们提供的一些很酷的新特性。这在现代浏览器中已经基本解决了。如果您对 CSS 中所有很酷的新特性感兴趣,我们建议您浏览 W3C 关于这个主题的页面:

  • CSS 1 选择器:http://www.w3.org/TR/CSS1/#basic-concepts
  • CSS 2.1 选择器:http://www.w3.org/TR/CSS21/selector.html
  • CSS 3 选择器:http://www.w3.org/TR/css3-selectors/

每个 CSS 选择器规范中可用的特性通常是相似的,因为每个后续版本也包含以前版本的所有特性。但是,每个版本都添加了许多新功能。例如,CSS 2.1 包含属性和子选择器,而 CSS 3 提供了额外的语言支持,通过属性类型选择和否定。对于现代浏览器,所有这些都是有效的 CSS 选择器:

  • #main <div> p:该表达式查找 ID 为main的元素、所有<div>元素的后代,然后是所有<p>元素的后代。所有这些都是一个合适的 CSS 1 选择器。
  • div.items > p:该表达式查找所有具有items类的<div>元素,然后定位所有子<p>元素。这是一个有效的 CSS 2 选择器。
  • div:not(.items):这将定位所有没有items类的<div>元素。这是一个有效的 CSS 3 选择器。

有两种方法可以通过 CSS 选择器访问元素:querySelectorquerySelectorAll。给querySelector一个有效的 CSS 选择器,它将返回第一个匹配该选择器的元素的引用。使用querySelectorAll时唯一改变的是你得到一个非活动的匹配元素的NodeList。(该列表不是实时的,因为实时列表会占用大量资源)。与getElementsByTagNamegetElementsByClassName一样,您可以从元素的任何实例中调用querySelectorquerySelectorAll。在可能的情况下,最好以这种方式限制搜索范围,以获得更高的效率和更快的回报。

我们现在有四种方法来访问元素。我们应该使用哪个?首先,对于单元素访问,document.getElementById应该总是最快的。但是对于多元素访问,或者如果您想要的元素没有 ID,可以考虑使用getElementsByTagName,然后是getElementsByClassName,然后是querySelectorAll。但是要记住,这只是考虑了速度。有时候,查询的方便性,或者匹配元素的准确性,甚至对实时集合的需求都比速度更重要。使用最适合您需求的方法。

等待 HTML DOM 加载

使用 HTML DOM 文档的困难之一是,JavaScript 代码可能会在 DOM 完全加载之前执行,这可能会导致代码中出现许多问题。浏览器内部的操作顺序如下所示:

  1. 解析 HTML。
  2. 加载外部样式表。
  3. 脚本在文档中被解析时被执行。
  4. HTML DOM 是完全构造的。
  5. 加载图像和外部内容。
  6. 页面加载完毕。

当然,所有这些很大程度上取决于 HTML 的结构。如果在加载 CSS 的<link>标签之前有一个<script>标签,那么 JavaScript 将在 CSS 加载之前加载。(顺便说一句,不要这么做。效率很低。)在实际构造 HTML DOM 之前,执行头部中的脚本并从外部文件加载。如前所述,这是一个严重的问题,因为在这两个地方执行的所有脚本都不能访问 DOM。这也是我们避免将脚本标签放在<head>部分的部分原因。但是,即使我们遵循最佳实践,在结束的<body>标签之前包含了我们的<script>标签,也有可能 DOM 还没有准备好被我们的 JavaScript 处理。幸运的是,这个问题有很多解决方法。

等待页面加载

到目前为止,最常见的技术是在执行任何 DOM 操作之前简单地等待整个页面加载。只需将一个在页面加载时触发的函数附加到窗口对象的 load 事件上,就可以使用这种技术。我们将在第六章中更详细地讨论这些事件。清单 5-4 展示了一个在页面加载完成后执行 DOM 相关代码的例子。

清单 5-4 。用于将回调附加到窗口load属性的addEventListener函数

// Wait until the page is loaded
// (Uses addEventListener, described in the next chapter)
window.addEventListener('load', function() {
    // Perform HTML DOM operations
    var theSquare = document.getElementById('square');
        theSquare.style.background = 'blue';
});

虽然这个操作可能是最简单的,但它总是最慢的。从加载操作的顺序来看,您会注意到正在加载的页面是最后一步。在所有具有src属性的元素下载完文件之前,load 事件不会触发。这意味着,如果您的页面有大量的图像、视频等,您的用户可能要等很长时间,直到 JavaScript 最终执行。另一方面,这是最向后兼容的解决方案。

等待合适的事件

如果你有更现代的浏览器,你可以查看DOMContentLoaded事件。当文档完全加载并解析后,将触发此事件。在我们的列表中,这大致匹配“HTML DOM 是完全构造的。”但是请记住,在该事件触发时,图像、样式表、视频、音频等可能还没有完全加载。如果需要在加载特定的图像或视频文件后触发代码,请考虑对该特定标记使用 load 事件。如果您需要等到所有具有src属性的元素都下载了它们的文件,请使用窗口加载事件。详情请看清单 5-5 。

清单 5-5 。使用DOMContentLoaded

document.addEventListener('DOMContentLoaded' functionHandler);

Internet Explorer 8 不支持DOMContentLoaded,但您可以查看文档上的就绪状态是否已更改。清单 5-6 展示了如何检测 DOM 是否以跨浏览器兼容的方式加载。

清单 5-6 。跨浏览器DOMContentLoaded

if(document.addEventListener){
      document.addEventListener('DOMContentLoaded', function(){
       document.removeEventListner('DOMContenLoded',arguments.callee);
})else if(document.attachEvent){
       document.attachEvent('onreadystatechange', function(){
      document.detachEvent('onreadystatechange', arguments.callee,); 
}

获取元素的内容

所有 DOM 元素都可以包含以下三种内容之一:文本、更多元素或文本和元素的混合。一般来说,最常见的情况是第一种和最后一种。在本节中,您将看到检索元素内容的常用方法。

获取元素的文本

对于不熟悉 DOM 的人来说,在元素中获取文本可能是最令人困惑的任务。然而,这也是一个在 HTML DOM 文档和 XML DOM 文档中都有效的任务,所以知道如何做将会很好地为您服务。在图 5-3 所示的示例 DOM 结构中,有一个根<p>元素,它包含一个<strong>元素和一个文本块。<strong>元素本身也包含一个文本块。

9781430263913_Fig05-03.jpg

图 5-3 。包含元素和文本的示例 DOM 结构

让我们看看如何获得这些元素的文本。<strong>元素是最容易开始的,因为它只包含一个文本节点,没有其他内容。

需要注意的是,在所有非基于 Mozilla 的浏览器中,有一个名为innerText的属性可以捕捉元素内部的文本。在这方面,它非常方便。不幸的是,因为它在浏览器市场的明显部分不起作用,并且它在 XML DOM 文档中不起作用,所以您仍然需要探索可行的替代方案。

获取一个元素的文本内容的技巧是,你需要记住文本并不直接包含在元素中;它包含在子文本节点中,这看起来有点奇怪。例如,清单 5-7 展示了如何使用 DOM 从元素内部提取文本;假设变量strongElem包含对<strong>元素的引用。

清单 5-7 。获取<strong>元素的文本内容

// Non-Mozilla Browsers:
strongElem.innerText

// All platforms including Non-Mozilla browsers:
strongElem.firstChild.nodeValue

既然您已经知道如何获取单个元素的文本内容,那么您需要看看如何获取<p>元素的组合文本内容。在这样做的时候,你也可以开发一个通用函数来获取任何元素的文本内容,不管它实际包含什么,如清单 5-8 所示。调用text( 元素 )将返回一个字符串,该字符串包含该元素及其包含的所有子元素的组合文本内容。

清单 5-8 。检索元素文本内容的通用函数

function text(e) {
    var t = '' ;
    // If an element was passed, get its children,
    // otherwise assume it's an array
    e = e.childNodes || e;

    // Look through all child nodes
    for ( var j = 0; j < e.length; j++ ) {
        // If it’s not an element, append its text value
        // Otherwise, recurse through all the element's children
        t += e[j].nodeType != 1 ?
            e[j].nodeValue : text(e[j].childNodes);
    }

    // Return the matched text
    return t;
}

使用一个可以用来获取任何元素的文本内容的函数,您可以检索前面示例中使用的<p>元素的文本内容。这样做的代码看起来会像这样:

// Get the text contents of the <p> Element
var pElm = document.getElementsByTagName ('p');
console.log(text( pElem ));

这个函数特别好的一点是,它保证可以在 HTML 和 XML DOM 文档中工作,这意味着您现在有了一种一致的方法来检索任何元素的文本内容。

获取一个元素的 HTML

与获取元素内部的文本不同,获取元素的 HTML 是可以执行的最简单的 DOM 任务之一。多亏了 Internet Explorer 团队开发的一个特性,所有现代浏览器现在都在每个 HTML DOM 元素上包含了一个额外的属性:innerHTML。有了这个属性,你就可以获得一个元素中的所有 HTML 和文本。此外,使用 i nnerHTML属性非常快——通常比递归搜索元素的所有文本内容要快得多。然而,事情并非一帆风顺。由浏览器决定如何实现innerHTML属性,因为没有真正的标准,浏览器可以返回它认为有价值的任何内容。例如,在使用innerHTML属性时,您可能会遇到一些奇怪的错误:

  • 基于 Mozilla 的浏览器不会在innerHTML语句中返回<style>元素。
  • Internet Explorer 8 和更低版本返回的所有元素都是大写的,如果您希望保持一致性,这可能会令人沮丧。
  • innerHTML属性始终只作为 HTML DOM 文档元素的属性;试图在 XML DOM 文档上使用它将导致检索空值。

使用innerHTML属性很简单;访问属性会为您提供一个包含元素的 HTML 内容的字符串。如果元素不包含任何子元素而只包含文本,则返回的字符串将只包含文本。为了了解它是如何工作的,我们将检查图 5-2 中的两个元素:

// Get the innerHTML of the <strong> element
// Should return "Hello"
strongElem.innerHTML
// Get the innerHTML of  the <p> element
// Should return "<strong>Hello</strong> how are you doing?"
pElem.innerHTML

如果您确定您的元素只包含文本,那么这个方法可以非常简单地替代获取元素文本的复杂性。另一方面,能够检索元素的 HTML 内容意味着您可以构建一些利用就地编辑的很酷的动态应用;关于这个主题的更多信息可以在第十章中找到。

使用元素属性

除了检索元素的内容,获取和设置元素的属性值是最常见的操作之一。通常,元素具有的属性列表预加载了从元素本身的 XML 表示中收集的信息,并存储在关联数组中以供以后访问,如以下网页中 HTML 片段的示例所示:

<form name="myForm" action="/test.cgi" method="POST">
    ...
</form>

一旦加载到 DOM 和变量formElem中,HTML 表单元素就会有一个关联数组,从中可以收集名称/值属性对。结果看起来会像这样:

formElem.attributes = {
    name: 'myForm',
    action: '/test.cgi',
    method: 'POST'
};

使用 attributes 数组判断元素的属性是否存在应该是非常简单的,但是有一个问题:出于某种原因,Safari 不支持这个特性。只要 IE8 处于标准模式,Internet Explorer 版本 8 和更高版本都支持它。那么,如何才能发现一个属性是否存在呢?一种可能的方法是使用getAttribute函数(将在下一节介绍)并测试看看返回值是否为空,如清单 5-9 所示。

清单 5-9 。确定元素是否具有某种属性

function hasAttribute( elem, name ) {
    return elem.getAttribute(name) != null;
}

有了这个函数,并且知道了如何使用属性,现在就可以开始检索和设置属性值了。

获取和设置属性值

根据所使用的 DOM 文档类型,有两种方法可以从元素中检索属性数据。如果你想安全并且总是使用通用的 XML DOM 兼容方法,有getAttribute()setAttribute()。它们可以这样使用:

// Get an attribute
document.getElementById('everywhere').getAttribute('id');
// Set an attribute value
document.getElementsByTagName('input')[0].setAttribute('value', 'Your Name');

除了这个标准的getAttribute / setAttribute对,HTML DOM 文档还有一组额外的属性,作为属性的快速获取器/设置器。这些在现代 DOM 实现中普遍可用(但只保证用于 HTML DOM 文档),因此在编写简短代码时使用它们会给你带来很大的优势。以下示例显示了如何使用 DOM 属性来访问和设置 DOM 属性:

// Quickly get an attribute
document.getElementsByTagName('input')[0].value;

// Quickly set an attribute
document.getElementsByTagName('div')[0].id = 'main';

有几个奇怪的例子,你应该知道它们的属性。最常见的是访问类名属性。如果你直接引用一个类的名字,elem.className会让你设置和获取名字。然而,如果你使用的是get / setAttribute方法,你可以称之为getAttribute('class')。为了在所有浏览器中一致地使用类名,你必须使用elem.className来访问className属性,而不是使用更合适的名称getAttribute('class')。这个问题也出现在for属性上,该属性被重命名为htmlFor。此外,一些 CSS 属性也是如此:cssFloatcssText。出现这种特殊的命名约定是因为像classforfloattext这样的单词都是 JavaScript 中的保留字。

为了解决所有这些奇怪的情况并简化获取和设置正确属性的过程,您应该使用一个函数来处理所有这些细节。清单 5-10 显示了一个获取和设置元素属性值的函数。调用带有两个参数的函数,比如attr(element, id),返回该属性的值。调用带有三个参数的函数,比如attr(element, class, test),将设置属性的值并返回它的新值。

清单 5-10 。获取和设置元素属性的值

function attr(elem, name, value) {
    // Make sure that a valid name was provided
    if ( !name || name.constructor != String ) return '' ;

    // Figure out if the name is one of the weird naming cases
    name = { 'for': 'htmlFor', 'className': 'class' }[name] || name;

    // If the user is setting a value, also
    if ( typeof value != 'undefined' ) {
        // Set the quick way first
        elem[name] = value;

        // If we can, use setAttribute
        if ( elem.setAttribute )
            elem.setAttribute(name,value);
    }

    // Return the value of the attribute
    return elem[name] || elem.getAttribute(name) || '';
}

拥有一个标准的方法来访问和更改属性,而不管它们的实现,这是一个强大的工具。清单 5-11 展示了一些例子,说明如何在一些常见的情况下使用attr函数来简化处理属性的过程。

清单 5-11 。使用attr函数设置和检索 DOM 元素的属性值

// Set the class for the first <h1> Element
attr( document.getElementByTagName('h1')[0], 'class', 'header' );

// Set the value for each <input> element
var input = document.getElementByTagName('input');
for ( var i = 0; i < input.length; i++ ) {
    attr( input[i], 'value', ''  );
}

// Add a border to the <input> Element that has a name of 'invalid'
var input = document.getElementByTagName('input');
for ( var i = 0; i < input.length; i++ ) {
    if ( attr( input[i], 'name' ) == 'invalid' ) {
        input[i].style.border = '2px solid red';
    }
}

到目前为止,我们只讨论了获取/设置 DOM 中常用的属性(ID、类、名称等等)。然而,一个非常方便的技巧是设置和获取非传统属性。例如,您可以添加一个新属性(只有通过访问元素的 DOM 版本才能看到),然后在以后再次检索它,所有这些都不需要修改文档的物理属性。例如,假设您想要一个条目的定义列表,并且每当单击一个术语时,都要展开定义。这个设置的 HTML 看起来类似于清单 5-12 。

清单 5-12 。带有定义列表的 HTML 文档,隐藏了定义

<html>
<head>
    <title>Expandable Definition List</title>
    <style>dd { display: none; }</style>
</head>
<body>
    <h1>Expandable Definition List</h1>

    <dl>
        <dt>Cats</dt>
        <dd>A furry, friendly, creature.</dd>
        <dt>Dog</dt>
        <dd>Like to play and run around.</dd>
        <dt>Mice</dt>
        <dd>Cats like to eat them.</dd>
    </dl>
</body>
</html>

我们将在第六章中讨论更多关于事件的细节,但是现在我们将尽量保持我们的事件代码足够简单。接下来是一个快速脚本,允许您单击术语并显示(或隐藏)它们的定义。清单 5-13 显示了构建一个可扩展定义列表所需的代码。

清单 5-13 。允许动态切换到定义

// Wait until the DOM is Ready
document.addEventListener('DOMContentLoaded', addEventClickToTerms);

// Watch for a user click on the term
function addEventClickToTerms(){
     var dt = document.getElementsByTagName('dt');
      for ( var i = 0; i < dt.length; i++ ) {
          dt[i].addEventListener('click', checkIfOpen);
      }
}

// See if the definition is already open or not
//Need two nextSiblings because the first sibling is a text node (the words that were clicked on).
//If it's never been clicked, the style will be blank ''. F it has been, the style will be 'none', so we check for both with an if statement.
function checkIfOpen(e){
    if(e.target.nextSibling.nextSibling.style.display == '' || e.target.nextSibling.nextSibling.style.display == 'none'){
        e.target.nextSibling.nextSibling.style.display = 'block';
    }else{
        e.target.nextSibling.nextSibling.style.display = 'none';
    }
}

既然您已经知道了如何遍历 DOM 以及如何检查和修改属性,那么您需要学习如何创建新的 DOM 元素,在需要的地方插入它们,以及删除不再需要的元素。

修改 DOM

通过了解如何修改 DOM,您可以做任何事情,从动态创建定制的 XML 文档到构建适应用户输入的动态表单;可能性几乎是无限的。修改 DOM 有三个步骤:首先你需要学习如何创建一个新元素,然后你需要学习如何将它插入 DOM,然后你需要学习如何再次移除它。

使用 DOM 创建节点

修改 DOM 的主要方法是createElement函数,它让您能够动态地创建新元素。然而,当您创建这个新元素时,它不会立即插入到 DOM 中(这是刚开始使用 DOM 的人容易混淆的一点)。首先,我们将着重于创建一个 DOM 元素。

createElement方法接受一个参数,即元素的标记名,并返回该元素的虚拟 DOM 表示——不包括属性或样式。如果您正在开发使用 XSLT 生成的 XHTML 页面的应用(或者如果应用是提供准确内容类型的 XHTML 页面),您必须记住您实际上使用的是 XML 文档,并且您的元素需要有正确的 XML 名称空间与之相关联。为了无缝地解决这个问题,您可以使用一个简单的函数来悄悄测试您正在使用的 HTML DOM 文档是否能够创建带有名称空间的新元素(XHTML DOM 文档的一个特性)。如果是这种情况,您必须用正确的 XHTML 名称空间创建一个新的 DOM 元素,如清单 5-14 所示。

清单 5-14 。创建新 DOM 元素的通用函数

function create( elem ) {
    return document.createElementNS ?
        document.createElementNS('http://www.w3.org/1999/xhtml', elem ) :
        document.createElement( elem );
}

例如,使用前面的函数,您可以创建一个简单的<div>元素,并向其附加一些附加信息:

var div = create('div');
div.className = 'items';
div.id = 'all';

此外,应该注意的是,有一个用于创建新文本节点的 DOM 方法,称为createTextNode。它接受一个参数,即您希望包含在节点中的文本,并返回创建的文本节点。

使用新创建的 DOM 元素和文本节点,现在可以将它们插入到 DOM 文档中需要它们的地方。

插入到 DOM 中

向 DOM 中插入内容令人困惑,有时感觉很笨拙,即使对于那些熟悉 DOM 的人来说也是如此。你的武器库中有两种功能可以用来完成工作。

第一个函数insertBefore ,允许您在另一个子元素之前插入一个元素。当您使用该函数时,它看起来像这样:

parentOfBeforeNode.insertBefore( nodeToInsert, beforeNode );

我们用来记住参数顺序的记忆方法是短语“你在第二个元素之前插入第一个元素。”

现在您有了一个在其他节点之前插入节点(包括元素和文本节点)的函数,您应该问自己:“如何将一个节点作为父节点的最后一个子节点插入?”你还可以使用另一个功能,叫做appendChild ,它可以让你做到这一点。在元素上调用appendChild,将指定的节点追加到子节点列表的末尾。使用函数看起来像这样:

parentElem.appendChild( nodeToInsert );

清单 5-15 是一个如何在应用中同时使用insertBeforeappendChild的例子。

清单 5-15 。在一个元素之前插入另一个元素的函数

document.addEventListener(DOMContentLoaded, 'addElement');

function addElement(){
 //Grab the ordered list that is in the document
 //Remember that getElementById returns an array like object

   var orderedList = document.getElementById('myList');

 //Create an <li>, add a text node then append it to <li>
 var li = document.createElement('li');
     li.appendChild(document.createTextNode('Thanks for visiting'));

 //element [0] is how we access what is inside the orderedList
  orderedList.insertBefore(li, orderedList[0]);
}

当您将这些信息“插入”DOM(使用insertBeforeappendChild)时,用户会立即看到这些信息。因此,您可以使用它来提供即时反馈。这在需要用户输入的交互式应用中尤其有用。

既然您已经看到了如何只使用基于 DOM 的方法来创建和插入节点,那么看看将内容注入 DOM 的其他方法应该会特别有用。

将 HTML 注入 DOM

一种比创建普通 DOM 元素并将其插入 DOM 更流行的技术是将 HTML 直接注入文档。实现这一点的最简单方法是使用前面讨论的 i nnerHTML方法。除了检索元素内部的 HTML 之外,它还是在元素内部设置 HTML 的一种方式。作为一个简单的例子,让我们假设你有一个空的<ol>元素,你想给它添加一些<li>;这样做的代码如下所示:

// Add some LIs to an OL element
document.getElementsByTagName('ol')[0].innerHTML = "<li>Cats.</li><li>Dogs.</li><li>Mice.</li>";

这难道不比痴迷于创建大量 DOM 元素及其关联的文本节点简单得多吗?你会很高兴地知道(根据http://www.quirksmode.org)这也比使用 DOM 方法要快得多。然而,这并不完美——使用innerHTML注射方法存在许多棘手的问题:

  • 如前所述,innerHTML方法不存在于 XML DOM 文档中,这意味着您必须继续使用传统的 DOM 创建方法。
  • 使用客户端 XSLT 创建的 XHTML 文档没有innerHTML方法,因为它们也是纯 XML 文档。
  • i nnerHTML完全删除元素中已经存在的任何节点,这意味着没有办法方便地添加或插入之前的节点,就像我们使用纯 DOM 方法一样。

最后一点特别麻烦,因为在另一个元素之前插入或追加到子列表的末尾是一个特别有用的特性。让我们看看如何在清单 5-16 中使用我们之前使用的相同方法来完成。

清单 5-16 。向现有有序列表中添加新的 DOM 节点

document.addEventListener('DOMContentLoaded', activateButtons);

function activateButtons(){
    //ad event listeners to buttons
    var appendBtn = document.querySelector('#appendButon');
        appendBtn.addEventListener('click', appendNode);

    var addBtn = document.querySelector('#addButton');
        addBtn.addEventListener('click', addNode);
}

function appendNode(e){

    //get the <li>s that exist and make a new one.
    var listItems = document.getElementsByTagName('li');
    var newListItem = document.createElement('li');
        //append a new text node
        newListItem.appendChild(document.createTextNode('Mouse trap.'));

        //append to existing list as the new 4th item
        listItems[2].appendChild(newListItem);
}

function addNode(e){

    //get the whole list
     var orderedList = document.getElementById('myList');

     //get all the <li>s
    var listItems = document.getElementsByTagName('li');
    //make a new <li> and attach text node
    var newListItem = document.createElement('li');
        newListItem.appendChild(document.createTextNode('Zebra.'));
        //add to list, pushing the 2nd one down to 3rd
        orderedList.insertBefore(newListItem,listItems[1]);
}

通过这个例子,您可以看到对现有文档进行修改并不困难。但是,如果您想从另一个方向移动并从 DOM 中删除节点呢?和往常一样,还有另一种方法来处理这个问题。

从 DOM 中删除节点

从 DOM 中删除节点几乎与创建和插入节点一样频繁。例如,当您创建一个要求无限数量条目的动态表单时,允许用户能够删除他们不想再处理的页面部分就变得很重要。删除节点的能力被封装在一个函数中:removeChild。跟appendChild用的一样,但是效果相反。这个函数看起来像这样:

NodeParent.removeChild( NodeToRemove );

考虑到这一点,您可以创建两个独立的函数来快速删除节点。第一个删除单个节点,如清单 5-17 所示。

清单 5-17 。从 DOM 中删除一个节点的函数

// Remove a single Node from the DOM
function remove( elem ) {
    if ( elem ) elem.parentNode.removeChild( elem );
}

清单 5-18 展示了一个从一个元素中移除所有子节点的函数,只使用了一个对 DOM 元素的引用。

清单 5-18 。从一个元素中移除所有子节点的函数

// Remove all of an Element’s children from the DOM
function empty( elem ) {
    while ( elem.firstChild )
        remove( elem.firstChild );
}

作为一个例子,假设您想要删除您在上一节中添加的一个<li>,假设您已经给了用户足够的时间来查看<li>,并且它可以被删除。清单 5-19 显示了 JavaScript 代码,您可以使用它来执行这样的操作,从而创建一个理想的结果。

清单 5-19 。从 DOM 中移除单个元素或所有元素

// Remove the last <li> from an <ol>
var listItems = document.getElementsByTagName('li');
remove(listItems[2]);

// The preceding will convert this:
<ol>
    <li>Learn Javascript.</li>
    <li>???</li>
    <li>Profit!</li>
</ol>
// Into this:
<ol>

    <li>Learn Javascript.</li>
    <li>???</li>
</ol>

// If we were to run the empty() function instead of remove()
var orderedList = document.getElementById('myList');
empty(orderedList);
// It would simply empty out our <ol>, leaving:
<ol></ol>

处理 DOM 中的空白字符

让我们回到 HTML 文档的例子。以前,您试图定位单个的<h1>元素,但是由于额外的文本节点而遇到了困难。对于单个元素来说,这可能是可以接受的,但是如果您想在<h1>元素之后找到下一个元素呢?你仍然会碰到臭名昭著的空白错误,你需要做.nextSibling.nextSibling来跳过<h1 >和<p>元素之间的结束线。然而,并非一切都没了。有一种技术可以作为空白错误的解决方法,如清单 5-20 所示。这种特殊的技术从 DOM 文档中删除了所有只有空白的文本节点,使得遍历更加容易。这样做不会对 HTML 的呈现方式产生明显的影响,但是会让你更容易手动导航。应该注意,这个函数的结果不是永久的,每次加载 HTML 文档时都需要重新运行。

清单 5-20 。XML 文档中空白错误的解决方法

function cleanWhitespace( element ) {
    // If no element is provided, do the whole HTML document
    element = element || document;
    // Use the first child as a starting point
    var cur = element.firstChild;
    // Go until there are no more child nodes
    while ( cur != null ) {
        // If the node is a text node, and it contains nothing but whitespace
        if ( cur.nodeType == 3 && ! /\S/.test(cur.nodeValue) ) {
            // Remove the text node
            element.removeChild( cur );
        // Otherwise, if it’s an element
        } else if ( cur.nodeType == 1 ) {
             // Recurse down through the document
             cleanWhitespace( cur );
        }
        cur = cur.nextSibling; // Move through the child nodes
    }
}

假设您想在示例文档中使用这个函数来查找第一个<h1>元素之后的元素。这样做的代码看起来会像这样:

cleanWhitespace();
// Find the H1 Element
document.documentElement
    .firstChild         // Find the Head Element
    .nextSibling        // Find the <body> Element
    .firstChild         // Get the H1 Element
    .nextSibling        // Get the adjacent Paragraph

这种技术既有优点也有缺点。最大的好处是,当您试图导航 DOM 文档时,可以保持一定程度的理智。然而,考虑到您必须遍历每一个 DOM 元素和文本节点来寻找只包含空白的文本节点,这种技术特别慢。如果你有一个包含大量内容的文档,它会大大降低网站的加载速度。此外,每次向文档中注入新的 HTML 时,都需要重新扫描 DOM 的这一部分,确保没有添加额外的填充空格的文本节点。

这个函数的一个重要方面是节点类型的使用。可以通过检查节点的nodeType属性的特定值来确定节点的类型。在这一章的开始我们有一个类型列表。因此,您可以看到许多可能的值,但您最常遇到的三个值如下:

  • 元素(nodeType = 1):匹配 XML 文件中的大多数元素。例如,<li><a><p><body>元素的nodeType都是 1。
  • 文本(nodeType = 3):匹配文档中的所有文本段。当使用previousSiblingnextSibling在 DOM 结构中导航时,您会经常遇到元素内部和元素之间的文本片段。
  • Document ( nodeType = 9):匹配文档的根元素。例如,在 HTML 文档中,它是<html>元素。

此外,您可以使用常量来引用不同的 DOM 节点类型(在 IE 版和更高版本中)。例如,你可以只使用document.ELEMENT_NODEdocument.TEXT_NODEdocument.DOCUMENT_NODE,而不必记住 1、3 或 9。因为不断清理 DOM 的空白可能会很麻烦,所以您应该探索其他导航 DOM 结构的方法。

简单的 DOM 导航

使用纯 DOM 导航的原则(在每个可导航的方向上都有指针),您可以开发更适合您导航 HTML DOM 文档的功能。这个特殊的原则反映了这样一个事实,即大多数 web 开发人员只需要在 DOM 元素中导航,很少在兄弟文本节点中导航。为了帮助你,有许多有用的函数可以用来代替标准的previousSiblingnextSiblingfirstChildlastChildparentNode。清单 5-21 显示了一个函数,它返回当前元素之前的元素,如果没有找到之前的元素,则返回 null,类似于previousSibling元素属性。

清单 5-21 。查找与元素相关的前一个兄弟元素的函数

function prev( elem ) {
    do {
        elem = elem.previousSibling;
    } while ( elem && elem.nodeType != 1 );
    return elem;
}

清单 5-22 显示了一个返回当前元素下一个元素的函数,或者如果没有找到下一个元素,返回 null,类似于nextSibling元素属性。

清单 5-22 。查找与某个元素相关的下一个兄弟元素的函数

function next( elem ) {
    do {
        elem = elem.nextSibling;
    } while ( elem && elem.nodeType != 1 );
    return elem;
}

清单 5-23 显示了一个返回元素的第一个元素子元素的函数,类似于firstChild元素属性。

清单 5-23 。查找元素的第一个子元素的函数

function first( elem ) {
    elem = elem.firstChild;
    return elem && elem.nodeType != 1 ?
        next ( elem ) : elem;
}

清单 5-24 显示了一个返回一个元素的最后一个子元素的函数,类似于lastChild元素属性。

清单 5-24 。查找元素的最后一个子元素的函数

function last( elem ) {
    elem = elem.lastChild;
    return elem && elem.nodeType != 1 ?
        prev ( elem ) : elem;
}

清单 5-25 显示了一个返回元素父元素的函数,类似于parentNode元素属性。您可以选择提供一个数字来一次向上导航多个父代—例如,parent(elem,2)相当于parent(parent(elem))

清单 5-25 。查找元素父元素的函数

function parent( elem, num ) {
    num = num || 1;
    for ( var i = 0; i < num; i++ )
        if ( elem != null ) elem = elem.parentNode;
    return elem;
}

使用这些新函数,您可以快速浏览 DOM 文档,而不必担心每个元素之间的文本。例如,如前所述,要查找<h1>元素旁边的元素,您现在可以执行以下操作:

// Find the Element next to the <h1> Element
next( first( document.body ) )

您应该注意这段代码中的两件事。第一,有了新的参照:document.body。所有现代浏览器都在 HTML DOM 文档的 body 参数中提供了对<body>元素的引用。你可以用它来使你的代码更短,更容易理解。您可能注意到的另一件事是,函数的编写方式非常违反直觉。通常,当您想到导航时,您可能会说,“从<body>元素开始,获取第一个元素,然后获取下一个元素”,但是就其物理书写方式而言,这似乎有些落后。

摘要

在这一章中,我们讨论了很多关于 DOM 是什么以及它是如何构造的。我们还讨论了节点之间的关系、节点类型以及如何使用 JavaScript 访问元素。当我们可以访问这些元素时,我们可以通过使用element.get/setAttribute()来改变它们的属性。我们还讨论了在 DOM 中创建和添加新节点、处理空白以及在 DOM 中导航。在下一章,我们将讨论 JavaScript 事件。

六、事件

这是最好的时代;那是最糟糕的时代。那是网景的时代;那是互联网浏览器的时代。我们面前的新事件处理者使它成为希望的春天。浏览器以不同方式实现事件处理的事实让我们陷入了绝望的冬天。但是最近几年,阳光变得清晰明亮,事件处理 API 已经跨浏览器实现了标准化(至少是 API 的大部分方面)。编写可用的 JavaScript 代码的最终目标一直是拥有一个能为用户工作的网页,不管他们使用什么浏览器或在什么平台上。长期以来,这意味着编写管理两种不同事件处理模型的事件处理代码。但是随着现代浏览器的出现,我们开发人员再也不用担心这个问题了。

多年来,JavaScript 中的事件概念已经发展到了我们现在所处的可靠、可用的平台。一旦 Internet Explorer 在版本 8 中实现了事件处理的 W3C 模型,我们就可以停止编写用于管理浏览器之间差异的库,而是专注于用事件做有趣和令人惊奇的事情。最终,这将我们引向 JavaScript 强大的模型-视图-控制器(MVC)模型,我们将在后面的章节中讨论。

在这一章中,我们将首先介绍 JavaScript 中的事件是如何工作的。根据这个理论的实际应用,我们将看看如何将事件绑定到元素。然后,我们将研究事件模型提供的信息,以及如何最好地控制它。当然,我们还需要涵盖我们可用的事件类型。我们以事件委托和一些关于事件和最佳实践的建议结束。

JavaScript 事件简介

如果你观察任何 JavaScript 代码的核心,你会发现事件是把所有东西粘在一起的粘合剂。无论是使用完整的基于 MVC 的单页面应用,还是简单地使用 JavaScript 为一两个页面添加一些功能,事件处理程序都是用户与我们的代码进行通信的方式。我们的数据将被绑定在 JavaScript 中,可能作为对象文字。我们将在 DOM 中表示这些数据,将其用作我们的视图。事件由 DOM 引发,由 JavaScript 代码处理,捕捉用户交互并指导应用的流程。结合使用 DOM 和 JavaScript 事件是使所有现代 web 应用成为可能的基本结合。

堆栈、队列和事件循环

在包括 JavaScript 在内的许多编程语言中,都有描述控制流、内存元素和下一步计划的隐喻。我们运行的代码,无论是从全局上下文,直接作为一个函数,还是作为一个从(或内部)调用的函数!)另一个功能,被称为。如果你正在运行一个函数foo,它调用一个函数bar,那么堆栈是三个深(全局,foo,然后bar)。这段代码运行后会发生什么?这是队列的职责,它管理当前堆栈解析后运行的下一组代码。每当堆栈清空时,它会进入队列并获取一段新的代码来运行。这些是我们理解事件的关键因素。不过,还有第三个元素:堆。这是变量、函数和其他命名对象存在的地方。当 JavaScript 需要访问一个对象、一个函数或一个变量时,它会进入堆来访问信息。对我们来说,堆不太重要,因为它在事件处理中的作用不如堆栈和队列大。

堆栈和队列是如何影响事件处理的?要回答这个问题,我们需要引入事件循环。这是浏览器中两个线程之间的协作:事件跟踪线程和 JavaScript 线程。

Image 注意记住,除了 web 工作者,JavaScript 是单线程的。

这些线程协同工作来捕获用户事件,然后根据我们注册了事件处理程序的事件对它们进行排序。这个过程统称为事件循环。每次运行时,都会检查用户事件,看是否有针对它们注册的事件处理程序。如果不是,那么什么都不会发生。如果有事件处理程序,循环会将它们推到 JavaScript 队列的顶部,以便在 JavaScript 最方便的时候执行处理程序。

困难就在这里。队列管理“最早方便”的概念通常,这意味着在当前堆栈被解析之后。这可能给事件处理一种异步的感觉,特别是如果堆栈有许多帧深或者包含长时间运行的代码。事件被允许跳到队列的头部,但是它们不能中断堆栈。大多数情况下,这种区别对于开发人员来说并不重要,因为从事件触发、堆栈帧解析到事件处理代码运行之间的持续时间可能是人类无法察觉的。尽管如此,对我们来说重要的是理解事件循环只将事件跳到行的前面;它不会将当前运行的代码推开。

我们现在了解了浏览器、队列和堆栈是如何一起决定事件处理程序何时运行的。很快,我们将研究将事件绑定到事件处理程序的机制。但是有一个架构问题我们需要首先解决。考虑一下:如果你点击了 HTML 文档主体中某个段落的无序列表中的列表项的链接,那么这些元素中的哪一个会处理这个事件呢?一个以上的元素可以处理这个事件吗?如果是,哪个元素先获得事件?要回答这个问题,我们需要看看事件阶段。

事件阶段

JavaScript 事件分两个阶段执行,称为捕获冒泡 。这意味着当一个事件被一个元素触发时(例如,用户点击一个链接,导致click事件被触发),被允许处理它的元素以及处理的顺序是不同的。你可以在图 6-1 中看到执行顺序的例子。它显示了每当用户单击页面上的第一个<a>元素时,触发了哪些事件处理程序以及触发的顺序。

9781430263913_Fig06-01.jpg

图 6-1 。事件处理的两个阶段

看看这个简单的例子,有人点击了一个链接,你可以看到一个事件的执行顺序。假设用户点击了<a>元素;首先触发文档的 click 处理程序,然后是<body>的处理程序,然后是<div>的处理程序,依此类推,直到<a>元素,这个循环称为捕获阶段。一旦完成,它再次沿着树向上移动,并且<li><ul><div><body>和文档事件处理程序都被依次触发。

事件处理以这种方式构建有非常具体的历史原因。当 Netscape 引入事件处理时,它决定应该使用事件捕获。当 Internet Explorer 赶上它自己的事件处理版本时,它也随之出现了事件冒泡。那是浏览器战争的时代,像这样截然相反的架构选择是司空见惯的。多年来,它们阻碍了 JavaScript 的开发,因为程序员不得不浪费时间来维护规范事件处理的库(以及一些 DOM、Ajax 和其他一些东西!).

好消息是我们现在生活在未来。现代浏览器允许用户选择在哪个阶段捕捉事件。事实上,如果您愿意,可以在两个阶段都分配事件处理程序。这是一个勇敢的新世界。

不管在哪个阶段绑定事件,有两件事应该是显而易见的。首先,我们讨论了在列表项中点击锚标记的想法。这难道不应该把你送到链接的href属性指向的任何地方吗?也许有某种方法可以克服这种行为。此外,考虑事件阶段的一般前提:无论是捕获还是冒泡,事件都是通过 DOM 层次结构进行通信的。如果我们不希望该事件被传播呢?我们能防止一个事件被向上(或向下)传递吗?

但是我们太超前了。我们甚至还没有讨论如何绑定事件监听器!让我们现在就解决这个问题。

绑定事件侦听器

将事件处理程序绑定到元素的最佳方式一直是 JavaScript 中不断发展的探索。它始于浏览器强迫用户在 HTML 文档中内联编写事件处理程序代码。第一次努力被认为是草案或阿尔法代码是有原因的!后来,当我们想要遵循既定的最佳实践时,比如将逻辑与表示分离,使用内联事件处理程序是次优的。好吧,这是个严重的问题。试着想象管理一个代码库,其中一半的关键路径依赖于嵌入在表示层中的代码。这不是专业 JavaScript 程序员想做的事情!幸运的是,随着浏览器 API 和最佳实践标准的发展,这种技术已经被淘汰了。

当 Netscape 和 Internet Explorer 相互竞争时,它们各自开发了独立但非常相似的事件注册模型。最终,网景的模型被修改成了 W3C 标准,而 Internet Explorer 的模型保持不变。直到 Internet Explorer 9,也就是微软最终屈服并实现了通常所说的 W3C 事件处理。事实上,它走得更远,不再支持旧的事件处理 API。这对开发人员来说是一个福音,因为现在我们不再需要编写和维护库来处理浏览器之间的争论。

今天,有两种可靠地登记事件的方法。传统的方法是附加事件处理程序的旧的内联方式的分支,但是它是可靠的并且一致地工作,甚至在旧的浏览器上。另一种方法是使用 W3C 标准来注册事件。当然,我们会两者都看,因为你很可能两者都会遇到。

传统绑定

绑定事件的传统方式是最简单的绑定事件处理程序的方式。这利用了事件处理程序是 DOM 元素的属性这一事实。要使用这个方法,您需要将一个函数作为属性附加到您希望观察的 DOM 元素上。用document.getElementById检索一个元素(或者我们在第五章的中讨论过的任何其他元素检索函数)。让我们假设您想要观察click事件。只需为检索到的元素的onclick属性分配一个函数。搞定了。

对于本章中的例子,我们将使用一个带有许多可定位元素的标准 HTML 页面。页面的内容显示在清单 6-1 的中。

清单 6-1 。用于事件处理的示例 HTML 代码

<!DOCTYPE html>
<html>
<head lang="en">
  <meta charset="UTF-8">
  <title>Event Handling</title>
  <link rel="stylesheet" href="school.css"/>
</head>
<body>

<div id="main">
  <nav id="navbar">
    <ul>
      <li>Students
        <ul>
          <li id="Academics">Academics</li>
          <li id="Athletics">Athletics</li>
          <li id="Extracurriculars">Extracurriculars</li>
        </ul>
      </li>
      <li>Faculty
        <ul>
          <li id="Frank Walsh">Frank Walsh</li>
          <li id="Diane Walsh">Diane Walsh</li>
          <li id="John Mullin">John Mullin</li>
          <li id="Lou Garaventa">Lou Garaventa</li>
          <li id="Dan Tully">Dan Tully</li>
         <li id="Emily Su">Emily Su</li>
        </ul>
      </li>
    </ul>
  </nav>
  <div id="welcome">
    <h1>Welcome to the School of JavaScript</h1>
    <h3 id="welcome-header">Click here for a welcome message!</h3>
    <p id="welcome-content">Welcome to the School of JavaScript. Here, you will find many
<a href="/examples" id="examples-link">examples</a> of JavaScript,
      taught by our most esteemed <a href="/faculty">faculty</a>.
<span id="disclaimer">Please note that these are only examples, and are not
      necessarily <a href="/production-ready">production-ready code</a>.</span></p>
  </div>
  <hr/>
  <div id="form-container">
    <h2>Contact Form</h2>

    <p>Thank you for your interest in the School of JavaScript. Please fill out the form
below so we can send you evenmore materials!</p>

    <form id="main-form">
      <ul>
        <li><label for="firstName">First Name: </label><input id="firstName" type="text"/></li>
        <li><label for="lastName">Last Name: </label><input id="lastName" type="text"/></li>
        <li><label for="city">City: </label><input id="city" type="text"/></li>
        <li><label for="state">State: </label><input id="state" type="text"/></li>
        <li><label for="postCode">Postal Code: </label><input id="postCode" type="text"/></li>
        <li><label for="comments">Comments: </label><br/>
          <textarea name="" id="comments" cols="30" rows="10"></textarea>
        </li>
        <li><input type="submit"/> <input type="reset"/></li>
      </ul>
    </form>
  </div>
</div>

</body>
</html>

正如你所看到的,首页上有很多我们想象中的 JavaScript 学校的元素。navbar 最终会有适当的事件处理来作为一个菜单,我们将为简单的验证添加事件处理(更复杂的验证将在第八章中介绍),我们还计划在欢迎消息中增加一些交互性。

现在,让我们做一些简单的事情。当我们点击进入firstName字段时,让我们在控制台上记录下来,然后为我们的元素设置一个黄色背景。很明显,我们计划很快做更多的事情,但是先从小事做起!让我们将这个事件与传统的事件处理结合起来(清单 6-2 )。

清单 6-2 。以传统方式绑定点击事件

// Retrieve the firstName element
var firstName = document.getElementById('firstName');

// Attach the event handler
firstName.onclick = function() {
    console.log('You clicked in the first name field!');
    firstName.style.background = 'yellow';
};

太棒了。有效。但它缺少某种东西。那就是灵活性。在这种情况下,我们必须为每个表单字段编写单独的事件处理函数。繁琐!有没有一种方法可以获得触发事件的元素的引用?

其实有两个办法!第一,也是最直接的,是在你的事件处理函数中提供一个参数,如清单 6-3 所示。这个参数是 event 对象,它包含关于刚刚触发的事件的信息。我们将很快更详细地查看事件对象。现在,我们知道事件对象的target属性指的是最初发出事件的 DOM 元素。

清单 6-3 。带参数的事件绑定

// Retrieve the firstName element
var firstName = document.getElementById('firstName');

// Attach the event handler
firstName.onclick = function(e) {
    console.log('You clicked in the ' + e.target.id + ' field!');
    e.target.style.background = 'yellow';
};

由于e.target指向firstName字段,并且实际上是对firstName字段的 DOM 元素的引用,我们可以检查它的id属性来查看我们点击了哪个字段。更重要的是,我们还可以改变它的样式属性!这意味着我们可以扩展这个事件处理程序来处理表单中的任何文本字段。

除了显式使用event对象,还有一种替代方法。我们也可以在函数中使用关键字this,如清单 6-4 中的所示。在事件处理函数的上下文中,this指的是事件的发出者。换句话说,event.targetthis是同义词,或者,至少,它们指向同一个事物。

清单 6-4 。使用this关键字的事件绑定

// Retrieve the firstName element
var firstName = document.getElementById('firstName');

// Attach the event handler
firstName.onclick = function() {
    console.log('You clicked in the ' + this.id + ' field!');
    this.style.background = 'yellow';
};

你应该使用哪一个?event对象为您提供了所需的所有信息,而this对象有所限制,因为它只指向发出事件的 DOM 元素。使用其中一个而不是另一个是没有成本的,所以,一般来说,我们推荐使用event对象,因为您总是可以立即获得事件的所有细节。然而,有些情况下this对象仍然有用。目标总是指向发出事件的最近的元素。看看清单 6-1 中 ID 为welcome<div>。假设我们添加了一个mouseover事件处理程序,用于在鼠标悬停在元素上时更改背景颜色,并添加了一个mouseout事件处理程序,用于在鼠标离开<div>时将背景颜色改回来。如果您在e.target上进行样式更改,事件将为每个子元素(welcome-headerwelcome-content等等)触发!另一方面,如果您在this上进行样式更改,则更改仅在欢迎<div>上进行。当我们讨论事件委托时,我们将更详细地讨论这种差异。

传统装订的优势

传统装订有以下优点:

  • 使用传统方法的最大优势是它非常简单和一致,因为你可以保证无论你在什么浏览器中使用它都是一样的。
  • 当处理一个事件时,this关键字引用当前元素,这可能是有用的(如清单 6-4 所示)。

传统装订的缺点

然而,它也有一些缺点:

  • 传统方法不允许控制事件捕获或冒泡。所有的事件都会冒泡,并且不可能改变为事件捕获。

  • It’s only possible to bind one event handler to an element at a time. This has the potential to cause confusing results when working with the popular window.onload property (effectively overwriting other pieces of code that have used the same method of binding events). An example of this problem is shown in Listing 6-5, where an event handler overwrites an earlier event handler.

    清单 6-5 。事件处理程序相互覆盖

    // Bind your initial load handler
    window.onload = myFirstHandler;
    
    // somewhere, in another library that you’ve included,
    // your first handler is overwritten
    // only 'mySecondHandler' is called when the page finishes loading
    window.onload = mySecondHandler;
    
    
  • 在 Internet Explorer 8 和更早版本中,event对象参数不可用。相反,你必须使用window.event

知道有可能盲目地覆盖其他事件,您可能应该选择仅在简单的情况下使用传统的事件绑定方法,在这种情况下,您可以信任与您的代码一起运行的所有其他代码。然而,避免这种麻烦的一种方法是使用现代浏览器实现的 W3C 事件绑定方法。

DOM 绑定:W3C

W3C 将事件处理程序绑定到 DOM 元素的方法是唯一真正标准化的方法。考虑到这一点,所有现代浏览器都支持这种附加事件的方式。Internet Explorer 8 和更老的版本没有,但旧版本的 Internet Explorer 几乎不是现代浏览器。如果你必须为这些设计,考虑使用传统的绑定。

附加新处理函数的代码很简单。它作为一个函数存在于每个 DOM 元素中。该函数名为addEventListener,带三个参数:事件的名称(如click;注意缺少前缀on、处理事件的函数以及启用或禁用事件捕获的布尔标志。清单 6-6 中的显示了一个使用中的addEventListener示例。

清单 6-6 。示例代码使用 W3C 方式绑定事件处理程序

// Retrieve the firstName element
var firstName = document.getElementById( 'firstName' );

// Attach the event handler
firstName.addEventListener( 'click', function ( e ) {
  console.log( 'You clicked in the ' + e.target.id + ' field!' );
  e.target.style.background = 'yellow';
} );

注意,在这个例子中,我们没有向addEventListener传递第三个参数。在这种情况下,第三个参数默认为 false,这意味着将使用事件冒泡。如果我们想使用事件捕获,我们可以显式地传递一个真值。

W3C 绑定的优势

W3C 事件绑定方法的优势如下:

  • 此方法支持事件处理的捕获和冒泡阶段。通过将addEventListener的最后一个参数设置为 false(默认值,用于冒泡)或 true(用于捕捉)来切换事件阶段。
  • 在事件处理函数内部,this关键字引用当前元素,就像在传统事件处理中一样。
  • event对象在处理函数的第一个参数中总是可用的。
  • 您可以将任意数量的事件绑定到一个元素,而不会覆盖先前绑定的处理程序。JavaScript 在内部堆叠处理程序,并按照它们注册的顺序运行。

W3C 绑定的缺点

W3C 事件绑定方法只有一个缺点:

  • 它在 Internet Explorer 8 和更低版本中不工作。IE 使用类似语法的attachEvent

取消绑定事件

既然我们已经绑定了事件,那么如果我们想解除事件的绑定呢?也许我们绑定了一个click事件处理程序的按钮现在被禁用了。或者我们不再需要在悬停时高亮显示该 div。断开事件及其处理程序的连接相对简单。

对于传统的事件处理,只需为事件处理程序分配一个空字符串或 null,如下所示:

document.getElementById('welcome-content').onclick = null;

不太难吧?

W3C 事件处理的情况稍微复杂一些。相关功能是removeEventListener。它的三个参数是相同的:要移除的事件的类型、关联的处理程序以及捕获或冒泡模式的 true/false 值。不过,有一个问题。首先也是最重要的,这个函数必须是对同一个被分配了addEventListener 的函数的引用。不只是相同的代码行,而是相同的引用。所以如果你用addEventListener指定了一个匿名的内嵌函数,你就不能删除它。

Image 提示如果你认为你以后可能需要移除事件处理程序,你应该总是为它使用一个命名函数。

同样,如果您在最初调用addEventListener时设置了第三个参数,那么您必须在removeEventListener中再次设置它。如果你不考虑这个参数,或者给它一个错误的值,removeEventListener就会无声无息地失败。清单 6-7 有一个解除绑定事件处理程序的例子。

清单 6-7 。解除事件处理程序的绑定

// Assume we have two buttons 'foo' and 'bar'
var foo = document.getElementById( 'foo' );
var bar = document.getElementById( 'bar' );

// When we click on foo, we want to log to the console "Clicked on foo!"
function fooHandler() {
  console.log( 'Clicked on the foo button!' );
}

foo.addEventListener( 'click', fooHandler );

// When we click on bar, we want to _remove_ the event handler for foo.
function barHandler() {
  console.log( 'Removing event handler for foo....' );
  foo.removeEventListener( 'click', fooHandler );
}

bar.addEventListener( 'click', barHandler );

常见事件特征

JavaScript 事件有许多相对一致的特性,在开发时给你更多的能力和控制。最简单也是最古老的概念是event对象,它为您提供了一组元数据和上下文函数,因此您可以处理诸如鼠标事件和按键之类的事情。此外,还有一些函数可用于修改事件的正常捕获/冒泡流程。从里到外了解这些特性可以让你的生活简单很多。

事件对象

事件处理程序的一个标准特性是以某种方式访问event对象,该对象包含有关当前 eventEvent 对象的上下文信息。这个对象对于某些事件来说是一个非常有价值的资源。例如,在处理键按下时,您可以访问对象的keyCode属性以获取被按下的特定键。event对象之间存在一些微妙的差异,但我们将在本章后面讨论这些。现在,让我们解决两个悬而未决的问题:事件传播和默认行为。

取消事件冒泡

您知道事件捕获/冒泡是如何工作的,所以让我们来探索如何控制它。前一个例子中提到的重要一点是,如果您希望一个事件只发生在它的目标上,而不发生在它的父元素上,您没有办法阻止它。停止事件气泡的流动将导致类似于图 6-2 中所示的情况,其中事件的结果被第一个<a>元素捕获,随后的气泡被取消。

9781430263913_Fig06-02.jpg

图 6-2 。第一个<a>元素捕获事件的结果

停止事件的冒泡(或捕获)在复杂的应用中非常有用。而且实现起来很简单。调用event对象的stopPropagation 方法,防止事件在层次结构中进一步向上(或向下)遍历。清单 6-8 显示了一个例子。

清单 6-8 。停止事件冒泡的示例

document.getElementById( 'disclaimer' ).addEventListener( 'click', function ( e ) {

  // When clicking on the disclaimer, highlight it by making it bold
  e.target.style.fontWeight = 'bold';

  // The parent element wants to hide itself if this element is clicked on. We need to prevent that behavior
  e.stopPropagation();
} );

document.getElementById( 'welcome-content' ).addEventListener( 'click', function ( e ) {
  e.target.style.visibility = 'hidden';
} );

清单 6-9 显示了一个简短的代码片段,它在用户悬停的元素周围添加了一个红色边框。您可以通过向每个 DOM 元素添加一个mouseover和一个mouseout事件处理程序来实现这一点。如果您不停止事件冒泡,每次鼠标移动到一个元素上时,该元素及其所有父元素都会有红色边框,这不是您想要的。

清单 6-9 。使用stopPropagation防止所有元素改变颜色

// Event handling functions
function mouseOverHandler( e ) {
  e.target.style.border = '1px solid red';
  e.stopPropagation();
}

function mouseOutHandler( e ) {
  this.style.border = '0px';
  e.stopPropagation();
}

// Locate, and traverse, all the elements in the DOM
var all = document.getElementsByTagName( '*' );
for ( var i = 0; i < all.length; i++ ) {

  // Watch for when the user moves the mouse over the element
  // and add a red border around the element
  all[i].addEventListener( 'mouseover', mouseOverHandler );

  // Watch for when the user moves back out of the element
  // and remove the border that we added
  all[i].addEventListener( 'mouseout', mouseOutHandler );

}

有了阻止事件冒泡的能力,您现在可以完全控制哪些元素可以看到和处理事件。这是探索动态 web 应用开发所必需的基本工具。最后一个方面是取消浏览器的默认操作,允许您完全覆盖浏览器的功能并实现新的功能。

覆盖浏览器的默认操作

对于大多数发生的事件,浏览器都有一些默认的总是会发生的动作。例如,点击一个<a>元素将把你带到它的相关网页;这是浏览器中的默认操作。这个动作总是发生在捕获和冒泡事件阶段之后,如图 6-3 中的所示,它展示了用户点击网页中的<a>元素的结果。事件从捕获和冒泡阶段遍历 DOM 开始(如前所述)。但是,一旦事件完成遍历,浏览器就会尝试执行该事件和元素的默认操作。在本例中,它访问/ web 页面。

9781430263913_Fig06-03.jpg

图 6-3 。一个事件的整个生命周期

默认动作可以概括为浏览器做的任何你没有明确告诉它去做的事情。下面是发生的不同类型的默认操作的示例,以及在什么事件上发生的示例:

  • 单击一个<a>元素会将您重定向到其href属性中提供的 URL。
  • 使用您的键盘并按下 Ctrl+S,浏览器将尝试保存站点的物理表示。
  • 提交 HTML <form>会将查询数据提交到指定的 URL,并将浏览器重定向到该位置。
  • 将鼠标移动到带有alttitle属性的<img>上(取决于浏览器)会出现一个工具提示,提供属性的值。

即使您停止了事件冒泡,或者您根本没有绑定事件处理程序,浏览器也会执行前面的所有操作。这可能会导致脚本中出现重大问题。如果您希望提交的表单有不同的表现,该怎么办?或者,如果您希望<a>元素的行为与其预期目的不同,该怎么办?因为取消事件冒泡不足以阻止默认操作,所以您需要一些特定的代码来直接处理它。W3C 事件处理 API 通过event对象的preventDefault方法提供了这一功能(清单 6-10 )。对于许多浏览器来说,你可以选择简单地从你的事件处理器返回 false 作为替代,你可以在一些例子和库中看到这种行为编码。使用preventDefault 是首选,因为它是自文档化的——不像偶尔从事件处理程序返回 false 的晦涩技术。

清单 6-10 。防止默认浏览器动作发生的通用函数

document.getElementById('examples-link').addEventListener('click', function(e) {
  e.preventDefault();
   console.log("examples-link clicked");
});

使用preventDefault功能,您现在可以停止浏览器显示的任何默认动作。例如,这允许您利用链接的mouseover事件,而不用担心用户意外点击链接并把浏览器发送到其他地方。您可以覆盖在状态栏中显示链接位置的默认行为。或者考虑一个用来启动表单验证的 Submit 按钮。如果验证失败,您现在可以推迟提交表单(默认行为)。

事件委托

我们已经有了几乎所有操作事件处理程序的工具。一个挥之不去的问题是技术问题。假设我们有一个包含 20 个条目的无序列表。我们希望为每个列表项添加一个事件处理程序。更准确地说,我们希望能够不同地处理每个列表项的点击。我们可以用document.querySelectorAll获取所有元素,迭代结果,并附加单独的事件处理程序。无论是作为一个过程还是在浏览器中,这都是低效的。我们设置了 20 个事件处理程序(即使它们都指向同一个处理函数),而我们只能设置一个。

所有列表项都包含在一个无序列表标签中,那么为什么不利用我们可以在<ul>级别捕获点击事件的事实呢?我们唯一需要的是区分不同列表项的方法。回到传统事件绑定的部分,当我们讨论this对象时,我们注意到this指的是捕获事件的元素,而event.target指的是实际发出事件的元素。显然,我们可以使用thisevent.target的组合。但是事件处理规范提供了event.currentTarget属性来解决这个问题。

在我们的列表项场景中,我们将一个click事件处理程序附加到无序列表。在事件处理程序中,<ul>event.currentTarget。每个列表项都将是event.target属性。因此,我们可以检查event.target来查看哪个列表项被点击并被分派到适当的函数。清单 6-11 展示了一个事件委托的例子。

清单 6-11 。事件委托

function clickHandler(e) {
  console.log( 'Handled at ' + e.currentTarget.id );
  console.log( 'Emitted by ' + e.target.id );
}

var navbar = document.getElementById('navbar');
navbar.addEventListener( 'click', clickHandler );

clickHandler函数处理<nav>级别的事件,但是它接收从<nav>元素下的各种列表项发出的事件。

事件对象

在每个事件处理函数中,都提供了或者可以使用event对象。一般来说,event对象的属性涵盖了您可能想知道的关于某个事件的细节:它是什么类型的事件,它来自哪里,点击了什么坐标,或者可能按了什么键。不过,不同浏览器交流这些信息的方式有一些细微的差别。

常规属性

对于每种被捕获的事件类型,event对象上都有许多属性。所有这些event对象属性都与事件本身直接相关,没有什么是特定于浏览器的。下面是所有event对象属性的列表,并附有解释和示例代码。

类型

该属性包含当前被触发的事件的名称(如clickmouseover)。它可以用来提供一个通用的事件处理函数,然后确定性地执行相关代码。清单 6-12 展示了一个使用这个属性使一个处理程序根据事件类型产生不同效果的例子。

清单 6-12 。使用type属性为元素提供类似悬停的功能

function mouseHandler(e){
  // Toggle the background color of the <div>, depending on the
  // type of mouse event that occurred
  this.style.background = (e.type === 'mouseover') ? '#EEE' : '#FFF';
}

// Locate the <div> that we want to hover over
var div = document.getElementById('welcome');

// Bind a single function to both the mouseover and mouseout events
div.addEventListener( 'mouseover', mouseHandler );
div.addEventListener( 'mouseout', mouseHandler );

目标

此属性包含对触发事件的元素的引用。例如,将一个点击处理程序绑定到一个<a>元素会有一个等于<a>元素本身的目标属性。

停止传播

stopPropagation方法停止事件冒泡(或捕获)过程,使当前元素成为最后一个接收特定事件的元素。

prevent default/return value = false

调用preventDefault方法会阻止浏览器的默认动作在所有现代 W3C 兼容浏览器中发生。

鼠标属性

只有当鼠标相关的事件被启动时,鼠标属性才会存在于event对象中(如clickmousedownmouseupmouseovermousemovemouseoutmouseentermouseleave)。在其他任何时候,您都可以假设返回的值不存在或不可靠。本节列出了鼠标事件期间存在于event对象上的所有属性。

pageX 和 pageY

这些属性包含鼠标光标相对于浏览器窗口绝对左上角的 x 和 y 坐标。无论如何滚动,它们都是一样的。

clientxand 客户〔??〕

这些属性包含鼠标光标相对于浏览器窗口的 x 和 y 坐标。因此,如果您向下(或横向)滚动文档,这些数字是相对于浏览器窗口的边缘的。当您在文档中滚动时,这些数字会发生变化。

layerX/layerY 和 offsetX/offsetY

这些属性应该包含鼠标光标相对于事件目标元素的 x 和 y 坐标。这些属性在 Chrome 和 IE 中有效,但在 Firefox 中无效。火狐支持 l ayerXlayerY,但是它们包含的信息不一样。相反,layer*属性似乎等同于适当的page*属性。

按钮

该属性仅在clickmousedownmouseup事件上可用,是一个代表当前被点击的鼠标按钮的数字。左键点击为 0(零),中键点击为 1,右键点击为 2。

relatedTarget

此事件属性包含对鼠标刚刚离开的元素的引用。通常情况下,relatedTarget用于需要使用mouseover / mouseout的情况,但是你也需要知道鼠标刚刚在哪里,或者它要去哪里。清单 6-13 显示了一个树形菜单的变体(<ol>元素包含其他<ol>元素),其中子树只在用户第一次将鼠标移动到<li>子元素上时显示。

清单 6-13 。使用relatedTarget属性创建一个可导航的树

// When DOMContent is ready, get the references to the elements.
document.addEventListener('DOMContentLoaded', init);
function init(){
    var top = document.getElementById("top");
    var bottom = document.getElementById("bottom");

    top.addEventListener("mouseover", onMouseOver);
    top.addEventListener("mouseout", onMouseOut);
    bottom.addEventListener("mouseover", onMouseOver);
    bottom.addEventListener("mouseout", onMouseOut);
}

function onMouseOut(event) {
  console.log("exited " + event.target.id + " for " + event.relatedTarget.id);
}

function onMouseOver(event) {
  console.log("entered " + event.target.id + " from " + event.relatedTarget.id);
}

// Sample HTML:
<style>
div > div {
  height: 128px;
  width: 128px;
}
#top    { background-color: red; }
#bottom { background-color: blue; }
</style>
<title>Untitled Document</title>
</head>

<body>

<div id="outer">
  <div id="top"></div>
  <div id="bottom"></div>
</div>

键盘属性

键盘属性一般只在键盘相关事件被启动时存在于event对象中(如keydownkeyupkeypress)。这个规则的例外是ctrlKeyshiftKey属性,它们在鼠标事件期间可用(允许用户按住 Ctrl 键并单击一个元素)。在其他任何时候,您都可以假设属性中包含的值不存在或不可靠。

ctrl ley

此属性返回一个布尔值,该值表示键盘 Ctrl 键是否被按住。ctrlKey属性可用于键盘和鼠标事件。

键码

这个属性包含一个对应于键盘上不同键的数字。某些键(如 PageUp 和 Home)的可用性可能会有所不同,但一般来说,所有其他键都工作可靠。表 6-1 是所有常用键盘键及其相关键码的参考。

表 6-1 。常用键码

|

钥匙

|

键码

|
| --- | --- |
| 退格 | eight |
| 标签 | nine |
| 进入 | Thirteen |
| 空间 | Thirty-two |
| 向左箭头 | Thirty-seven |
| 向上箭头 | Thirty-eight |
| 向右箭头 | Thirty-nine |
| 下箭头键 | Forty |
| 0–9 | 48–57 |
| 阿塞拜疆(Azerbaijan 的缩写) | 65–90 |

shiftKey 键

该属性返回一个布尔值,该值表示键盘上的 Shift 键是否被按住。shiftKey属性可用于键盘和鼠标事件。

事件的类型

常见的 JavaScript 事件可以分为几类。可能最常用的类别是鼠标交互,紧随其后的是键盘和表单事件。下面的列表概括介绍了 web 应用中存在并可以处理的不同事件类别。

  • 加载和错误 事件:该类的事件与页面本身相关,观察其加载状态。它们发生在用户第一次加载页面时(load事件)和用户最终离开页面时(unloadbeforeunload事件)。此外,使用error事件跟踪 JavaScript 错误,使您能够单独处理错误。
  • UI 事件 : 这些用于跟踪用户何时与页面的一个方面而不是另一个方面进行交互。例如,使用这些工具,您可以可靠地知道用户何时开始输入表单元素。用于跟踪这一点的两个事件是focusblur(当对象失去焦点时)。
  • 鼠标 事件:分为两类:跟踪鼠标当前所在位置的事件(mouseovermouseout),以及跟踪鼠标点击位置的事件(mouseupmousedownclick)。
  • 键盘 事件:这些事件负责跟踪键盘键何时被按下以及在什么上下文中被按下——例如,跟踪表单元素内的击键,而不是整个页面内发生的击键。与鼠标一样,三种事件类型用于跟踪键盘:keyupkeydownkeypress
  • 表单 事件:这些事件直接关系到只与表单和表单输入元素发生的交互。submit事件用于跟踪表单提交的时间;change事件观察用户对元素的输入;当一个<select>元素被更新时,就会触发select事件。

页面事件

所有页面事件专门处理整个页面的功能和状态。大多数事件类型处理页面的加载和卸载(每当用户访问页面然后再次离开时)。

负荷

一旦页面完全完成加载,就会触发load事件;此事件包括所有图像、外部 JavaScript 文件和外部 CSS 文件。它也适用于大多数具有src属性的元素(imgscriptaudiovideo等等)。加载事件不会冒泡。

卸载前

这个活动有点奇怪,因为它完全不规范,但却得到了广泛的支持。它的行为与unload事件非常相似,但有一个重要的区别。在您的beforeunload 事件的事件处理程序中,如果您返回一个字符串,该字符串将显示在一条确认消息中,询问用户是否希望离开当前页面。如果他们拒绝,他们将能够停留在当前页面。Gmail 等动态 web 应用利用这一点来防止用户丢失任何未保存的数据。

错误

每当 JavaScript 代码中出现错误时,就会触发error事件。它可以作为捕获错误消息并优雅地显示或处理它们的一种方式。该事件处理程序的行为不同于其他事件处理程序,因为它没有传入一个event对象,而是包含一条解释所发生的特定错误的消息。

调整大小

页面事件:resize事件在每次用户调整浏览器窗口大小时发生。当用户调整浏览器窗口大小时,resize事件只会在调整完成后触发,而不是在整个过程中的每一步。

卷起

当用户在浏览器窗口中移动文档的位置时,scroll事件发生。这可以通过按键盘(如使用箭头键、上/下翻页或空格键)或使用滚动条来实现。

卸载

每当用户离开当前页面(例如,通过单击链接、点击“后退”按钮,甚至关闭浏览器窗口)时,都会触发此事件。阻止默认动作对这个事件不起作用(下一个最好的事情是beforeunload事件)。

UI 事件

用户界面事件处理用户如何与浏览器或页面元素交互。UI 事件可以帮助您确定用户当前正在与页面上的哪些元素进行交互,并为它们提供更多的上下文(比如突出显示或帮助菜单)。

集中

focus事件是确定页面光标当前位置的一种方式。默认情况下,焦点在整个文档中;但是,每当使用键盘单击或切换到某个链接或表单输入元素时,它就会转到那个位置。(该事件的一个例子如清单 6-14 所示)。

虚化

当用户将焦点从一个元素转移到另一个元素时,就会发生blur事件(在链接、输入元素或页面本身的上下文中)。(该事件的一个例子如清单 6-14 所示)。

鼠标事件

当用户移动鼠标指针或单击鼠标按钮时,就会发生鼠标事件。

点击

当用户在一个元素上按下鼠标左键(参见mousedown事件)并在同一元素上释放鼠标键(参见mouseup事件)时,就会发生click事件。

dblclick(数据库点击)

dblclick事件发生在用户快速连续完成两个click事件之后。双击的速度取决于操作系统的设置。

老鼠洞

当用户按下鼠标按钮时,mousedown事件发生。与keydown事件不同,这个事件只会在鼠标按下时触发一次。

老鼠!老鼠

当用户释放被按下的鼠标按钮时,mouseup事件发生。如果在按钮被按下的同一元素上释放按钮,也会发生一个click事件。

摩门教徒

每当用户将鼠标指针在页面上移动至少一个像素时,就会发生一个mousemove事件。触发的mousemove事件的数量(对于鼠标的完整移动)取决于用户移动鼠标的速度以及浏览器能够跟上更新的速度。

鼠标悬停

每当用户将鼠标从一个元素移到另一个元素时,就会发生mouseover事件。要查找用户来自哪个元素,请使用relatedTarget属性。此事件是资源密集型的,因为它可以为经过的每个像素或子元素触发一次。更喜欢mouseenter,描述简短。

鼠标移出

每当用户将鼠标移出一个元素时,mouseout事件就会发生。这包括将鼠标从一个父元素移动到一个子元素(乍一看可能不直观)。要找到用户要去的元素,使用relatedTarget属性。这个事件是资源密集型的,因为与mouseover配对,它可以触发很多很多次。偏爱mouseleave,简述。

鼠标输入

功能类似于mouseover,但是智能地注意它在元素中的位置。不会再次触发,直到它离开元素的盒子。

moueleve〔??〕

功能类似于mouseout,但是智能地注意它何时离开一个元素。

清单 6-14 展示了一个将事件对附加到元素上的例子,以允许键盘访问(和鼠标访问)网页。每当用户将鼠标移动到链接上或使用键盘导航到该链接时,该链接将会得到一些额外的颜色突出显示。

清单 6-14 。通过使用mouseovermouseout事件创建悬停效果

// mouseEnter handler
function mouseEnterHandler() {
  this.style.backgroundColor = 'blue';
}

// mouseLeave handler
function mouseLeaveHandler() {
  this.style.backgroundColor = 'white';
}

// Find all the <a> elements, to attach the event handlers to them
var a = document.getElementsByTagName('a');
for ( var i = 0; i < a.length; i++ ) {

  // Attach a mouseover and focus event handler to the <a> element,
  // which changes the <a>s background to blue when the user either
  // mouses over the link, or focuses on it (using the keyboard)
  a[i].addEventListener('mouseenter', mouseEnterHandler);
  a[i].addEventListener('focus', mouseEnterHandler);

  // Attach a mouseout and blur event handler to the <a> element
  // which changes the <a>s background back to its default white
  // when the user moves away from the link
  a[i].addEventListener('mouseleave', mouseLeaveHandler);
  a[i].addEventListener('blur', mouseLeaveHandler);

}

键盘事件

键盘事件处理用户在键盘上按键的所有实例,无论是在文本输入区域内部还是外部。

击键/按键

keydown事件是按键时发生的第一个键盘事件。如果用户继续按住键,keydown事件将继续触发。keypress事件是keydown事件的常见同义词;它们的行为实际上是一样的,只有一个例外:如果你想阻止一个按键被按下的默认动作,你必须在keypress事件上这样做。

好好享受吧

keyup事件是发生的最后一个键盘事件(在keydown事件之后)。与keydown事件不同,这个事件在释放时只会触发一次(因为你不能长时间释放一个键)。

表单事件

表单事件主要处理 HTML 表单的主要元素<form><input><select><button><textarea>

挑选

每当用户使用鼠标在输入区域中选择不同的文本块时,就会触发select 事件。通过该事件,您可以重新定义用户与表单交互的方式。

变化

当用户修改输入元素(包括<select><textarea>元素)的值时,会发生change和事件。该事件仅在用户已经离开元素,使其失去焦点后触发。

使服从

submit 事件只在表单中发生,并且只在用户点击提交按钮(位于表单中)或点击输入元素之一中的 Enter/Return 时发生。通过将绑定到表单的提交处理程序,而不是绑定到提交按钮的点击处理程序,可以确保捕捉到用户提交表单的所有尝试。

重置

reset 事件仅在用户单击表单内的重置按钮时发生(与提交按钮相反,提交按钮可以通过按回车键来复制)。

事件可访问性

在开发一个完全不引人注目的 web 应用时,要考虑的最后一点是确保即使不使用鼠标,您的事件也能工作。通过这样做,你帮助了两类人:需要辅助功能的人(视力受损的用户)和不喜欢使用鼠标的人。(有一天坐下来,断开鼠标和电脑的连接,学习如何只用键盘浏览网页。真是大开眼界的经历)。

为了让您的 JavaScript 事件更容易访问,无论何时使用clickmouseovermouseout事件,您都应该考虑提供替代的非鼠标绑定。幸运的是,有一些简单的方法可以快速补救这种情况:

  • click事件** : 浏览器开发人员的一个聪明举措是让click事件在每次按下回车键时都能工作。这完全消除了为该事件提供替代的需要。然而,需要注意的一点是,一些开发人员喜欢绑定点击处理程序来提交表单中的按钮,以便在用户提交网页时进行监视。开发人员应该绑定到表单对象上的submit事件,而不是使用该事件,这是一个可靠的智能替代方法。**
    *** mouseover事件** : 使用键盘浏览网页时,你实际上是在将焦点转移到不同的元素上。通过将事件处理程序附加到mouseoverfocus事件,可以确保为键盘和鼠标用户提供相同的解决方案。***** mouseout事件** :mouseover事件的the focus事件一样,blur事件在用户的焦点从某个元素移开时发生。然后你可以使用blur事件来模拟键盘上的mouseout事件。******

****实际上,除了典型的鼠标事件之外,添加处理键盘事件的能力完全是微不足道的。如果没有别的,这样做可以帮助依赖键盘的用户更好地使用你的网站,这对每个人来说都是一个巨大的胜利。

摘要

在这一章中,我们首先介绍了 JavaScript 中的事件是如何工作的,并将它们与其他语言中的事件模型进行了比较。然后,您看到了事件模型提供了什么信息,以及如何最好地控制它。然后,我们探讨了将事件绑定到 DOM 元素,以及可用的不同类型的事件。我们最后讨论了event对象属性、事件类型以及如何为可访问性编码。****

七、JavaScript 和表单验证

不可避免的是,当遇到一个表单时,人们会考虑该表单数据的命运。JavaScript 的第一个实际应用之一是提供一种在客户端验证数据的方法,而不是必须忍受与服务器之间的往返。表单验证在当时有点特别,没有实用的 API,也没有与浏览器的真正集成。相反,程序员将事件和基本的文本操作绑定在一起,以提供方便的用户界面增强。

快进到今天,表单验证的情况已经好得多了。对于现代浏览器,我们有一个集成的验证 API,它与 HTML 和 CSS 一起工作,以提供一组广泛的表单验证功能。我们也有正则表达式,尽管它们很复杂,但对于数据验证来说,它比一个字符一个字符地迭代要好得多。

本章我们关注的是 JavaScript 和表单。虽然我们将重点关注表单验证,但我们也将关注 JavaScript 与表单交互方式的总体改进,以及一些新的与表单相关的 API。

HTML 和 CSS 表单验证

如前所述,自 JavaScript 早期以来,表单验证已经走过了漫长的道路。要真正深入表单验证的状态,我们不仅需要了解 JavaScript,还需要了解 HTML5 和 CSS。让我们从事物的 HTML 方面开始。在过去的几年里,由于 Web 超文本应用技术工作组(WHATWG) 的辛勤工作,HTML 已经发展并增加了许多新特性。这个组织推动了 HTML 的发展和更新,使之成为众所周知的 HTML5。虽然 HTML5 规范的范围意味着我们不能在这里讨论细节,但你可以在 Jonathan Reid 的 HTML5 程序员参考中找到更多信息(Apress,2015)。

HTML5 中特别值得注意的是对表单控件集的改进。这些变化大致分为两类:添加新的控件或控件样式(URL 字段、日期选择器等),以及表单验证。最初,我们的重点是后者。简单的表单验证已经转移到 HTML 中,不需要任何 JavaScript。这种验证是通过向表单控件添加某些属性来实现的。一个简单的例子是required属性,它与input元素配对,并在表单提交之前强制字段具有值。清单 7-1 是一个基本的例子。

清单 7-1 。简单的形式

<!DOCTYPE html>
<html>
<head>
  <title>A basic form</title>
</head>
<body>
<h2>A basic form</h2>

<p>Please provide your first and last names.</p>

<form>
  <fieldset>
    <label for="firstName">First Name:</label><br/>
    <input id="firstName" name="firstName" type="text" required/><br/>
    <label for="lastName">Last Name:</label><br/>
    <input type="text" name="lastName" id="lastName"/><br/>
  </fieldset>
  <input type="submit" value="Submit the form"/> <input type="reset" value="Reset the form"/>
</form>

</body>
</html>

注意,在表单中,我们有一个 ID 为firstName的输入字段,它添加了前面提到的required属性。如果我们试图在没有填写这个字段的情况下提交表单,我们将会看到类似于图 7-1 的结果。

9781430263913_Fig07-01.jpg

图 7-1 。此基本表单中缺少名字

Chrome 和 IE 11 上的显示看起来大致相同(Chrome 没有用红色边框包围字段,但 IE 有一个更块状、更自信的红色边框)。如果您要使firstNamelastName字段都成为必填字段,边框将出现在每个字段上,但是弹出工具提示将只与第一个有问题的字段相关联。自定义弹出窗口怎么样?我们将很快处理这个问题,但是它需要 JavaScript。

有几种其他类型的验证可以通过 HTML 属性激活。他们是

  • pattern:该属性采用正则表达式作为参数。正则表达式不需要用斜杠括起来。正则表达式语言引擎与 JavaScript 的相同(也有相同的问题)。这是附加到输入元素上的。请注意,emailurl的输入类型分别意味着适用于有效电子邮件地址和 URL 的模式值。模式验证不适用于 Safari 8、iOS Safari 8.1 或 Opera Mini。
  • step:强制值匹配指定步长值的倍数。限制为输入类型numberrange或日期时间之一。步骤验证在 Chrome 6.0、Firefox 16.0、IE 10、Opera 10.62 和 Safari 5.0 中有效。
  • min / max:最小值或最大值,不仅适用于数字,也适用于日期时间。这种方法在 Chrome 41、Opera 27 和 Chrome for Android 41 中都有效。
  • maxlength:字段中数据的最大长度,以字符为单位。仅对textemailsearchpasswordtelurl输入类型有效。这种方法通常不会进行太多的验证,以防止用户在它所附加的字段中输入过多的数据。它可以在所有现代浏览器上运行。

在表单级别,您可以通过以下两种方法之一整体关闭验证。您可以将formnovalidate属性添加到表单的提交按钮,或者将novalidate属性添加到表单元素本身。

半铸钢ˌ钢性铸铁(Cast Semi-Steel)

不满足于让 HTML5 做所有的工作,CSS 规范已经被更新来处理表单验证。处于无效状态的表单元素可以通过:invalid伪类来访问。不幸的是,这个伪类的实现还有待改进。首先,在页面加载时检查表单元素的有效性。因此,如果您有如下样式:

:invalid { background-color: yellow }

当页面加载时,许多字段会有黄色背景。第二,Chrome 和 IE 只对表单元素应用:invalid。如果表单中的任何元素无效,Firefox 会将其应用到整个表单。考虑清单 7-2 中的。

清单 7-2 。使用:invalid伪类

<!DOCTYPE html>
<html>
<head>
  <title>A basic form</title>
  <style>
    :invalid {
      background-color: yellow
    }
  </style>
</head>
<body>
<h2>A basic form</h2>

<p>Please provide your first and last names.</p>

<form>
  <fieldset>
    <label for="firstName">First Name:</label><br/>
    <input id="firstName" name="firstName" type="text" required/><br/>
    <label for="lastName">Last Name:</label><br/>
    <input type="text" name="lastName" id="lastName"/><br/>
  </fieldset>
  <input type="submit" value="Submit the form"/> <input type="reset" value="Reset the form"/>
</form>

</body>
</html>:

在这个清单中,Firefox 用黄色背景显示整个表单,因为表单的一个元素处于无效状态。通过将:invalid的样式更改为input:invalid来解决这个问题,这将为您提供跨浏览器的一致行为。

CSS 还提供了一些其他的伪类:

  • :valid包含处于有效状态的元素。
  • :required获取其required属性设置为 true 的元素。
  • :optional获取没有设置required属性的元素。
  • :in-range用于最小/最大边界内的元素;IE 不支持。
  • :out-of-range用于那些在那些界限之外的;IE 不支持。

最后说一下红光和弹出消息。在 Firefox 中提交后,无效元素周围会出现红色光晕效果。(在 Internet Explorer 中,它是一个简单的红色边框,没有发光效果。)Firefox 将效果公开为:-moz-ui-invalid伪类。您可以按如下方式覆盖它:

:-moz-ui-invalid { box-shadow: none }

唉,Internet Explorer 并没有将它的效果公开为一个伪类。这意味着我们已经达到了单独使用 HTML 和 CSS 的极限。有些功能我们想改变,有些功能我们想实现。这就是 JavaScript 重新发挥作用的地方。>:

JavaScript 表单验证

很大程度上得益于 HTML5 living standard,JavaScript 现在有了一个用于表单验证的连贯 API。这依赖于一个相对简单的验证检查生命周期:这个表单元素有验证例程吗?如果有,是否通过?如果失败了,为什么会失败?与这个过程交织在一起的是 JavaScript 的逻辑访问点,要么通过方法调用,要么通过捕获事件。这是一个好系统,虽然这并不是说它不能忍受一点改进。但是我们不要想太多。

检查表单元素有效性的最简单方法是对其调用 checkValidity。支持每个表单元素的 JavaScript 对象现在可以使用这个checkValidity方法。此方法访问 HTML 中为元素设置的验证约束。每个约束都根据元素的当前值进行测试。如果任何约束失败,checkValidity将返回 false。如果全部通过,checkValidity返回 true。对checkValidity的调用不限于单个元素。它们也可以根据表单标签来制作。如果是这种情况,checkValidity调用将被委托给表单中的每个表单元素。如果所有的子调用都返回 true(即所有的表单元素都有效),那么表单作为一个整体是有效的。相反,如果任何子调用返回 false,则表单无效。

除了获得关于元素有效性的简单布尔答案之外,我们还可以找出元素有效性失败的原因。任何元素的validity属性都是一个对象,它包含所有可能导致验证失败的原因,称为ValidityState对象。我们可以迭代它的属性,如果有一个是真的,我们知道这是元素有效性检查失败的原因之一。其性能见表 7-1 。

表 7-1 。Validity State属性

|

财产

|

说明

|
| --- | --- |
| valid | 元素的值是否有效。先从这个属性开始。 |
| valueMissing | 没有值的必需元素。 |
| patternMismatch | 对模式指定的正则表达式的检查失败。 |
| rangeUnderflow | 值低于最小值。 |
| rangeOverflow | 值高于最大值。 |
| stepMismatch | 值不是有效的步长值。 |
| tooLong | 值大于maxlength允许的值(以字符为单位)。 |
| typeMismatch | 值未通过对emailurl输入类型的检查。 |
| customError | 如果引发了自定义错误,则为 True。 |
| badInput | 当浏览器认为值是无效的,但不是因为已经列出的原因之一时,这是一种总括;未在 Internet Explorer 中实现。 |

检查元素的validity属性的动作运行有效性检查。没有必要调用元素。 checkValidity第一。

让我们来看看有效性检查的运行情况。首先,清单 7-3 显示了我们的 HTML 的相关部分。

清单 7-3 。我们的 HTML 表单

<body>
<h2>A basic form</h2>

<p>Please fill in the requested information.</p>

<form id="nameForm">
  <div id="fields">
    <label for="firstName">First Name:</label><br/>
    <input id="firstName" name="firstName" type="text" class="foo" required/><br/>
    <label for="lastName">Last Name:</label><br/>
    <input type="text" name="lastName" id="lastName" required/><br/>
    <label for="phone">Phone</label><br/>
    <input type="tel" id="phone"/><br/>

    <label for="age">Age (must be over 13):</label><br/>
    <input type="number" name="age" id="age" step="2" min="14" max="100"/><br/>
    <label for="email">Email</label><br/>
    <input type="email" id="email"/><br/>
    <label for="url">Website</label><br/>
    <input type="url" id="url"/><br/>
  </div>

  <div id="buttons">
    <input id="overallBtn" value="Check overall validity" type="button"/>
    <input id="validBtn" type="button" value="Display validity"/>
    <input id="submitBtn" type="submit" value="Submit the form"/>
    <input type="reset" id="resetBtn" value="Reset the form"/>
  </div>
</form>

<div>
  <h2>Validation results</h2>
  <div id="vResults"></div>
  <div id="vDetails"></div>
</div>

</body>

注意submitresetvalidity检查按钮在各自的div中。这使得使用document.querySelectorAll只检索相关的表单字段变得更加容易,这些字段在单独的div中。现在,继续我们的 JavaScript ( 清单 7-4 )。

清单 7-4 。表单验证和有效性

window.addEventListener( 'DOMContentLoaded', function () {
  var validBtn = document.getElementById( 'validBtn' );
  var overAllBtn = document.getElementById( 'overallBtn' );
  var form = document.getElementById( 'nameForm' ); // Or document.forms[0]
  var vDetails = document.getElementById( 'vDetails' );
  var vResults = document.getElementById( 'vResults' );

  overallBtn.addEventListener( 'click', function () {
    var formValid = form.checkValidity();
    vResults.innerHTML = 'Is the form valid? ' + formValid;
  } );

  validBtn.addEventListener( 'click', function () {
    var output = '';

    var inputs = form.querySelectorAll( '#fields > input' );

    for ( var x = 0; x < inputs.length; x++ ) {
      var el = inputs[x];
      output += el.id + ' : ' + el.validity.valid;
      if (! el.validity.valid) {
        output += ' [';
        for (var reason in el.validity) {
          if (el.validity[reason]) {
            output += reason
          }
        }
        output += ']';
      }
      output += '<br/>'
    }

    vDetails.innerHTML = output;
  } );
} );

整个代码块是一个绑定到 DOM 加载时间的事件。回想一下,我们不想尝试向可能还没有创建的元素添加事件处理程序。首先,我们将检索页面中的相关元素:两个有效性检查按钮、输出 div 和表单。接下来,我们将为整体有效性检查设置事件处理。注意,在这种情况下,为了简单起见,我们检查整个表单的有效性。我们在vResults div 中显示这个检查的结果。

第二个事件处理程序包括检查每个表单元素的有效性状态。我们通过使用querySelectorAll获取 ID 为fieldsdiv下的所有输入字段来获取适当的元素。(这比编写一个扩展的 CSS 选择器来查找不包括提交、重置和按钮的输入类型更简单。)获得我们想要的元素后,很简单,迭代元素并检查它们的validity属性的valid子属性。如果它们无效(valid为假),那么我们打印出该字段无效的原因。我们鼓励您使用各种不同的输入值进行尝试。

这个演示揭示了一些有趣的事情。首先,如果您加载页面并单击“显示有效性”按钮,firstNamelastName字段无效(正如您所料,因为它们是空的),但是phoneageemailurl字段(也是空的)有效!如果该字段不是必需的,则空值有效。另外,注意 email 字段有两个验证,email 的隐含验证,以及一个模式需求。尝试输入一个不包含“@foo.com”之类的电子邮件,您会发现有可能一次多次验证失败。Firefox 还会告诉你,如果你输入了不完整的电子邮件地址(比如说,只是一个用户名),那么typeMismatchbadInput的值就会失效。您可能倾向于只依赖于valid属性,但是知道字段验证失败的原因对于传达给用户来说是非常重要的信息,毕竟,如果不通过各种验证约束,用户将无法成功提交表单。

验证和用户

到目前为止,我们已经把大部分时间花在了表单验证的技术方面。我们还应该讨论什么时候应该执行表单验证。我们有许多选择。简单地使用表单验证 API 意味着我们可以在提交时自动进行表单验证。多亏了checkValidity方法,我们能够在需要时调用任何给定元素的验证。作为最佳实践,我们应该尽早执行表单验证。这在实践中意味着什么取决于被验证的字段。首先,我们应该验证表单字段中的更改。将 change 事件处理程序附加到您的表单控件,并让它在该控件上调用checkValidity。在表单验证 API 中工作,对于何时进行验证的问题,这是一个相当简单的答案。

但是如果我们不在表单验证 API 中工作呢?在表单验证 API 中工作的一个更重要的限制是它没有定制验证的功能。你不能添加一大块自定义代码,绑定在一个函数中,比如说,作为一个验证例程运行。但是毫无疑问,在某个时候你会想要这么做。当您发现自己处于这种情况时,将验证与变更事件处理程序联系起来仍然具有一般和实际的意义。可能会有例外。考虑一个需要 Ajax 调用来验证其值的字段,可能是基于字段中输入的前几个字符。在这种情况下,您可以将验证与按键事件联系起来,可能还会集成自动建议功能。在下一章讨论 Ajax 时,我们将看一个这样的例子。

无论您选择在哪个阶段进行验证,都要记住您的用户。填写完一张表格,然后发现很多数据由于各种原因无效,这是非常令人沮丧的。用户倾向于更容易接受内嵌的修正,而不是在提交时给出一个错误列表。

验证事件

表单验证 API 的另一个增加是无效表单元素现在抛出一个invalid事件。该事件仅在响应对checkValidity的调用时抛出。可以在元素本身或包含元素的表单上进行checkValidity调用。invalid事件不冒泡。表单本身没有invalid事件,尽管表单可能是无效的。

您可以通过调用发射控件上的addEventListener来捕获事件。一旦进入事件处理程序,事件对象本身不提供任何与验证相关的信息。您必须通过event.target属性检索元素,然后查询它的validity属性,找出元素无效的确切原因。但是你可以用事件的preventDefault方法做一些相当有趣的事情。当您调用preventDefault时,浏览器对无效元素的样式行为将不会被应用。请记住,只有在提交表单时,样式更改才会被一致地应用。(如果您更改了表单控件的值并远离它进行模糊处理,Firefox 将应用样式更改。)这对于不同的浏览器意味着不同的事情:

  • Chrome 不会设置无效元素的样式,但会给出一个弹出消息,它会抑制该元素的弹出窗口。
  • Firefox 既有弹出窗口又有样式,它会抑制弹出窗口,但不会抑制或阻止元素周围的红光效果。
  • Internet Explorer 在元素周围既有弹出窗口又有红色边框,它将取消元素周围的弹出窗口和边框。

让我们看一个展示这种行为的例子。从清单 7-5 中一个相对熟悉的 HTML 表单开始。

清单 7-5 。有效性事件表单

<!DOCTYPE html>
<html>
<head>
  <title>A basic form</title>
  <style>
    input:invalid {
      background-color: yellow
    }
  </style>
</head>
<body>
<h2>A basic form</h2>

<p>Please provide your first and last names.</p>

<form id="nameForm">
  <fieldset>
    <label for="firstName">First Name:</label><br/>
    <input id="firstName" name="firstName" type="text" required/><br/>
    <label for="lastName">Last Name:</label><br/>
    <input type="text" name="lastName" id="lastName" required/><br/>
  </fieldset>
  <div>
    <input type="submit" value="Submit the form"/> <input type="reset" value="Reset the form"/>
  </div>
  <div>
    <input id="firstNameBtn" type="button" value="Check first name validity."/>
    <input id="formBtn" type="button" value="Check form validity"/>
    <input id="preventBtn" type="button" value="Prevent default behavior"/>
    <input id="restoreBtn" type="button" value="Restore default behavior"/>
  </div>
</form>

<div id="vResults"></div>

<script src="listing_7_5.js"></script>
</body>
</html> .

请注意,我们已经为无效的输入元素添加了样式。这种样式与无效事件的默认行为无关。让我们看看支持代码(清单 7-6 )。

清单 7-6 。JavaScript 中的有效性事件

window.addEventListener( 'DOMContentLoaded', function () {
  var outputDiv = document.getElementById( 'vResults' );
  var firstName = document.getElementById( 'firstName' );

   firstName.addEventListener("focus", function(){
    outputDiv.innerHTML = '';
   });

  function preventDefaultHandler( evt ) {
    evt.preventDefault();
  }

  firstName.addEventListener( 'invalid', function ( event ) {
    outputDiv.innerHTML = 'firstName is invalid';
  } );

  document.getElementById( 'firstNameBtn' ).addEventListener( 'click', function () {
    firstName.checkValidity();
  } );

  document.getElementById( 'formBtn' ).addEventListener( 'click', function () {
    document.getElementById( 'nameForm' ).checkValidity();
  } );

  document.getElementById( 'preventBtn' ).addEventListener( 'click', function () {
    firstName.addEventListener( 'invalid', preventDefaultHandler );
  } );

  document.getElementById( 'restoreBtn' ).addEventListener( 'click', function () {
    firstName.removeEventListener( 'invalid', preventDefaultHandler );
  } );

} ); .

像往常一样,我们所有的代码都在DOMContentLoaded事件触发后被激活。对于firstName字段上的无效事件,我们有一个基本的事件处理程序,它输出到vResults div。然后,我们为专用按钮添加事件处理程序。首先我们创建两个方便的按钮:一个用于检查firstName字段的有效性,另一个用于检查整个表单的有效性。然后我们添加行为来覆盖或恢复与无效事件相关的默认行为。试试吧!

自定义验证

我们现在拥有几乎所有的工具来全面控制表单验证。我们可以选择激活哪些验证。我们可以控制何时执行验证。我们可以捕获无效事件,并防止默认行为(特别是关于样式)触发。如前所述,我们不能定制实际的验证例程(唉)。那么我们还能做些什么呢?我们可能希望控制用户在提交表单时看到的验证弹出窗口中的消息。(弹出窗口的外观也是不可定制的。还记得我们提到过 API 及其实现有一些缺点吗?)

要更改表单域无效时出现的验证消息,请使用与表单控件相关联的setCustomValidity函数。将一个字符串作为参数传递给setCustomValidity ,该字符串将作为弹出窗口的文本出现。不过,这确实有一些其他的副作用。在 Firefox 中,该字段在页面加载时会显示为无效,带有红色光晕效果。在浏览器或 Chrome 上使用setCustomValidity对页面加载没有影响。如前所述,可以通过覆盖:-moz-ui-invalid伪类来关闭 Firefox 样式。但更成问题的是,当使用setCustomValidity时,问题中的表单控件的 validityStatecustomError属性被设置为 true。这意味着validityStatevalid属性为假,这意味着它读取为无效。只需更改与有效性检查相关的消息!这是不幸的,并使setCustomValidity几乎无用。

另一种方法是使用多填充物。有一个很长的多填充列表,不仅用于表单验证,还用于其他 HTML5 项目,这些项目可能不支持您需要使用的每个浏览器。您可以在这里找到它们:

https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills

阻止表单验证

表单验证还有一个方面我们没有探讨:关闭它。本章的大部分内容都集中在使用新的表单验证 API,并探索它的局限性。但是如果有一个 API 的交易破坏者,一些阻止我们大规模使用 API 的特性(或者 bug,或者不一致性)会怎么样呢?如果是这种情况,我们需要做两件事:停止自动表单验证,用我们自己的表单来代替。我们感兴趣的是前者,因为后者只是根据我们的想法重新实现一个 API。要关闭表单验证行为,请将novalidate属性添加到表单元素中。您可以通过向 submit 按钮添加formnovalidate属性来防止每次点击 submit 按钮时的行为,但是这并不能从整体上关闭表单的表单验证。由于我们可能希望用自己定制的 API 来替代表单验证,所以我们希望使用novalidate属性来完全禁用表单验证(针对父元素)。

摘要

在这一章中,我们花了大部分时间来看新的 JavaScript 表单验证 API。在其约束下,它是自动验证用户数据的强大工具。表单提交时会自动进行验证,我们也可以在自己选择的任何时间进行验证。如果需要,可以关闭验证。我们可以定制有效和无效元素的外观,甚至可以定制元素无效时显示的消息。

API 并不是没有问题。它缺乏一些关键的定制能力,比如允许错误消息的样式,或者定制验证例程。在主要的浏览器之间有一些小的实现差异,清理起来可能会有点痛苦。API 的一些官方部分(想想willValidate属性)目前还没有实现,其他部分(setCustomValidity)有严重的问题。

总的来说,API 是 JavaScript、HTML、CSS 和浏览器的一大进步。我们期待着看到它在未来将如何完善。

八、Ajax 简介

Ajax 是 Adaptive Path 的 Jesse James Garrett 创造的一个术语,用来描述使用 XMLHttpRequest 对象实现的客户端到服务器的异步通信,所有现代浏览器都提供了这个对象。Ajax 是 A 同步JavaScriptAndXML 的首字母缩写,它已经发展成为一个术语,用来封装创建动态 web 应用所必需的技术。此外,Ajax 技术的各个组件是完全可以互换的——例如,使用 JSON 代替 XML 是完全有效的。

自从这本书的第一版以来,Ajax 的用法已经发生了显著的变化。Ajax 曾经是一种奇特的 API,现在是专业 JavaScript 程序员工具箱中的标准部分。W3C 已经对 XMLHttpRequest 对象 Ajax 的基础——进行了彻底改革,增加了新的特性并阐明了其他特性的行为。Ajax 的核心规则之一:不要连接到外部域;这是通过使用跨源资源共享标准(也称为 CORS)来实现的。

在这一章中,你将看到构成整个 Ajax 过程的细节。我们将集中讨论 XMLHttpRequest 对象 API,但是我们也将讨论一些辅助问题,比如处理响应、管理 HTTP 状态代码等等。目的是让您全面了解 Ajax 请求/响应周期中发生的事情。

我们不会为 Ajax 交互提供 API。按照管理 Ajax 的各种规范编写代码是一件简单的事情,但是为现实世界编写一个完整的 Ajax 库肯定不是那么简单。考虑 jQuery 库的 Ajax 部分,它有十几个边缘案例,处理 Internet Explorer、Firefox、Chrome 和其他浏览器中 API 的各种奇怪之处。此外,因为 jQuery、Dojo、Ext JS 和其他几个较小的库已经有了 Ajax 实现,我们认为没有理由在这里重新发明这个特殊的轮子。相反,我们将提供 Ajax 交互的例子,这些例子是根据当前(截至发布时)的规范编写的。这些示例旨在提供指导和示范,而非最终结果。我们鼓励您在使用 Ajax 时研究一下实用程序库,如 jQuery、Zepto、Dojo、Ext JS 和 MooTools,或者侧重于 Ajax 的库,如 Fermata 和 reqwest。

这留下了相当多的讨论!本章将涵盖以下内容:

  • 检查不同类型的 HTTP 请求
  • 确定向服务器发送数据的最佳方式
  • 查看整个 HTTP 响应,思考如何不仅处理成功的响应,还处理出错的响应(以某种方式)
  • 读取、遍历和操作来自服务器响应的数据结果
  • 处理异步响应
  • 跨域请求,由 CORS 支持

使用 Ajax

创建一个简单的 Ajax 实现不需要太多代码;然而,这种实现给你带来的好处是巨大的。例如,您的代码可以异步处理提交过程,在完成后加载一小部分期望的结果,而不必强迫用户在提交表单后请求一个全新的网页。事实上,当我们将 Ajax 请求绑定到按键等事件的处理程序时,根本不需要等待表单提交。这就是谷歌自动建议搜索功能“神奇”的背后。当您开始输入一个搜索词时,Google 会根据您的输入发出一个 Ajax 请求。当您细化搜索时,它会发出其他 Ajax 请求。谷歌不仅会显示建议,还会根据第一个可能的选项,显示结果的第一页。图 8-1 显示了这个过程的一个例子。

9781430263913_Fig08-01.jpg

图 8-1 。一个即时域名搜索的例子 在你输入的时候寻找域名

HTTP 请求

Ajax 过程中最重要也可能是最一致的方面是 HTTP 请求 部分。超文本传输协议(HTTP) 被简单地设计用来传输 HTML 文档和相关文件。幸运的是,所有现代浏览器都支持使用 JavaScript 动态异步建立 HTTP 连接的方法。事实证明,这对于开发响应速度更快的 web 应用非常有用。

Ajax 的最终目的是向服务器异步发送数据并接收额外的数据。数据如何格式化最终取决于您的特定需求。

在下面几节中,您将看到如何使用不同的 HTTP 请求来格式化要传输到服务器的数据。然后,您将看到如何建立与服务器的基本连接,以及在跨浏览器环境中实现这一点所需的细节。

建立连接

所有 Ajax 进程都从连接到服务器开始。到服务器的连接通常通过 XMLHttpRequest 对象来组织。(唯一的例外是在旧版本的 Internet Explorer 中进行跨域请求时。但我们稍后会谈到这一点。现在,我们将依赖 XMLHttpRequest 对象。)

与 XMLHttpRequest 对象的通信遵循一个生命周期:

  1. 创建 XMLHttpRequest 的实例。
  2. 使用适当的设置配置对象。
  3. 通过特定的 HTTP 谓词和目的地打开请求。
  4. 发送请求。

清单 8-1 显示了如何与服务器建立一个基本的 GET 请求。

清单 8-1 。与服务器建立 HTTP GET 请求的跨浏览器方式

// Create the request object
var xml = new XMLHttpRequest();

// If we needed to do any special configuration, we would do it here

// Open the socket
xml.open('GET', '/some/url.cgi', true);

// Establish the connection to the server and send any additional data
xml.send();

如您所见,与服务器建立连接所需的代码非常简单;真的没什么大不了的。当您想要高级特性(例如检查超时或修改的数据)时,会出现一系列困难;我们将在本章的“HTTP 响应”部分讨论这些细节。当您想将数据从客户机(您的浏览器)传输到服务器时,另一组困难就出现了。这是整个 Ajax 方法最重要的特性之一。我们会在 URL 上发送简单的数据吗?发布的数据呢?更复杂的格式呢?带着这些问题(当然还有其他问题),让我们看看打包一些数据并将其发送到服务器所需的细节。

序列化数据

将一组数据发送到服务器的第一步是对其进行格式化,以便服务器可以轻松读取;这个过程叫做序列化。在序列化数据之前,我们需要问几个问题。首先:

  1. 我们在发送什么数据?我们是在发送变量和值对吗?大数据集?文件?
  2. 我们如何发送这些数据,GET?帖子?另一个 HTTP 动词?
  3. 我们使用的是什么格式的数据?有两个:application/x-www-form-urlencodedmultipart/form-data。前者有时被称为查询字符串编码,采用常见的形式 var1=val1 & var2=val2...

从 JavaScript 的角度来看,第三个问题是最重要的。第一个和第二个问题是设计的问题。它们会对我们的应用产生影响,但不一定需要不同的代码。但是我们使用哪种数据格式对我们的应用有很大的影响。

在现代浏览器中,处理多部分/表单数据信息实际上更容易。由于有了 FormData 对象,我们可以非常容易地将数据序列化为一个对象,浏览器会自动将其转换为多部分/表单数据格式。不幸的是,并不是所有的浏览器都支持规范中的每个选项。但是现在我们可以做很多事情。

表单数据对象

表单数据对象是 HTML5 中一个相对较新的提议。WHATWG 和 W3C 打算为作为 Ajax(或任何 HTTP)请求的一部分发送的信息提供一种更加面向对象、类似地图的方法。因此,FormData 对象可以被初始化为空,也可以与表单相关联。如果使用表单进行初始化,获取对包含表单 DOM 元素的引用(通常通过 getElementById)并将其传递给 FormData 构造函数。否则,如上所述,FormData 对象将为空。无论哪种方式,您都可以选择通过 append 函数添加新数据(清单 8-2 )。

清单 8-2 。对 FormData 使用 append 方法的示例

// Create the formData object
var  formDataObj= new FormData();

//append name/values to be sent to the server
formDataObj.append('first',  'Yakko');
formDataObj.append('second', 'Wakko');
formDataObj.append('third', 'Dot');

// Create the request object
var xml = new XMLHttpRequest();

// Set up a POST to the server
xml.open('POST', '/some/url.cgi');

// Send over the formData
xml.send(formDataObj);

尽管在规格上有一些不同。WHATWG 规范还包括删除、获取和设置对象值的函数。现代的浏览器都没有实现这些功能。在某种程度上,这是因为 W3C 版本的规范只有 append 功能。现代浏览器遵循这个 W3C 规范,至少目前是这样。这意味着 FormData 对象是单向的:数据传入,但只能在 HTTP 请求的另一端访问。

FormData 对象的替代方法是用 JavaScript 序列化。也就是说,将您打算传输到服务器的数据进行 URL 编码,然后作为请求的一部分发送到服务器。这并不太难,尽管有一些需要注意的地方。

让我们来看一些你可以发送给服务器的数据类型的例子,以及它们产生的服务器友好的、序列化的输出,如清单 8-3 所示。

清单 8-3 。原始 JavaScript 对象转换成序列化形式的例子

// A simple object holding key/value pairs
{
    name: 'John',
    last: 'Resig',
    city: 'Cambridge',
    zip: 02140
}
// Serialized form
name=John&last=Resig&city=Cambridge&zip=02140

// Another set of data, with multiple values
[
    { name: 'name', value: 'John' },
    { name: 'last', value: 'Resig' },
    { name: 'lang', value: 'JavaScript' },
    { name: 'lang', value: 'Perl' },
    { name: 'lang', value: 'Java' }
]

// And the serialized form of that data
name=John&last=Resig&lang=JavaScript&lang=Perl&lang=Java

// Finally, let's find some input elements
[
    document.getElementById( 'name' ),
    document.getElementById( 'last' ),
    document.getElementById( 'username' ),
    document.getElementById( 'password' )
]
// And serialize them into a data string
name=John&last=Resig&username=jeresig&password=test

您用来序列化数据的格式是在 HTTP 请求中传递附加参数的标准格式。您可能在标准的 HTTP GET 请求中看到过它们,如下所示:

http://someurl.com/?name=John&last=Resig

这些数据也可以传递给 POST 请求(比简单的 GET 要多得多)。我们将在下一节中讨论这些差异。

现在,让我们建立一个标准的方法来序列化清单 8-3 中的数据结构。在清单 8-4 中可以找到一个这样做的函数。这个函数能够序列化大多数表单输入元素,多选输入除外。

清单 8-4 。将数据结构序列化为 HTTP 兼容参数方案的标准函数

// Serialize a set of data. It can take two different types of objects:
//  - An array of input elements.
//  - A hash of key/value pairs
// The function returns a serialized string
function serialize(a) {
    // The set of serialize results
    var s = [];

    // If an array was passed in, assume that it is an array
    // of form elements
    if ( a.constructor === Array ) {

        // Serialize the form elements
        for ( var i = 0; i < a.length; i++ )
            s.push( a[i].name + '=' + encodeURIComponent( a[i].value ) );

    // Otherwise, assume that it's an object of key/value pairs
    } else {

        // Serialize the key/values
        for ( var j in a )
            s.push( j + '=' + encodeURIComponent( a[j] ) );
    }

    // Return the resulting serialization
    return s.join('&'); 
}

现在已经有了数据的序列化形式(在一个简单的字符串中),您可以看看如何使用 GET 或 POST 请求将数据发送到服务器。

建立 GET 请求

让我们使用 XMLHttpRequest 重新建立一个与服务器的 HTTP GET 请求,但是这次发送额外的序列化数据。清单 8-5 展示了一个简单的例子。

清单 8-5 。与服务器建立 HTTP GET 请求的跨浏览器方式(不读取任何结果数据)

// Create the request object
var xml = new XMLHttpRequest();

// Open the asynchronous GET request
xml.open('GET', '/some/url.cgi?' + serialize( data ), true);

// Establish the connection to the server
xml.send();

需要注意的重要部分是,序列化数据被附加到服务器 URL(用?性格)。所有的 web 服务器和应用框架都知道如何解释?作为一组序列化的键/值对。

建立员额请求

使用 XMLHttpRequest 与服务器建立 HTTP 请求的另一种方法是 POST,这涉及到一种完全不同的向服务器发送数据的方法。首先,POST 请求能够发送任何格式和任何长度的数据(不仅限于序列化的数据字符串)。

当传递给服务器时,您一直用于数据的序列化格式通常被赋予内容类型 application/x-www-form-urlencoded。这意味着您还可以向服务器发送纯 XML(内容类型为 text/xml 或 application/xml)或者甚至是 JavaScript 对象(使用内容类型 application/json)。

清单 8-6 中的给出了一个建立请求和发送额外序列化数据的简单例子。

清单 8-6 。跨浏览器意味着与服务器建立 HTTP POST 请求(并且不读取任何结果数据)

// Create the request object
var xml = new XMLHttpRequest();

// Open the asynchronous POST request
xml.open('POST', '/some/url.cgi', true);

// Set the content-type header, so that the server
// knows how to interpret the data that we're sending
xml.setRequestHeader(
    'Content-Type', 'application/x-www-form-urlencoded');

// Establish the connection to the server and send the serialized data
xml.send( serialize( data ) );

为了扩展前面的观点,让我们看一个向服务器发送非序列化格式的数据的例子。清单 8-7 显示了一个例子。

清单 8-7 。向服务器发布 XML 数据的示例

// Create the request object
var xml = new XMLHttpRequest();

// Open the asynchronous POST request
xml.open('POST', '/some/url.cgi', true);

// Set the content-type header, so that the server
// knows how to interpret the XML data that we're sending
xml.setRequestHeader( 'Content-Type', 'text/xml');

// Establish the connection to the server and send the serialized data
xml.send( '<items><item id='one'/><item id='two'/></items>' );

发送大量数据的能力(您可以发送的数据量没有限制;相比之下,GET 请求的最大数据量只有几 KB,这取决于浏览器)是非常重要的。有了它,您可以创建不同通信协议的实现,比如 XML-RPC 或 SOAP。

然而,为了简单起见,这里的讨论仅限于一些最常见和最有用的数据格式,它们可以作为 HTTP 响应使用。

HTTP 响应

XMLHttpRequest 类的第 2 级现在提供了更好的控制,告诉浏览器我们希望如何取回数据。我们通过设置 responseType 属性来实现这一点,并使用 response 属性接收请求的数据。

首先,让我们看一个处理来自服务器响应的数据的非常简单的例子,如清单 8-8 所示。

清单 8-8 。与服务器建立连接并读取结果数据

// Create the request object
var request = new XMLHttpRequest();

// Open the asynchronous POST request
request.open('GET', '/some/image.png', true);

//Blob is a Binary Large Object
request.responseType = 'blob';

request.addEventListener('load', downloadFinished, false);

function downloadFinished(evt){
    if(this.status == 200){
        var blob = new Blob([this.response], {type: 'img/png'});
    }
}

在这个示例中,您可以看到如何接收二进制数据并将其转换为 PNG 文件。responseType 属性可以设置为以下任意值:

  • 文本:结果以文本字符串的形式返回
  • ArrayBuffer:结果以二进制数据数组的形式返回
  • 文档:假设结果是 XML 文档,但也可能是 HTML 文档
  • Blob:结果作为原始数据的类似文件的对象返回
  • JSON:结果作为 JSON 文档返回

现在我们知道了如何设置 responseType,我们可以看看如何监控请求的进度。

监控进度

正如我们之前看到的,使用 addEventListener 使我们的代码易于阅读并且非常灵活。这里,我们在请求对象上使用相同的技术。无论你是从服务器下载数据还是上传数据,你都可以监听这些事件,如清单 8-9 所示。

清单 8-9 。使用 addEventListener 侦听服务器请求的进度

var myRequest = new XMLHttpRequest();

myRequest.addEventListener('loadstart', onLoadStart, false);
myRequest.addEventListener('progress', onProgress, false);
myRequest.addEventListener('load', onLoad, false);
myRequest.addEventListener('error', onError, false);
myRequest.addEventListener('abort', onAbort, false);

//Must add eventListeners before running a send or open method

myRequest.open('GET', '/fileOnServer');

function onLoadStart(evt){
    console.log('starting the request');
}

function onProgress(evt){
     var currentPercent = (evt.loaded / evt.total) * 100;
     console.log(currentPercent);
}

function onLoad(evt){
    console.log('transfer is complete');
}

function onError(evt){
  console.log('error during transfer');
}

function onAbort(evt){
  console.log('the user aborted the transfer');
}

与以前相比,您现在可以更好地了解您的文件发生了什么。使用 loaded 和 total 属性,您可以计算出正在下载的文件的百分比。如果出于某种原因,用户决定停止下载,您将会收到中止事件。如果文件有问题,或者它已经完成加载,您将会收到错误或 load 事件。最后,当您第一次向服务器发出请求时,您会收到 loadstart 事件。现在让我们快速看一下超时。

检查超时和跨源资源共享

简单地说,超时允许您设置一个时间,让应用等待多长时间,直到它停止寻找服务器的响应。很容易设置一个暂停时间,并监听它。

清单 8-10 展示了如何在你自己的应用中检查超时。

清单 8-10 。检查请求超时的示例

// Create the request object
var xml = new XMLHttpRequest();

// We're going to wait for a request for 5 seconds, before giving up
xml.timeout = 5000;

//Listen for the timeout event
xml.addEventListener('timeout', onTimeOut, false);

// Open the asynchronous POST request
xml.open('GET', '/some/url.cgi', true);

// Establish the connection to the server
xml.send();

默认情况下,浏览器不允许应用向服务器发出请求,除非该站点来自该服务器。这可以保护用户免受跨站点脚本攻击。服务器必须允许请求;否则,将出现 INVALID_ACCESS 错误。服务器给出的头看起来像这样:

Access-Control-Allow-Origin:*
   //Using a wild card (*) to allow access from anyone.
Access-Control-Allow_origin:http://spaceappleyoshi.com
   //Allowing from a certain domain

摘要

我们现在有了在服务器上处理数据的坚实基础。我们可以告诉服务器我们期望得到什么样的结果。我们还可以监听一些事件,这些事件会告诉我们文件传输的进度,或者在传输过程中是否有错误。最后,我们讨论了超时和跨源资源共享(CORS)。在下一章中,我们将看看一些用于网页制作的开发工具。

九、网站开发工具

多年来,开发网站的工具已经成熟。我们从使用记事本这样的简单编辑器发展到了 WebStorm 这样的全面开发环境。我们也有像 JQuery 这样的库。我们可以使用 Handlebars 作为模板引擎,AngularJS 作为完整的 MVC 框架。还有单元测试框架和版本控制系统来帮助我们更好更快地完成工作。那么现在我们有了所有这些可用的东西,我们如何把它们组织起来呢?

为了回答这个问题,我们将把它分成两部分。首先,我们将看看创建网站的工具,然后看看跟踪网站变化的工具。为了创建一个站点,我们将探索 Yeoman、Grunt 、Bower 和节点包管理器(NPM) 。为了跟踪变化,我们将使用 Git 。

所有这些工具都可以协同工作,所以让我们来分析一下每个工具的作用:

  • Bower 是一个包管理系统。它的目的是确保您的项目所依赖的所有客户端代码都已经下载并安装。地点:http://bower.io/
  • Grunt 就是所谓的构建工具。它允许您自动化许多类型的任务,包括单元测试、林挺(检查 JavaScript 错误),以及将您的代码添加到版本控制中。它还可以用于将您的代码部署到服务器。地点:http://gruntjs.com/
  • 约曼就是所谓的脚手架工具。它创建了创建项目的基本版本所需的文件和文件夹。然后,它使用 Bower 收集项目所依赖的所有代码。最后,它使用一个构建工具(比如 Grunt)来自动化任务。这是通过使用发电机来实现的。地点:http://yeoman.io/
  • 节点包管理器(NPM) ,顾名思义,管理包。这些包运行在 Node.js 之上。随着 Node 变得越来越流行,其中一些包是为客户端开发而开发的,而不仅仅是服务器端,在服务器端使用 Node.js。地点:https://nodejs.org/
  • Git是一个版本控制系统。如果你听说过 Subversion 或 Perforce 这样的工具,Git 也是类似的。它会跟踪你处理的所有文件,并告诉你文件之间的区别。地点:http://git-scm.com/

搭建你的项目

计算机非常擅长做人们不想做的任务,它们可以一遍又一遍地做,而不会感到厌烦。没有人希望每次创建项目时都为图像、CSS 和 JavaScript 文件创建文件夹。有很多我们认为理所当然的小任务现在可以自动化。启动一个项目,只需一个命令就可以完成所有的文件夹,这不是很好吗?

这就是脚手架背后的理念。因为大多数网站都是以同样的方式组织的,所以没有必要手工设计结构。Yeoman 可以让你搭建任何你想要的网络项目。利用来自社区的最佳实践,Yeoman 使用发电机来快速方便地建立我们的项目。

生成器实际上是任何人都可以制作的模板。有一些团队赞助项目来创建“官方”发电机,但是如果那个不做你想要的事情,其他人可能已经做了一个。发电机也是开源的,所以你可以看看一个人的引擎盖下,看看它是如何制造的。为了使用 Yeoman,我们首先需要安装 Node.js。

NPM 是一切的基础

节点包管理器(NPM )让您能够管理应用中的依赖关系。这意味着,如果你的项目需要代码(比如说 JQuery),NPM 可以很容易地添加到你的项目中。这也是我们将要安装的大多数工具在幕后运行的内容。NPM 是 Node.js 的一部分,node . js 是一个开源的跨平台环境,用于使用 JavaScript 制作服务器端应用。尽管我们不打算在这里创建 Node.js 项目,但我们确实需要安装node。有几种方法可以做到这一点;对于我们的例子,我们将尽量使它简单。

当你去nodejs.org的时候,网站会算出你用的是什么操作系统。单击安装按钮下载并运行安装程序。

安装完成后,您可以进入终端模式(Mac、Linux)或命令提示符(Windows)并输入node –version。您应该会看到当前版本的node显示在窗口中。

安装好之后,现在我们可以得到我们需要的一切。安装约曼时,键入npm install –g yo,对于咕噜型npm install –g grunt-cli,对于鲍尔型npm install –g bower。使用-g意味着安装将在全球范围内进行;创建新项目时,可以在任何文件夹中运行这些实用程序。cli代表命令行界面。在我们的练习中,我们将花时间在命令行上。习惯了是好事,值得努力。现在我们可以安装一个发电机,并开始寻找其他工具。

发电机

正如我们之前谈到的,生成器实际上是描述站点结构的模板。您可以通过向 Yeoman 传递不同的参数来调整这些模板。在Yeoman.io你可以找到一个生成器列表和 GitHub 库的链接。存储库有关于如何使用生成器的所有说明。例如,如果您想使用 AngularJS 创建一个站点,您可以输入

npm install –g generator-angular

这将安装 AngularJS 发生器。如果您想要一个 AngularJS 站点,并且还想添加 Karma(一个 JavaScript 测试运行器)来帮助运行您的单元测试,安装应该是这样的:

npm install –g generator-angular generator-karma

现在您已经安装了一个生成器,通过在命令行键入yo,您可以查看已安装的生成器列表并更新它们。从这里你也可以安装新的发电机。

此时,您应该为您的项目创建一个文件夹,当您在该文件夹中时,下一个命令应该是

yo angular

约曼会开始问你关于你的应用的问题。比如它会问你是否愿意使用 Sass,如图图 9-1 所示。

9781430263913_Fig09-01.jpg

图 9-1 。约曼建立了一个 AngularJS 网站

您将被问及其他一些关于您希望如何设置项目的问题。一旦完成,Bower 将从 GitHub 获取您需要的所有库的最新版本,并为您一起搭建您的项目。安装好所有东西后,输入以下命令来查看站点的运行情况:

grunt serve

这一次我们使用 Grunt 在你当前的文件夹中创建一个本地 web 服务器,并提供主页服务(图 9-2 )。

9781430263913_Fig09-02.jpg

图 9-2 。约曼在本地服务器上运行 AngularJS

这就是你需要做的。你现在已经有了一个可以运行的站点。这是将我们的代码置于版本控制中的好时机。

版本控制

变化是不断的。我们的文件被一遍又一遍地更新。当我们工作时,东西有时会坏掉。在某些情况下,简单的撤销就可以了。在其他时候,我们可能需要恢复到以前的状态,特别是在与团队合作并且有许多变化的时候。一个改变可能会破坏整个网站,并且很难找到问题所在。这就是版本控制非常有用的地方。

Git 是我们将要使用的版本控制系统。它很受欢迎,也有 GUI 客户端。正如我们对 Node.js 示例所做的那样,我们将采用最快的方式来安装它。为此,请转到git-scm.com下载并安装 Git。

安装完成后,您可以使用命令行对其进行配置。要添加您的身份,请键入git config –global user.name "your name" and git config –global user.email your@email.com

现在您已经安装并配置了 Git,我们将快速添加文件,然后在本地提交它们。确保您位于项目文件夹中。在该文件夹中键入git init。这样就创建了一个. Git 文件夹,其中包含了项目的所有信息。此文件夹通常是不可见的,因此如果您想看到它,可能需要更改操作系统中的一些设置。接下来,让我们检查提交的状态。键入git status,您可以看到在这一点上,没有文件被添加到版本控制中(图 9-3 )。

9781430263913_Fig09-03.jpg

图 9-3 。文件尚未添加到 Git

添加文件、更新和首次提交

接下来我们添加文件,这样 Git 就可以跟踪它们。向存储库添加文件就像输入git add file name/folder一样简单。Git 将开始跟踪文件。在图 9-4 中,我们通过输入git add app/来添加应用文件夹。您可以再次检查状态并查看结果。

9781430263913_Fig09-04.jpg

图 9-4 。应用文件夹已经被添加到 Git

继续添加文件,尤其是bower.jsonpackage.json。这些文件跟踪您的依赖模块以及这些模块的版本。Gruntfile.js会有你能运行的所有任务。我们之前通过输入grunt serve运行了一个任务。那个任务为我们运行一个本地服务器。

作为一个最佳实践,node_modulesbower_components文件夹不会被添加到版本控制中。您可以稍后使用npm installbower install命令重新安装它们。

您不想添加到 Git 的文件在.gitignore文件中定义。您可以修改该文件,以包含文件类型或任何您希望 Git 忽略的内容。

我们讨论了文件如何随时间变化,所以你可能会问 Git 是如何知道文件何时发生变化的。首先,需要将文件添加到存储库中。我们用git add命令做到了。然后,当发生变化时,您可以再次请求状态。这将列出自上次添加以来已更改的所有文件。请记住,在进行更改之前,您需要 Git 了解该文件,这样就可以随时跟踪更改。在图 9-5 的中,我们可以改变README.md文件来说明我们的观点。

9781430263913_Fig09-05.jpg

图 9-5 。Git 可以看到文件何时被修改

文件更改后,您可以像以前一样再次添加它。下次您检查状态时,它将回到跟踪项目列表中。

现在 Git 已经跟踪了所有文件,可以提交所有文件了。将提交视为项目当前状态的快照(图 9-6 )。如果项目发生了任何事情,您总是可以恢复到上一个状态。

9781430263913_Fig09-06.jpg

图 9-6 。提交并显示在日志中的文件

要提交,请键入git commit –m "notes about the commit"–m标志代表消息。或者,您可以使用文本编辑器来制作您的消息。如果你想查看信息的历史,你可以输入git log

我们现在有一种方法来跟踪本地机器上的变化。如果你是一个孤独的开发者,这将会很好。

但是,如果您想要共享代码或与团队合作,您将需要添加一个服务器端组件。最流行的两个选项是 GitHub ( https://github.com/)和 bit bucket(https://bitbucket.org/)??。使用这两个选项中的任何一个,除了本地机器上的文件之外,您还可以拥有一个远程存储库。

摘要

我们希望在读完这一章后,你会看到大量的资源可供你使用。使用 Yeoman generators 快速组合一个网站的能力,将为你在组合新项目时节省时间和精力。Yeoman 也可以作为一个学习工具来理解不同框架是如何工作的。

我们只是给出了 Git 的一个基本概述。这个主题本身就可以成为一本书。幸运的是,有斯科特·沙孔和本·施特劳布的 Pro Git 。在 Git 主页上也可以找到它的链接(http://git-scm.com/ ))。它是在线的或者是一个数字文件。现在我们可以很快地把一个网站放在一起,让我们看看第十章中一个非常流行的框架 AngularJS。

十、Angular 和测试

在前一章中,你学习了如何使用当前的一套工具来快速组合一个站点,并使用版本控制来跟踪你使用的所有文件以及它们之间的差异。在这一章中,我们将深入研究并理解像 Angular 这样的框架是如何工作的。

简而言之,框架帮助你以一种更有组织性和更容易维护的方式构建大型应用。使用框架的另一个好处是团队的学习曲线更短。一旦新成员学会了这个框架,他们对整个网站的工作原理就有了更好的理解。在我们的例子中,我们将对 AngularJS 进行高层次的观察。

在撰写本文时,Angular 的当前版本是 1.4.1。信息可以在https://angularjs.org/找到。关于 Angular 2 的信息可以在https://angular.io/找到。

Angular 试图解决的问题之一是使开发动态应用变得容易。HTML 本身并不是为制作单页应用而设计的。Angular 提供了一种以快速学习的方式开发现代 web 应用的方法。它通过将应用的每个部分保持独立来实现这一点,这样每个部分都可以独立于其他部分进行更新。这种架构模式被称为模型-视图-控制器(MVC)。你会发现其他框架,如 Backbone 和 Ember,也是以类似的方式工作的。

在第九章中,我们介绍了一些可以帮助我们提高效率的开发工具。自耕农 ( http://yeomanio/)。)使用社区构建的生成器快速开发基本站点运行所需的所有文件和文件夹。Grunt ( http://gruntjs.com/)用于自动化任务,例如为生产就绪的站点进行单元测试和优化文件。本章假设两种工具都已安装。请参考上一章或列出的站点,了解有关安装它们的信息。

要创建新的 Angular 项目,键入your angular。作为回应,约曼问了一些关于你想如何建立这个项目的问题:

  • Would you like to use Sass (with Compass)? Sass (http://sass-lang.com/)stands for Syntactically Awesome Style Sheets. Sass gives you features like nesting selectors and using variables to develop style sheets. Compass is a tool written in Ruby that uses Sass files and adds features like generating sprite sheets out of a series of images.

    同意这个选项,你将得到一个默认使用 Twitter Bootstrap 风格的 SCSS 文件。如果你选择否,Yeoman 会给你一个普通的 CSS 文件,使用相同的 CSS 选择器。

  • Would you like to include Bootstrap? Twitter Bootstrap (http://getbootstrap.com/``) helps you develop the user interface for your website. Bootstrap can help make your site responsive so it can look good on multiple devices and gives you items like buttons, an image carousel, and other user interface components.

    如果您选择使用这个工具,那么 Yeoman 会询问是否使用 Sass 版本的 Bootstrap。

  • 你想包括哪些模块?模块给予角额外的能力。例如,angular-aria模块提供可访问性特性,而angular-route给你添加深度链接特性的能力。您可以选择添加或删除任何模块。模块也可以在以后手动添加。

一旦你回答了这些问题,所有需要的文件都将被下载,Grunt 将启动一个本地服务器,加载默认的浏览器http://localhost:9000,如图图 10-1 所示。

9781430263913_Fig10-01.jpg

图 10-1 。港口 9000 开始运行

从这里我们可以看到构成这个项目的文件夹。app文件夹包含了我们的主应用,这是我们将要开始的地方。

这些文件夹是非常标准的,你可以在 HTML 站点中找到。在scripts文件夹中,事情开始变得有趣起来。

scripts f 的根源是app.js年长。这是 Angular 的主要应用文件。打开这个文件来看看 Angular 是如何被引导的。在图 10-2 中,你会发现应用的名称chapter10app (因为这是创建该应用的文件夹的名称)。这与index.html中的ng-app指令一起工作。指令赋予 DOM 元素额外的能力。在这种情况下,它告诉 Angular 应用从这里开始,并被称为chapter10app

9781430263913_Fig10-02.jpg

图 10-2 。使用ng-app指令告诉 Angular 应用的根元素在哪里。

当你查看app.js时,你会看到在应用的名字后面加载了许多模块,这给 Angular 提供了额外的能力。其中一个模块被命名为ngRoute;它将允许您处理应用的 URL。.config方法使用$routeProvider来理解 URL,并通过使用一系列when语句用控制器加载正确的模板。

when语句使您能够为应用定制 URL。在这个例子中,如果你输入/about,Angular 会知道加载about.html模板并使用AboutCtrl作为控制器。我们来详细解释一下这是什么意思。

视图和控制器

我们已经讨论过 Angular,像其他框架一样,使用 MVC 模式。这意味着应用被分成三个不同的部分。

  • 模型:存储应用的数据。
  • 视图:创建模型数据的表示;例如,生成图表来表示数据。
  • 控制器:向模型发送命令以更新数据。还向视图发送命令以更新模型数据的显示。

文件夹views包含 HTML 模板,可以用来自模型的数据更新。controllers文件夹包含与模型和视图文件通信的 JavaScript 文件。

我们来看一下about.html文件。这里我们将添加一个按钮,并让控制器使用它。

打开about.html,在views文件夹中找到。在它的内部,添加一个按钮标签。在这个按钮标签中,我们将使用另一个指令,它将让 Angular 知道按钮何时被点击。

我们需要添加指令到按钮上。如清单 10-1 中的所示,键入ng-click='onButtonClick()'。这将由我们的控制器解决,让我们分离应用的可视部分和业务逻辑。

清单 10-1 。使用ng-click指令定义方法

<button ng-click="onButtonClick()">Button</button>

打开controllers文件夹中的about.js。控制器允许您添加应用这一部分工作所需的所有业务逻辑。在controller方法中,你可以看到名字AboutCtrl,它与我们在app.js文件中看到的相匹配。您在控制器方法中看到的另一个东西是 $scope属性。

$scope允许您向正在使用的视图添加方法或属性。这里我们将处理一个在 HTML 文件中声明的函数。因为ng-click="onButtonClick()"是在 HTML 代码中定义的,所以它是这个控制器范围的一部分。

清单 10-2 显示了我们如何让控制器和 HTML 一起工作。

清单 10-2 。使用控制器定义 HTML 文件中声明的函数

angular.module('chapter10App')
.controller('AboutCtrl', function ($scope, $http) {
    $scope.awesomeThings = [
      'HTML5 Boilerplate',
      'AngularJS',
      'Karma'
    ];
  $scope.onButtonClick = function(){
    console.log('hello world');
  };
   });

如果应用目前正在浏览器中运行,它应该会看到 JavaScript 文件已经更新,并重新编译一切。完成后,您可以单击按钮,查看控制台,并看到消息。

否则,转到命令行,在应用的主文件夹中键入grunt serve

这仅仅是能够将应用的视图与其业务逻辑分离的开始。例如,如果您想要显示一个项目列表,该怎么办?

回到控制器,我们将在我们的作用域中添加一个 JSON 对象;清单 10-3 显示了代码。

清单 10-3 。分配给about控制器范围的数据

$scope.bands = [
{'name':"Information Society", 'album':"_hello world"},
{'name':"The Cure", 'album':"Wish"},
{'name':"Depeche Mode", 'album':"Delta Machine"}];

现在我们有了需要的数据,下一步是将数据传递给 HTML 模板。回到about.html文件,我们将使用 ng-repeat指令(清单 10-4 )。

清单 10-4ng-repeat指令遍历控制器提供的数据,以显示列表中正确的项目数

<ul>
<li ng-repeat="band in bands">{{band.name}}<p>{{band.album}}</p></li>
</ul>

此时,你的页面看起来应该类似于图 10-3 。

9781430263913_Fig10-03.jpg

图 10-3 。呈现在页面上的来自控制器的数据

到目前为止,我们已经能够使用指令将控制器连接到 HTML 视图中定义的项目。我们的数据也被定义在控制器中。所以,你可能会问,假设我们想从外部来源获取数据;我们如何调用远程服务器并显示结果?我们将在下一节讨论这个问题。

远程数据源

让我们以按钮方法为例,用它来获取一些远程数据。

我们将使用$http服务,它将为我们处理远程呼叫。这类似于 JQuery 中的 AJAX 方法。为了利用该服务,我们首先需要将其添加到controller方法中。

controller方法中添加$http服务。它现在看起来应该类似于清单 10-5 。

清单 10-5 。将$http服务添加到AboutCtrl控制器

.controller('AboutCtrl', function($scope, $http){
$scope.awesomeThings = [
'HTML5 Boilerplate',
'AngularJS',
'Karma'
];
});

现在控制器可以使用服务了,我们可以通过onButtonClick方法来使用它。我们将使用get方法,它匹配其余的get动词。有关 REST 方法的更多信息,请查看这篇维基百科文章:

http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods

我们还将使用 JSONPlaceholder,这是一个让您测试 REST 请求并返回假数据的服务。在onButtonClick方法中,删除现有代码并添加清单 10-6 中的代码。

清单 10-6 。进行 HTTP 调用并将结果分配给results属性

$http.get('http://jsonplaceholder.typicode.com/photos').success(function(data){
    $scope.results = data;
});

查看这段代码,我们看到我们正在使用get方法,但是我们也在监听success事件。如果调用成功,我们使用一个匿名函数将结果赋给一个名为results的属性。

现在我们有了结果,我们可以更新模板,不仅显示文本,还显示从服务返回的缩略图。我们通过对image标签使用另一个指令来做到这一点。

打开about.html并更新现有代码。删除之前的列表,并将清单 10-7 中的 fcode 添加到模板中。

清单 10-7 。基于 REST 服务返回的结果创建一个无序列表

<ul>
    <li ng-repeat="result in results">
    <p>ID: {{result.id}}</p>
    <p>{{result.title}}</p>
    <p><img ng-src="{{result.thumbnailUrl}}"/></p>
</li>
</ul>

这里我们使用了ng-src指令来告诉image标签在哪里可以找到列表中的每张图片。就像前面的例子一样,这将遍历整个结果列表,并将它们全部显示在屏幕上。使用这个指令的一个好处是image标签在收到数据之前不会尝试显示图像。所以我们只需要从 REST 服务中获取结果。

此时,页面看起来应该类似于图 10-4 。

9781430263913_Fig10-04.jpg

图 10-4 。显示 REST 服务的结果

只需几个步骤,我们就有了一个能够从远程数据源检索数据并在需要时将其呈现给浏览器的站点。Angular 有很多特性,但是我们还会介绍一个。我们的下一课将涵盖路线。

路线

Routes 允许用户为我们的应用创建自定义的 URL,并给我们标记一个页面的能力,以便我们以后可以直接访问它。

我们在app.js文件中看到了一个这样的例子。.config方法使用了$routeProvider API。在一系列的when语句中,我们能够计算出将加载什么样的 HTML 模板,以及哪个控制器将与该模板一起使用。

既然我们已经很好地理解了它是如何工作的,那么如果您想将参数传递给应用呢?例如,假设我们只想显示上一个例子中的一篇文章。在这里,我们将创建一条路由来实现这一目的。

如果您在前面的课程中使用 Yeoman 创建了该应用,请转到命令行并键入:

yo angular:route post

使用该命令,Yeoman 将通过添加一条新路线来更新app.js文件。它还将为您的控制器创建一个新的 JavaScript 文件,并为视图创建一个 HTML 文件。对于一个命令来说还不错。

如果应用正在运行,您可以转到浏览器并键入:

http://localhost:9000/#/post

您应该看到 post 视图已经准备好了。

这里的目标是根据添加到我们的 URL 的数量来加载一篇文章。例如,如果 URL 看起来像这样:

http://localhost:9000/#/post/4

基于我们在上一个例子中使用的服务,您应该会看到列表中的第四篇文章。

路线参数

为了实现这一点,我们需要让我们的路由功能更加灵活。打开app.js;这里我们将更新 post route,这样它就可以在 URL 中加入变量。

它应该从:

/post

致:

 /post/:postId

通过添加:postId,我们创建了一个可以在控制器中使用的变量。这个变量将代表 URL 中的数字。为了说明这一点,让我们更新控制器。

打开post.js,你会看到在controller方法中有一个使用$scope的匿名函数。在我们的其他例子中,我们看到$scope给了我们控制 HTML 模板的能力。我们将添加一个名为$ routeParams的额外参数,这样我们就可以在 URL 中访问我们的变量。

现在我们可以从 URL 中获取变量,并将其分配给$scope。这将使我们能够在更新模板后显示它。

controller方法中键入以下内容:

$scope.postId = $routeParams.postId;

要在屏幕上看到我们的号码,我们可以快速更新模板。

打开views文件夹中的post.html文件。在这里我们可以快速更新这个模板。首先删除段落标记之间的副本,使其看起来像这样:

<p>{{postId}}</p>

完成后,你可以在url中输入一个数字,并看到它显示在屏幕上。此时,浏览器应该看起来像图 10-5 。

9781430263913_Fig10-05.jpg

图 10-5 。基于 URL 在屏幕上显示变量

还不算太糟。所以现在我们只需要将它连接到一个 GET 请求并显示结果。这里的代码将与我们之前所做的非常相似。在 post 控件中,我们需要添加$HTTP服务,这样我们就可以进行 REST 调用。清单 10-8 显示了代码。

清单 10-8 。添加了$ routeParams$http服务的完整 PostCtrl

.controller('PostCtrl', function($scope, $routeParams, $http){
$scope.awesomeThings = [
'HTML5 Boilerplate',
'AngularJS',
'Karma'
];
});

就在这个下面,我们将像以前一样进行相同的 REST 调用,但是这次添加了postId变量,如清单 10-9 中的所示。

清单 10-9 。使用$http服务和routeParam获得一个结果

$http.get('http://jsonplaceholder.typicode.com/photos/'+$routeParams.postId).
success(function(data){
  $scope.results = data;
});

至于 HTML 模板(清单 10-10 ,我们将使用与about示例相同的代码;唯一的区别是,我们将删除ng-repeat指令,并确保我们使用单词results

清单 10-10 。用于显示文章的 HTML 模板

<ul>
    <li>
    <p>ID: {{results.id}}</p>
    <p>{{results.title}}</p>
    <p><img ng-src='{{results.thumbnailUrl}}'/></p>
</li>
</ul>

现在,当你更新地址栏中的数字时,你应该会看到一个新的帖子(图 10-6 )。

9781430263913_Fig10-06.jpg

图 10-6 。基于URL变量显示的单个帖子

应用测试

当您阅读 Angular 的文档时,您将会看到如何编写覆盖您正在构建的应用的不同部分的测试。

测试有助于确保您编写的代码始终是稳定的。例如,如果您需要对一个函数进行更改,即使您更改了该函数的工作方式,它仍然会给出相同的结果。如果这种改变在应用的其他地方产生了意外的结果,测试应该让你知道。

因此,在这种情况下,如何测试 Angular 应用呢?我们将考虑两种类型的测试:单元测试端到端(E2E)测试

单元测试

当你写一个 f 函数时,你要考虑它应该接收什么参数,它用这些信息做什么,以及结果应该是什么。对该代码单元进行测试将确保它按照您期望的方式运行。

如果你一直在使用前几章中的约曼生成的 Angular 版本,我们需要安装一些额外的项目来让测试工作。转到命令行并键入

npm install grunt-karma -–save-dev

这将安装因果报应。

Karma 在 Angular developer guide 中被描述为“一个 JavaScript 命令行工具,可用于生成一个 web 服务器,该服务器加载应用的源代码并执行测试。”简而言之,Karma 将启动浏览器,根据您编写的测试运行代码,并给出结果。如果你没有安装想要测试的浏览器(例如,如果你使用的是 Mac),你可以使用 BrowserStack ( https://www.browserstack.com/)或 Sauce Labs ( https://saucelabs.com/ ))这样的服务。关于因果报应的最新信息,请访问http://karma-runner.github.io/

接下来我们需要安装 PhantomJS 。类型

npm install karma-phantomjs-launcher -–save-dev

在命令行中。

PhantomJS 是一个无头的 ?? 浏览器,一个没有用户界面的浏览器。通过包含它,您可以在浏览器中运行您的应用,并且所有命令都将从命令行执行。有关幻想曲的最新信息,请访问http://phantomjs.org/

最后,键入

npm install karma-jasmine -–save-dev

在命令行。这将安装 Jasmine ,我们将使用它来测试我们的应用。您可以在http://jasmine.github.io/找到文档。

现在,让我们通过输入grunt test来确保一切正常。这将运行test文件夹中的测试。

添加新测试

使用 Yeoman 的一个好处是,当您创建新的控制器时,它也会为单元测试创建相应的文件。

看主文件夹。在那里你会找到一个test文件夹。在这个文件夹中将会有一个spec文件夹,包含一个controllers文件夹,其中包含了对已经创建的每个控制器进行单元测试的文件。

打开about.js让我们看看如何测试控制器。

在我们开始编写测试之前,让我们先看看现有的代码,了解一下发生了什么。

在顶部有一个describe方法,用于谈论即将编写的测试。describe方法可以用来在高层次上描述将要测试的一切。

接下来有一个beforeEach方法,它将在每次测试之前运行。这使我们能够通过将应用作为一个模块加载来访问整个应用。

创建两个变量,AboutCtrl和 scope;然后我们创建另一个beforeEach方法,它给变量分配控制器的值和控制器内部的范围,就像我们直接使用控制器一样。

最后,我们可以编写我们的测试,我们用一系列的it方法来描述。这有助于使测试易于文档化。在这里你描述这个函数应该如何工作。

默认测试有消息"should attach a list of awesomeThings to the scope";然后它用一个expect方法运行一个函数。这种方法很重要,因为它给你一个测试预期结果的机会。在这种情况下,我们检查数组awesomeThings的长度,预计它是 3。如果不是,测试将失败。

我们现在可以测试之前创建的数组带的长度。添加一个新的it方法,如清单 10-11 中的所示。

清单 10-11 。对应该在 about控制器中的波段数进行单元测试

it('should have at least 3 bands', function(){
    expect(scope.bands.length).toBe(3);
});

如果您在命令行输入grunt test,这个测试应该会通过。如果在expect方法中有不同的数字,例如 2,测试将会失败。你可以在图 10-7 中看到。

9781430263913_Fig10-07.jpg

图 10-7 。测试预期两个项目,但收到三个项目

我们还可以检查数组中某一项的值(清单 10-12 )。

清单 10-12 。检查数组中的值的单元测试

it('should have the second album be Wish', function(){
    expect(scope.bands[1].album).toEqual('Wish');
});

这是一个如何测试控制器的快速概述。从这里,您可以测试已经编写的方法并评估结果。Jasmine 给了你很多方法来确保你写的代码是可靠的。让我们再来看看测试,打开post.js并测试我们的 HTTP 请求。

使用$httpBackend测试 HTTP 请求

在前面的例子中,我们测试了一些与控制器相关的数据。在这种情况下,数据来自控制器内部。在大多数应用中,您将从远程数据源获取数据。那么,如何在无法控制数据源的情况下编写测试呢?在这种情况下,您使用$httpBckend来确保您创建的请求独立于服务工作。

这个测试将重现我们对$routeParams所做的一切。它将是独立的,实际上不会调用服务器。

首先,我们将添加几个变量。除了PostCtrlscope,再增加httpBackendrouteParams。在这种情况下,我们不引用指令,所以您不需要添加美元($)符号。

第二个beforeEach方法是我们当前初始化控制器的地方。这是我们将添加指令的地方,就像在真正的控制器中一样;这里加上$httpBackend$routeParams

现在我们给之前创建的变量赋值。在浏览器中,我们可以通过给 URL 中的postId赋值来获得一篇文章。我们模拟它,如清单 10-13 所示。

清单 10-13 。赋值,这样我们就可以模拟从浏览器中获取值(第一部分)

beforeEach(inject(function($controller,$rootScope,$httpBackend,$routeParams){
  scope = $routeScope.$new();
  routeParams = $routeParams;
  routeParams.postId = 1;
  httpBackend = $httpBackend;
  httpBackend.expectGet('http://jsonplaceholder.typicode.com/photos/'+routeParams.postId).respond({id:'1', title:'title 1', thumbnailUrl:'myImage.png'});
PostCtrl = $controller('PostCtrl', {
  $scope: scope
});
httpBackend.flush();
});

因为我们没有将它加载到浏览器中以确保它能够工作,所以我们将把postId的值硬编码为 1。然后,使用httpBackend,我们以与控制器中相同的方式模拟调用。在这种情况下我们使用expectGet的方法。这将模拟一个 HTTP GET请求。如果你想做一个POST请求,你可以使用expectPost方法。

r esponse方法给了我们可以测试的有效负载。这里我们传回一个简单的对象,就像 API 所传递的一样。

在范围被分配后,我们再次看到使用flush方法的httpBackend对象。这将使响应可用于测试,就像您进行了 HTTP 调用一样。

现在开始测试。正如在另一个例子中一样,一系列的it方法描述了您所期望的。

让我们首先确保我们只从服务器返回一个结果(清单 10-14 )。

清单 10-14 。赋值,这样我们就可以模拟从浏览器中获取值(第二部分)

it('should be a single post', function(){
    expect(scope.results).not.toBeGreaterThan(1);
});

Jasmine 使测试易于阅读。我们有结果,只是想确定我们只有一个对象。

如果我们想确保这个对象有一个 ID 属性,我们可以使用清单 10-15 中的代码。

清单 10-15 。检查 ID 属性

it('should have an id', function(){
    expect(scope.results.id).toBeDefined();
}

就像以前一样,我们可以添加测试,当我们向控制器添加更多功能时,这些测试将允许我们理解控制器。开发代码时首先编写测试的过程被称为 测试驱动开发。在这种方法中,您首先编写一个测试,知道它会失败,然后返回并编写最少的代码来使测试工作。之后,您可以根据需要进行重构。Jasmine 用于测试代码单元。它还用于测试与浏览器的集成。那么如何在多个浏览器上模拟按钮点击呢?毕竟,从 web 开发的历史来看,我们知道即使是看起来简单的东西有时在某些浏览器中也是行不通的。这就是量角器的用武之地。

用量角器进行端到端测试

量角器 ( http://angular.github.io/protractor)是一个让你运行真正的浏览器并在其中运行测试的工具。例如,您可以确保当一个按钮被单击时,它提交一个表单。与测试小代码单元的单元测试不同,端到端(E2E)测试 让你可以针对 HTTP 服务器测试应用的所有部分。这类似于你在浏览器中打开网站,并确保一切正常。这种方法的好处在于它是自动化的。

像这样的任务应该自动化。量角器能够做到这一点,因为它是建立在 WebDriver 之上的,web driver 是一种用于在浏览器内部自动测试的工具。量角器也有支持 Angular 的特性,所以你只需要很少的配置。

让我们安装量角器并进行一些测试。在命令行中,键入:

npm install –g protractor –-save-dev

和其他节点包一样,该行将在全球范围内安装量角器,因此您可以在其他项目中使用它。

我们需要做一些配置来让它工作。让我们创建config文件。

创建一个新文件,我们将其命名为protractor.conf.js,并保存在紧挨着karma.config.js文件的test文件夹中。

在这个文件中,我们将向量角器提供一些关于在哪里可以找到 Selenium 服务器、运行什么浏览器以及测试文件在哪里的信息。清单 10-16 显示了代码。

清单 10-16 。量角器 r 的基本配置文件

export.config = {
    seleniumAddress: 'http://localhost:4444/wd/hub',
    multiCapabilities: [{browserName: ‘firefox’},{browserName: ‘chrome’}],
    baseUrl: 'http://localhost.9000',
    framework: 'jasmine',
    specs: ['protractor/*.js']
};

这里有几个东西要解包,我们来看看。

属性告诉量角器 Selenium 服务器在哪里运行。接下来,multiCapabilities属性告诉量角器在哪些浏览器上运行测试。如您所见,这是一个对象数组,列出了每个浏览器的名称。

因为我们是在本地测试,所以我们只能测试安装在机器上的浏览器。所以你运行的是 Mac 就不能测试 IE。如果您需要测试像 IE 或移动浏览器这样的浏览器,您可以添加允许您连接到 SauceLabs 或 BrowserStack 的属性。

接下来我们有baseUrl属性,它告诉量角器哪个服务器正在托管被测试的应用。当您运行测试时,站点运行在本地服务器上是很重要的。属性被设置为Jasmine,因为这是我们测试使用的框架。

属性很重要,因为它告诉量角器哪个文件夹中有测试。在我们的例子中,它在protractor文件夹中,我们使用通配符告诉它查看该文件夹中的任何 JavaScript 文件。

我们现在有量角器设置。是时候写一些测试了。在tests文件夹中创建一个Protractor文件夹。这里我们将编写一个基本的测试。

Protractor文件夹 ?? 中创建一个名为app-spec.js的文件。格式将与我们在前面的例子中所做的非常相似。

我们从将要运行的测试套件的describe方法开始。紧接着是我们的一组it语句(清单 10-17 )。为了简单起见,我们将使用量角器网站上的例子。

清单 10-17It检查站点是否有标题的方法

it('should have a title', function(){
    browser.get('http://juliemr.github.io/protractor-demo');
    expect(browser.getTitle()).toEqual('Super Calculator');
});

我们现在需要的东西都有了。我们将从命令行运行它。如果您还没有运行serve任务并在本地查看站点,请键入:

grunt serve

这将让你从本地服务器的端口 9000 运行网站,这是我们告诉量角器运行测试时要查看的地方。

现在打字

protractor test/protractor.conf.js

这将在test文件夹中查找,运行配置文件,并启动浏览器,以便它可以运行测试。

如图 10-8 所示,我们应该会得到一个通过的结果。

9781430263913_Fig10-08.jpg

图 10-8 。火狐和 Chrome 都通过了量角器测试

这将引导浏览器找到 URL 并检查标题。很简单。

现在让我们编写一个测试,我们可以使用同一个计算器将两个值相加,然后检查结果。清单 10-18 显示了代码。

清单 10-18 。键入两个文本字段,然后测试结果

describe('Protractor Demo App', function() {
  it('should add one and two', function() {
    browser.get('http://juliemr.github.io/protractor-demo/');
    element(by.model('first')).sendKeys(1);
    element(by.model('second')).sendKeys(2);

    element(by.id('gobutton')).click();

    expect(element(by.binding('latest')).getText()).
      toEqual('3');
  });
});

在这里,我们可以直接看到 Angular 的ng-model指令来访问文本字段并给它们赋值。然后我们可以通过它的 ID 找到按钮并点击它。该点击触发方法doAddition。最后,我们能够查看和检查由该方法的结果更新的值。

摘要

这是对 AngularJS 以及编写单元测试和端到端测试的一个非常高层次的审视。这两个主题都可以成为他们自己的书。

随着您的项目变得越来越大,越来越复杂,拥有一个框架可以帮助您保持一切井井有条。此外,能够测试您的代码会让您对自己编写的代码更有信心。

单元测试让您知道您的前端代码正在按预期工作。集成测试让您知道相同的代码是否适用于不同的浏览器。

Moo ( www.yearofmoo.com/2013/09/advanced-testing-and-debugging-in-angularjs.html)的站点年有一个用 Angular 进行测试和调试的优秀细分。它涵盖了诸如何时应该编写测试、在旧浏览器中测试以及测试什么和不测试什么等主题。

现在可以放心重构了,知道自己写的东西不会弄坏 app 如果有,你会尽快知道。

十一、JavaScript 的未来

在本书中,我们已经对 JavaScript 进行了相当深入的研究。很明显,JavaScript 是一种转型中的语言。JavaScript 最初只是一种玩具语言,现在已经上升到企业关键语言的水平。在这个过程中,接缝已经开始出现,坦率地说,变得有些松散。考虑到 JavaScript 的局限性,当来自更成熟语言的开发人员开始使用 JavaScript 时,他们通常会惊叹于我们所取得的成就。他们习惯于想知道这门语言是如何发展到今天的,以及它在未来是否会有所改进。

JavaScript 1995 年的“毕业班”拥有一些最耀眼的编程之光:Java、Ruby、PHP 甚至 ColdFusion 直到今天都是欣欣向荣的语言。很多开发者会说 JavaScript 的同学远远领先于 JavaScript。然而,他们中的许多人正在从 JavaScript 中寻找线索,看它对原型的使用,它作为一等公民的功能实现,它灵活的风格,以及更多作为他们自己新功能的灵感。

JavaScript 的未来会怎样?它从这里去哪里?值得庆幸的是,与我们自己模糊不清的未来不同,JavaScript 的未来有一个路线图,甚至还有一些不断发展的规范。到本书出版时,ECMAScript 6 很可能会成为一个完全被采用(如果不是完全实现)的标准。ECMAScript 7 已经在开发中,正在讨论中。套用一句老话,JavaScript 的未来看起来确实很光明。

让我们来看看 JavaScript 的未来。我们将讨论一下我们是如何走到这一步的,以及我们将向何处去,看看标准流程。然后,我们将看看在当前的工具集上使用 JavaScript 需要做些什么。但是这一章的大部分内容都是关于 ECMAScript 6 的细节:在接下来的几年里,你可能会用到的语言特性。我们甚至会暗示一些遥远的未来,可能会发生,也可能不会发生...

当然,我们没有足够的空间来讨论 ECMAScript 6 规范的全部内容。我们努力挑选更有用的、定义更好的和最有趣的特性来研究。

曾经和未来的 ECMAScript

我们应该从我们知道的开始。欧洲计算机制造商协会,现在被称为 Ecma 国际,是监督 JavaScript 所遵循的标准的机构。人们可以写本书来说明这是如何发生的,但这对我们 JavaScript 程序员的工作来说并不重要。重要的是 Ecma 内部的一个组织,技术委员会 39 (TC39 ),已经举起了 JavaScript 标准的大旗,并正在发布更新。也许更重要的事实是,各种计算机制造商、软件公司和其他利益相关者都受益于这一标准的成功。作为 JavaScript 社区,我们有一个可行的系统来指引语言的未来。这应该有助于消除我们过去忍受的一些敌意分歧,以及简化将 JavaScript 定位为有效的企业级语言的过程。

将 JavaScript 变成一个有效的和有效治理的标准的过程是漫长的。我们认为“标准”JavaScript 的大多数特性来自 ECMAScript 版本 3。这个版本来自 Ecma 还在追赶浏览器厂商的时代。尽管有人试图创建 ECMAScript 标准的第四个版本,但最终都被放弃了。第 3 版十年后,该标准的第 5 版于 2009 年发布。这旨在建立一个新的现状,赶上中间的变化,并澄清版本 3 中的许多模糊之处。该标准被广泛接受,并为 Ecma 恢复管理 JavaScript 标准的职责扫清了道路。

即使是现在,ECMAScript 版本 5 的广泛接受也不是必然的。Internet Explorer 9 是第一个实现该标准的 IE 版本。世界上很大一部分人仍然在使用 IE 8 和更早的版本。Ecma International 和 TC39 面临的挑战与其说是设定标准,不如说是一旦提供了标准,就让观众升级到这些标准。

对于版本 5 之后的下一步会是什么还有些困惑。最终采取了两条轨道:有一个 5.1 标准,它使 ECMAScript 符合国际标准化组织对 ECMAScript 的规范(这本身就是一个漫长而无聊的故事)。ECMAScript 标准将会有一个新的版本,版本 6,通常被称为 ECMAScript Harmony (这个名称来自于随着时间的推移而出现的各种提案,以协调 JavaScript、ECMAScript、JScript 等各种标准,以及 ECMAScript 版本 4 的原始代码名称)。

作为程序员,我们最感兴趣的是新标准以及它能让我们做什么。ES6/Harmony 提案应于 2015 年年中最终确定。所以,让我们把标准的世界抛在脑后,谈谈今天我们如何和谐地工作。

使用 ECMAScript 和声

ECMAScript 5 在成为法律上的标准之前,曾作为事实上的标准存在了很长时间,与 ECMAScript 5 不同,Harmony 是领先的,而不是跟随的。这意味着当前(在撰写本章时)对 Harmony 的实现参差不齐。我们需要一些工具来管理 Harmony 的各种实现状态。首先,我们需要资源来告诉我们哪些浏览器实现了和谐的哪些方面。第二,我们将看看如何在这些浏览器中实现和谐,第三,我们将研究一些软件工具,它们可以将我们的 ES6 代码转换成符合 ES5 的代码。之后,我们应该能够深入了解该标准的一些特性,这些特性要么被广泛使用,要么被广泛期待成为最终标准。

和声资源

TC39 维护一个 wiki,允许公众在http://wiki.ecmascript.org/跟踪和谐提案的状态。有两个页面特别有趣:需求/目标/方法页面和提议页面。需求页面列出了指导 ES6 开发的方法。虽然对我们理解语言并不重要,但它确实告诉我们为什么要做出某些决定,根据为 Harmony 项目定义的目标和方法。例如,Harmony 提出了适当的块范围,但是通过添加一个关键字(let)来实现它,而不是简单地重新定义 JavaScript 解释器的工作方式。提案本身遵循第一个目标的前两个要点:成为编写复杂应用和库的更好的语言。但是实现遵循第四个目标(保持版本控制尽可能简单和线性)以及第一个方法(最小化超出 ES5 所需的额外语义状态)。您可以在http://wiki.ecmascript.org/doku.php?id=harmony:harmony找到和谐项目的要求、目标和方法。

协调流程的另一个重要页面是提案页面。它跟踪为 ES6 提出的各种建议以及每个建议的状态。提案征集已于 2011 年截止,因此该页面不会有任何新增内容。在大多数情况下,您应该会看到现有的提议,偶尔还会看到从规范中删除的提议。您可以在http://wiki.ecmascript.org/doku.php?id=harmony:proposals找到提案页面。

规范作为参考文档是很棒的,但是它们有时缺乏实现细节。我们希望有一个参考,告诉我们跨浏览器和其他 JavaScript 引擎的 ES6 实现的状态。幸运的是,我们有两个这样的页面。最好的页面是由自称 kangax(jury Zaytsev)的著名 JavaScript 开发人员编写的,被称为 ECMAScript 6 兼容表 。可以在这里找到:http://kangax.github.io/compat-table/es6/。兼容性表按功能对 ES6 实施进行了细分,并对照大多数现代浏览器(桌面和移动)以及 Node.js 等其他 JavaScript 实施进行了检查。测试有些简单化,通常侧重于提案的存在,而不是其功能,也不是实施与提案的一致性。尽管如此,这是一个很好的起点。kangax 还维护了 ES5、即将到来的 ECMAScript 7 规范的兼容性表,以及非标准特性,如函数上的__defineGetter__caller属性。

Thomas Lahn 还维护 ECMAScript 矩阵,该矩阵跟踪当前版本 JavaScript 引擎中 ECMAScript 标准的实现。你可以在http://pointedears.de/es-matrix/找到他的努力。Lahn 的方法和 kangax 的有些不同。Lahn 对当前的 JavaScript 引擎感兴趣,所以他只跟踪 JavaScript、V8 (Google Chrome 的引擎)、Opera 和其他一些引擎。相比之下,kangax 更加面向浏览器和软件。此外,Lahn 的 matrix 跟踪所有 ECMAScript,至少到第 6 版,所以你可以看到与数组和for循环一起评估的let关键字。他的方法更彻底,但也导致了更大的表(和加载页面更慢)。尽管如此,这和 kangax 的兼容性表一样,是专业 JavaScript 开发人员不可或缺的资源。

和谐地工作

浏览器可以在四种状态下使用 Harmony 特性。

  • 浏览器,尤其是 evergreen Chrome 和 Firefox,可能已经有了一个现成的和谐特性的实现,不需要程序员的特别努力。不过,截至发稿时,很少有特性是这样工作的。
  • 大多数浏览器会要求你“选择加入”使用 Harmony 特性。我们稍后会详细讨论这一点。
  • 如果您的浏览器没有启用或实现某个功能(或者它没有正确实现!),你可以考虑使用 transpiler,它可以让你编写 ES6 级别的代码,然后输出可以在你选择的引擎上运行的 ES5。
  • 或者,您可以为 ECMAScript 6 中您想要使用的特定功能使用聚合填充。

显然,第一种状态几乎不需要解释,所以我们来谈谈第二种状态。Chrome 和 Firefox 在使用 ECMAScript 6 时都有自己的怪癖。

在 Chrome 中,你必须访问chrome://flags URL,这允许你启用实验性功能。具体来说,您需要启用chrome://flags/#enable-javascript-harmony。现在,请记住,这样做可能会改变 Chrome 在许多情况下的行为,并且可能会在 Chrome 呈现某些页面的方式上产生奇怪的结果。并且对enable-javascript-harmony状态的改变是持久的。相反,如果您希望只在特定会话启动时进行更改,可以使用--javascript-harmony开关从命令行运行 Chrome。此外,一些示例需要在严格模式下运行 JavaScript,这可以在代码级别进行管理。

对于 Firefox 来说,启动时不需要更改任何设置,但可能需要更改代码。一般来说,Firefox 会要求你给你的代码贴上不同于标准 JavaScript 的标签。将类型属性添加到脚本标签中,并将类型设置为application/javascript;version=1.7,这将启用大多数和声特性。如果运行 Harmony code 需要任何额外的更改,我们会用它们所支持的特定特性记录下来。

有趣的是,Internet Explorer 需要最少的配置来运行 ECMAScript 6 代码——“最少”是指“无”。另一方面,IE 10 只实现了规范中的四项。Internet Explorer 11 总共实现了规范中的 12 项,但远远落后于 Firefox 和 Chrome。可以说,IE 11 虽然实现的不多,但是实现的东西,做的很简单。

蒸发器〔??〕〔??〕

使用 ECMAScript 6 的第三个选项是一个 transpiler 。transpiler 将为 ECMAScript 6 编写的代码交叉编译成 ECMAScript 5 兼容的代码。有许多不同的传输工具。Addy Osmani 在 GitHub 上维护着一份 transpilers 和 polyfills 的清单。您可以在https://github.com/addyosmani/es6-tools查看列表。您可以从该链接中看到有许多 transpilers 和 polyfills。我们将演示使用 Traceur transpiler 获取一些 ECMAScript 6 代码,并在使用 ECMAScript 5 的浏览器中运行它。Traceur 是最受欢迎的 transpilers 之一,也是更新最频繁的。

使用 Traceur 最简单的方法是通过 Node.js 加载它。类似于我们在 JavaScript 工具一章中使用 Node 作为自己的 JavaScript VM 的方式,我们将在这里使用 Node 通过 NPM 加载额外的代码。从加载 Traceur 开始

npm install traceur

这将安装当前版本为 0.0.72 的 Traceur transpiler。回想一下,如果希望 Traceur 在全球范围内可用,可以使用-g 选项。不管怎样,您可能需要更新您的PATH变量来包含 Traceur。在 Windows 上,在你的node_modules文件夹中你会发现下面的文件:.bin\traceur.cmd,这是一个用 Node.js 包装运行 Traceur 的 Windows 批处理文件,如果你把node_modules\.bin目录添加到你的PATH中,你应该可以直接运行 Traceur。通过运行traceur--version检查 Traceur 是否在您的路径上,如果找不到 Traceur,应该会返回版本号或错误消息。

您可以针对现有的 ES6 代码运行 Traceur。从命令行调用traceur,并作为参数传递一个包含 ES6 代码的 JavaScript 文件。(当然,你可以传递给它一个只有 ES5 代码的文件,但是这有什么意思呢?)Traceur 将运行您的代码,并将其输出到控制台。

考虑一个 JavaScript 中新的类语法的例子。很快,ES6 将允许您创建类,尽管代码只是围绕类型的函数风格的语法包装。该语法易于阅读和理解,所以让我们将它用于我们的 Traceur 示例,如清单 11-1 所示。

清单 11-1 。带 Traceur 的 ECMAScript 6 类

class Car {
  constructor( make, model ) {
    this.make = make;
    this.model = model;
    this.speed = 0;
  }

  drive( newSpeed ) {
    console.log( 'DEBUG: Speed was previously %d', this.speed );
    this.speed = newSpeed;
    console.log( 'DEBUG: Speed is now %d', this.speed );
  }

  brake() {
    this.speed = 0;
    console.log( 'DEBUG: Setting speed to 0' );
  }

  getSpeed() {
    return this.speed;
  }

  toString() {
    return this.make + ' ' + this.model;
  }
}

var honda = new Car( 'Honda', 'Civic' );
console.log( 'honda.toString(): %s', honda.toString() );
honda.drive( 55 );
console.log( 'The Honda is going %d mph', honda.getSpeed() );

如您所见,我们创建了一个类型,Car,并定义了三个属性(makemodelspeed)以及一些方法,这些方法是属性的包装(brakedrivegetSpeed)或便利性(toString)。不要太疯狂。

这段代码不能在任何现代浏览器中运行。我们知道,因为我们测试过。此外,如果您查看 kangax 的兼容性表(截止到发布时),您会发现这些类没有被任何主流浏览器实现。所以这段代码是试验 Traceur 的一个很好的选择。

如果您要将代码保存在一个文件中(实际上,在本章的文件夹中),您可以使用 Traceur 运行它:

traceur classes.js

您的输出将如下所示:

9781430263913_unFig11-01.jpg

如您所见,Traceur 很好地处理了代码。在幕后,Traceur 将代码编译成 ES5,然后简单地使用 Node.js 本身运行代码。没什么大不了的。

但是浏览器呢?我们有两种不同的选择。我们可以使用 Traceur 生成在支持 ES5 的浏览器中运行的输出文件。或者,我们可以直接在浏览器中使用 ES6 代码,并让 Traceur 实时传输它。从传输输出开始。使用--out选项告诉 Traceur 将输出生成到您选择的文件中。输出文件不是独立文件。显然,你可以用 Traceur 运行它,但是你不能简单地把它包含到一个 HTML 页面中。您需要首先加载 Traceur 运行时,然后加载您想要运行的脚本。清单 11-2 是一个 Traceur HTML 外壳的例子。

清单 11-2 。Traceur 的 HTML 外壳

<!DOCTYPE html>
<html>
<head>
  <title>Traceur and classes</title>
</head>
<body>
<h2>Running Traceur output in the browser</h2>

<script src="../node_modules/traceur/bin/traceur-runtime.js"></script>
<script src="classes-es5.js"></script>
</body>
</html>

注意,我们正在从 Traceur 的 bin 文件中加载traceur-runtime.js文件。这个文件是运行 Traceur-trans filed 文件所需代码的精华。(目录中另一个大得多的文件是 Traceur 本身的代码。)加载清单 11-2 (在章节文件夹中作为classes-es5.html提供)在控制台上产生预期的结果。或许更重要的是,它对当前版本的 Firefox、Chrome 和 Internet Explorer 都非常适用。

如果您想直接使用 ECMAScript Harmony 代码,您总是可以让 Traceur 实时传输您的代码。我们可以使用来自清单 11-1 的原始和谐代码,并修改来自清单 11-2 的 HTML 外壳,如下所示:

<script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script>
<script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script>
<script src="classes.js" type="module"></script>

我们已经改用 Google 的 GitHub repository for Traceur,因为这是访问第二个文件最简单的方法:bootstrap.js。该文件不包括在 NPM 安装的 Traceur 中。它也不包括在 Traceur 的 Bower 安装中。所以我们就直接参考了。Bootstrap 允许您在 JavaScript 上下文中运行 Traceur。此外,我们现在将文件classes.js称为类型module。这是bootstrap.js的约定,它通过 Ajax 加载classes.js,作为一个明确的 ES6 文件。此外,type属性还有一个副作用,就是不会将引发错误的代码加载到浏览器中。或者,您可以简单地将 JavaScript ES6 代码包含在内联脚本块中,尽管您仍然需要将type属性设置为module

虽然内联传输是一个有趣的实验,但我们不建议将其作为常规的开发或部署方式。这是每次加载页面时都要做的额外工作,这些工作可以通过传输到输出文件来完成。更不用说traceur.jsbootstrap.js加你的代码比traceur-runtime.js加你的代码下载量大得多。

多孔填料

最后,对于 ECMAScript 6 的某些方面,您可以将 polyfill 加载到页面中。这个解决方案的用例有点窄,但是应用要广泛得多。与使用 Traceur 等工具获得的整套 ECMAScript 6 不同,您可以通过使用 polyfills 专注于您想要的特性。但这是有代价的。对于 polyfill 来说,在字符串、数字和数组的原型上提供新的方法,或者实现 WeakMap 和 Set,这是很容易和合乎逻辑的。另一方面,polyfills 本质上不能替代语言特性,如letconst或箭头函数。因此,您可以使用聚合填充访问的和声功能集是有限的。

综上所述,对于可实现的 polyfills,有相当多的高质量实现。Addy Osmani 的 ES6 工具目录包含一个关于 ES6 polyfills 的部分。值得注意的是保罗·米勒的 ES6-Shim ( https://github.com/paulmillr/es6-shim/),它有多种填充,可以用于和声的大多数方面。当我们在本章后面查看 ECMAScript 6 的特性列表时,我们会注意到 ES6-Shim 提供的那些特性。

ECMAScript 和谐语言功能

ECMAScript 6 引入了大量新的语言特性。这些特性填补了 JavaScript 中明显的盲点(块范围),精简了语法以关注核心功能(箭头函数),并扩展了 JavaScript 处理更复杂代码模式(类、模块和承诺)的能力。

以 block scope 为开头的例子。JavaScript 奇怪的作用域和提升方法(将变量和函数定义提升到局部作用域顶部的做法)多年来一直是新 JavaScript 程序员的绊脚石。这也是一种文化障碍:来自“真正”语言的程序员(不管这意味着什么)嘲笑 JavaScript,因为它缺乏块范围(或者类,或者这个,或者那个)。不管这种批评的实质是什么,它阻止了一些人来体验 JavaScript 的优点。所以,让我们解决这个问题,继续前进。

let关键字允许你将变量作用于任意块。范围为let的变量不会被提升。这是letvar的两个关键区别。可以说,以let为作用域的变量的行为方式与大多数局部变量的行为方式相同(而以var为作用域的变量有一些有趣的特性/功能)。如果您试图访问其块之外的let范围的变量,您会得到一个 ReferenceError,就像您试图访问一个不可访问的var范围的变量一样。没有比这更复杂的了。当你想要块作用域时,用let代替var

作为let的伴侣,还有const,它允许你声明一个常量值变量。对于用const声明的变量,你应该在声明时初始化,否则有什么意义呢?以后您将无法更改该变量的值!像letconst变量没有被提升,它们的作用域是声明它们的块。尝试访问const变量的语义因浏览器而异。如果您试图修改一个声明为const的变量,您的浏览器要么会无声地失败,要么会抛出某种类型的错误。常量使某些代码能够被编译成更快的代码,因为 JavaScript 引擎知道它们永远不会改变。此外,当与我们将在本章后面看到的一些集合配对时,你可以使用常量来存储一个类的私有数据。详见http://fitzgeraldnick.com/weblog/53/

箭头功能

ECMAScript 6 中的另一类改进是引入了简化某些声明的语法变化。这些变化之一是箭头功能。箭头函数为定义函数提供了一个更短的语法,尤其是内嵌函数。考虑以下代码:

var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
numbers.forEach(function(num) {
  // Do something with the number here
});

它并不笨拙,但是有点冗长。JavaScript 有一个独特的挑战:在不编译代码的同时,尽量减少文字代码的长度。使用关键字function来定义函数对带宽来说并不轻松。而且我们用的功能越多,就要用到function这个词的次数越多。如果有更短的函数语法就好了。

受 CoffeeScript 的启发,TC39 引入了一个关于箭头函数的提议,或者更明确地说,是箭头定义的函数。前面的例子可以这样写:

var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
numbers.forEach(num => {
  // Do something with the number here
});

更优雅,不是吗?以下是语法的关键部分:

arguments => { code }
()        => { code } // No arguments
i         => { code } // One argument
(i, j)    => { code } // multiple arguments

为了指定函数体,你可以用一组花括号把多行代码括起来,或者,如果你只有一行代码,你可以把它留空。因此

x => x * 2

相当于

function(x) {
  return x * 2;
}

箭头功能和常规或标准功能有一些不同。首先,箭头函数总是绑定到定义它们的上下文中。这听起来很复杂。你可能遇到过这样的问题:

var courseAssignments = {
  teacher : 'Stephen Duffy',
  canTeach: function(courses) {
    courses.forEach( function ( course ) {
      console.log( 'Ask %s if he can teach %s', this.teacher, course );
    });
  }
};

courseAssignments.canTeach(['Greek', 'Latin', 'Theology', 'History']);

当您运行这段代码时,您将看到如下输出:

Ask undefined if he can teach Greek

显然不是我们想要的。forEach函数中this的上下文和值指回canTeach()函数,而非courseAssignments对象。通常问题是这样修复的:

var courseAssignments = {
  teacher : 'Stephen Duffy',
  canTeach: function(courses) {
    var that = this; // Store the context of canTeach
    courses.forEach( function ( course ) {
      console.log( 'Ask %s if he can teach %s', that.teacher, course );
    });
  }
};

courseAssignments.canTeach(['Greek', 'Latin', 'Theology', 'History']);

注意粗体的代码,它强调了在canTeach级别存储this上下文的策略,而不是在 f orEach中。

箭头函数极大地简化了这种情况。它们会自动绑定到适当的上下文。您可以将前面的代码重写为

var courseAssignments = {
  teacher : 'Stephen Duffy',
  canTeach: function(courses) {
    courses.forEach( course => console.log( 'Ask %s if he can teach %s', this.teacher, course ) );
  }
};

courseAssignments.canTeach(['Greek', 'Latin', 'Theology', 'History']);

有了箭头函数,this.teacher自动绑定到canTeachthis的上下文中,这正是我们一直想要的。太棒了!

箭头函数还有两个不同之处。箭头函数不能用作构造函数。他们缺乏这样做的内部代码。箭头功能也不支持arguments对象。这很好,因为 ECMAScript 6 还定义并允许参数默认值和 rest 参数,所以我们不再需要依赖函数中的arguments对象。

类别

ECMAScript 6 中最大的语法变化之一是引入了类。关键字class从一开始就被 JavaScript 所保留。但背后并没有执行。随着面向对象 JavaScript 的兴起,TC39 承认 JavaScript 需要一种更加语法清晰的方式来实现类和继承。与此同时,没有人希望添加另一种方法来实现类型、类或类似类的东西。这个想法是为了简化和减轻混乱,而不是制造更多的混乱。

ECMAScript 6:ECMAScript Harmony 规范决定使用class关键字作为语法糖来实现基于函数的类型。如果这听起来有点奇怪,实际情况是这样的:

  • 您基于新的 ES6 语法编写了一个 JavaScript 类。
  • JavaScript 引擎将其编译成定义类型的函数。
  • 你与类的交互方式和你与类型的交互方式是一样的。

所以与类/类型及其实例交互的语义没有改变。定义类/类型的许多语义也是一样的。让我们回到前面的例子来了解更多细节。回想一下我们如何在 JavaScript 中定义一个类:

class [*classname*] {
  constructor(*arg1*, *arg2*) { ... }
  [*other methods*]
}

class关键字定义类。包括一个名为constructor的函数。这是稍后调用new [classname]时将被调用的函数。根据需要提供其他方法。其他方法将被复制到被定义为构造函数的函数原型上。相当漂亮!

类还将拥有 JavaScript 面向对象方面渴望已久的两个关键特性:简单继承和超级访问器。继承可以通过extends关键字获得:

class Car extends Vehicle

在子类中,您可以使用super作为超类的方法和属性的访问器。与某些语言(例如 Java)不同,ECMAScript 6 不允许将super作为方法本身调用。相反,它保存对超类的引用,类似于this保存对当前实例的引用。

不幸的是,在当前任何主流浏览器中,类都没有被实现。类规范将是 2015 年发布的 ECMAScript 6 最终规范的一部分,似乎浏览器制造商正在等待最终规范,以确保他们的实现不会出错。同时,Traceur transpiler 对类的处理相当不错,包括extendssuper

承诺

当我们讨论 Ajax 时,我们讨论了基于 Ajax 的系统的一个问题:代码流。当涉及异步返回结果的函数时,很难编写相关代码。很长一段时间,没有具体、直接的解决方案。大多数程序员要么编写了很长的回调函数(反过来,这可能调用了他们自己的异步函数,使事情变得复杂!)或使用命名函数,但在调用堆栈中反复使用。(且不说这种方法引入了另一个并发症;有许多命名函数。你用一个模块来管理它们吗?只是一个命名空间?)异步运行代码是 JavaScript 特性列表的核心,因此我们需要一种更好的方式来管理异步交互。输入承诺。

一个承诺就是,一种帮助管理异步返回代码的模式。它封装了代码,并添加了一个管理层,有点事件处理的味道。我们可以用 promise 注册代码,不管它成功返回还是不成功返回,都应该运行。当承诺完成时,运行适当的代码。我们可以注册任意数量的函数在成功或失败时运行(非常像事件处理),我们可以在任何时候注册一个处理程序(无论承诺是否完成;非常不像事件处理)。

让我们进行一次承诺的技术演练,以阐明特性的操作,如清单 11-3 所示。概括地说,承诺有两种状态:待定或已确定。该承诺是挂起的,直到它所包装的异步调用返回、超时或以其他方式结束。在这一点上,承诺是解决。被解决的状态有两种味道(如果你愿意的话):解决和拒绝。已解决的承诺成功解决,被拒绝的承诺未成功解决。因为承诺是任意的(从技术上讲,它们甚至不需要异步代码!),拒绝或者解决的定义真的由你自己决定。

清单 11-3 。承诺

var p = new Promise( function(resolve, reject) {
  // Do some things here, maybe some Ajax work?

  setTimeout(function() {
    var result = 10 * 5;

    if (result === 50) {
      resolve(50);
    } else {
      reject( new Error( 'Bad math, mate' ) );
    }
  }, 1000);

});

p.then(function(result) {
  console.log( 'Resolved with a value of %d', result );

});

p.catch(function(err) {
  console.error( 'Something went horribly wrong.' );
});

诚然,这有点做作,但它仍然是一些异步代码的一个很好的简单示例。问题的核心是对setTimeout的调用。当然,我们计划稍后执行的函数除了运行一个数学方程之外什么也不做。关键部分是对resolvereject的调用。resolve函数告诉承诺消费者承诺已经解决(成功完成)。并且reject函数对未成功结算的承诺做同样的事情。请注意,在这两种情况下,我们也在传递一个值。该值将是消费者在处理解决方案或拒绝的函数中收到的内容。

我们这里也有一些消耗承诺的代码。注意thencatch的使用。可以把它们想象成“onSuccess”和“onFailure”事件处理器。严格来说,then的签名是Promise.then(onSuccess, onFailure)。所以你可以直接给then打电话。不过,就风格而言,单独调用catch来处理出现的任何错误要清楚得多。

真正聪明的一点是,我们已经把承诺的处理和承诺的状态分开了。也就是说,我们可以调用p.then(或p.catch)任意多次,不管承诺的状态如何。如果我们仍然在 1000 毫秒内完成承诺,那么用p.then注册的代码将被添加到堆栈中,以便在承诺完成时调用。如果我们超过了 1000 毫秒的时间周期(很可能是这样),代码将立即执行。您可以想象 promise 实现中的一些伪代码是这样工作的:

function then(onSuccess, onFailure) {
  if (this.state === "pending") {
    addSuccessStack(onSuccess);
    addFailureStack(onFailure);
  } else if (this.state === "settled") {
    if (this.settledState === "success") {
      onSuccess();
    } else {
      onFailure();
    }
  }
}

实际的实现有更多的细节,我们不会在这里深入讨论。尽管如此,这是一个有用的近似,它描述了当您注册代码以便在承诺实现时运行时,承诺内部会发生什么。

承诺是管理异步代码的标准 ECMAScript 6 方式。事实上,许多 JavaScript 库已经在使用 promises 来管理 Ajax、动画和其他典型的异步交互。正如我们将在下一节中看到的,我们将需要拥有并理解使用模块的和谐实现的承诺。

如果你想同时履行承诺,有多种选择。Traceur 理解承诺,并在内部使用它们来实现模块(和一些其他功能)。像 jQuery、Angular 和其他一些库都有自己的 promises 实现,尽管与官方规范有一些不同。jQuery 尤其如此,所以要小心。ES6-Shim 包含一个 promise 实现,Kris Kowal 的优秀 Q 库有一个实现 ES6 API 的 Q.Promise 类型。

我们在这里的轻量级讨论只触及了承诺的许诺)。除了 API 和类型之外,还有更多。网上有很多关于承诺的精彩文章,但是如果你打算从某个地方开始,考虑一下 Jake Archibald 的这篇 HTML5Rocks 文章。

模块

等等,我们以前没见过模块吗?嗯,是的,说到,比如说,RequireJS,你可以说 AMD(异步模块定义)模块。如果你在 Node.js 的世界里,你可能会想到 CommonJS 模块。ECMAScript 6 标准试图将这两种不同风格的模块解析成一种语法。这多少是成功的。不可避免地,双方都会有抱怨。但是通过使用两种标准的某些方面,TC39 为程序员提供了一条清晰的道路。如果你想坚持使用 AMD 风格的模块,或者更喜欢 CommonJS 风格的模块,有人最终会写一个 transpiler...可能是你。

话虽如此,让我们看看 ES6 模块的实现。我们不会花时间将 ES6 模块与 AMD 或 CommonJS 模块进行比较和对比。这是另一本书的另一个章节的练习(或者亚历克斯·劳施迈尔的一篇出色的博客文章:http://www.2ality.com/2014/09/es6-modules-final.html)。相反,我们将与模块一起工作,看看它们为我们提供了什么。

模块背后的想法很简单:我们想要一个安全的、封装的名称空间,在其中我们可以定义数据和功能。然后,根据我们的判断,我们可以提供部分或全部数据和功能。可重用性是模块的主要用途,它允许我们定义一次功能,然后在任何需要的地方使用它。清单 11-4 显示了模块是如何实现这些需求的。

清单 11-4 。一个模块定义

export const schoolName = 'Mickey Kullen Memorial High School';
export const firstSport = 'Basketball';

export function getPrincipal() {
  // Probably a call to a server in the real world
  return 'Jason Franzke';
}

export function fgPct(shots, baskets) {
  return baskets / shots;
}

正如你所看到的,我们通过关键字为模块导出了公共 API。当在逐个成员的基础上使用export时,我们可以导出常量、常规变量和函数。如果我们愿意,我们可以将所有导出列为文件的最后一行(甚至在此过程中更改它们的名称!):

export {schoolName, firstSport, getPrincipal, fgPct};
export {schoolName as name, firstSport as sport ... };

您还可以将模块的单个成员设置为默认导出。不出所料,这个成员标有default关键字,如下所示:

export default function () { ... };
export default 'foo';

您可以混合使用默认成员和命名成员,尽管这在您以后导入这些成员时会使事情变得复杂。

有这么多选择,我们应该选择哪一个?选一个管用的!但是如果你在寻找专家指导,ES6 设计过程中不可或缺的一部分 Dave Herman 建议使用默认的单一导出模块。在这一点上有很多讨论,许多人反对,或者至少表达了不同的偏好。这是 JavaScript:选择适合您需求的风格。

当使用一个模块时,我们有两种方式与它交互:声明式和编程式。清单 11-5 中的所示的声明性语法简单明了。

清单 11-5 。以声明方式使用模块

import * as school from 'test-module';
console.log('The principal of %s is %s.', school.schoolName, school.getPrincipal());

关键字import允许你定义模块的哪个部分被导入到哪个名称空间。模块名与文件名相同,只是去掉了扩展名.js。路径是允许的,所以foo/bar会在相对于当前文件位置的foo目录下找到bar.js

import命令非常灵活,因为您可以导入任意多或任意少的模块:

// Imports 'schoolName' only
import schoolName from 'test-module';

// Imports both of these named members
import {schoolName, getPrincipal} from 'test-module;

// Imports the default exports
import someDefault from 'test-module';

// Imports the default, plus a named member; DOES NOT WORK IN TRACEUR
import someDef {schoolName} from 'test-module';

// Imports the entire module, the default function/variable/whatever is
// available as default
import * as school from 'test-module';
console.log(s.default()); // Executes the function exported as a default

模块是异步加载的。在执行任何代码之前,您的浏览器将等待所有模块都已加载。如果你想更好地控制这个过程(或者喜欢不同的语法),你可以使用模块导入的编程风格。

清单 11-6 。程序化导入

System.import( 'test-module' )
  .then( school => {
    console.log('The principal of %s is %s.', school.schoolName, school.getPrincipal());
  } )
  .catch( error => {
    console.error( 'Something has gone horribly wrong.' );
  } );

基础模块的语法不会改变。相反,如果您想将特定代码绑定到特定模块的加载或执行,我们有一个基于承诺的语法来加载模块。(我们使用新的 arrow 函数语法只是为了好玩!)使用该语法还可以做什么的细节比这里的篇幅所允许的要复杂得多。简单地说,如果你想要或者需要的话,你将对你的模块的加载和运行有广泛的控制权。

有几种用于模块的多孔填料可供选择。除了一两个例外,Traceur 在模块方面做得还不错。ES6-Tools 页面保持对其他几个包的更新,包括一个名为 ES6 Module Loader Polyfill ( https://github.com/ModuleLoader/es6-module-loader)的包。请注意,ES6-Shim 没有模块聚合填充。

类型扩展

ECMAScript 的最后一类变化是对现有类型的改进。其中一些变化使 JavaScript 中长期可用的特性形式化,比如字符串类型上的 HTML 函数等等。其中一些是不言自明的,但其他人可以使用一些澄清,这就是我们在这里提供的!

字符串

这组字符串函数应该从String的实例中调用。换句话说,它们在String.prototype可用。首先是 HTML 函数:anchorbigboldfixedfontcolorfontsizeitalicslinksmallstrikesubsup。每一个都接受String实例,并返回一个包装在适当标签中的副本。各种浏览器可能会增加或减少这个组(例如,Chrome 有一个blink功能)。

还有一些字符串实用函数:

| String.prototype.startsWith(str | 字符串是否以提供的子字符串开头? |
| String.prototype.endsWith(str | 字符串是否以提供的子字符串结尾? |
| String.prototype.contains(str, [startPos]) | 字符串是否包含提供的子字符串? |
| String.prototype.repeat( 计数 ) | 生成一个新字符串,它是对String的重复count次。 |

数字

Number 类型获得了几个静态方法,其中大多数用于确定传递的参数的特征。

| Number.isNaN(num) | num 是一个数字吗?取代了有一些问题的全局isNaN function。 |
| Number.isFinite(num) | num有限吗?正负无穷大、NaN、非数都不是有限的。 |
| Number.isInteger(num) | 数字是整数吗?NaN 不是整数,也不是任何非数字的东西。 |
| Number.isSafeInteger(num) | 这个数是否可以安全地表示为 IEEE-754 双精度数,并且没有其他 IEEE-754 双精度数舍入到这个数? |
| Number.parseInt( , [ ]) | 替换全局parseInt();最好提供基数,因为这可以改善实现差异。 |
| Number.parseFloat( ) | 替换全局parseFloat()。 |

数学

数学工具库 扩展了一些有用的函数。其中大部分比我们在这里讨论的时间更深奥,但这里有一个简短的列表:

| Math.imul(x??y | 返回两个参数的类 C 32 位乘法的结果。 |
| Math.clz32( )中 | 返回数字的 32 位二进制表示形式中前导零的位数。 |
| Math.fround( )中 | 返回数字的最接近的单精度浮点表示形式。 |
| Math.log10( )中 | 返回一个数以 10 为底的对数。 |
| Math.log2( )中 | 返回一个数的以 2 为底的对数。 |
| Math.log1p( )中 | 返回 1 +一个数字的自然对数(以 e 为底)。 |
| Math.expm1(x | 返回 e x - 1,其中 x 是自变量,e 是自然对数的底数。 |
| Math.cosh( )中 | 返回一个数字的双曲余弦值。 |
| Math.sinh( )中 | 返回一个数字的双曲正弦值。 |
| Math.tanh( )中 | 返回一个数字的双曲正切值。 |
| Math.acosh( )中 | 返回一个数字的双曲反余弦值。 |
| Math.asinh( )中 | 返回一个数字的双曲反正弦值。 |
| Math.atanh( )中 | 返回一个数字的双曲反正切值。 |
| Math.hypot([,,, ...])``, ...])** | 返回其参数平方和的平方根。 |
| Math.trunc( )中 | 通过删除任何小数来返回一个数的整数部分。它不舍入任何数字。 |
| Math.sign( )中 | 返回数字的符号,可以是正数、负数或零。 |
| Math.cbrt( )中 | 返回一个数的立方根。 |

数组

在数组的世界里发生了许多新的事情。我们先来看函数。第一个是简单的效用函数:Array.from(),在数组类型上静态可用。它接受类似数组的对象,或者可迭代对象,并将它们转换成数组(为您提供数组的所有功能和特性)。想想函数内部的arguments,或者document.querySelectorAll的返回值,它们都像数组,但是缺少数组的许多特性。现在,您可以动态地将 iterables 转换为数组。

数组还获得了三个新的工具来迭代它们的内容:keysvaluesentries。您可能会推断出,keys给出了数组的索引,values给出了值的索引,entries给出了由每个数组条目的键和值组成的数组。有一点不同的是,这是通过迭代器完成的。也就是说,不是获取所有的值,而是在经过它们时看到单个的元素。这对于动态数组、更好的内存管理、数组搜索等非常有用。

为了搜索数组,我们已经有了Array.prototype.indexOf函数。但是如果你想测试简单等式之外的东西,你可以使用Array.prototype.findArray.prototype.findIndex。两者都接受一个谓词函数的参数和一个可选的运行上下文。谓词函数很像forEachmap等的谓词函数,它接受元素、索引和对原始代码的引用的参数。清单 11-7 显示了一个例子。

清单 11-7 。使用Array.prototype.find()

var names = ['John', 'Jon', 'José', 'Joseph', 'Mike',
  'Andre', 'Melanie', 'Jaymi', 'Kathy', 'Jennifer'];

names.find( function ( element, index ) {
  if ( element.startsWith( 'J' ) ) {
    console.log( 'The name %s at position %d starts with "J"!', element, index );
  }
} );

最后,还有Array.prototype.fill ,它允许你用值填充一个数组,可选地传入开始和结束位置。

数组还有一个更重要的改进:操作符spread。长期以来,JavaScript 程序员一直希望能够将数组作为函数的参数“展开”。在 ECMAScript 5 中,没有办法做到这一点,尽管您可以使用Function.prototype.apply,它接受一个数组并将其作为参数传播给被调用的函数。至少可以说,这是笨拙和不清楚的。现在,您可以使用 spread 运算符:

[1, 2, 3].push(...[4, 5, 6]) 
[1, 2, 3, 4, 5, 6]

不错吧。在许多方面,这使得pushpopshiftunshift更加强大,并且使得spliceconcat更加专业化。

多孔填料

Traceur transpiler 不支持这些标准类型的扩展。一般来说,Traceur 关注的是新的语法变化,而不是现有类型的扩展。

另一方面,ES6-shim 支持所有这些特性。在 Addy Osmani 的 ES6 工具页面上,您可以找到大多数这些函数的 polyfills,通常按函数或类型分类。

新集合类型

JavaScript 因缺乏集合实现而遭受损失。在 ECMAScript Harmony 之前,您对原生数据结构的唯一选择是数组和对象。这并不理想。ECMAScript 6 引入了 Set、WeakSet、Map 和 WeakMap 作为新的数据结构。从最基本的方面来说,这些都是 JavaScript 的一个新的集合 API。将来,您应该能够将 JavaScript 集合想象成 Map、WeakMap、Set、WeakSet 和 Array,让对象只作为对象工作,而不是同时作为 Map/关联数组。

先说这些对象的非弱版本。映射是一组键和值。键可以是任何原语或对象值,值也可以是。这些显然是为了取代作为数据结构的对象。有一些关键的区别:

  • 对象的键总是字符串,而地图的键可以是任何数据类型。
  • 地图有大小的属性,而物体没有;换句话说,你必须手动追踪物体的大小,而地图会自动追踪。
  • 对象有原型。严格地说,地图也有原型,但是地图实例不像对象实例那样有原型。这很重要,因为对象有默认的键,而贴图没有。

集合是保证唯一性的数组。根据===(三重等于),集合项目必须是唯一的。可以按插入顺序从集合中检索数据。通过将数组作为参数传递给 Set 构造函数,可以从数组创建集合。在跨浏览器 ECMAScript 中将集合转换成数组并不容易,但是如果需要的话,您可以使用for...ofSet.prototype.forEach遍历集合中的元素,并将单个元素推送到数组中。

最新版本的 Firefox 和 Chrome 都支持这些新集合。Internet Explorer 具有对地图、WeakMap 和 Set 的“基本”支持(虽然不是 WeakSet!).这意味着 IE 知道这个类型,并且实现了它的一部分,但是不允许你使用new Map(iterable)来创建 Map 的一个实例。但是您可以创建空地图、集合和 WeakMaps,然后通过适当的 API 调用向它们添加元素。据status.modern.ie报道,IE 的下一个版本有望提供全面支持。

那太弱了

那么 WeakSet 和 WeakMap 是怎么回事?要理解这些,您必须对 JavaScript 垃圾收集有一点了解。通常,当对象的引用计数为零时,该对象可用于垃圾收集。这意味着没有对正在讨论的对象的现存引用:没有变量、没有键、没有值、没有条目等等。地图和集合中的条目算作参考。

假设您创建了一个对复杂对象的引用,并将其存储在一个变量中。稍后,您还将该引用隐藏在一个集合中。在函数或代码段返回之前,您释放了变量,将其设置为 null。很好,对吧?全部清理干净!没那么快。集合中的条目保持不变,并且您的对象不适合垃圾收集,直到您将它从集合中移除(或者释放整个集合!).

输入武器集和武器地图。在弱集合的情况下,对象引用被弱地持有。这意味着集合中保存的实例不计入总引用数。这种提法站不住脚。如果对该对象的所有其他引用都已被释放,则该对象可用于垃圾回收。这对于内存管理来说非常有用,但是如果你指望这个引用一直存在的话,就有点棘手了。

弱映射的相似之处在于它们的键被弱控制,就像集合中的条目被弱控制一样。此外,WeakMap 中的键只能是对象,而不是原型。与常规地图不同,WeakMaps 的键不能被迭代,因为由于潜在的垃圾收集,它们的状态是不确定的。您必须自己维护一个键的列表,如果您想访问它,可能是一个数组。

收款 API

各种集合实现共享一个公共 API。不是所有的函数都可以在所有的集合中找到,但是大多数函数是由两个或更多的集合类共享的。

Tabled.jpg

多填充物

与 JavaScript 类型的扩展一样,Traceur 没有实现这些特性的 ES5 编译。这是多填充物的领域。如前所述,ES6-Shim polyfill 具有符合 ES5 的 Map、Set、WeakMap 和 WeakSet 实现。还有 harmony-collections poly fill(https://github.com/Benvie/harmony-collections),它只实现 Map、Set、WeakMap 和 WeakSet。

但是请记住,虽然 polyfill 可以模仿 API,但是它不能复制 WeakMaps 和 WeakSets 的一个关键特性:弱保存引用,这样它们就不会计入垃圾收集的总引用计数中。这只能通过修改 JavaScript 引擎来实现。它只能模拟 WeakMap 的行为,而与垃圾收集器没有实际关系,因此可以释放弱持有的引用。

摘要

在这一章中,我们试图给你一个关于 ECMAScript 6 中 JavaScript 的一些新特性的概述。在这样做的时候,我们必然忽略了大量的规范。在 2015 年的某个时候,该规范将进入最终形式,并且 APress 将提供几本涵盖该材料的书籍。

与此同时,我们已经把重点放在了改善 JavaScript 的一些更令人讨厌的怪癖的工具上(想想这里的箭头函数)。我们还介绍了如何使用许多程序员现在正在使用的一些特性(承诺、类、模块)。我们对 JavaScript 的未来感到非常兴奋。随着 ECMAScript 6 在本书出版时得到很好的定义,我们期待着 ECMAScript 7,TC39 已经开始着手开发了!

十二、附录 A:DOM 参考

本附录作为第五章中讨论的文档对象模型所提供功能的参考。

资源

DOM 功能有很多种,从最初的预指定 DOM Level 0 到 DOM Level 3。关于 DOM 需要理解的一件事是,它被认为是一种生活标准。每个级别都描述了添加的特性和行为。DOM 本身是带有节点和属性的文档的表示,这些节点和属性可以有与之相关联的事件。

如果您想了解 DOM 的一些细节,W3C 的网站是学习 DOM 应该如何工作以及 web 超文本应用技术工作组(WHATWG)的极好参考:

此外,有许多学习 DOM 功能如何工作的优秀参考资料,但是没有一个比 Quirksmode.org(Peter-Paul Koch 经营的网站)上的资源更好。他全面考察了每一种可用的 DOM 方法,并比较了它在所有现代浏览器中的结果。这是一个非常宝贵的资源,可以帮助你判断在你开发的浏览器中什么是可能的,什么是不可能的。另一个来源也是亚历克西斯·德韦拉创造的 caniuse.com。在这里,您可以搜索想要使用的功能,并查看支持该功能的浏览器的兼容性表。

术语

在关于文档对象模型的第五章和本附录中,我使用通用的 XML 和 DOM 术语来描述 XML 文档的 DOM 表示的不同方面。以下单词和短语是与文档对象模型和一般 XML 文档相关的术语。所有的术语示例都与列出 A-1 的中显示的 HTML 文档样本相关。

清单 。讨论 DOM 和 XML 术语的参考点

<!doctype html>
<html>
<head>
    <title>Introduction to the DOM</title>
</head>
<body>
    <h1>Introduction to the DOM</h1>
    <p class="test">There are a number of reasons why the DOM is awesome,
        here are some:</p>
    <ul>
        <li id="everywhere">It can be found everywhere.</li>
        <li class="test">It's easy to use.</li>
        <li class="test">It can help you to find what you want, really quickly.</li>
    </ul>
</body>
</html>

祖先

与系谱术语非常相似,祖先是指当前元素的父元素,以及该父元素的父元素,以及该父元素的父元素,以此类推。在清单 A-1 中,< ul >元素的祖先元素是< body >元素和< html >元素。

属性

属性是包含附加信息的元素的属性。在清单 A-1 中,< p >元素具有包含值 test 的属性 class

孩子

任何元素都可以包含任意数量的节点(每个节点都被认为是父元素的子节点)。在列表 A-1 中,< ul >包含七个子节点;其中三个子节点是 li 元素,另外四个子节点是每个元素之间的结束行(包含在文本节点中)。

文件

一个 XML 文档由一个元素(称为根节点文档元素)组成,它包含文档的所有其他方面。在清单 A-1 中,< html >是包含文档其余部分的文档元素。

后裔

一个元素的后代包括它的子节点、子节点的子节点、子节点的子节点等等。在列表中,< body >元素的后代包括< h1 >、< p >、< ul >,所有的< li >元素,以及所有这些元素中包含的所有文本节点。

元素

一个元素是一个保存属性和其他节点的容器。任何 HTML 文档的主要的,也是最引人注目的组件是它的元素。在 A-1 的清单中有大量的元素;< html >、< head >、< title >、< body >、< h1 >、< p >、< ul >、< li >标签都代表元素。

节点

一个节点是 DOM 表示中的公共单元。元素、属性、注释、文档和文本节点都是节点,因此具有典型的节点属性(例如,nodeType、nodeName 和 nodeValue 存在于每个节点中)。

Parent 是指包含当前节点的元素。除了根节点之外,所有节点都有父节点。在清单 A-1 中,< p >元素的父元素是< body >元素。

兄弟姐妹

一个兄弟节点是同一父节点的子节点。通常这个术语用在 previousSibling 和 nextSibling 的上下文中,这两个属性在所有 DOM 节点上都可以找到。在清单 A-1 中,< p >元素的兄弟元素是< h1 >和< ul >元素(以及两个填充空格的文本节点)。

文本节点

文本节点是一个只包含文本的特殊节点;这包括可见文本和所有形式的空白。因此,当您看到元素内部的文本时(例如,<b>hello world!</b>),实际上在元素内部有一个单独的文本节点,包含“hello world!”文字。在清单的中,第二个< li >元素中的文本“很容易使用”包含在一个文本节点中。

全局变量

全局变量存在于代码的全局范围内,但它们的存在是为了帮助您处理常见的 DOM 操作。

文件

该变量包含活动的 HTML DOM 文档,可在浏览器中查看。然而,仅仅因为这个变量存在并且有一个值,并不意味着它的内容已经被完全加载和解析。关于等待 DOM 加载的更多信息,参见第五章。清单 A-2 展示了一些使用文档变量来访问文档元素的例子,该变量保存了 HTML DOM 的表示。

清单 A-2 。使用文档变量访问文档元素

// Locate the element with the ID of 'body'
document.getElementById("body")

// Locate all the elements with the tag name of <div>.
document.getElementsByTagName("div")

htmlelemont

这个变量是所有 HTML DOM 元素的超类对象。扩展这个元素的原型就扩展了所有的 HTML DOM 元素。这个超类在基于 Mozilla 的浏览器和 Opera 中默认可用。可以将其添加到 Internet Explorer 和 Safari 中。清单 A-3 显示了一个将新函数绑定到全局 HTML 元素超类的例子。附加一个 hasClass 函数可以查看一个元素是否有特定的类。

清单 A-3 。将新函数绑定到全局 HTML 元素超类

// Add a new method to all HTML DOM Elements
// that can be used to see if an Element has a specific class, or not.
HTMLElement.prototype.hasClass = function( class ) {
    return new RegExp("(^|\\s)" + class + "(\\s|$)").test( this.className );
};

DOM 导航

以下属性是所有 DOM 元素的一部分,可用于遍历 DOM 文档。

身体

全局 HTML DOM 文档(文档变量)的这个属性直接指向 HTML 元素(应该只有一个)。这个特殊的属性是 DOM 0 JavaScript 时代遗留下来的。清单 A-4 展示了一些从 HTML DOM 文档中访问< body >元素的例子。

清单 。访问 HTML DOM 文档中的主体元素

// Change the margins of the <body>
document.body.style.margin = "0px";

// document.body is equivalent to:
document.getElementsByTagName("body")[0]

子节点

这是所有 DOM 元素的属性,包含所有子节点的数组(包括元素、文本节点、注释等。).此属性是只读的。清单 A-5 展示了如何使用 childNodes 属性向父元素的所有子元素添加样式。

清单 A-5 。使用 childNodes 属性在< body >元素的子元素周围添加红色边框

// Add a border to all child elements of <body>
var c = document.body.childNodes;
for ( var i = 0; i < c.length; i++ ) {
    // Make sure that the Node is an Element
    if ( c[i].nodeType == 1 )
        c[i].style.border = "1px solid red";
}

文档元素

这是所有 DOM 节点的一个属性,作为对文档根元素的引用(对于 HTML 文档,这将总是指向元素)。清单 A-6 展示了一个使用 documentElement 查找 DOM 元素的例子。

清单 A-6 。从任何 DOM 节点定位根文档元素的示例

// Find the documentElement, to find an Element by ID
someRandomNode.documentElement.getElementById("body")

第一个孩子

这是所有 DOM 元素的属性,指向该元素的第一个子节点。如果元素没有子节点,firstChild 将等于null。清单 A-7 展示了一个使用 firstChild 属性从一个元素中移除所有子节点的例子。

清单 。从元素中移除所有子节点

// Remove all child nodes from an element
var e = document.getElementById("body");
while ( e.firstChild )
    e.removeChild( e.firstChild );

getElementById( elemID)

这是一个强大的功能,可以定位文档中具有指定 ID 的元素。该功能仅在文档元素上可用。此外,该函数在非 HTML DOM 文档中可能无法正常工作;通常,对于 XML DOM 文档,必须在 DTD(文档类型定义)或模式中显式指定 ID 属性。

这个函数接受一个参数:您正在搜索的 ID 的名称,如清单 A-8 中的所示。

清单 A-8 。通过 ID 属性定位 HTML 元素的两个例子

// Find the Element with an ID of body
document.getElementById("body")

// Hide the Element with an ID of notice
document.getElementById("notice").style.display = 'none';

getElementsByTagName( tagName)

此属性查找从当前元素开始的所有具有指定标记名的后代元素。这个函数在 XML DOM 和 HTML DOM 文档中的工作方式是一样的。

在所有现代浏览器中,您可以指定*作为标记名,并查找所有后代元素,这比使用纯 JavaScript 递归函数要快得多。

这个函数只有一个参数:您要搜索的元素的标记名。清单 A-9 显示了 getElementsByTagName 的例子。第一个块向文档中的所有< div >元素添加一个高亮类。第二个块查找 ID 为 body 的元素中的所有元素,并隐藏任何具有 highlight 类的元素。

清单 A-9 。演示如何使用 getElementsByTagName 的两个代码块

// Find all <div> Elements in the current HTML document
// and set their class to 'highlight'
var d = document.getElementsByTagName("div");
for ( var i = 0; i < d.length; i++ ) {
    d[i].className = 'hilite';
}
// Go through all descendant elements of the element with
// an ID of body. Then find all elements that have one class
// equal to 'hilite'. Then hide all those elements that match.
var all = document.getElementById("body").getElementsByTagName("*");
for ( var i = 0; i < all.length; i++ ) {
    if ( all[i].className == 'hilite' )
        all[i].style.display = 'none';
}

最后一个孩子

这是一个在所有 DOM 元素上都可用的引用,指向该元素的最后一个子节点。如果不存在子节点,lastChild 将是null。列出 A-10 展示了一个使用 lastChild 属性在文档中插入元素的例子。

清单 。创建一个新的< div >元素,并将其插入到<主体>的最后一个元素之前

// Insert a new Element just before the last element in the <body>
var n = document.createElement("div");
n.innerHTML = "Thanks for visiting!";

document.body.insertBefore( n, document.body.lastChild );

下一个兄弟姐妹

这是一个在所有 DOM 节点上都可用的引用,指向下一个兄弟节点。如果该节点是最后一个兄弟节点,则 nextSibling 将是null。重要的是要记住,nextSibling 可能指向一个 DOM 元素、一个注释,甚至是一个文本节点;它不是导航 DOM 元素的唯一方式。清单 A-11 是使用 nextSibling 属性创建交互式定义列表的一个例子。

清单 A-11 。点击后,使所有< dt >元素展开它们的兄弟< dd >元素

// Find all <dt> (Defintion Term) elements
var dt = document.getElementsByTagName("dt");
for ( var i = 0; i < dt.length; i++ ) {
    // Watch for when the term is clicked
    dt[i].onclick = function() {
        // Since each Term has an adjacent <dd> (Definition) element
        // We can display it when it's clicked

        // NOTE: Only works when there's no whitespace between <dd> elements
        this.nextSibling.style.display = 'block';
    };
}

parent node-父节点

这是所有 DOM 节点的属性。每个 DOM 节点的 parentNode 都指向包含它的元素,除了 document 元素,它指向null(因为没有任何元素包含根元素)。清单 A-12 是使用 parentNode 属性创建定制交互的一个例子。单击“取消”按钮会隐藏父元素。

清单 A-12 。使用 parentNode 属性创建自定义交互

// Watch for when a link is clicked (e.g. a Cancel link)
// and hide the parent element
document.getElementById("cancel").onclick = function(){
    this.parentNode.style.display = 'none';
};

前兄弟姐妹

这是一个在所有 DOM 节点上都可用的引用,指向前一个兄弟节点。如果该节点是第一个兄弟节点,前一个兄弟节点将是null。重要的是要记住,previousSibling 可能指向一个 DOM 元素、一个注释,甚至是一个文本节点;它不是导航 DOM 元素的唯一方式。清单 A-13 展示了一个使用 previousSibling 属性隐藏元素的例子。

清单 A-13 。隐藏当前元素之前的所有元素

// Find all elements before this one and hide them
var cur = this.previousSibling;
while ( cur != null ) {
    cur.style.display = 'none';
    cur = this.previousSibling;
}

节点信息

这些属性存在于大多数 DOM 元素中,以便于您访问公共元素信息。

internal text(内部文字)

这是所有 DOM 元素的属性(只存在于非基于 Mozilla 的浏览器中,因为它不是 W3C 标准的一部分)。该属性返回一个包含当前元素中所有文本的字符串。因为基于 Mozilla 的浏览器不支持该属性,所以您可以利用一种变通方法(使用一个函数来收集后代文本节点的值)。清单 A-14 展示了使用来自第五章的 innerText 属性和 Text()函数的例子。

清单 A-14 。使用 innerText 属性从元素中提取文本信息

// Let's assume that we have an <li> element like this, stored in the variable 'li':
// <li>Please visit <a href="http://mysite.com/">my web site</a>.</li>

// Using the innerText property
li.innerText

// or the text() function described in Chapter `5`
text( li )

// The result of either the property or the function is:
"Please visit my web site."

节点名称

这是一个所有 DOM 元素都有的属性,包含元素名的大写版本。例如,如果您有一个

  • Element, and you access its nodeName property, it will return LI. Listing A-15 shows an example of using the nodeName attribute to modify the class name of the parent element.

清单 A-15 。找到所有父元素 li 和 ?? 并将它们的类设置为当前

// Find all the parents of this node, that are an <li> element
var cur = this.parentNode;
while ( cur != null ) {
    // Once the element is found, and the name verified, add a class
    if ( cur.nodeName == 'LI' )
        cur.className += " current";
    cur = this.parentNode;
}

节点类型

这是所有 DOM 节点的共同属性,包含一个与节点类型相对应的数字。HTML 文档中最常用的三种节点类型如下:

  • 元素节点(值为 1 或文档。元素 _ 节点)
  • 文本节点(值为 3 或文档。文本 _ 节点)
  • 文档节点(值为 9 或文档。文档 _ 节点)

使用 nodeType 属性是一种可靠的方法,可以确保您试图访问的节点具有您认为它具有的所有属性(例如,nodeName 属性只对 DOM 元素有用;所以在访问它之前,您可以使用 nodeType 来确保它等于 1)。清单 A-16 展示了一个使用 nodeType 属性向多个元素添加一个类的例子。

清单 A-16 。定位 HTML <主体>中的第一个元素,并对其应用一个 header 类

// Find the first element in the <body>
var cur = document.body.firstChild;
while ( cur != null ) {
    // If an element was found, add the header class to it
    if ( cur.nodeType == 1 ) {
        cur.className += " header";
        cur = null;

    // Otherwise, continue navigating through the child nodes
    } else {
        cur = cur.nextSibling;
    }
}

节点值

这是文本节点的一个有用属性,可用于访问和操作它们包含的文本。使用中最好的例子是第五章中的文本函数,它用于检索一个元素的所有文本内容。清单 A-17 展示了一个使用 nodeValue 属性构建简单文本值函数的例子。

清单 A-17 。接受一个元素并返回该元素及其所有后代元素的文本内容的函数

function text(e) {
    var t = " ";
    // If an element was passed, get its children,
    // otherwise assume it's an array
    e = e.childNodes || e;

    // Look through all child nodes
    for ( var j = 0; j < e.length; j++ ) {
        // If it's not an element, append its text value
        // Otherwise, recurse through all the element's children
        t += e[j].nodeType != 1 ?
            e[j].nodeValue : text(e[j].childNodes);
    }

    // Return the matched text
    return t;
}

属性

大多数属性都可以作为其包含元素的属性使用。例如,可以使用简单的 element.id 来访问属性 ID。这个特性是 DOM 0 days 的残余,但是由于它的简单性和流行性,它很可能不会有任何发展。

className

这个属性允许你在 DOM 元素中添加和移除类。该属性存在于所有 DOM 元素中。我特别提到这一点的原因是,它的名称 className 与预期的的名称非常不同。奇怪的命名是由于单词在大多数面向对象编程语言中是保留字;因此避免使用它来限制 web 浏览器的编程难度。清单 A-18 展示了一个使用 className 属性隐藏一些元素的例子。

清单 A-18 。找到所有有特殊类的 div 元素并隐藏它们

// Find all the <div> elements in the document
var div = document.getElementsByTagName("div");
for ( var i = 0; i < div.length; i++ ) {
    // Find all the <div> elements that have a single class of 'special'
    if ( div[i].className == "special" ) {
        // And hide them
        div[i].style.display = 'none';
    }
}

getAttribute( attrName )

这是一个函数,作为访问包含在 DOM 元素中的属性值的正确方式。属性用用户在 HTML 文档中提供的值初始化。

该函数只接受一个参数:要检索的属性的名称。清单 A-19 展示了一个使用 getAttribute()函数查找特定类型的输入元素的例子。

清单 A-19 。找到名为 text 的<输入>元素,并将其值复制到一个 ID 为 preview 的元素中

// Find all the form input elements
var input = document.getElementsByTagName("input");
for ( var i = 0; i < input.length; i++ ) {

    // Find the element that has a name of "text"
    if ( input[i].getAttribute("name") == "text" ) {

        // Copy the value into another element
        document.getElementById("preview").innerHTML =
            input[i].getAttribute("value");
    }
}

removeAttribute( attrName )

这是一个可以用来从元素中完全删除属性的函数。通常,使用此函数的结果相当于使用 valueof " "(空字符串)或null执行 setAttribute 然而,在实践中,您应该确保总是清理额外的属性,以避免任何意想不到的后果。

这个函数只有一个参数:您希望删除的属性的名称。列出 A-20 展示了一个取消选中表单中一些复选框的例子。

清单 。查找文档中的所有复选框并取消选中它们

// Find all the form input elements
var input = document.getElementsByTagName("input");
for ( var i = 0; i < input.length; i++ ) {

    // Find all the checkboxes
    if ( input[i].getAttribute("type") == "checkbox" ) {

        // Uncheck the checkbox
        input[i].removeAttribute("checked");

    }

}

箭头属性(attrName、attrValue)

这是一个函数,用来设置包含在 DOM 元素中的属性值。此外,还可以添加自定义属性,以后可以再次访问这些属性,同时不影响 DOM 元素的外观。setAttribute 在 Internet Explorer 中的行为往往很奇怪,使您无法设置特定的属性(如 class 或 maxlength)。这在第五章中有更多解释。

该函数有两个参数。第一个是属性的名称。第二个是要设置属性的值。清单 A-21 展示了一个在 DOM 元素上设置属性值的例子。

清单 A-21 。使用 setAttribute 函数创建一个到 Google 的链接

// Create a new <a> element
var a = document.createElement("a").

// Set the URL to visit to Google's web site
a.setAttribute("href","http://google.com/");

// Add the inner text, giving the user something to click
a.appendChild( document.createTextNode( "Visit Google!" ) );

// Add the link at the end of the document
document.body.appendChild( a );

DOM 修改

以下是可用于操作 DOM 的所有属性和函数。

附录子(nodetoappend)

这是一个可用于向包含元素添加子节点的函数。如果要追加的节点已经存在于文档中,它将从当前位置移动并追加到当前元素。必须在希望追加的元素上调用 appendChild 函数。

该函数接受一个参数:一个对 DOM 节点的引用(这可能是您刚刚创建的节点,也可能是对文档中其他地方存在的节点的引用)。清单 A-22 展示了一个创建新的< ul >元素并将所有< li >元素从它们在 DOM 中的原始位置移动到其中,然后将新的< ul >追加到文档主体的例子。

清单 A-22 。将一系列< li >元素附加到单个< ul >

// Create a new <ul> element
var ul = document.createElement("ul");

// Find all the first <li> elements
var li = document.getElementsByTagName("li");
for ( var i = 0; i < li.length; i++ ) {

    // append each matched <li> into  our new <ul> element
    ul.appendChild( li[i] );
}

// Append our new <ul> element at the end of the body
document.body.appendChild( ul );

克隆代码(true|false)

这个函数是开发人员通过复制现有节点来简化代码的一种方式,然后可以将节点插入到 DOM 中。由于执行普通的 insertBefore 或 appendChild 调用会在文档中物理移动一个 DOM 节点,因此可以使用 cloneNode 函数来复制它。

该函数接受一个 true 或 false 参数。如果参数为真,则克隆节点及其内部的所有内容;如果为 false,则仅克隆节点本身。清单 A-23 展示了一个使用这个函数克隆一个元素并把它附加到自身的例子。

清单 A-23 。找到文档中的第一个< ul >元素,制作它的完整副本,并将其附加到自身

// Find the first <ul> element
var ul = document.getElementsByTagName("ul")[0];

// Clone the node and append it after the old one
ul.parentNode.appendChild( ul.cloneNode( true ) );

createElement( tagName)

这是用于在 DOM 结构中创建新元素的主要函数。该函数作为您希望在其中创建元素的文档的属性而存在。

Image 注意如果你使用的是内容类型为 application/xhtml+xml 的 XHTML,而不是内容类型为 text/html 的普通 HTML,你应该使用 createElementNS 函数而不是 createElement 函数。

这个函数有一个参数:要创建的元素的标记名。清单 A-24 展示了一个使用这个函数创建一个元素并将其包装在其他元素周围的例子。

清单 A-24 。将一个< p >元素的内容包装在一个<强>元素中

// Create a new <strong> element
var s = document.createElement("strong");

// Find the first paragraph
var p = document.getElementsByTagName("p")[0];

// Wrap the contents of the <p> in the <strong> element
while ( p.firstChild ) {
    s.appendChild( p.firstChild );
}

// Put the <strong> element (containing the old <p> contents)
// back into the <p> element
p.appendChild( s );

createelementns(命名空间,tagName)

这个函数与 createElement 函数非常相似,因为它创建一个新元素;但是,它还提供了为元素指定名称空间的能力(例如,如果您要向 XML 或 XHTML 文档中添加一个项目)。

这个函数有两个参数:要添加的元素的名称空间和元素的标记名。清单 A-25 展示了一个使用这个函数在有效的 XHTML 文档中创建 DOM 元素的例子。

清单 A-25 。创建一个新的 XHTML < p >元素,用一些文本填充它,并将它附加到文档主体

// Create a new XHTML-compliant <p>
var p = document.createElementNS("http://www.w3.org/1999/xhtml", "p");

// Add some text into the <p> element
p.appendChild( document.createTextNode( "Welcome to my site." ) );

// Add the <p> element into the document
document.body.insertBefore( p, document.body.firstChild );

createTextNode(文本字符串)

这是创建要插入 DOM 文档的新文本字符串的正确方法。因为文本节点只是文本的 DOM 包装器,所以记住不能对它们进行样式化或追加是很重要的。该函数只作为 DOM 文档的一个属性存在。

该函数接受一个参数:将成为文本节点内容的字符串。清单 A-26 展示了一个使用这个函数创建一个新的文本节点并将它附加到 HTML 页面主体的例子。

清单 。创建一个< h1 >元素并追加一个新的文本节点

// Create a new <h1> element
var h = document.createElement("h1");

// Create the header text and add it to the <h1> element
h.appendChild( document.createTextNode("Main Page") );

// Add the header to the start of the <body>
document.body.insertBefore( h, document.body.firstChild );

内部 HTML

这是一个特定于 HTML DOM 的属性,用于访问和操作 DOM 元素的 HTML 内容的字符串版本。如果您只处理 HTML 文档(而不是 XML 文档),这种方法会非常有用,因为生成一个新的 DOM 元素所需的代码可以大大减少(更不用说它是传统 DOM 方法的一种更快的替代方法)。虽然该属性不是任何特定 W3C 标准的一部分,但它仍然存在于每个现代浏览器中。清单 A-27 展示了一个例子,每当<文本区域>的内容改变时,使用 innerHTML 属性来改变元素的内容。

清单 A-27 。观察<文本区域>的变化,并用其值更新实时预览

// Get the textarea to watch for updates
var t = document.getElementsByTagName("textarea")[0];

// Grab the current value of a <textarea> and update a live preview,
// everytime that it's changed
t.onkeypress = function() {
    document.getElementById("preview").innerHTML = this.value;
};

"我不会成为一个好人,"他说。

这个函数用于在文档中的任意位置插入一个 DOM 节点。必须在要插入的节点的父元素上调用该函数。这样做是为了让您可以为 nodeToInsertBefore 指定null,并将您的节点作为最后一个子节点插入。

该函数有两个参数。第一个参数是您希望插入 DOM 的节点;第二个是之前插入的 DOM 节点。这应该是对有效节点的引用。清单 A-28 展示了一个使用这个函数在页面上的一组 URL 旁边插入一个站点的图标(你在浏览器地址栏的 URL 旁边看到的图标)。

清单 A-28 。遍历所有的< a >元素,并添加一个由网站的 Favicon 组成的图标

// Find all the <a> links within the document
var a = document.getElementsByTagName("a");
for ( var i = 0; i < a.length; i++ ) {

    // Create an image of the linked-to site's favicon
    var img = document.createElement("img");
    img.src = a[i].href.split('/').splice(0,3).join('/') + '/favicon.ico';

    // Insert the image before the link
    a[i].parentNode.insertBefore( img, a[i] );
}

remove child(nodetormove)

这个函数用于从 DOM 文档中移除一个节点。必须在要移除的节点的父元素上调用 removeChild 函数。

该函数接受一个参数:对要从文档中移除的 DOM 节点的引用。清单 A-29 展示了一个遍历文档中所有< div >元素的例子,删除了所有具有单个警告类的元素。

清单 A-29 。删除具有特定类名的所有元素

// Find all <div> elements
var div = document.getElementsByTagName("div");
for ( var i = 0; i < div.length; i++ ) {
    // If  the <div> has one class of 'warning'
    if ( div[i].className == "warning" ) {

        // Remove the <div> from the document
        div[i].parentNode.removeChild( div[i] );
    }
}

replaceChild( nodeToInsert,nodeToReplace)

此功能是删除一个节点并在其位置插入另一个节点的过程的替代方法。该函数必须由您要替换的节点的父元素调用。

这个函数有两个参数:希望插入到 DOM 中的节点,以及要替换的节点。清单 A-30 展示了一个用包含最初链接到的 URL 的< strong >元素替换所有< a >元素的例子。

清单 A-30 。将一组链接转换成普通 URL

// Convert all links to visible URLs (good for printing
// Find all <a> links in the document
var a = document.getElementsByTagName("a");
while ( a.length ) {

    // Create a <strong> element
    var s = document.createElement("strong");

    // Make the contents equal to the <a> link URL
    s.appendChild( document.createTextNode( a[i].href ) );

    // Replace the original <a> with the new <strong> element
    a[i].replaceChild( s, a[i] );
}
posted @ 2024-08-19 17:12  绝不原创的飞龙  阅读(5)  评论(0编辑  收藏  举报