JavaScript-面向对象编程-全-

JavaScript 面向对象编程(全)

原文:zh.annas-archive.org/md5/9BD01417886F7CF4434F47DFCFFE13F5

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

JavaScript 已经成为最强大和多功能的编程语言之一。现代 JavaScript 包含了大量经过时间考验和尖端的特性。其中一些特性正在慢慢塑造下一代 Web 和服务器平台。ES6 引入了非常重要的语言构造,如 promises、classes、箭头函数等,以及其他备受期待的特性。本书详细介绍了语言构造及其实际用途。本书不假设读者有任何 JavaScript 的先验知识,从基础开始全面讲解语言。了解该语言的人仍会发现它有用和信息丰富。对于已经了解 JavaScript 并熟悉 ES5 语法的人来说,本书将是 ES6 特性的非常有用的入门读物。

本书涵盖的内容

第一章, 面向对象的 JavaScript,简要介绍了 JavaScript 的历史、现状和未来,然后继续探讨了面向对象编程(OOP)的基础知识。然后,您将学习如何设置训练环境(Firebug),以便根据书中的示例自己深入学习语言。

第二章, 原始数据类型、数组、循环和条件,讨论了语言基础-变量、数据类型、原始数据类型、数组、循环和条件。

第三章, 函数,涵盖了 JavaScript 使用的函数,您将学会掌握它们。您还将了解变量的作用域和 JavaScript 的内置函数。语言的一个有趣但经常被误解的特性-闭包-在本章末尾被揭开神秘面纱。

第四章, 对象,讨论了对象,如何处理属性和方法,以及创建对象的各种方式。本章还介绍了 Array、Function、Boolean、Number 和 String 等内置对象。

第五章, ES6 迭代器和生成器,介绍了 ES6 最受期待的特性,迭代器和生成器。有了这些知识,您将进一步详细了解增强的集合构造。

第六章, 原型,专门讨论了 JavaScript 中重要的原型概念。它还解释了原型链的工作原理,hasOwnProperty()以及原型的一些陷阱。

第七章, 继承,讨论了继承的工作原理。本章还介绍了一种创建子类的方法,就像其他经典语言一样。

第八章, 类和模块,展示了 ES6 引入的重要语法特性,使得编写经典面向对象编程构造更加容易。ES6 类语法包装了 ES5 略微复杂的语法。ES6 还完全支持模块的语言。本章详细介绍了 ES6 中引入的类和模块构造。

第九章, Promises and Proxies,解释了 JavaScript 一直以来都是一种支持异步编程的语言。直到 ES5,编写异步程序意味着您需要依赖回调-有时会导致回调地狱。ES6 的 promises 是语言中引入的一个备受期待的特性。Promises 在 ES6 中提供了一种更清晰的方式来编写异步程序。Proxies 用于为一些基本操作定义自定义行为。本章讨论了 ES6 中 promises 和 proxies 的实际用途。

第十章,浏览器环境,专门讨论浏览器。本章还涵盖了 BOM(浏览器对象模型)、DOM(W3C 的文档对象模型)、浏览器事件和 AJAX。

第十一章,编码和设计模式,深入探讨了各种独特的 JavaScript 编码模式,以及从《四人帮》中翻译成 JavaScript 的几种与语言无关的设计模式。本章还讨论了 JSON。

第十二章,测试和调试,讨论了现代 JavaScript 配备了支持测试驱动开发和行为驱动开发的工具。Jasmine 是目前最流行的工具之一。本章讨论了使用 Jasmine 作为框架进行 TDD 和 BDD。

第十三章,响应式编程和 React,解释了随着 ES6 的出现,一些激进的想法正在形成。响应式编程以非常不同的方式处理我们使用数据流管理状态变化。而 React 是一个专注于 MVC 视图部分的框架。本章讨论了这两个想法。

附录 A,保留字,列出了 JavaScript 中的保留字。

附录 B,内置函数,是内置 JavaScript 函数的参考,以及示例用法。

附录 C,内置对象,是一个参考,提供了 JavaScript 中每个内置对象的每个方法和属性的使用细节和示例。

附录 D,正则表达式,是一个正则表达式模式参考。

附录 E,练习题答案,提供了章节末尾提到的所有练习的解决方案。

本书所需内容

您需要一个现代浏览器-推荐使用 Google Chrome 或 Firefox,以及一个可选的 Node.js 设置。本书中的大部分代码可以在babeljs.io/repl/jsbin.com/中执行。要编辑 JavaScript,可以使用任何您喜欢的文本编辑器。

本书适合谁

本书适用于刚开始学习 JavaScript 的人,或者懂 JavaScript 但不擅长面向对象部分的人。如果您已经熟悉语言的 ES5 特性,本书可以作为 ES6 的有用入门。

约定

在本书中,您会发现一些文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名显示如下:"Triangle构造函数接受三个点对象并将它们分配给this.points(它自己的点集合)"。

代码块设置如下:

    function sum(a, b) { 
    var c = a + b;
    return c;
    }

任何命令行输入或输出都以以下形式书写:

  **mkdir babel_test**
 **cd babel_test && npm init**
 **npm install --save-dev babel-cli**

新术语和重要单词以粗体显示。例如,屏幕上看到的单词,比如菜单或对话框中的单词,会以这种形式出现:"为了在 Chrome 或 Safari 中打开控制台,右键单击页面的任何位置,然后选择检查元素。弹出的额外窗口就是 Web Inspector 功能。选择控制台选项卡,然后就可以开始了"。

注意

警告或重要提示会以这种形式出现在一个框中。

提示

提示和技巧会以这种形式出现。

第一章:面向对象的 JavaScript

自 Web 早期以来,人们就需要更动态和响应迅速的界面。阅读静态 HTML 文本页面是可以的,而且当它们通过 CSS 精美呈现时更好,但在浏览器中与应用程序进行互动,如电子邮件、日历、银行业务、购物、绘图、游戏和文本编辑,会更有趣。所有这些都得益于 JavaScript,这是 Web 的编程语言。JavaScript 始于嵌入 HTML 中的简单一行代码,但现在以更复杂的方式使用。开发人员利用语言的面向对象特性构建可重用部分组成的可扩展代码架构。

如果您看一下 Web 开发中过去和现在的热词,DHTML、Ajax、Web 2.0、HTML5,它们本质上都意味着 HTML、CSS 和 JavaScript——HTML 用于内容,CSS 用于呈现,JavaScript 用于行为。换句话说,JavaScript 是使一切协同工作的粘合剂,这样我们就可以构建丰富的 Web 应用程序。

然而,这还不是全部;JavaScript 不仅仅可以用于 Web。

JavaScript 程序在主机环境中运行。Web 浏览器是最常见的环境,但不是唯一的环境。使用 JavaScript,您可以创建各种小部件、应用程序扩展和其他软件,稍后您将看到。学习 JavaScript 是一个明智的投资;您学习一种语言,然后可以编写在多个平台上运行的各种不同的应用程序,包括移动和服务器端应用程序。如今,可以说 JavaScript 无处不在。

本书从零开始,不假设除了对 HTML 的一些基本理解之外,读者具有任何先前的编程知识。虽然有一章专门讲解 Web 浏览器环境,但本书的其余部分都是关于 JavaScript 的一般知识,因此适用于所有环境。

让我们从以下内容开始:

  • JavaScript 背后故事的简要介绍

  • 在面向对象编程讨论中会遇到的基本概念

一点历史

最初,Web 不过是一系列以静态 HTML 文档形式连接在一起的科学出版物。信不信由你,曾经有一段时间页面上无法放置图像。然而,这很快就改变了。随着 Web 的普及和规模的扩大,创建 HTML 页面的网站管理员感到他们需要更多的东西。他们想要创建更丰富的用户交互,主要是出于希望为简单任务(如表单验证)节省服务器往返时间的愿望。出现了两个选择——Java 小程序和 LiveScript,这是 Brendan Eich 于 1995 年在Netscape构思的一种语言,后来被包含在 Netscape 2.0 浏览器中,名为 JavaScript。

小程序并没有完全流行起来,但 JavaScript 却流行起来了。网站管理员社区欣然接受了在 HTML 文档中嵌入的简短代码片段,并改变了本来静态的 Web 页面元素。很快,竞争对手浏览器供应商微软发布了带有 JScript 的Internet ExplorerIE)3.0,这是 JavaScript 的一个反向工程版本,还加入了一些 IE 特定的功能。最终,有人努力标准化语言的各种实现,这就是 ECMAScript 诞生的原因。欧洲计算机制造商协会ECMA)创建了名为 ECMA-262 的标准,描述了 JavaScript 编程语言的核心部分,而没有浏览器和网页特定的功能。

您可以将 JavaScript 视为涵盖以下三个部分的术语:

  • ECMAScript:核心语言——变量、函数、循环等。这部分与浏览器无关,这种语言可以在许多其他环境中使用。

  • 文档对象模型DOM):它提供了处理 HTML 和 XML 文档的方法。最初,JavaScript 只能对页面上可脚本化的内容进行有限的访问,主要是表单、链接和图像。后来,它被扩展为使所有元素都可以进行脚本化。这导致了 W3C 制定 DOM 标准,作为一种独立于语言的(不再与 JavaScript 绑定)操纵结构化文档的方式。

  • 浏览器对象模型BOM):这是一组与浏览器环境相关的对象,直到 HTML5 开始标准化一些跨浏览器存在的常见对象之前,它从未成为任何标准的一部分。

虽然本书有一章专门讲述浏览器、DOM 和 BOM,但本书的大部分内容描述了核心语言,并教授了你可以在任何 JavaScript 程序运行的环境中使用的技能。

浏览器战争和复兴

不管好坏,JavaScript 的即时流行发生在浏览器战争 I 时期(大约 1996 年至 2001 年)。那是在最初的互联网繁荣时期,两大浏览器供应商网景和微软正在争夺市场份额。他们不断为他们的浏览器和 JavaScript、DOM 和 BOM 的版本添加更多的花哨功能,这自然导致了许多不一致性。在添加更多功能的同时,浏览器供应商在提供适当的开发和调试工具以及充分的文档方面落后了。开发经常是一种痛苦;你在一个浏览器中测试脚本,一旦开发完成,你在另一个浏览器中测试,结果发现你的脚本无缘无故地失败,你能得到的最好结果就是一个晦涩的错误消息,比如操作中止。

不一致的实现、缺失的文档和不合适的工具让 JavaScript 显得如此不堪,以至于许多程序员根本不愿意费心去处理它。

另一方面,试图尝试使用 JavaScript 进行实验的开发人员有些过分,他们在页面上添加了太多的特效,而并不太关心最终结果的可用性。开发人员渴望利用浏览器提供的每一种新可能性,结果最终在他们的网页上添加了诸如状态栏中的动画、闪烁的颜色、闪烁的文本、跟踪鼠标光标的对象等许多创新,实际上却损害了用户体验。这些滥用 JavaScript 的方式现在大多已经消失,但它们是语言声誉不佳的原因之一。许多严肃的程序员认为 JavaScript 只是设计师玩耍的玩具,并认为它不适合严肃的应用程序。JavaScript 的反弹导致一些网络项目完全禁止任何客户端编程,只信任可预测和严格控制的服务器。而且,为什么要将交付成品的时间加倍,然后花费额外的时间来调试不同浏览器的问题呢?

一切在浏览器战争结束后的几年里发生了改变。一些事件以积极的方式重塑了网络开发的格局。其中一些如下:

  • 微软在 IE6 推出后赢得了这场战争,当时是最好的浏览器,多年来他们停止了 Internet Explorer 的开发。这给其他浏览器赶上甚至超越 IE 的能力提供了时间。

  • 网络标准的运动被开发人员和浏览器供应商所接受。自然地,开发人员不喜欢为了应对浏览器的差异而编写两次(或更多次)代码;因此,他们喜欢有一套大家都会遵循的约定标准的想法。

  • 开发人员和技术不断成熟,越来越多的人开始关注可用性、渐进增强技术和可访问性等问题。诸如 Firebug 之类的工具使开发人员更加高效,开发变得不那么痛苦。

在这种更健康的环境中,开发人员开始发现使用已经可用的工具的新方法和更好的方法。在 Gmail 和 Google Maps 等富有客户端编程的应用程序公开发布后,人们开始意识到 JavaScript 是一种成熟的、在某些方面独特的、强大的原型对象导向语言。其重新发现的最好例子是XMLHttpRequest对象提供的功能的广泛采用,这个对象曾经是 IE 独有的创新,但后来被大多数其他浏览器实现。XMLHttpRequest对象允许 JavaScript 发出 HTTP 请求,并从服务器获取新内容,以便更新页面的某些部分而无需完全重新加载页面。由于广泛使用XMLHttpRequest对象,一种新型的类似桌面的 Web 应用程序,称为 Ajax 应用程序,诞生了。

现在

关于 JavaScript 的一个有趣之处是它总是在主机环境中运行。Web 浏览器只是其中一个可用的主机。JavaScript 也可以在服务器、桌面和移动设备上运行。今天,您可以使用 JavaScript 执行以下所有操作:

  • 创建丰富而强大的 Web 应用程序(在 Web 浏览器内运行的应用程序)。HTML5 的增加,如应用程序缓存、客户端存储和数据库,使浏览器编程对在线和离线应用程序都变得越来越强大。Chrome WebKit 的强大增加还包括对服务工作者和浏览器推送通知的支持。

  • 使用Node.js编写服务器端代码,以及可以在 Rhino(用 Java 编写的 JavaScript 引擎)上运行的代码。

  • 制作移动应用程序;您可以使用PhoneGapTitanium完全使用 JavaScript 为 iPhone、Android 和其他手机和平板电脑创建应用程序。此外,为移动电话的 Firefox OS 应用程序完全由 JavaScript、HTML 和 CSS 创建。来自 Facebook 的 React Native 是一种令人兴奋的新方法,可以使用 JavaScript 开发本机 iOS、Android 和 Windows(实验性)应用程序。

  • 使用基于 ECMAScript 的 ActionScript 创建丰富的媒体应用程序,如 Flash 或 Flex。

  • 使用Windows Scripting HostWSH)或 WebKit 的JavaScriptCore在桌面上编写命令行工具和脚本,以自动化管理任务。这些工具在所有 Mac 上都可用。

  • 为大量桌面应用程序编写扩展和插件,如 Dreamweaver、Photoshop 和大多数其他浏览器。

  • 使用 Mozilla 的XULRunnerElectron创建跨操作系统的桌面应用程序。Electron 用于构建一些最受欢迎的桌面应用程序,如 Slack、Atom 和 Visual Studio Code。

  • 另一方面,Emscripten允许用 C/C++编写的代码编译成asm.js格式,然后在浏览器内运行。

  • PhantomJS这样的测试框架是使用 JavaScript 编程的。

  • 这绝不是一个详尽的列表。JavaScript 最初在网页内部开始,但今天可以说它几乎无处不在。此外,浏览器供应商现在将速度作为竞争优势,并竞相创建最快的 JavaScript 引擎,这对用户和开发人员都是好事,并为 JavaScript 在图像、音频和视频处理以及游戏开发等新领域的更强大用途打开了大门。

未来

我们只能推测未来会是什么样子,但可以肯定的是它将包括 JavaScript。有一段时间,JavaScript 可能被低估和低使用(或者可能在错误的方式上被过度使用),但每天我们都见证语言在更有趣和创造性的方式中得到新的应用。一切都始于简单的一行代码,通常嵌入在 HTML 标签属性中,比如onclick。如今,开发人员发布复杂、设计良好、可扩展的应用程序和库,通常支持单个代码库的多个平台。JavaScript 确实被认真对待,开发人员开始重新发现并越来越多地享受其独特的特性。

曾经在招聘职位的 nice-to-have 部分列出,如今,对 JavaScript 的了解往往是招聘 Web 开发人员时的决定性因素。今天你可能会听到的常见面试问题包括- JavaScript 是一种面向对象的语言吗?好的。那么,在 JavaScript 中如何实现继承?阅读完本书后,你将准备好在 JavaScript 面试中脱颖而出,甚至用一些他们可能不知道的知识来给面试官留下深刻印象。

ECMAScript 5

ECMAScript 修订中最重要的里程碑是ECMAScript 5ES5),于 2009 年 12 月正式被接受。ECMAScript 5 标准在所有主要浏览器和服务器端技术上都得到了实施和支持。

ES5 是一个重大的修订,因为除了一些重要的语法变化和标准库的添加之外,ES5 还在语言中引入了几个新的构造。

例如,ES5 引入了一些新的对象和属性,以及所谓的严格模式。严格模式是语言的一个子集,排除了已弃用的特性。严格模式是选择加入的,不是必须的,这意味着如果你希望你的代码在严格模式下运行,你将使用以下字符串声明你的意图(每个函数一次,或整个程序一次):

    "use strict"; 

这只是一个 JavaScript 字符串,将字符串漂浮在任何变量之外是可以的。因此,不支持 ES5 的旧版浏览器将简单地忽略它,因此这种严格模式是向后兼容的,不会破坏旧版浏览器。

为了向后兼容,本书中的所有示例都适用于 ES3,但与此同时,本书中的所有代码都是这样编写的,以便在 ES5 的严格模式下不会出现警告。此外,任何 ES5 特定的部分都将被清楚地标记出来。附录 C,内置对象,详细列出了 ES5 的新添加。

ES6 中的严格模式

虽然 ES5 中的严格模式是可选的,但所有 ES6 模块和类默认都是严格模式。正如你很快会看到的,我们在 ES6 中编写的大部分代码都驻留在一个模块中;因此,默认情况下强制执行严格模式。然而,重要的是要理解,所有其他构造都没有隐式的严格模式强制执行。曾经有努力使新的构造,比如箭头和生成器函数,也强制执行严格模式,但后来决定这样做会导致非常分散的语言规则和代码。

ECMAScript 6

ECMAScript 6 修订花了很长时间才完成,最终于 2015 年 6 月 17 日被接受。ES6 的特性正在逐渐成为主要浏览器和服务器技术的一部分。可以使用转译器将 ES6 编译为 ES5,并在尚未完全支持 ES6 的环境中使用该代码(我们稍后将详细讨论转译器)。

ES6 大大升级了 JavaScript 作为一种语言,并带来了非常令人兴奋的语法变化和语言构造。总的来说,这个 ECMAScript 修订中有两种基本的变化,如下所示:

  • 改进了现有功能的语法和标准库的版本;例如,类和承诺

  • 新的语言特性;例如,生成器

ES6 允许您以不同的方式思考您的代码。新的语法变化可以让您编写更清洁、更易于维护的代码,而且不需要特殊的技巧。语言本身现在支持了以前需要第三方模块的几种构造。ES6 引入的语言变化需要认真重新考虑我们一直以来在 JavaScript 中编码的方式。

关于命名法-ECMAScript 6、ES6 和 ECMAScript 2015 是相同的,但可以互换使用。

ES6 的浏览器支持

大多数浏览器和服务器框架都在逐步实现 ES6 功能。您可以通过点击kangax.github.io/compat-table/es6/来查看支持和不支持的内容。

尽管并非所有浏览器和服务器框架都完全支持 ES6,但我们可以借助转译器几乎使用 ES6 的所有功能。转译器是源到源编译器。ES6 转译器允许您以 ES6 语法编写代码,并将其编译/转换为等效的 ES5 语法,然后可以在不支持整个 ES6 功能范围的浏览器上运行。

目前事实上的 ES6 转译器是 Babel。在本书中,我们将使用 Babel 来编写和测试我们的示例。

Babel

Babel 几乎支持所有 ES6 功能,可以直接使用或使用自定义插件。Babel 可以从各种构建系统、框架和语言到模板引擎中使用,并且具有良好的命令行和 REPL 内置。

要了解 Babel 如何将 ES6 代码转译为其 ES5 等效形式,请转到 Babel REPL(babeljs.io/repl/)。

Babel REPL 允许您快速测试 ES6 的小片段。当您在浏览器中打开 Babel REPL 时,您会看到一些 ES6 代码默认在那里。在左窗格中,删除代码并输入以下文本:

    var name = "John", mood = "happy"; 
    console.log(`Hey ${name}, are you feeling ${mood} today?`) 

当您输入此内容并从左窗格切换时,您将看到 REPL 将此 ES6 代码转换为以下代码:

    "use strict"; 
    var name = "John", 
      mood = "happy"; 
    console.log("Hey " + name + ", 
      are you feeling " + mood + " today?"); 

这是我们在左窗格中早些时候编写的代码的 ES5 等效代码。您可以看到右窗格中的结果代码是熟悉的 ES5。正如我们所说,Babel REPL 是一个尝试和实验各种 ES6 构造的好地方。然而,我们需要 babel 自动将您的 ES6 代码转译成 ES5,为此,您可以将 Babel 包含到您现有的构建系统或框架中。

让我们首先安装 Babel 作为一个命令行工具。为此,我们将假设您熟悉 node 和 Node Package Manager(npm)。使用npm安装 Babel 很容易。让我们首先创建一个目录,我们将在其中安装 Babel 作为一个模块和其余的源代码。在我的 Mac 上,以下命令将创建一个名为babel_test的目录,使用npm init初始化项目,并使用npm安装 Babel 命令行:

 **mkdir babel_test**
 **cd babel_test && npm init**
 **npm install --save-dev babel-cli**

如果您熟悉npm,您可能会想要全局安装 Babel。但是,通常不建议将 Babel 安装为全局模块。一旦您在项目中安装了 Babel,您的package.json文件将看起来像以下代码块:

    { 
      "name": "babel_test", 
      "version": "1.0.0", 
      "description": "", 
      "main": "index.js", 
      "scripts": { 
        "test": "echo "Error: no test specified" && exit 1" 
      }, 
      "author": "", 
      "license": "ISC", 
      "devDependencies": { 
        "babel-cli": "⁶.10.1" 
      } 
    } 

您可以看到为 Babel 创建了一个版本大于 6.10.1 的开发依赖项。您可以通过从命令行调用它或作为构建步骤的一部分来使用 Babel 来转译您的代码。对于任何非平凡的工作,您将需要后一种方法。要在项目构建步骤的一部分调用 Babel,您可以将一个build步骤添加到您的package.json文件的script标签中,例如:

    "scripts": { 
      "build": "babel src -d lib" 
    }, 

当您执行 npm build 时,Babel 将在您的src目录上调用,并将转译后的代码放在lib目录中。或者,您也可以通过编写以下命令手动运行 Babel:

 **$ ./node_modules/.bin/babel src -d lib**

我们将在本书的后面讨论各种 Babel 选项和插件。本节将使您能够开始探索 ES6。

面向对象编程

在深入研究 JavaScript 之前,让我们花点时间回顾一下人们在说到面向对象时指的是什么,以及这种编程风格的主要特点是什么。以下是在谈论面向对象编程OOP)时最常用的概念列表:

  • 对象,方法和属性

  • 封装

  • 聚合

  • 可重用性/继承

  • 多态性

让我们更仔细地看看这些概念中的每一个。如果您对面向对象编程术语感到陌生,这些概念可能听起来太理论化,您可能会在一次阅读中难以理解或记住它们。不要担心,这需要几次尝试,而且从概念层面上来说,这个主题可能有点枯燥。但是,我们将在本书的后面看到大量的代码示例,您会发现实际上事情要简单得多。

对象

正如名字所暗示的那样,对象很重要。对象是事物(某人或某物)的表示,这种表示是通过编程语言来表达的。事物可以是任何东西,现实生活中的对象,或者更复杂的概念。以常见的对象猫为例,您可以看到它具有某些特征-颜色,名称,重量等,并且可以执行一些动作-喵喵叫,睡觉,躲藏,逃跑等。对象的特征在面向对象编程中称为属性,动作称为方法。

与口语的类比如下:

  • 对象通常使用名词命名,例如书,人等

  • 方法是动词,例如读,运行等

  • 属性的值是形容词

以句子“黑猫睡在垫子上”为例。 “猫”(名词)是对象,“黑色”(形容词)是颜色属性的值,“睡觉”(动词)是面向对象编程中的动作或方法。为了类比,我们可以再进一步说“在垫子上”指定了关于动作“睡觉”的一些内容,因此它充当了传递给sleep方法的参数。

在现实生活中,可以根据某些标准对类似的对象进行分组。蜂鸟和老鹰都是鸟类,因此它们可以被归类为某个虚构的Birds类。在面向对象编程中,类是对象的蓝图或配方。对象的另一个名称是实例,因此我们可以说老鹰是Birds类的一个具体实例。您可以使用相同的类创建不同的对象,因为类只是一个模板,而对象是基于模板的具体实例。

JavaScript 和经典的面向对象语言(如 C++和 Java)之间存在差异。您应该从一开始就意识到,在 JavaScript 中,没有类;一切都基于对象。 JavaScript 具有原型的概念,它们也是对象(我们稍后将详细讨论它们)。在经典的面向对象语言中,您会说类似于-为我创建一个名为Bob的新对象,它属于Person类。在原型面向对象语言中,您会说-我将采用我已经准备好的 Bob 爸爸对象(在电视前的沙发上?)并将其重用作我将称之为Bob的新对象的原型。

封装

封装是另一个与面向对象编程相关的概念,它说明了一个对象包含(封装)以下内容:

  • 数据(存储在属性中)

  • 使用方法对数据进行操作

封装是与封装相配的另一个术语。这是一个相当广泛的术语,可能意味着不同的事情,但让我们看看人们在面向对象编程的上下文中使用它时通常指的是什么。

想象一个对象,比如 MP3 播放器。作为对象的用户,您被赋予一些接口来使用,例如按钮,显示屏等。您使用接口来使对象对您有用,比如播放一首歌。设备内部的工作方式,您不知道,而且通常也不关心。换句话说,接口的实现对您是隐藏的。当您的代码通过调用其方法使用对象时,面向对象编程中也会发生同样的事情。您的代码不需要知道方法内部是如何工作的,无论您自己编写了对象还是它来自某个第三方库;您的代码都不需要知道方法的内部工作方式。在编译语言中,您实际上无法阅读使对象工作的代码。在 JavaScript 中,因为它是一种解释性语言,您可以看到源代码,但概念仍然是一样的-您使用对象的接口而不用担心其实现。

信息隐藏的另一个方面是方法和属性的可见性。在某些语言中,对象可以具有publicprivateprotected方法和属性。这种分类定义了对象用户的访问级别。例如,只有相同对象的方法才能访问private方法,而任何人都可以访问public方法。在 JavaScript 中,所有方法和属性都是public,但我们将看到有方法来保护对象内部的数据并实现隐私。

聚合

将几个对象组合成一个新对象称为聚合或组合。这是将问题分解为更小且更易管理的部分的强大方式(分而治之)。当问题范围如此复杂以至于不可能在整体上以详细级别考虑它时,您可以将问题分解为几个较小的领域,可能然后将每个领域分解为更小的部分。这使您可以在几个抽象级别上考虑问题。

例如,个人计算机。这是一个复杂的对象。您无法考虑启动计算机时需要发生的所有事情。但是,您可以抽象地说,您需要初始化Computer对象包含的所有单独对象,Monitor对象,Mouse对象,Keyboard对象等。然后,您可以深入研究每个子对象。通过组装可重用部分,您可以组合复杂对象。

再举一个类比,一个Book对象可以包含(聚合)一个或多个Author对象,一个Publisher对象,几个Chapter对象,一个TOC(目录)等。

继承

继承是重用现有代码的一种优雅方式。例如,您可以拥有一个通用对象Person,它具有诸如namedate_of_birth之类的属性,并且还实现了walktalksleepeat功能。然后,您发现自己需要另一个名为Programmer的对象。您可以重新实现Person对象具有的所有方法和属性,但更明智的做法是只说Programmer对象继承了Person对象,并节省一些工作。 Programmer对象只需要实现更具体的功能,例如writeCode方法,同时重用Person对象的所有功能。

在经典面向对象编程中,类继承自其他类,但在 JavaScript 中,由于没有类,对象继承自其他对象。

当一个对象从另一个对象继承时,通常会向继承的方法中添加新方法,从而扩展旧对象。通常,以下短语可以互换使用-B 从 A 继承和 B 扩展 A。此外,继承的对象可以选择一个或多个方法并重新定义它们,根据自己的需要进行定制。这样,接口保持不变,方法名相同,但在新对象上调用时,方法的行为会有所不同。重新定义继承方法的工作方式的这种方式被称为重写

多态性

在前面的例子中,一个Programmer对象继承了父对象Person的所有方法。这意味着两个对象都提供了talk方法,以及其他方法。现在想象一下,在你的代码中的某个地方,有一个名为Bob的变量,碰巧你不知道Bob是一个Person对象还是一个Programmer对象。你仍然可以在Bob对象上调用talk方法,代码将正常工作。在不同对象上调用相同的方法,并让它们以自己的方式做出响应的能力,称为多态性。

OOP 总结

以下是一个快速总结迄今为止讨论的概念的表格:

特征 说明概念
Bob 是一个男人(一个对象)。 对象
Bob 的出生日期是 1980 年 6 月 1 日,性别-男性,头发-黑色。 属性
Bob 可以吃饭,睡觉,喝水,做梦,交谈,计算自己的年龄。 方法
Bob 是Programmer类的一个实例。 类(在经典 OOP 中)
Bob 基于另一个名为Programmer的对象。 原型(在原型 OOP 中)
Bob 保存数据,比如birth_date,以及处理这些数据的方法,比如calculateAge() 封装
你不需要知道计算方法是如何内部工作的。对象可能有一些私有数据,比如闰年二月的天数。你不知道,也不想知道。 信息隐藏
Bob 是WebDevTeam对象的一部分,与 Jill 一起,她是Designer对象,以及 Jack,他是ProjectManager对象。 聚合和组合
DesignerProjectManagerProgrammer都基于并扩展了Person对象。 继承
你可以调用Bob.talk()Jill.talk()Jack.talk()方法,它们都能正常工作,尽管产生不同的结果。Bob 可能会更多地谈论性能,Jill 谈论美丽,Jack 谈论截止日期。每个对象都从 Person 继承了 talk 方法并对其进行了定制。 多态性和方法重写

设置你的培训环境

这本书在编写代码时采取了自助式的方法,因为我坚信真正学习编程语言的最佳方式是通过编写代码。没有现成的代码下载供你直接放入你的页面。相反,你应该输入代码,看看它是如何工作的,然后调整它并进行调试。在尝试代码示例时,鼓励你将代码输入 JavaScript 控制台。让我们看看你如何做到这一点。

作为开发人员,你很可能已经在你的系统上安装了许多网络浏览器,比如 Firefox,Safari,Chrome 或 Internet Explorer。所有现代浏览器都有一个 JavaScript 控制台功能,你将在整本书中使用它来帮助你学习和实验语言。更具体地说,这本书使用的是 WebKit 的控制台,它在 Safari 和 Chrome 中可用,但示例应该在任何其他控制台中工作。

WebKit 的 Web 检查器

这个例子展示了你如何使用控制台输入一些代码,将 google.com 主页上的标志替换为你选择的图像。正如你所看到的,你可以在任何页面上实时测试你的 JavaScript 代码:

WebKit 的 Web 检查器

为了在 Chrome 或 Safari 中打开控制台,请在页面的任何位置右键单击,然后选择检查元素。弹出的附加窗口是 Web 检查器功能。选择控制台选项卡,然后您就可以开始了。

您可以直接在控制台中输入代码,当您按下Enter时,您的代码将被执行。代码的返回值将打印在控制台中。代码是在当前加载的页面的上下文中执行的,因此,例如,如果您键入location.href,它将返回当前页面的 URL。

控制台还具有自动完成功能。它的工作方式类似于操作系统的普通命令行提示或完整的 IDE 的自动完成功能。例如,如果您键入docu并按Tab或右箭头键,docu将自动完成为 document。然后,如果您键入.(点运算符),您可以遍历可以在document对象上调用的所有可用属性和方法。

通过使用上下箭头键,您可以浏览已执行命令的列表,并将它们带回控制台。

控制台只允许您输入一行,但您可以使用分号将多个 JavaScript 语句分开执行。如果您需要更多行,可以按Shift + Enter换行,而不立即执行结果。

Mac 上的 JavaScriptCore

在 Mac 上,您实际上不需要浏览器;您可以直接从命令行终端应用程序中探索 JavaScript。

如果您以前从未使用过终端,您可以在Spotlight 搜索中简单搜索它。启动后,输入以下命令:

 **alias jsc='/System/Library/Frameworks/JavaScriptCore.framework/
      Versions/Current/Resources/jsc'**

这个命令创建了一个别名,指的是 JavaScriptCore 的小应用程序,它是 WebKit 引擎的一部分。JavaScriptCore 与 Mac 操作系统一起发布。

您可以将先前显示的alias行添加到您的~/.profile文件中,这样当您需要时jsc就会一直在那里。

现在,为了启动交互式 shell,您只需从任何目录键入jsc。然后,您可以输入 JavaScript 表达式,当您按下Enter时,您将看到表达式的结果。看一下以下的屏幕截图:

Mac 上的 JavaScriptCore

更多控制台

所有现代浏览器都内置了控制台。您之前已经看到了 Chrome/Safari 控制台。在任何 Firefox 版本中,您都可以安装 Firebug 扩展,其中包含一个控制台。此外,在较新的 Firefox 版本中,有一个内置的控制台,可以通过工具 | Web 开发人员 | Web 控制台菜单访问。

更多控制台

自 Internet Explorer 8 以来,它具有 F12 开发人员工具功能,其中包含脚本选项卡中的控制台。

熟悉Node.js也是一个好主意,您可以通过尝试它的控制台来开始。从nodejs.org安装Node.js,并在命令提示符(终端)中尝试控制台:

更多控制台

正如您所看到的,您可以使用Node.js控制台尝试快速示例。但是,您也可以编写更长的 shell 脚本(屏幕截图中的test.js)并使用scriptname.js节点运行它们。

Node REPL 是一个强大的开发工具。当您在命令行上键入'node'时,REPL 会被调用。您可以在这个 REPL 上尝试 JavaScript:

    node 
    > console.log("Hellow World"); 
    Hellow World 
    undefined 
    > a=10, b=10; 
    10 
    > console.log(a*b); 
    100 
    undefined 

总结

在本章中,您了解了 JavaScript 的诞生以及它的现状。您还了解了面向对象编程的概念,并看到 JavaScript 不是基于类的面向对象语言,而是基于原型的语言。最后,您学会了如何使用您的训练环境- JavaScript 控制台。现在,您已经准备好深入学习 JavaScript,并学习如何使用它强大的面向对象特性。不过,让我们从头开始。

下一章将指导您了解 JavaScript 中的数据类型(只有几种)、条件、循环和数组。如果您认为自己已经了解这些主题,可以随意跳过下一章,但在此之前,请确保您能够完成本章末尾的几个简短练习。

第二章:原始数据类型、数组、循环和条件

在深入研究 JavaScript 的面向对象特性之前,让我们首先看一些基础知识。本章将介绍以下主题:

  • JavaScript 中的原始数据类型,如字符串和数字

  • 数组

  • 常见运算符,如+-deletetypeof

  • 流程控制语句,如循环和if...else条件

变量

变量用于存储数据;它们是具体值的占位符。编写程序时,使用变量而不是实际数据更方便,因为写pi比写3.141592653589793要容易得多;特别是当它在程序中多次出现时。存储在变量中的数据可以在最初分配后进行更改,因此称为变量。您还可以使用变量存储在编写代码时对您未知的数据,例如稍后操作的结果。

使用变量需要以下两个步骤。您需要:

  • 声明变量

  • 初始化它,即给它一个值

要声明变量,您将使用var语句,如下面的代码片段:

    var a; 
    var thisIsAVariable;  
    var _and_this_too;  
    var mix12three; 

对于变量的名称,可以使用字母、数字、下划线字符和美元符号的任何组合。但是,不能以数字开头,这意味着以下代码声明无效:

    var 2three4five; 

初始化变量意味着为第一次(初始)赋予它一个值。以下是两种方法:

  • 首先声明变量,然后初始化它

  • 声明并用单个语句初始化它

后者的示例如下:

    var a = 1; 

现在名为a的变量包含值1

您可以使用单个var语句声明,并可选择初始化多个变量;只需用逗号分隔声明,如下行代码所示:

    var v1, v2, v3 = 'hello', v4 = 4, v5; 

为了可读性,通常使用每行一个变量来编写,如下所示:

    var v1,  
        v2,  
        v3 = 'hello',  
        v4 = 4,  
        v5; 

注意

变量名称中的$字符

您可能会看到变量名称中使用美元符号字符($),如$myvar或不太常见的my$var。变量名称中允许此字符出现在任何位置,尽管以前的 ECMA 标准版本不鼓励在手写程序中使用它,并建议它只能在生成的代码(由其他程序编写的程序)中使用。JavaScript 社区并不太尊重这个建议,实际上$在实践中常用作函数名。

变量区分大小写

变量名称区分大小写。您可以轻松通过 JavaScript 控制台验证此语句。尝试按下每行后的Enter键输入以下代码:

    var case_matters = 'lower'; 
    var CASE_MATTERS = 'upper';  
    case_matters; 
    CASE_MATTER; 

在输入第三行时,为了节省按键次数,您可以输入case并按Tab或右箭头键。Console会自动将变量名补全为case_matters。类似地,对于最后一行,输入CASE并按Tab键。最终结果如下图所示:

变量区分大小写

在本书的其余部分,只提供示例的代码,而不是屏幕截图,如下所示:

    > var case_matters = 'lower'; 
    > var CASE_MATTERS = 'upper'; 
    > case_matters; 
    "lower" 
    > CASE_MATTERS; 
    "upper" 

大于号(>)显示您键入的代码;其余部分是Console中打印的结果。再次提醒,当您看到这样的代码示例时,强烈建议您自己键入代码。然后,您可以通过稍微调整代码来进行实验,以更好地了解其工作原理。

注意

你可以在前面的截图中看到,有时你在控制台中输入的内容会导致undefined这个词。你可以简单地忽略它,但如果你好奇的话,当评估(执行)你输入的内容时,控制台会打印返回的值。一些表达式,比如var a = 1;,不会显式地返回任何东西,在这种情况下,它们会隐式地返回特殊值undefined(稍后会详细介绍)。当一个表达式返回某个值(例如,前面例子中的case_matters或类似1 + 1的东西)时,结果值会被打印出来。并非所有的控制台都会打印undefined值;例如,Firebug 控制台。

运算符

运算符接受一个或两个值(或变量),执行一个操作,并返回一个值。让我们看一个使用运算符的简单例子,以澄清术语:

    > 1 + 2; 
    3 

在上面的代码中:

  • +符号是运算符

  • 操作是加法

  • 输入值是12(它们也被称为操作数)

  • 结果值是3

  • 整个东西被称为表达式

不要直接在表达式中使用值12,你可以使用变量。你也可以使用一个变量来存储操作的结果,如下面的例子所示:

    > var a = 1; 
    > var b = 2; 
    > a + 1; 
    2 
    > b + 2; 
    4 
    > a + b; 
    3 
    > var c = a + b; 
    > c; 
    3 

以下表格列出了基本的算术运算符:

运算符符号 操作 示例
+ 加法
> 1 + 2;   
3   

|

- 减法
> 99.99 - 11;   
88.99   

|

* 乘法
> 2 * 3;   
6   

|

/ 除法
> 6 / 4;   
1.5   

|

% 取模,除法的余数
> 6 % 3;   
0   
> 5 % 3;   
2   

有时候测试一个数字是偶数还是奇数是很有用的。使用取模运算符,很容易做到这一点。所有奇数被 2 整除时返回1,而所有偶数返回0,例如:

> 4 % 2;   
0   
> 5 % 2;   
1   

|

| ++ | 将值增加1 | 后增加是指在返回之后增加输入值,例如:

> var a = 123;    
> var b = a++;   
> b;   
123   
> a;   
124   

相反的是前增加。输入值首先增加1,然后返回,例如:

> var a = 123;    
> var b = ++a;   
> b;   
124   
> a;   
124   

|

| -- | 将值减 1 | 后减:

> var a = 123;    
> var b = a--;   
> b;   
123   
> a;   
122   

前减:

> var a = 123;    
> var b = --a;   
> b;   
122   
> a;   
122   

|

var a = 1;也是一个操作;它是简单的赋值操作,=简单赋值运算符

还有一类运算符,它们是赋值和算术运算符的组合。这些被称为复合运算符。它们可以使你的代码更加简洁。让我们看一些例子:

    > var a = 5; 
    > a += 3; 
    8 

在这个例子中,a += 3;只是a = a + 3;的一种更简洁的方式。例如:

    > a -= 3; 
    5 

在这里,a -= 3;a = a - 3;是一样的:

    > a *= 2; 
    10 
    > a /= 5; 
    2 
    > a %= 2; 
    0 

除了之前讨论的算术和赋值运算符之外,还有其他类型的运算符,你将在本章和下一章中看到。

注意

最佳实践

始终用分号结束你的表达式。JavaScript 有一个分号插入机制,如果你忘记在行尾加上分号,它会自动添加分号。然而,这也可能是一个错误的来源,所以最好确保你总是明确地声明你想要结束表达式的地方。换句话说,> 1 + 1> 1 + 1;都可以工作;但在整本书中,你总是会看到第二种类型,以分号结束,只是为了强调这个习惯。

基本数据类型

你使用的任何值都是某种类型的。在 JavaScript 中,以下是一些基本的数据类型:

  1. 数字:这包括浮点数和整数。例如,这些值都是数字-11003.14

  2. 字符串:这些由任意数量的字符组成,例如,aoneone 2 three

  3. 布尔:这可以是truefalse

  4. 未定义:当你尝试访问一个不存在的变量时,你会得到特殊值未定义。当你声明一个变量但尚未给它赋值时,也会发生同样的情况。JavaScript 在幕后用值undefined初始化变量。未定义数据类型只能有一个值-特殊值undefined

  5. Null:这是另一种特殊的数据类型,只能有一个值-null值。它表示没有值,空值或无。与未定义的区别在于,如果变量具有空值,则仍然已定义;只是它的值恰好是空的。您很快就会看到一些例子。

不属于这里列出的五种原始类型之一的任何值都是对象。甚至 null 也被认为是对象,这有点尴尬,有一个(东西)实际上是什么都没有的对象。我们将在第四章对象中了解更多关于对象的知识,但目前,只需记住在 JavaScript 中,数据类型如下:

  • 原始(先前列出的五种类型)

  • 非原始(对象)

查找值类型-typeof 运算符

如果您想知道变量或值的类型,可以使用特殊的typeof运算符。此运算符返回表示数据类型的字符串。使用typeof的返回值是以下之一:

  • 数字

  • 字符串

  • 布尔值

  • 未定义

  • 对象

  • 函数

在接下来的几节中,您将看到typeof在使用每种五种原始数据类型的示例中的作用。

数字

最简单的数字是整数。如果将1分配给变量,然后使用typeof运算符,它将返回字符串number,如下所示:

    > var n = 1; 
    > typeof n; 
    "number" 
    > n = 1234; 
    > typeof n; 
    "number" 

在前面的示例中,您可以看到第二次设置变量值时,不需要var语句。

数字也可以是浮点数(小数),例如:

    > var n2 = 1.23; 
    > typeof n; 
    "number" 

您可以直接在值上调用typeof,而无需首先将其分配给变量,例如:

    > typeof 123; 
    "number" 

八进制和十六进制数

当数字以0开头时,它被视为八进制数。例如,八进制0377是十进制255

    > var n3 = 0377; 
    > typeof n3; 
    "number" 
    > n3; 
    255 

前面示例中的最后一行打印了八进制值的十进制表示。

ES6 提供了一个前缀0o(或0O,但在大多数等宽字体中看起来非常令人困惑)来表示八进制。例如,考虑以下代码行:

    console.log(0o776); //510 

虽然您可能不太熟悉八进制数,但您可能已经在 CSS 样式表中使用十六进制值来定义颜色。

在 CSS 中,您有几种选项来定义颜色,其中两种如下:

  • 使用十进制值来指定 R(红色)、G(绿色)和 B(蓝色)的数量,范围从0255。例如,rgb(0, 0, 0)是黑色,rgb(255, 0, 0)是红色(红色的最大量,没有绿色或蓝色)。

  • 使用十六进制并为每个 R、G 和 B 值指定两个字符。例如,#000000是黑色,#ff0000是红色。这是因为ff255的十六进制值。

在 JavaScript 中,您可以在十六进制值之前加上0x,也称为十六进制,例如:

    > var n4 = 0x00; 
    > typeof n4; 
    "number" 
    > n4; 
    0 
    > var n5 = 0xff; 
    > typeof n5; 
    "number" 
    > n5; 
    255 

二进制文字

直到 ES6,如果您需要整数的二进制表示,您必须将它们作为字符串传递给parseInt()函数,基数为2,如下所示:

    console.log(parseInt('111',2)); //7 

在 ES6 中,您可以使用0b(或0B)前缀表示二进制整数。例如:

    console.log(0b111); //7 

指数文字

1e1(也写作1e+11E11E+1)表示数字 1 后面跟着一个 0,换句话说,是10。同样,2e+3表示数字 2 后面跟着三个 0,或者2000,例如:

    > 1e1; 
    10 
    > 1e+1; 
    10 
    > 2e+3; 
    2000 
    > typeof 2e+3; 
    "number" 

2e+3表示将数字2的小数点向右移动三位。还有2e-3,意思是将数字2的小数点向左移动三位。看一下下面的图:

指数文字

以下是代码:

    > 2e-3; 
    0.002 
    > 123.456E-3; 
    0.123456 
    > typeof 2e-3; 
    "number" 

无穷大

JavaScript 中有一个称为 Infinity 的特殊值。它表示 JavaScript 无法处理的数字太大。Infinity 确实是一个数字,因为在控制台中键入typeof Infinity将确认。您还可以快速检查具有308个零的数字是否正常,但309个零太多。准确地说,JavaScript 可以处理的最大数字是1.7976931348623157e+308,而最小数字是5e-324,请看下面的示例:

    > Infinity; 
    Infinity 
    > typeof Infinity; 
    "number" 
    > 1e309; 
    Infinity 
    > 1e308; 
    1e+308 

除以零会得到无穷大,例如:

    > var a = 6 / 0; 
    > a; 
    Infinity 

Infinity是最大的数(或者比最大的数稍微大一点),但最小的数呢?它是带有负号的无穷大;-Infinity,例如:

    > var i = -Infinity; 
    > i; 
    -Infinity 
    > typeof i; 
    "number" 

这是否意味着你可以有一个正好是无穷大两倍的东西,从 0 到无穷大,然后从 0 到负无穷大?嗯,并不是真的。当你把Infinity-Infinity相加时,你得到的不是0,而是一个被称为Not a NumberNaN)的东西,例如:

    > Infinity - Infinity; 
    NaN 
    > -Infinity + Infinity; 
    NaN 

任何其他算术运算中的Infinity作为操作数之一都会得到Infinity,例如:

    > Infinity - 20; 
    Infinity 
    > -Infinity * 3; 
    -Infinity 
    > Infinity / 2; 
    Infinity 
    > Infinity - 99999999999999999; 
    Infinity 

有一个不太为人知的全局方法isFinite(),它告诉你值是否是无穷大。ES6 添加了一个Number.isFinite()方法来做到这一点。你可能会问为什么还需要另一个方法。全局的isFinite()方法试图通过 Number(value)来转换值,而Number.isFinite()不会,因此它更准确。

NaN

在前面的例子中,这个NaN是什么?原来,尽管它的名字是 Not a Number,NaN是一个特殊的值,也是一个数字:

    > typeof NaN; 
    "number" 
    > var a = NaN; 
    > a; 
    NaN 

当你尝试执行假定数字的操作,但操作失败时,你会得到NaN。例如,如果你尝试将10乘以字符"f",结果是NaN,因为"f"显然不是乘法的有效操作数:

    > var a = 10 * "f"; 
    > a;   
    NaN 

NaN是具有传染性的,所以如果你的算术运算中有一个NaN,整个结果都会泡汤,例如:

    > 1 + 2 + NaN; 
    NaN 

Number.isNaN

ES5 有一个全局方法-isNaN()。它确定一个值是否是NaN。ES6 提供了一个非常相似的方法-Number.isNaN()(请注意,这个方法不是全局的)。

全局isNaN()Number.isNaN()之间的区别在于,全局isNaN()在评估之前会转换非数字值为NaN。让我们看下面的例子。我们使用 ES6 的Number.isNaN()方法来测试某个值是否是NaN

    console.log(Number.isNaN('test')); //false : Strings are not NaN 
    console.log(Number.isNaN(123)); //false : integers are not NaN 
    console.log(Number.isNaN(NaN)); //true : NaNs are NaNs 
    console.log(Number.isNaN(123/'abc')); //true : 123/'abc' results in an NaN 

我们看到 ES5 的全局isNaN()方法首先转换非数字值,然后进行比较;其结果与 ES6 的对应方法不同:

    console.log(isNaN('test')); //true 

总的来说,与其全局变量相比,Number.isNaN()更正确。然而,它们都不能用来判断某个值是否不是一个数字-它们只是回答这个值是否是NaN。实际上,你更感兴趣的是知道一个值是否被识别为一个数字。Mozilla 建议使用以下 polyfill 方法来做到这一点:

    function isNumber(value) { 
      return typeof value==='number' && !Number.isNaN(value); 
    } 

Number.isInteger

这是 ES6 中的一个新方法。如果数字是有限的并且不包含任何小数点(是一个整数),它返回true

    console.log(Number.isInteger('test')); //false 
    console.log(Number.isInteger(Infinity)); //false 
    console.log(Number.isInteger(NaN)); //false 
    console.log(Number.isInteger(123)); //true 
    console.log(Number.isInteger(1.23)); //false 

字符串

字符串是用来表示文本的字符序列。在 JavaScript 中,放在单引号或双引号之间的任何值都被视为字符串。这意味着1是一个数字,但"1"是一个字符串。当与字符串一起使用时,typeof返回字符串"string",例如:

    > var s = "some characters"; 
    > typeof s; 
    "string" 
    > var s = 'some characters and numbers 123 5.87'; 
    > typeof s; 
    "string" 

这是一个在字符串上下文中使用的数字的例子:

    > var s = '1'; 
    > typeof s; 
    "string" 

如果你在引号中什么都不放,它仍然是一个字符串(一个空字符串),例如:

    > var s = ""; typeof s; 
    "string" 

正如你已经知道的,当你用加号和两个数字一起使用时,这是算术加法运算。然而,如果你用加号和字符串一起使用,这是一个字符串连接操作,并且返回两个字符串粘在一起:

    > var s1 = "web";  
    > var s2 = "site";  
    > var s = s1 + s2;  
    > s; 
    "website" 
    > typeof s; 
    "string" 

+运算符的双重用途是错误的根源。因此,如果你打算连接字符串,最好确保所有的操作数都是字符串。加法也是一样;如果你打算加上数字,那么确保操作数是数字。你将在本章和本书的后面学到各种方法来做到这一点。

字符串转换

当你使用类似数字的字符串,例如,"1",作为算术运算中的操作数时,字符串在幕后被转换为数字。这对所有算术运算都有效,除了加法,因为它存在歧义。考虑以下例子:

    > var s = '1';  
    > s = 3 * s;  
    > typeof s; 
    "number" 
    > s; 
    3 
    > var s = '1'; 
    > s++; 
    > typeof s; 
    "number" 
    > s; 
    2 

将任何类似数字的字符串转换为数字的一种懒惰方法是将其乘以1(另一种方法是使用一个名为parseInt()的函数,您将在下一章中看到):

    > var s = "100"; typeof s; 
    "string" 
    > s = s * 1; 
    100 
    > typeof s; 
    "number" 

如果转换失败,您将得到NaN

    > var movie = '101 dalmatians'; 
    > movie * 1; 
    NaN 

您可以通过将其乘以1将字符串转换为数字。相反-将任何东西转换为字符串-可以通过与空字符串连接来完成,如下所示:

    > var n = 1; 
    > typeof n; 
    "number" 
    > n = "" + n; 
    "1" 
    > typeof n; 
    "string" 

特殊字符串

还有一些具有特殊含义的字符串,如下表所示:

String Meaning Example

| \\``'``" | \是转义字符。当您想在字符串中使用引号时,您可以转义它们,以便 JavaScript 不认为它们意味着字符串的结束。如果您想在字符串中有一个实际的反斜杠,请用另一个反斜杠转义它。| > var s = 'I don't know';: 这是一个错误,因为 JavaScript 认为字符串是I don,其余是无效的代码。以下代码是有效的:

> var s = 'I don't know';   
> var s = "I don't know";   
> var s = "I don't know";   
> var s = '"Hello",   he said.';   
> var s = ""Hello",   he said.";   
Escaping the escape:   
> var s = "1\\2"; s;   
"1\2"   

|

\n 行尾。
> var s = '\n1\n2\n3\n';   
> s;   
"   
1   
2   
3   
"   

|

| \r | 回车。|考虑以下陈述:

> var s = '1\r2';   
> var s = '1\n\r2';   
> var s = '1\r\n2';   

所有这些的结果如下:

> s;   
"1   
2"   

|

\t 制表符。
> var s = "1\t2";   
> s;   
"1 2"   

|

| \u | \u后跟字符代码,允许您使用 Unicode。|以下是我的保加利亚名字,用西里尔字母写成:

> "\u0421\u0442\u043E\u044F\u043D";   
"Стoян"   

|

还有一些很少使用的其他字符:\b(退格)、\v(垂直制表符)和\f(换页符)。

字符串模板文字

ES6 引入了模板文字。如果您熟悉其他编程语言,Perl 和 Python 现在已经支持模板文字一段时间了。模板文字允许在常规字符串中嵌入表达式。ES6 有两种文字:模板文字和标记文字。

模板文字是带有嵌入表达式的单行或多行字符串。例如,您一定做过类似的事情:

    var log_level="debug"; 
    var log_message="meltdown"; 
    console.log("Log level: "+ log_level + 
      " - message : " + log_message); 
    //Log level: debug - message : meltdown 

您也可以使用模板文字来实现相同的效果,如下所示:

    console.log(`Log level: ${log_level} - message: ${log_message}`) 

模板文字用反引号(`)(重音符)字符,而不是通常的双引号或单引号。模板文字占位符由美元符号和花括号(${expression})表示。默认情况下,它们被连接在一起形成一个字符串。以下示例显示了一个带有稍微复杂的表达式的模板文本:

    var a = 10; 
    var b = 10; 
    console.log(`Sum is ${a + b} and Multiplication would be ${a * b}.`);   
    //Sum is 20 and Multiplication would be 100\. 

嵌入一个函数调用怎么样?

    var a = 10; 
    var b = 10; 
    function sum(x,y){ 
      return x+y 
    } 
    function multi(x,y){ 
      return x*y 
    } 
    console.log(`Sum is ${sum(a,b)} and Multiplication 
      would be ${multi(a,b)}.`); 

模板文字也简化了多行字符串的语法。不需要写下面这行代码:

    console.log("This is line one \n" + "and this is line two"); 

你可以使用模板文字得到更清晰的语法,如下所示:

    console.log(`This is line one and this is line two`); 

ES6 还有另一种有趣的文字类型,叫做标记模板文字。标记模板允许你使用函数修改模板文字的输出。如果你在模板文字前面加上一个表达式,那么这个前缀被认为是一个要调用的函数。在使用标记模板文字之前,这个函数需要先定义好。例如,以下表达式:

    transform`Name is ${lastname}, ${firstname} ${lastname}` 

前述表达式被转换为一个函数调用:

    transform([["Name is ", ", ", " "],firstname, lastname) 

标记函数'transform'接收两个参数-类似Name is的模板字符串和${}定义的替换项。替换项只有在运行时才知道。让我们扩展transform函数:

    function transform(strings, ...substitutes){ 
      console.log(strings[0]); //"Name is" 
      console.log(substitutes[0]); //Bond 
    }   
    var firstname = "James"; 
    var lastname = "Bond" 
    transform`Name is ${lastname}, ${firstname} ${lastname}` 

当模板字符串(Name is)传递给标记函数时,每个模板字符串有两种形式,如下所示:

  • 反斜杠不被解释的原始形式

  • 有特殊含义的反斜杠的转义形式

你可以通过使用原始属性来访问原始字符串形式,如下例所示:

    function rawTag(strings,...substitutes){ 
      console.log(strings.raw[0]) 
    } 
    rawTag`This is a raw text and \n are not treated differently` 
    //This is a raw text and \n are not treated differently 

布尔值

只有两个值属于布尔数据类型-不带引号的truefalse值:

    > var b = true;  
    > typeof b; 
    "boolean" 
    > var b = false;  
    > typeof b; 
    "boolean" 

如果你引用truefalse,它们会变成字符串,如下例所示:

    > var b = "true";  
    > typeof b; 
    "string" 

逻辑运算符

有三个称为逻辑操作符的运算符,可以与布尔值一起使用。它们分别是:

    ! - logical NOT (negation) 
    && - logical AND 
    || - logical OR 

你知道当某事不是真的时,它必须是假的。下面是用 JavaScript 和逻辑!运算符表达这一点的方式:

    > var b = !true; 
    > b; 
    false 

如果你连续使用逻辑NOT两次,你会得到原始值,如下所示:

    > var b = !!true; 
    > b; 
    true 

如果你在非布尔值上使用逻辑运算符,该值会在后台被转换为布尔值,如下所示:

    > var b = "one"; 
    > !b; 
    false 

在前面的情况下,字符串值"one"被转换为布尔值true,然后取反。取反true的结果是false。在下面的示例中,有一个双重取反,所以结果是true

    > var b = "one"; 
    > !!b; 
    true 

你可以使用双重取反将任何值转换为它的布尔等价值。理解任何值如何转换为布尔值是很重要的。大多数值转换为true,除了以下这些,它们转换为false

  • 空字符串""

  • null

  • undefined

  • 数字0

  • 数字NaN

  • 布尔false

这六个值被称为假值,而所有其他值都被称为真值(包括,例如,字符串"0"" ""false")。

让我们看一些关于另外两个运算符-逻辑AND&&)和逻辑OR||)的例子。当你使用&&时,只有当所有操作数都为true时结果才为true。当你使用||时,只要至少有一个操作数为true结果就为true

    > var b1 = true, b2 = false; 
    > b1 || b2; 
    true 
    > b1 && b2; 
    false 

以下是可能的操作及其结果的列表:

操作 结果
true && true true
true && false false
false && true false
false && false false
true || true true
true || false true
false || true true
false || false false

你可以像下面一样连续使用几个逻辑操作:

    > true && true && false && true; 
    false 
    > false || true || false; 
    true 

你还可以在同一表达式中混合使用 &&||。在这种情况下,你应该使用括号来阐明操作的意图。考虑以下示例:

    > false && false || true && true; 
    true 
    > false && (false || true) && true; 
    false 

运算符优先级

你可能会想知道为什么之前的表达式(false && false || true && true)返回了true。答案在于运算符的优先级,就像你从数学中知道的那样:

    > 1 + 2 * 3; 
    7 

这是因为乘法具有比加法更高的优先级,所以 2 * 3 首先被计算,就好像你打了:

    > 1 + (2 * 3); 
    7 

同样,在逻辑操作中,! 具有最高的优先级,并首先执行,假设没有需要其他操作的括号。然后,按照优先级的顺序,接下来是 &&,最后是 || 。换句话说,以下两段代码片段是相同的。第一个如下所示:

    > false && false || true && true; 
    true 

第二个如下所示:

    > (false && false) || (true && true); 
    true 

注意

最佳实践

使用括号而不依赖于运算符优先级。这样可以使你的代码更容易阅读和理解。

ECMAScript 标准定义了运算符的优先级。虽然这可能是一种很好的记忆练习,但本书并没有提供。首先,你会忘掉它,而且即使你设法记住了它也不应依赖于它。阅读和维护你的代码的人可能会感到困惑。

惰性评估

如果你连续进行几个逻辑操作,但在最后一刻结果已经很明显,那么最后的操作就不会被执行,因为它们不影响最终结果。考虑以下行代码作为例子:

    > true || false || true || false || true; 
    true 

由于这些都是OR操作并且具有相同的优先级,如果至少有一个操作数为true,则结果将为true。在第一个操作数计算后,就很明显结果将为true,不管后面跟着什么值。因此,JavaScript 引擎决定偷懒(好吧,高效),并通过评估不影响最终结果的代码来避免不必要的工作。你可以通过在控制台中进行实验来验证这种短路行为,如下面的代码块所示:

    > var b = 5; 
    > true || (b = 6); 
    true 
    > b; 
    5 
    > true && (b = 6); 
    6 
    > b; 
    6 

这个例子还展示了另一个有趣的行为-如果 JavaScript 在逻辑操作中遇到非布尔表达式作为操作数,那么将返回非布尔值作为结果:

    > true || "something"; 
    true 
    > true && "something"; 
    "something" 
    > true && "something" && true; 
    true 

这种行为不是你应该依赖的,因为这会使代码更难理解。通常在不确定之前是否已定义变量时,会使用这种行为来定义变量。在下一个例子中,如果mynumber变量已被定义,则保留其值;否则,将其初始化为值10

    > var mynumber = mynumber || 10; 
    > mynumber; 
    10 

这看起来很简单并且优雅,但要注意它并不是完全可靠的。如果mynumber被定义并初始化为0,或者任何六个假值之一,这段代码可能会表现得与预期不同,如下面的代码示例所示:

    > var mynumber = 0; 
    > var mynumber = mynumber || 10; 
    > mynumber; 
    10 

比较

另一组运算符也都会返回布尔值作为操作的结果。这些是比较运算符。以下表格列出了它们以及示例用法:

运算符符号 描述 示例
== 相等比较:当两个操作数相等时返回true。在比较之前,操作数将被转换为相同的类型。它们也被称为宽松比较。
> 1 == 1;   
true   
> 1 == 2;   
false   
> 1 =='1';   
true   

|

=== 相等和类型比较:如果两个操作数相等并且类型相同,则返回true。这种比较方式更好、更安全,因为不会进行后台类型转换。它也被称为严格比较。
> 1 === '1';   
false   
> 1 === 1;   
true   

|

!= 不相等比较:如果进行类型转换后,操作数不相等,则返回true
> 1 != 1;   
false   
> 1 != '1';   
false   
> 1 != '2';   
true   

|

!== 非相等比较,不进行类型转换:如果操作数不相等或者它们的类型不同,则返回true
> 1 !== 1;   
false   
> 1 !== '1';   
true   

|

> 如果左操作数大于右操作数,则返回true
> 1 > 1;   
false   
> 33 > 22;   
true   

|

>= 如果左操作数大于或等于右操作数,则返回true
> 1 >= 1;   
true   

|

< 如果左操作数小于右操作数,则返回true
> 1 < 1;   
false   
> 1 < 2;   
true   

|

<= 如果左操作数小于或等于右操作数,则返回true
> 1 <= 1;   
true   
> 1 <= 2;   
true   

|

注意,NaN不等于任何东西,甚至不等于它自己。看一下下面这行代码:

    > NaN == NaN; 
    false 

未定义和 null

如果你试图使用一个不存在的变量,你会得到以下错误:

    > foo; 
    ReferenceError: foo is not defined 

对于不存在的变量使用typeof运算符不会报错。你将得到"undefined"字符串作为返回,如下所示:

    > typeof foo; 
    "undefined" 

如果声明一个不给出值的变量,当然不会报错。但是,typeof依然返回"undefined"

    > var somevar; 
    > somevar; 
    > typeof somevar; 
    "undefined" 

这是因为,当你声明一个变量但不初始化它时,JavaScript 会自动将其初始化为undefined值,如下面的代码所示:

    > var somevar; 
    > somevar === undefined; 
    true 

另一方面,null值并不是由 JavaScript 在后台分配的;它是由你的代码分配的,如下所示:

    > var somevar = null; 
    null 
    > somevar; 
    null 
    > typeof somevar; 
    "object" 

尽管nullundefined之间的差异很小,但有时也可能非常关键。例如,如果尝试进行算术运算,将会得到不同的结果:

    > var i = 1 + undefined; 
    > i; 
    NaN 
    > var i = 1 + null; 
    > i; 
    1 

这是因为nullundefined转换为其他原始类型的方式不同。以下示例显示了可能的转换:

  • 转换为数字:
            > 1 * undefined; 

    ```

+   转换为 NaN:

```js
            > 1 * null; 
            0 

    ```

+   转换为布尔值:

```js
            > !!undefined; 
            false 
            > !!null; 
            false 

    ```

+   转换为字符串:

```js
            > "value: " + null; 
            "value: null" 
            > "value: " + undefined; 
            "value: undefined" 

    ```

## 符号

ES6 引入了一个新的原始类型-符号。几种语言都有类似的概念。符号看起来非常类似于普通字符串,但它们非常不同。让我们看看如何创建这些符号:

```js
    var atom = Symbol() 

注意,在创建符号时,我们不使用new运算符。当你这样做时会出错:

    var atom = new Symbol() //Symbol is not a constructor 

你也可以描述Symbol

    var atom = Symbol('atomic symbol') 

描述符号在调试大型程序时非常方便,因为其中有许多散布的符号。

Symbol的最重要特性(也即其存在的原因)是它们是唯一的和不可变的:

    console.log(Symbol() === Symbol()) //false 
    console.log(Symbol('atom') === Symbol('atom')) // false 

目前,我们不得不暂停探讨关于符号的讨论。符号用作属性键和需要唯一标识符的地方。我们将在本书的后面讨论符号。

原始数据类型回顾

让我们快速总结一下到目前为止讨论的一些主要要点:

  • JavaScript 中有五种原始数据类型:

    • 数字

    • 字符串

    • 布尔

    • 未定义

  • 不是原始数据类型的所有东西都是对象。

    原始数字数据类型可以存储正整数和负整数或浮点数、十六进制数、八进制数、指数以及特殊的数-NaNInfinity-Infinity

  • 字符串数据类型包含带引号的字符。模板文字允许在字符串中嵌入表达式。

  • 布尔数据类型的唯一值是truefalse

  • null 数据类型的唯一值是null值。

  • 未定义数据类型的唯一值是undefined值。

  • 当转换为布尔值时,所有的值都变为true,除了以下六个虚假值:

    • ""

    • null

    • undefined

    • 0

    • NaN

    • false

数组

现在你已经了解了 JavaScript 中的基本原始数据类型,是时候转向更强大的数据结构-数组了。

那么,什么是数组?它只是一个值的列表(一个序列)。你可以用一个数组变量而不是一个变量来存储任意数量的值作为数组的元素。

要声明一个包含空数组的变量,你可以使用两个方括号之间没有任何内容的方式,就像下面的代码行所示:

    > var a = []; 

要定义一个有三个元素的数组,你可以写下面的代码行:

    > var a = [1, 2, 3]; 

当你简单地在控制台中键入数组的名称时,你可以得到数组的内容:

    > a; 
    [1, 2, 3] 

现在的问题是如何访问这些数组元素中存储的值。数组中包含的元素是用连续的数字进行索引的,从零开始。第一个元素的索引(或位置)为 0,第二个元素的索引为 1,依此类推。以下是上一个示例中的三个元素数组:

索引
0 1
1 2
2 3

要访问数组元素,你可以在方括号内指定该元素的索引。所以,a[0]给出了数组a的第一个元素,a[1]给出了第二个元素,如下面的例子所示:

    > a[0]; 
    1 
    > a[1]; 
    2 

添加/更新数组元素

使用索引,你也可以更新数组元素的值。下面的示例更新了第三个元素(索引为 2),并打印了新数组的内容,如下所示:

    > a[2] = 'three'; 
    "three" 
    > a; 
    [1, 2, "three"] 

你可以通过访问之前不存在的索引来添加更多的元素,就像下面的代码行中所示:

    > a[3] = 'four'; 
    "four" 
    > a; 
    [1, 2, "three", "four"] 

如果你添加新元素但在数组中留下一个空隙,那么这些之间的元素就不存在,并且在访问时返回undefined值。查看以下示例:

    > var a = [1, 2, 3]; 
    > a[6] = 'n`xew'; 
    "new" 
    > a; 
    [1, 2, 3, undefined x 3, "new"] 

删除元素

要删除一个元素,你可以使用delete运算符。但是,在删除后,数组的长度不会改变。在某种意义上,你可能会在数组中得到一个空缺:

    > var a = [1, 2, 3]; 
    > delete a[1]; 
    true 
    > a; 
    [1, undefined, 3] 
    > typeof a[1]; 
    "undefined" 

数组的数组

数组可以包含各种类型的值,包括其他数组:

    > var a = [1, "two", false, null, undefined]; 
    > a; 
    [1, "two", false, null, undefined] 
    > a[5] = [1, 2, 3]; 
    [1, 2, 3] 
    > a; 
    [1, "two", false, null, undefined, Array[3]] 

控制台中的结果中的Array[3]是可点击的,它会展开数组值。让我们看一个例子,你有一个包含两个元素的数组,它们都是其他数组:

    > var a = [[1, 2, 3], [4, 5, 6]]; 
    > a; 
    [Array[3], Array[3]] 

数组的第一个元素是[0],也是一个数组:

    > a[0]; 
    [1, 2, 3] 

要访问嵌套数组中的元素,你可以参考另一组方括号中的元素索引,如下所示:

    > a[0][0]; 
    1 
    > a[1][2]; 
    6 

请注意,你可以使用数组表示法来访问字符串内的单个字符,就像下面的代码块中所示:

    > var s = 'one'; 
    > s[0]; 
    "o" 
    > s[1]; 
    "n" 
    > s[2]; 
    "e" 

注意

许多浏览器已经支持对字符串的数组访问(不包括较旧的 IE),但它直到 ECMAScript 5 才被官方承认。

有更多与数组有趣的玩法(我们将在第四章对象中介绍),但现在让我们先停在这里,记住以下几点:

  • 数组是一个数据存储

  • 一个数组包含索引元素

  • 索引从零开始,并且对于数组中的每个元素递增一次

  • 要访问数组的一个元素,你可以使用它在方括号中的索引

  • 一个数组可以包含任何类型的数据,包括其他数组

条件和循环

条件提供了一个简单但强大的方式来控制代码执行流程。循环允许你以较少的代码执行重复操作。让我们来看一下:

  • if条件

  • switch语句

  • whiledo...whilefor,和for...in循环

注意

以下各节中的示例需要你切换到多行的 Firebug 控制台。或者,如果你使用 WebKit 控制台,按下Shift + Enter而不是Enter来添加新行。

代码块

在前面的示例中,你看到了代码块的使用。让我们花一点时间澄清什么是代码块,因为在构建条件和循环时,你会广泛使用代码块。

一块代码由花括号括起来的零个或多个表达式组成,就像下面的代码行所示:

    { 
      var a = 1; 
      var b = 3; 
    } 

你可以无限嵌套块,就像下面的例子:

    { 
      var a = 1; 
      var b = 3; 
      var c, d; 
      { 
        c = a + b; 
        { 
          d = a - b; 
        } 
      } 
    } 

注意

最佳实践提示

使用一行结束的分号,正如前面章节中讨论的那样。虽然当每行只有一个表达式时分号是可选的,但最好养成使用它们的习惯。为了最佳可读性,块中的单个表达式应该每行放置一个,并用分号分隔。

缩进任何放置在花括号内的代码。一些程序员喜欢一个制表符缩进,一些使用四个空格,一些使用两个空格。实际上这并不重要,只要你保持一致就行。在前面的示例中,外部块缩进两个空格,第一个嵌套块中的代码缩进四个空格,最里面的块缩进六个空格。

使用花括号。当一个块只由一个表达式组成时,花括号是可选的,但出于可读性和可维护性的考虑,你应该养成始终使用它们的习惯,即使它们是可选的。

if 条件

下面是一个if条件的简单示例:

    var result = '', a = 3; 
    if (a > 2) { 
      result = 'a is greater than 2'; 
    } 

if条件的部分如下:

  • if语句

  • 括号中的条件-is a greater than 2

  • 一个被{}包裹的一段代码,如果条件满足则执行

条件(括号中的部分)始终返回一个布尔值,并且还可能包含以下内容:

  • 逻辑操作-!&&||

  • 比较,如===!=>

  • 任何可以转换为布尔值的值或变量

  • 上述的组合

else 子句

if 条件还可以有一个可选的 else 部分。else语句后面跟着一段代码,该代码在条件评估为false时运行:

    if (a > 2) { 
      result = 'a is greater than 2'; 
    } else { 
      result = 'a is NOT greater than 2'; 
    } 

ifelse语句之间,还可以有无限数量的else...if条件。下面是一个例子:

    if (a > 2 || a < -2) { 
      result = 'a is not between -2 and 2'; 
    } else if (a === 0 && b === 0) { 
      result = 'both a and b are zeros'; 
    } else if (a === b) { 
      result = 'a and b are equal'; 
    } else { 
      result = 'I give up'; 
    } 

你还可以通过在任何块中嵌套条件来嵌套条件,如下面的代码片段所示:

    if (a === 1) { 
      if (b === 2) { 
        result = 'a is 1 and b is 2'; 
      } else { 
        result = 'a is 1 but b is definitely not 2'; 
     } 
    } else { 
      result = 'a is not 1, no idea about b'; 
    } 

检查变量是否存在

让我们将新的有关条件的知识应用到一些实际问题上。通常需要检查变量是否存在。这样做的最懒的方法是简单地将变量放在if语句的条件部分中,例如if (somevar) {...}。但是,这未必是最好的方法。让我们看一个测试变量somevar是否存在的例子,如果存在,则将result变量设置为yes

    > var result = ''; 
    > if (somevar) {  
        result = 'yes';  
      } 
    ReferenceError: somevar is not defined 
    > result;   
    "" 

这段代码显然有效,因为最终结果不是yes。但首先,代码生成了一个错误-somevar未定义,你不希望你的代码表现出这样的行为。其次,仅仅因为if (somevar)返回false,并不意味着somevar未定义。可能是somevar已经定义和初始化,但包含一个类似false0的假值。

检查变量是否定义的更好方法是使用typeof

    > var result = ""; 
    > if (typeof somevar !== "undefined") {  
        result = "yes";  
      } 
    > result; 
    "" 

typeof运算符始终返回一个字符串,可以将该字符串与字符串"undefined"进行比较。注意,somevar变量可能已经声明但尚未赋值,你仍然会得到相同的结果。因此,通过像这样使用typeof进行测试时,你实际上在测试变量是否具有除undefined值以外的任何值:

    > var somevar; 
    > if (typeof somevar !== "undefined") {  
        result = "yes";  
      } 
    > result; 
    "" 
    > somevar = undefined; 
    > if (typeof somevar !== "undefined") {  
        result = "yes";  
      } 
    > result; 
    "" 

如果一个变量被定义并初始化为除了undefined之外的任何值,那么通过typeof返回的类型就不再是"undefined",如下面的代码片段所示:

    > somevar = 123; 
    > if (typeof somevar !== "undefined") {  
        result = 'yes';  
      } 
    > result; 
    "yes" 

备用的 if 语法

当你有一个简单条件时,可以考虑使用另一种if语法。看看这个:

    var a = 1; 
    var result = ''; 
    if (a === 1) { 
      result = "a is one"; 
    } else { 
      result = "a is not one"; 
    } 

你也可以这样写:

    > var a = 1; 
    > var result = (a === 1) ? "a is one" : "a is not one"; 

你应该只在简单条件下使用这种语法。要小心不要滥用它,因为它很容易使你的代码难以阅读。以下是一个示例。

假设你想确保一个数字在一个特定范围内,比如在50100之间:

    > var a = 123; 
    > a = a > 100 ? 100 : a < 50 ? 50: a; 
    > a; 
    100 

可能不太清楚这个代码是如何工作的,因为有多个?。添加括号可以让它稍微清晰一些,如下面的代码块所示:

    > var a = 123; 
    > a = (a > 100 ? 100 : a < 50) ? 50 : a; 
    > a; 
    50 
    > var a = 123; 
    > a = a > 100 ? 100 : (a < 50 ? 50 : a); 
    > a; 
    100 

?:被称为三元运算符,因为它需要三个操作数。

Switch

如果你发现自己在使用if条件并且有太多else...if部分,可以考虑将if改为switch,如下所示:

    var a = '1', 
        result = ''; 
    switch (a) { 
    case 1: 
      result = 'Number 1'; 
      break; 
    case '1': 
      result = 'String 1'; 
      break; 
    default: 
      result = 'I don't know'; 
      break; 
    } 

执行完之后的结果是"String 1"。让我们来看看switch的各部分是什么:

  • switch语句。

  • 括号中的表达式。最常见的表达式包含一个变量,但可以是任何返回值的东西。

  • 一系列用花括号括起来的case块。

  • 每个case语句后面跟着一个表达式。这个表达式的结果会与switch语句后面的表达式进行比较。如果比较的结果为true,则执行case后面冒号之后的代码。

  • 存在一个可选的break语句,用于表示case块的结束。如果到达这个break语句,switch语句就执行完成了。否则,如果缺少break,程序执行就进入下一个case块。

  • 有一个可选的默认情况,用default语句标记,并跟着一块代码。如果之前的任何情况都没被评估为true,则执行default情况。

换句话说,执行switch语句的逐步过程如下:

  1. 评估括号中的switch表达式;记住它。

  2. 移动到第一个case并将其值与步骤 1 中的值进行比较。

  3. 如果步骤 2 中的比较返回true,则执行case块中的代码。

  4. 在执行case块之后,如果在其末尾有一个break语句,则退出switch

  5. 如果没有break或者步骤 2 返回false,则继续下一个case块。

  6. 重复步骤 2 到 5。

  7. 如果你还在这里(第 4 步没有退出),执行default语句后面的代码。

小贴士

缩进跟在case行之后的代码。你也可以缩进switchcase,但这对可读性没有太大帮助。

别忘了加上break

有时,你可能有意地省略 break,但这很少见。这被称为穿透,一定要记录下来,因为它可能看起来像是一个意外的遗漏。另一方面,有时你可能希望省略跟在一个 case 后面的整个代码块,并且让两个 case 共享相同的代码。这是可以的,但这并不改变以下规则:如果有跟在一个 case 语句后面的代码,这段代码应该以 break 结尾。至于缩进,将 breakcasecase 内部的代码对齐是个人偏好;再次强调,保持一致才是最重要的。

使用默认情况。这可以确保在 switch 语句之后始终有一个有意义的结果,即使没有任何一个 case 与被切换的值匹配。

循环

if...elseswitch 语句允许你的代码走不同的路径,就好像你站在十字路口一样,在依据条件决定往哪个方向走。另一方面,循环允许你的代码在回到主干路之前绕几个圈。重复多少次?这取决于在每次迭代之前(或之后)评估条件的结果。

假设你(你的程序执行)从 AB。在某一时刻,你将到达一个需要评估条件 C 的地方。评估 C 的结果告诉你是否应该进入一个循环 L。你进行一次迭代,再次到达 C。然后,再次评估条件,看是否需要另一个迭代。最终,你继续前往 B

循环

无限循环是当条件总是 true 时发生的,你的代码会永远停留在循环中。这显然是一个逻辑错误,你应该注意这种情况。

在 JavaScript 中,循环有以下四种类型:

  • while 循环

  • do-while 循环

  • for 循环

  • for-in 循环

while 循环

while 循环是最简单的迭代类型,它看起来像下面这样:

    var i = 0; 
    while (i < 10) { 
      i++; 
    } 

while 语句后面跟着一个括号中的条件,然后是一个花括号中的代码块。只要条件评估为 true,代码块就会一遍又一遍地执行。

do-while 循环

do...while 循环是 while 循环的轻微变种,示例如下所示:

    var i = 0; 
    do { 
      i++; 
    } while (i < 10); 

在这里,do 语句后面是一个代码块,在代码块后面是一个条件。这意味着在评估条件之前,代码块总是被执行至少一次。

如果在最后两个示例中将 i 初始化为 11 而不是 0,则第一个示例中的代码块(while 循环)将不会被执行,最后 i 仍然是 11,而在第二个示例中(do...while 循环),代码块将被执行一次,i 将变为 12

For 循环

for 循环是最常用的循环类型,你应该确保自己熟悉这一点。它在语法方面需要稍微多一点:

For 循环

除了C条件和L代码块,你还有以下内容:

  • 初始化:这是在进入循环之前执行的代码(在图表中标为0

  • 增量:这是在每次迭代后执行的代码(在图表中标为++

以下是最广泛使用的for循环模式:

  • 在初始化部分,你可以定义一个变量(或设置现有变量的初始值),通常称为i

  • 在条件部分,你可以比较i和边界值,比如i < 100

  • 在增量部分,你可以增加i,比如i++

例子如下:

    var punishment = ''; 
    for (var i = 0; i < 100; i++) { 
      punishment += 'I will never do this again, '; 
    } 

所有三个部分(初始化、条件和增量)都可以包含多个用逗号分隔的表达式。假设你想重写示例并在循环的初始化部分定义变量punishment

    for (var i = 0, punishment = ''; i < 100; i++) { 
      punishment += 'I will never do this again, '; 
    } 

你能把循环体移到增量部分吗?可以,特别是当它是一行代码的时候。这会给你一个看起来有点奇怪的循环,因为它没有循环体。请注意,这只是一种智力练习;不建议你写出看起来奇怪的代码:

    for ( 
      var i = 0, punishment = ''; 
      i < 100; 
      i++, punishment += 'I will never do this again, ') { 

      // nothing here 

    } 

这三个部分都是可选的。以下是重写相同示例的另一种方式:

    var i = 0, punishment = ''; 
    for (;;) { 
      punishment += 'I will never do this again, '; 
      if (++i == 100) { 
        break; 
      } 
    } 

尽管最后的重写与原始代码的作用方式完全相同,但它更长,更难阅读。也有可能使用while循环来实现相同的结果。但是,for循环使得代码更加紧凑和稳健,因为for循环的语法本身让你思考三个部分(初始化、条件和增量),从而帮助你重新确认逻辑,避免陷入无限循环的情况。

for循环可以嵌套在彼此之中。以下是一个循环嵌套在另一个循环中,并组装一个包含十行十列星号的字符串的示例。把i想象为一幅图像的行,j想象为列:

    var res = '\n'; 
    for (var i = 0; i < 10; i++) { 
      for (var j = 0; j < 10; j++) { 
        res += '* '; 
      } 
      res += '\n'; 
    } 

结果是一个字符串,如下所示:

    " 
    * * * * * * * * * *  
    * * * * * * * * * *  
    * * * * * * * * * *  
    * * * * * * * * * *  
    * * * * * * * * * *  
    * * * * * * * * * *  
    * * * * * * * * * *  
    * * * * * * * * * *  
    * * * * * * * * * *  
    * * * * * * * * * *  
    " 

这是另一个例子,它使用嵌套循环和取模操作来绘制类似雪花的结果:

    var res = '\n', i, j; 
    for (i = 1; i <= 7; i++) { 
      for (j = 1; j <= 15; j++) { 
        res += (i * j) % 8 ? ' ' : '*'; 
      } 
      res += '\n'; 
    } 

结果如下:

    " 
           *        
       *   *   *    
           *        
     * * * * * * *  
           *        
       *   *   *    
           *        
    " 

对于...在循环中

for...in循环用于遍历数组或对象的元素,稍后你会看到。这是它的唯一用途;它不能用作替换forwhile的通用重复机制。让我们看一个使用for-in循环来遍历数组元素的例子。但是请记住,这仅用于信息目的,因为for...in主要适用于对象,而常规的for循环应该用于数组。

在这个例子中,你可以遍历数组的所有元素,并打印出每个元素的索引(键)和值,例如:

    // example for information only 
    // for-in loops are used for objects 
    // regular for is better suited for arrays 

    var a = ['a', 'b', 'c', 'x', 'y', 'z']; 

    var result = '\n'; 

    for (var i in a) { 
      result += 'index: ' + i + ', value: ' + a[i] + '\n'; 
    } 
    The result is: 
    " 
    index: 0, value: a 
    index: 1, value: b  
    index: 2, value: c  
    index: 3, value: x 
    index: 4, value: y  
    index: 5, value: z 
    " 

注释

这一章的最后一件事-注释。在您的 JavaScript 程序中,您可以放置注释。这些被 JavaScript 引擎忽略,并不会对程序的运行方式产生任何影响。但是,当您在几个月后重新访问代码,或将代码转交给其他人进行维护时,它们可能会非常宝贵。

允许以下两种类型的注释:

  • 单行注释以 // 开头,并在行尾结束。

  • 多行注释以 /* 开头,并以同一行或任何后续行的 */ 结束。请注意,注释开始和注释结束之间的任何代码都将被忽略。

一些示例如下:

    // beginning of line 

    var a = 1; // anywhere on the line 

    /* multi-line comment on a single line */ 

    /* 
      comment that spans several lines 
    */ 

甚至有一些工具,比如 JSDoc 和 YUIDoc,可以解析你的代码,并根据你的注释提取有意义的文档。

练习

  1. 在控制台执行这些行的结果是什么?为什么?
            > var a; typeof a; 
            > var s = '1s'; s++; 
            > !!"false"; 
            > !!undefined; 
            > typeof -Infinity; 
            > 10 % "0"; 
            > undefined == null; 
            > false === ""; 
            > typeof "2E+2"; 
            > a = 3e+3; a++; 

    ```

1.  在以下操作后的 v 的值是多少?

```js
            > var v = v || 10; 

    ```

首先尝试将`v`设置为`100`,`0`或`null`进行实验。

1.  编写一个打印乘法表的小程序。提示:在另一个循环内嵌套使用循环。

# 总结

在本章中,你学到了关于 JavaScript 程序的基本构建块。现在你知道以下原始数据类型:

+   数字

+   字符串

+   布尔值

+   未定义

+   空

你也知道了相当多的运算符,它们如下:

+   **算术运算符**:`+`,`-`,`*`,`/`和`%`

+   **递增运算符**:`++`和`-`

+   **赋值运算符**:`=`,`+=`,`-=`,`*=`,`/=`和`%=` 

+   **特殊运算符**:`typeof`和`delete`

+   **逻辑运算符**:`&&`,`||`和`!`

+   **比较运算符**:`==`,`===`,`!=`,`!==`,`<`,`>`,`>=`和`<=`

+   **三元运算符**:`?`

然后你学会了如何使用数组来存储和访问数据,最后你看到了使用条件(`if...else`或`switch`)和循环(`while`,`do...while`,`for`和`for...in`)来控制程序流程的不同方法。

这是相当多的信息;在深入下一章之前,给自己一个当之无愧的鼓励。更有趣的内容即将到来!


# 第三章:函数

掌握函数是学习任何编程语言时的重要技能,尤其是在学习 JavaScript 时更是如此。这是因为 JavaScript 对函数有许多用途,语言的灵活性和表现力大部分来自于函数。大多数编程语言对于一些面向对象的特性都有特殊的语法,而 JavaScript 只使用函数。本章将涵盖以下主题:

+   如何定义和使用函数

+   向函数传递参数

+   您可以免费使用的预定义函数

+   JavaScript 中变量的作用域

+   函数只是数据的概念,尽管是一种特殊类型的数据

理解这些主题将为您提供一个坚实的基础,使您能够深入本章的第二部分,其中展示了一些有趣的函数应用,如下所示:

+   使用匿名函数

+   回调

+   立即(自调用)函数

+   内部函数(在其他函数内定义的函数)

+   返回函数的函数

+   重新定义自己的函数

+   闭包

# 什么是函数?

函数允许您将代码组合在一起,给它一个名称,并在以后重复使用,通过您给它的名称进行调用。让我们考虑以下代码作为示例:

```js
    function sum(a, b) { 
      var c = a + b; 
      return c; 
    } 

组成函数的部分如下所示:

  • function关键字。

  • 函数的名称;在这种情况下是sum

  • 函数参数;在这种情况下是ab。函数可以接受任意数量的参数,或者没有参数,用逗号分隔。

  • 代码块,也称为函数的主体。

  • return语句。函数总是返回一个值。如果它没有显式返回一个值,它将隐式返回值undefined

请注意,函数只能返回单个值。如果需要返回更多值,您可以简单地返回一个包含您需要的所有值的数组作为此数组的元素。

前面的语法称为函数声明。这只是在 JavaScript 中创建函数的一种方式,还有更多的方式即将出现。

调用函数

为了使用函数,您需要调用它。您可以简单地使用函数的名称调用函数,可选地在括号中跟随任意数量的值。调用函数是另一种说法是调用。

让我们调用sum()函数,传递两个参数,并将函数返回的值赋给变量result

    > var result = sum(1, 2); 
    > result; 
    3 

参数

在定义函数时,您可以指定函数在调用时期望接收的参数。函数可能不需要任何参数,但如果需要,并且您忘记传递它们,JavaScript 将为您跳过的参数分配undefined值。在下一个示例中,函数调用返回NaN,因为它尝试对1undefined进行求和:

    > sum(1); 
    NaN 

从技术上讲,参数和参数之间有区别,尽管两者经常可以互换使用。参数与函数一起定义,而参数在调用函数时传递给函数。考虑以下示例:

    > function sum(a, b) { 
        return a + b; 
      } 
    > sum(1, 2); 

这里,ab是参数,而12是参数。

当涉及接受参数时,JavaScript 并不挑剔。如果您传递的参数多于函数所期望的,额外的参数将被静默忽略,如下例所示:

    > sum(1, 2, 3, 4, 5); 
    3 

此外,您可以创建接受参数数量灵活的函数。这是由于在每个函数内部自动创建的特殊值arguments。以下是一个简单返回传递给它的任何参数的函数:

    > function args() { 
        return arguments; 
      } 
    > args(); 
    [] 
    > args( 1, 2, 3, 4, true, 'ninja'); 
    [1, 2, 3, 4, true, "ninja"] 

使用arguments,您可以改进sum()函数以接受任意数量的参数并将它们全部相加,如下例所示:

    function sumOnSteroids() { 
      var i, 
          res = 0, 
          number_of_params = arguments.length; 
      for (i = 0; i < number_of_params; i++) { 
        res += arguments[i]; 
      } 
      return res; 
    } 

如果您通过使用不同数量的参数或甚至根本不使用参数来调用此函数进行测试,您可以验证它是否按预期工作,如下例所示:

    > sumOnSteroids(1, 1, 1); 
    3 
    > sumOnSteroids(1, 2, 3, 4); 
    10 
    > sumOnSteroids(1, 2, 3, 4, 4, 3, 2, 1); 
    20 
    > sumOnSteroids(5); 
    5 
    > sumOnSteroids(); 
    0 

arguments.length表达式返回函数调用时传递的参数数量。如果语法不熟悉,不用担心,我们将在下一章节详细讨论。您还会看到arguments不是一个数组(尽管看起来像),而是一个类似数组的对象。

ES6 在函数参数周围引入了几个重要的改进。ES6 函数参数现在可以有默认值、剩余参数,并允许解构。下一节将详细讨论这些概念。

默认参数

函数参数可以分配默认值。在调用函数时,如果省略了参数,则使用分配给参数的默认值:

    function render(fog_level=0, spark_level=100){ 
      console.log(`Fog Level: ${fog_level} and spark_level:
       ${spark_level}`) 
    } 
    render(10); //Fog Level: 10 and spark_level: 100 

在这个例子中,我们省略了spark_level参数,因此使用了分配给参数的默认值。重要的是要注意undefined被视为参数值的缺失;例如考虑以下代码行:

    render(undefined,10); //Fog Level: 0 and spark_level: 10 

在提供参数的默认值时,也可以引用其他参数:

    function t(fog_level=1, spark_level=fog_level){
      console.log(`Fog Level: ${fog_level} and spark_level: 
       ${spark_level}`) 
      //Fog Level: 10 and spark_level: 10 
    } 
    function s(fog_level=10, spark_level = fog_level*10){ 
      console.log(`Fog Level: ${fog_level} and spark_level:
       ${spark_level}`) 
      //Fog Level: 10 and spark_level: 100 
    } 
    t(10); 
    s(10); 

默认参数有它们自己的作用域;这个作用域夹在外部函数作用域和函数内部作用域之间。如果参数被内部作用域中的变量遮蔽,令人惊讶的是,内部变量是不可用的。下面的例子将有助于解释这一点:

    var scope="outer_scope"; 
    function scoper(val=scope){ 
      var scope="inner_scope"; 
      console.log(val); //outer_scope 
    } 
    scoper(); 

你可能期望val被内部定义的scope变量所遮蔽,但是由于默认参数有它们自己的作用域,所以赋给val的值不受内部作用域的影响。

剩余参数

ES6 引入了剩余参数。剩余参数允许我们以数组的形式向函数发送任意数量的参数。剩余参数只能是参数列表中的最后一个,并且只能有一个剩余参数。在最后一个形式参数之前放置一个剩余运算符(...)表示该参数是一个剩余参数。以下示例显示在最后一个形式参数之前添加一个剩余运算符:

    function sayThings(tone, ...quotes){ 
      console.log(Array.isArray(quotes)); //true 
      console.log(`In ${tone} voice, I say ${quotes}`) 
    } 
    sayThings("Morgan Freeman","Something serious"," 
     Imploding Universe"," Amen"); 
    //In Morgan Freeman voice, I say Something serious,
     Imploding Universe,Amen 

传递给函数的第一个参数在tone中接收,而其余的参数作为数组接收。可变参数(var-args)已经成为其他几种语言的一部分,并且是 ES6 的一个受欢迎的新增功能。剩余参数可以替代略有争议的arguments变量。剩余参数和arguments变量之间的主要区别在于剩余参数是真正的数组。所有数组方法都适用于剩余参数。

展开运算符

展开运算符看起来与剩余运算符完全相同,但执行相反的功能。在调用函数或定义数组时,展开运算符用于提供参数。展开运算符接受一个数组并将其元素分割成单独的变量。以下示例说明了展开运算符在调用以数组作为参数的函数时提供了更清晰的语法:

    function sumAll(a,b,c){ 
      return a+b+c 
    } 
    var numbers = [6,7,8] 
    //ES5 way of passing array as an argument of a function 
    console.log(sumAll.apply(null,numbers)); //21 
    //ES6 Spread operator 
    console.log(sumAll(...numbers))//21 

在 ES5 中,当将数组作为参数传递给函数时,通常使用apply()函数。在前面的例子中,我们有一个数组需要传递给一个函数,而函数接受三个变量。将数组传递给这个函数的 ES5 方法使用apply()函数,第二个参数允许将数组传递给被调用的函数。ES6 的展开运算符提供了一种更清晰和精确处理这种情况的方法。在调用sumAll()时,我们使用展开运算符(...)并将numbers数组传递给函数调用。然后数组被分割成单独的变量-abc

展开运算符提高了 JavaScript 中数组的功能。如果要创建由另一个数组组成的数组,则现有的数组语法不支持这一点。您必须使用pushspliceconcat来实现这一点。然而,使用展开运算符,这变得微不足道:

    var midweek = ['Wed', 'Thu']; 
    var weekend = ['Sat', 'Sun']; 
    var week = ['Mon','Tue', ...midweek, 'Fri', ...weekend]; 
     //["Mon","Tue","Wed","Thu","Fri","Sat","Sun"] 
    console.log(week); 

在上面的例子中,我们使用两个数组midweekweekend,使用扩展运算符构造了一个week数组。

预定义函数

JavaScript 引擎中内置了许多函数,供您使用。让我们来看看它们。在这样做的过程中,您将有机会尝试使用函数、它们的参数和返回值,并且变得熟悉使用函数。以下是内置函数的列表:

  • parseInt()

  • parseFloat()

  • isNaN()

  • isFinite()

  • encodeURI()

  • decodeURI()

  • encodeURIComponent()

  • decodeURIComponent()

  • eval()

注意

黑匣子函数

通常,当您调用函数时,您的程序不需要知道这些函数在内部是如何工作的。您可以将函数视为一个黑匣子,给它一些值(作为输入参数),然后获取它返回的输出结果。这对于任何函数都是正确的-无论是内置在 JavaScript 引擎中的函数,您创建的函数,还是同事或其他人创建的函数。

parseInt()

parseInt()函数接受任何类型的输入(通常是字符串)并尝试将其转换为整数。如果失败,它将返回NaN,如下面的代码所示:

    > parseInt('123'); 
    123 
    > parseInt('abc123'); 
    NaN 
    > parseInt('1abc23'); 
    1 
    > parseInt('123abc'); 
    123 

该函数接受一个可选的第二个参数,即基数,告诉函数期望的数字类型-十进制、十六进制、二进制等。例如,尝试从字符串FF中提取一个十进制数是没有意义的,因此结果是NaN,但如果您尝试将FF作为十六进制,则会得到255,如下面的代码片段所示:

    > parseInt('FF', 10); 
    NaN 
    > parseInt('FF', 16); 
    255 

另一个例子是解析带有基数10(十进制)和基数8(八进制)的字符串:

    > parseInt('0377', 10); 
    377 
    > parseInt('0377', 8); 
    255 

如果在调用parseInt()时省略第二个参数,函数将假定10(十进制),以下是一些例外情况:

  • 如果您传递以0x开头的字符串,则假定基数为16(假定为十六进制数)。

  • 如果您传递的字符串以0开头,函数会假定基数为8(假定为八进制数)。请考虑以下示例:

        > parseInt('377'); 
        377 
        > console.log(0o377); 
        255 
        > parseInt('0x377'); 
        887 

最安全的做法是始终指定基数。如果省略基数,您的代码在 99%的情况下可能仍然有效(因为大多数情况下您解析十进制数);然而,偶尔可能会在调试一些边缘情况时导致您有点头发丢失。例如,想象一下,您有一个表单字段接受日历天数或月份,用户输入0608

注意

ECMAScript 5 删除了八进制文字值,并避免了与parseInt()和未指定基数的混淆。

parseFloat()

parseFloat()函数类似于parseInt()函数,但在尝试从输入中找出数字时,它还会寻找小数。该函数只接受一个参数,如下所示:

    > parseFloat('123'); 
    123 
    > parseFloat('1.23'); 
    1.23 
    > parseFloat('1.23abc.00'); 
    1.23 
    > parseFloat('a.bc1.23'); 
    NaN 

parseInt()一样,parseFloat()在遇到意外字符的第一次出现时就会放弃,即使字符串的其余部分可能包含可用的数字:

    > parseFloat('a123.34'); 
    NaN 
    > parseFloat('12a3.34'); 
    12 

parseFloat()函数理解输入中的指数(与parseInt()不同):

    > parseFloat('123e-2'); 
    1.23 
    > parseFloat('1e10'); 
    10000000000 
    > parseInt('1e10'); 
    1 

isNaN()

使用isNaN(),您可以检查输入值是否是一个有效的数字,可以安全地用于算术运算。这个函数也是一个方便的方法来检查parseInt()parseFloat()或任何算术操作是否成功:

    > isNaN(NaN); 
    true 
    > isNaN(123); 
    false 
    > isNaN(1.23); 
    false 
    > isNaN(parseInt('abc123')); 
    true 

该函数还将尝试将输入转换为数字:

    > isNaN('1.23'); 
    false 
    > isNaN('a1.23'); 
    true 

isNaN()函数很有用,因为特殊值NaN与任何东西都不相等,包括它自己。换句话说,NaN === NaNfalse。因此,NaN不能用来检查一个值是否是有效的数字。

isFinite()

isFinite()函数检查输入是否是既不是Infinity也不是NaN的数字:

    > isFinite(Infinity); 
    false 
    > isFinite(-Infinity); 
    false 
    > isFinite(12); 
    true 
    > isFinite(1e308); 
    true 
    > isFinite(1e309); 
    false 

如果您对最后两个调用返回的结果感到困惑,请记住前一章中提到的 JavaScript 中最大的数字是1.7976931348623157e+308,因此1e309实际上是无穷大。

编码/解码 URI

统一资源定位符URL)或统一资源标识符URI)中,一些字符具有特殊含义。如果你想转义这些字符,你可以使用encodeURI()encodeURIComponent()函数。第一个函数将返回一个可用的 URL,而第二个函数假定你只传递了 URL 的一部分,比如一个查询字符串,它将编码所有适用的字符,如下所示:

    > var url = 'http://www.packtpub.com/script.php?q=this and that'; 
    > encodeURI(url); 
    "http://www.packtpub.com/script.php?q=this%20and%20that" 
    > encodeURIComponent(url); 
    "http%3A%2F%2Fwww.packtpub.com%2Fscript.php%3Fq%3Dthis%20and%20that" 

encodeURI()encodeURIComponent()的相反函数分别是decodeURI()decodeURIComponent()

有时,在旧代码中,你可能会看到escape()unescape()函数用于编码和解码 URL,但这些函数已经被弃用;它们进行编码的方式不同,不应该使用。

eval()

eval()函数接受一个字符串输入并将其作为 JavaScript 代码执行,如下所示:

    > eval('var ii = 2;'); 
    > ii; 
    2 

因此,eval('var ii = 2;')等同于var ii = 2;

eval()函数有时可能会有用,但如果有其他选择,应该避免使用它。大多数情况下,都有替代方案,而且在大多数情况下,这些替代方案更加优雅、更容易编写和维护。Eval is evil是一个你经常会听到有经验的 JavaScript 程序员说的口头禅。使用eval()的缺点如下:

  • 安全性:JavaScript 很强大,这也意味着它可能会造成损害。如果你不信任传递给eval()的输入源,就不要使用它。

  • 性能:评估实时代码比直接在脚本中编写代码要慢。

一个奖励 - alert()函数

让我们再看一个常见的函数-alert()。它不是核心 JavaScript 的一部分(在 ECMA 规范中找不到它),但它是由宿主环境-浏览器提供的。它在消息框中显示一串文本。它也可以作为一个原始的调试工具,尽管现代浏览器中的调试器更适合这个目的。

这里有一张截图显示了执行alert("Hi There")代码的结果:

一个奖励 - alert()函数

在使用这个函数之前,请记住它会阻塞浏览器线程,这意味着在用户关闭警告框之前不会执行其他代码。如果你有一个繁忙的 Ajax 类型应用程序,通常不建议使用alert()

变量的作用域

特别要注意的是,如果你从其他语言转到 JavaScript,JavaScript 中的变量不是在块作用域中定义的,而是在函数作用域中定义的。这意味着如果一个变量在函数内部定义,它在函数外部是不可见的。但是,如果它在iffor代码块中定义,它在块外是可见的。全局变量这个术语描述了你在任何函数之外定义的变量(在全局程序代码中),与局部变量相对,局部变量是在函数内部定义的。函数内部的代码可以访问所有全局变量以及它自己的局部变量。

在下一个例子中:

  • f()函数可以访问global变量

  • f()函数外部,local变量不存在

        var global = 1; 
        function f() { 
          var local = 2; 
          global++; 
          return global; 
        } 

让我们测试一下:

    > f(); 
    2 
    > f(); 
    3 
    > local; 
    ReferenceError: local is not defined 

还要注意的是,如果你不使用var来声明一个变量,这个变量会自动分配一个全局作用域。让我们看一个例子:

变量的作用域

发生了什么?f()函数包含local变量。在调用函数之前,这个变量是不存在的。当你第一次调用函数时,local变量会被创建为全局作用域。然后,如果你在函数外部访问local变量,它将是可用的。

注意

最佳实践提示

减少全局变量的数量以避免命名冲突。想象两个人在同一个脚本中工作的两个不同函数中工作,他们都决定使用相同的名称作为他们的全局变量。这很容易导致意外的结果和难以找到的错误。始终使用var语句声明变量。考虑使用单一var模式。在函数中定义所需的所有变量,这样你就有一个地方可以查找变量,希望可以防止意外的全局变量。

变量提升

下面是一个有趣的例子,展示了本地作用域与全局作用域的一个重要方面:

    var a = 123; 

    function f() { 
      alert(a); 
      var a = 1; 
      alert(a); 
    } 

    f(); 

你可能期望第一个alert()函数将显示123(全局变量a的值),第二个将显示1(局部变量a)。但是,情况并非如此。第一个警报将显示undefined。这是因为,在函数内部,局部作用域比全局作用域更重要。因此,局部变量会覆盖同名的全局变量。在第一个alert()时,a变量尚未定义(因此为undefined值),但它仍然存在于局部空间中,这是由于称为提升的特殊行为。

当 JavaScript 程序执行进入新函数时,函数中任何地方声明的所有变量都会被移动、提升或提升到函数顶部。这是一个重要的概念要记住。此外,只有声明被提升,意味着只有变量的存在被移动到顶部。任何赋值保持原样。在前面的例子中,局部变量a的声明被提升到顶部。只有声明被提升,而不是对1的赋值。就好像函数是这样写的:

    var a = 123; 

    function f() { 
      var a; // same as: var a = undefined; 
      alert(a); // undefined 
      a = 1; 
      alert(a); // 1 
    } 

你也可以采用之前提到的最佳实践部分的单一 var 模式。在这种情况下,你将进行一种手动的变量提升,以防止与 JavaScript 提升行为混淆。

块作用域

ES6 在声明变量时提供了额外的作用域。我们看了函数作用域以及它对使用var关键字声明的变量的影响。如果你在 ES6 中编码,块作用域将大多取代你使用var声明变量的需求。虽然,如果你仍在使用 ES5,我们希望你确保仔细观察变量提升的行为。

ES6 引入了letconst关键字,允许我们声明变量。

使用let声明的变量是块作用域的。它们只存在于当前块中。使用var声明的变量是函数作用域的,正如我们之前所看到的。下面的例子说明了块作用域:

    var a = 1; 
    { 
        let a = 2; 
        console.log( a );   // 2 
    } 
    console.log( a );       // 1 

大括号'{''}'之间的作用域是一个块。如果你来自 Java 或 C/C++的背景,块作用域的概念对你来说将非常熟悉。在这些语言中,程序员引入块只是为了定义一个作用域。然而,在 JavaScript 中,有必要习惯性地引入块,因为它们没有与之关联的作用域。然而,ES6 允许你使用let关键字创建块作用域变量。正如你在前面的例子中看到的,块内创建的变量a在块内是可用的。在声明块作用域变量时,通常建议在块的顶部添加let声明。让我们看另一个例子,以清楚地区分函数作用域和块作用域:

    function swap(a,b){ // <--function scope starts here 
      if(a>0 && b>0){   // <--block scope starts here 
        let tmp=a; 
        a=b; 
        b=tmp; 
      }                // <--block scope ends here 
      console.log(a,b); 
      console.log(tmp); // tmp is not defined as it is available
       only in the block scope 
      return [a,b]; 
    } 
    swap(1,2); 

如你所见,tmp是用let声明的,并且只在它被定义的块中可用。在实际操作中,你应该最大化使用块作用域变量。除非有非常特定的事情需要你使用var声明,否则请确保你优先使用块作用域变量。然而,错误地使用let关键字可能会导致一些问题。首先,你不能在同一个函数或块作用域中使用let关键字重新声明相同的变量:

    function blocker(x){ 
      if(x){ 
        let f; 
        let f; //duplicate declaration "f" 
      } 
    } 

在 ES6 中,使用let关键字声明的变量被提升到块作用域。然而,在声明之前引用变量是一个错误。

ES6 中引入的另一个关键字是const。使用const关键字声明的变量创建一个只读引用值。这并不意味着引用持有的值是不可变的。然而,变量标识符不能被重新分配。常量与使用let关键字创建的变量一样是块作用域的。此外,在声明变量时必须为变量赋值。

尽管它听起来像是,const与不可变值无关。常量创建不可变绑定。这是一个重要的区别,需要正确理解。让我们考虑下面的例子:

    const car = {} 
    car.tyres = 4 

这是一个有效的代码;在这里我们将{}赋值给一个常量car。一旦赋值,这个引用就不能被改变。在 ES6 中,你应该这样做:

  • 尽可能使用const。对所有值不会改变的变量使用它:
        Use let 

  • 避免使用var

函数是数据

JavaScript 中的函数实际上是数据。这是一个我们以后会需要的重要概念。这意味着你可以创建一个函数并将它分配给一个变量,如下所示:

    var f = function () { 
      return 1; 
    }; 

这种定义函数的方式有时被称为函数文字表示法

function () { return 1;}部分是一个函数表达式。函数表达式可以选择地有一个名字,这样它就成为了命名函数表达式NFE)。因此,这也是允许的,尽管在实践中很少见(并且会导致 IE 错误地在封闭作用域中创建两个变量-fmyFunc):

    var f = function myFunc() { 
      return 1; 
    }; 

如你所见,命名函数表达式和函数声明之间没有区别。但实际上它们是不同的。区分两者的唯一方法是看它们被使用的上下文。函数声明只能出现在程序代码中(在另一个函数的主体中或在主程序中)。你将在本书的后面看到更多的函数示例,这将澄清这些概念。

当你在一个包含函数值的变量上使用typeof运算符时,它返回字符串"function",如下例所示:

    > function define() { 
        return 1;  
      } 

    > var express = function () {  
        return 1;  
      }; 

    > typeof define; 
    "function" 

    > typeof express; 
    "function" 

因此,JavaScript 函数是数据,但是一种具有以下两个重要特征的特殊数据:

  • 它们包含代码

  • 它们是可执行的(它们可以被调用)

正如你之前所见,执行函数的方法是在函数名后面加括号。如下一个例子所示,这种方法可以在不管函数是如何定义的情况下工作。在这个例子中,你还可以看到函数是如何被视为一个常规值的;它可以被复制到另一个变量中,如下所示:

    > var sum = function (a, b) { 
        return a + b; 
      }; 

    > var add = sum; 
    > typeof add; 
    function 
    > add(1, 2); 
    3 

因为函数是分配给变量的数据,所以命名函数的命名规则与变量的命名规则相同-函数名不能以数字开头,它可以包含任意组合的字母、数字、下划线字符和美元符号。

匿名函数

正如你现在所知道的,存在一种函数表达式语法,你可以像下面这样定义一个函数:

    var f = function (a) { 
      return a; 
    }; 

这也经常被称为匿名函数(因为它没有名字),特别是当这样的函数表达式即使没有分配给变量也被使用时。在这种情况下,这样的匿名函数有两种优雅的用法,如下所示:

  • 可以将匿名函数作为参数传递给另一个函数。接收函数可以对您传递的函数执行一些有用的操作。

  • 您可以定义一个匿名函数并立即执行它。

让我们更详细地看看匿名函数的这两个应用。

回调函数

由于函数就像分配给变量的任何其他数据一样,它可以被定义、复制,并且也可以作为参数传递给其他函数。

这是一个接受两个函数作为参数、执行它们并返回它们各自返回值之和的函数的例子:

    function invokeAdd(a, b) { 
      return a() + b(); 
    } 

现在,让我们使用仅返回硬编码值的函数声明模式来定义两个简单的附加函数:

    function one() { 
      return 1; 
    } 

    function two() { 
      return 2; 
    } 

现在,您可以将这些函数传递给原始函数invokeAdd(),并获得以下结果:

    > invokeAdd(one, two); 
    3 

将函数作为参数传递的另一个例子是使用匿名函数(函数表达式)。您可以简单地执行以下操作,而不是定义one()two()

    > invokeAdd(function () {return 1; }, function () {return 2; }); 
    3 

或者,您可以使其更易读,如下面的代码所示:

    > invokeAdd( 
        function () { return 1; },  
        function () { return 2; } 
      ); 
    3 

或者,您可以这样做:

    > invokeAdd( 
        function () { 
          return 1; 
        },  
        function () { 
          return 2; 
        } 
      ); 
    3 

当您将函数 A 传递给另一个函数 B,然后 B 执行 A 时,通常会说 A 是一个回调函数。如果 A 没有名称,那么您可以说它是一个匿名回调函数。

回调函数何时有用?让我们看一些示例,演示回调函数的好处,即:

  • 它们让您无需命名即可传递函数,这意味着浮动的变量更少。

  • 您可以将调用函数的责任委托给另一个函数,这意味着要编写的代码更少

  • 它们可以通过推迟执行或解除阻塞调用来提高性能

回调函数示例

看看这种常见情况-您有一个返回值的函数,然后将其传递给另一个函数。在我们的例子中,第一个函数multiplyByTwo()接受三个参数,循环遍历它们,将它们乘以二,并返回包含结果的数组。第二个函数addOne()接受一个值,将其加一,并返回它,如下所示:

    function multiplyByTwo(a, b, c) { 
      var i, ar = []; 
      for (i = 0; i < 3; i++) { 
        ar[i] = arguments[i] * 2; 
      } 
      return ar; 
    } 

    function addOne(a) { 
      return a + 1; 
    } 

让我们测试这些函数:

    > multiplyByTwo(1, 2, 3); 
    [2, 4, 6] 
    > addOne(100); 
    101 

现在,假设您想要有一个包含三个元素的数组myarr,并且每个元素都要通过这两个函数传递。首先,让我们从调用multiplyByTwo()开始:

    > var myarr = []; 
    > myarr = multiplyByTwo(10, 20, 30); 
    [20, 40, 60] 

现在,循环遍历每个元素,将其传递给addOne()

    > for (var i = 0; i < 3; i++) { 
        myarr[i] = addOne(myarr[i]); 
      } 
    > myarr; 
    [21, 41, 61] 

正如您所看到的,一切都运行正常,但还有改进的空间。例如,有两个循环。如果循环次数很多,循环可能会很昂贵。您可以通过只有一个循环来实现相同的结果。以下是如何修改multiplyByTwo()以便接受回调函数并在每次迭代时调用该回调的方法:

    function multiplyByTwo(a, b, c, callback) { 
      var i, ar = []; 
      for (i = 0; i < 3; i++) { 
        ar[i] = callback(arguments[i] * 2); 
      } 
      return ar; 
    } 

使用修改后的函数,所有工作都是通过一个函数调用完成的,该函数传递了起始值和callback函数,如下所示:

    > myarr = multiplyByTwo(1, 2, 3, addOne); 
    [3, 5, 7] 

您可以使用匿名函数来定义addOne(),从而节省额外的全局变量:

    > multiplyByTwo(1, 2, 3, function (a) { 
        return a + 1; 
      }); 
    [3, 5, 7] 

匿名函数很容易更改,如果需要的话:

    > multiplyByTwo(1, 2, 3, function (a) { 
        return a + 2; 
      }); 
    [4, 6, 8] 

立即函数

到目前为止,我们已经讨论了使用匿名函数作为回调。让我们看看匿名函数的另一个应用-在定义后立即调用函数。这是一个例子:

    ( 
      function () { 
        alert('boo'); 
      } 
    )(); 

语法一开始可能看起来有点吓人,但你所做的就是简单地将一个函数表达式放在括号内,然后再加上另一组括号。第二组括号表示立即执行,也是放置您的匿名函数可能接受的任何参数的地方,例如:

    ( 
      function (name) { 
        alert('Hello ' + name + '!'); 
      } 
    )('dude'); 

或者,您可以将第一组括号的关闭移到末尾。这两种方法都可以:

    (function () { 
      // ... 
    }()); 

    // vs.  

    (function () { 
      // ... 
    })(); 

立即(自我调用)匿名函数的一个很好的应用是在不创建额外全局变量的情况下完成一些工作。当然,缺点是您无法两次执行相同的函数。这使得立即函数最适合一次性或初始化任务。

如果需要,立即函数也可以选择返回一个值。看到以下代码并不罕见:

    var result = (function () { 
      // something complex with 
      // temporary local variables... 
      // ... 

      // return something; 
    }()); 

在这种情况下,您不需要将函数表达式包装在括号中;您只需要调用函数的括号。因此,以下代码片段也有效:

    var result = function () { 
      // something complex with 
      // temporary local variables 
      // return something; 
    }(); 

这种语法有效,但可能看起来有点令人困惑;如果没有阅读函数的结尾,您就不知道result是一个函数还是立即函数的返回值。

内部(私有)函数

请记住,函数就像任何其他值一样,没有什么能阻止您在另一个函数中定义一个函数,下面是一个例子:

    function outer(param) { 
      function inner(theinput) { 
        return theinput * 2; 
      } 
      return 'The result is ' + inner(param); 
    } 

使用函数表达式,这也可以写成如下形式:

    var outer = function (param) { 
      var inner = function (theinput) { 
        return theinput * 2; 
      }; 
      return 'The result is ' + inner(param); 
    }; 

当您调用全局outer()函数时,它将在内部调用本地inner()函数。由于inner()是本地的,所以在outer()之外是无法访问的,因此可以说它是一个私有函数:

    > outer(2); 
    "The result is 4" 
    > outer(8); 
    "The result is 16" 
    > inner(2); 
    ReferenceError: inner is not defined 

使用私有函数的好处如下:

  • 您可以保持全局命名空间的清洁,这样不太可能引起命名冲突

  • 隐私-您只能向外界公开您决定的那些函数,并将不打算被应用程序的其余部分使用的功能保留给自己

返回函数的函数

如前所述,函数总是返回一个值,如果没有使用return显式返回,则会隐式返回undefined。函数只能返回一个值,而这个值也可以很容易地是另一个函数,例如:

    function a() { 
      alert('A!'); 
      return function () { 
        alert('B!'); 
      }; 
    } 

在这个例子中,a()函数完成其工作(警报A!),然后返回另一个执行其他操作的函数(警报B!)。您可以将返回值分配给一个变量,然后像普通函数一样使用这个变量,如下所示:

    > var newFunc = a(); 
    > newFunc(); 

在这里,第一行将警报A!,第二行将警报B!

如果您想立即执行返回的函数而不将其分配给一个新变量,您可以简单地使用另一组括号。最终结果将是相同的:

    > a()(); 

函数,重写自己!

由于函数可以返回函数,您可以使用新函数来替换旧函数。继续使用前面的例子,您可以使用调用a()的返回值来覆盖实际的a()函数:

    > a = a(); 

前一行代码会警报A!,但下一次调用a()时会警报B!。当函数有一些初始的一次性工作要做时,这是很有用的。函数在第一次调用后会覆盖自身,以避免每次调用时都做不必要的重复工作。

在前面的例子中,函数是从外部重新定义的,并且返回的值被重新分配给函数。但是,函数实际上可以从内部重写自身,如下例所示:

    function a() { 
      alert('A!'); 
      a = function () { 
        alert('B!'); 
      }; 
    } 

如果您第一次调用此函数,它将执行以下操作:

  • 警报A!(将其视为一次性的准备工作)

  • 重新定义全局变量a并将新函数分配给它

每次调用该函数时,它都会警报B!

这是另一个例子,结合了本章最后几节讨论的几种技术:

    var a = (function () { 

      function someSetup() { 
        var setup = 'done'; 
      } 

      function actualWork() { 
        alert('Worky-worky'); 
      } 

      someSetup(); 
      return actualWork; 

    }()); 

从这个例子中,您可以注意到以下几点:

  • 您有私有函数;someSetup()actualWork()

  • 您有一个立即函数:一个匿名函数,使用其定义后面的括号调用自身。

  • 该函数首次执行时,调用someSetup(),然后返回对actualWork变量的引用,该变量是一个函数。请注意,在return语句中没有括号,因为您返回的是函数引用,而不是调用此函数的结果。

  • 由于整个过程以var a =开始,自调用函数的返回值被分配给a

如果你想测试一下刚才讨论的话题的理解程度,请回答以下问题。在以下情况下,前面的代码会弹出什么:

  • 它最初加载了吗?

  • 之后你调用a()

在浏览器环境中,这些技术可能非常有用。不同的浏览器可能有不同的实现相同结果的方式。如果你知道浏览器特性在函数调用之间不会改变,你可以让一个函数确定在当前浏览器中做工作的最佳方式,然后重新定义自己,以便浏览器能力检测只做一次。你将在本书的后面看到这种情况的具体例子。

闭包

本章的其余部分是关于闭包的(还有什么更好的方式来结束一个章节呢?)。闭包可能一开始有点难以理解,所以如果你在第一次阅读时没有理解,不要感到沮丧。你应该阅读本章的其余部分,并自己尝试示例,但如果你觉得自己没有完全理解这个概念,可以在本章前面讨论的话题有机会消化后再回来看。

在继续讨论闭包之前,让我们首先回顾并扩展 JavaScript 中作用域的概念。

作用域链

如你所知,在 JavaScript 中,没有花括号作用域,但有函数作用域。在函数中定义的变量在函数外部不可见,但在代码块(例如iffor循环)中定义的变量在块外部可见,例如:

    > var a = 1;  
    > function f() { 
        var b = 1;  
        return a; 
      } 
    > f(); 
    1 
    > b; 
    ReferenceError: b is not defined 

变量a在全局空间中,而b在函数f()的作用域中。所以,我们有以下情况:

  • f()内部,ab都是可见的

  • f()外部,a是可见的,但b不可见

如果你在outer()内部嵌套定义一个inner()函数,它将可以访问其自己的作用域中的变量,以及其父级的作用域。这就是所谓的作用域链,链可以很长(深),可以根据需要延伸:

    var global = 1; 
    function outer() { 
      var outer_local = 2; 
      function inner() { 
        var inner_local = 3; 
        return inner_local + outer_local + global; 
      } 
      return inner(); 
    } 

让我们测试一下inner()函数是否可以访问所有变量:

    > outer(); 
    6 

使用闭包打破链条

让我们通过一个示例来介绍闭包,并看看以下代码发生了什么:

    var a = "global variable"; 
    var F = function () { 
      var b = "local variable"; 
      var N = function () { 
        var c = "inner local"; 
      }; 
    }; 

首先是全局作用域G。把它想象成宇宙,好像它包含了一切:

使用闭包打破链条

它可以包含全局变量,如a1a2,以及全局函数,如F

使用闭包打破链条

函数有自己的私有空间,并且可以用它来存储其他变量,比如b,以及内部函数,比如N(用于内部)。在某个时候,你会得到以下的图片:

使用闭包打破链条

如果你在点 a,你就在全局空间内。如果你在点b,也就是在F函数的空间内,那么你就可以访问全局空间和F空间。如果你在点c,也就是在N函数的空间内,那么你可以访问全局空间、F空间和N空间。你无法从a到达b,因为bF外部是不可见的。但是,如果你愿意,你可以从c到达b,或者从N到达b。有趣的是,当N以某种方式打破了F并进入了全局空间时,闭包效果就会发生。

使用闭包打破链条

然后会发生什么?N在与a相同的全局空间中。而且,由于函数记住了它们被定义的环境,N仍然可以访问F空间,因此可以访问b。这很有趣,因为Na所在的地方,但N确实可以访问b,但a不行。

另外,N是如何打破链条的?通过使自己成为全局的(省略var)还是通过让F将其传递(或return)到全局空间。让我们看看这在实践中是如何做的。

闭包 #1

看一下下面的函数,它和之前的一样,只是F返回N,而N返回b,通过作用域链它可以访问到b

    var a = "global variable"; 
    var F = function () { 
      var b = "local variable"; 
      var N = function () { 
        var c = "inner local"; 
        return b; 
      }; 
      return N; 
    }; 

F函数包含b变量,它是局部的,因此无法从全局空间访问:

    > b; 
    ReferenceError: b is not defined 

N函数可以访问它的私有空间,F()函数的空间和全局空间。因此,它可以看到b。由于F()可以从全局空间调用(它是一个全局函数),你可以调用它并将返回的值赋给另一个全局变量。结果是一个新的全局函数,它可以访问F()函数的私有空间:

    > var inner = F(); 
    > inner(); 
    "local variable" 

闭包#2

下一个例子的最终结果将与前一个例子相同,但实现方式略有不同。F()不返回一个函数,而是在其内部创建一个新的全局函数inner()

让我们首先声明一个全局函数的占位符。这是可选的,但总是好习惯。然后,你可以定义F()函数如下:

    var inner; // placeholder 
    var F = function () { 
      var b = "local variable"; 
      var N = function () { 
        return b; 
      }; 
      inner = N; 
    }; 

现在,让我们看看如果你调用F()会发生什么:

    > F(); 

F()内部定义了一个新函数N(),并赋值给全局的inner函数。在定义时,N()F()内部,因此它可以访问F()函数的作用域。即使inner()函数是全局空间的一部分,它也将保持对F()函数作用域的访问,例如:

    > inner(); 
    "local variable". 

定义和闭包#3

每个函数都可以被视为一个闭包。这是因为每个函数都保持着一个秘密的链接到它被创建的环境(作用域)。但是,大多数情况下,这个作用域会被销毁,除非发生一些有趣的事情(如前面的代码所示)导致它被保留。

根据你目前所见,你可以说当一个函数在其父级返回后仍保持对其父级作用域的链接时,就创建了一个闭包。而且,每个函数都是一个闭包,因为至少每个函数都保持对全局作用域的访问,而全局作用域永远不会被销毁。

让我们再看一个闭包的例子,这次使用函数参数。函数参数的行为就像这个函数的局部变量,但它们是隐式创建的;你不需要为它们使用var。你可以创建一个返回另一个函数的函数,然后返回其父级的参数,如下所示:

    function F(param) { 
      var N = function () { 
        return param; 
      }; 
      param++; 
      return N; 
    } 

你可以按以下方式使用该函数:

    > var inner = F(123); 
    > inner(); 
    124 

注意param++在函数定义后递增,但当调用inner()时,它返回了更新后的值。这表明函数保持对其定义时的作用域的引用,而不是在函数执行期间在作用域中找到的变量和它们的值的引用。

循环中的闭包

让我们看看闭包时一个经典的新手错误。这很容易导致难以发现的错误,因为表面上一切都很正常。

让我们循环三次,每次创建一个返回循环序列号的新函数。新函数将被添加到一个数组中,并在最后返回该数组。以下是函数:

    function F() { 
      var arr = [], i; 
      for (i = 0; i < 3; i++) { 
        arr[i] = function () { 
          return i; 
        }; 
      } 
      return arr; 
    } 

让我们运行该函数,并将结果赋给arr数组:

    > var arr = F(); 

现在你有了一个包含三个函数的数组。让我们在每个数组元素后面加上括号来调用它们。预期的行为是打印出循环序列012。让我们试试:

    > arr[0](); 
    3 
    > arr[1](); 
    3 
    > arr[2](); 
    3 

嗯,不太符合预期。这里发生了什么?所有三个函数指向同一个局部变量:i。为什么?这些函数不记住值,它们只保留一个指向它们创建时的环境的链接(引用)。在这种情况下,i变量恰好存在于定义这三个函数的环境中。因此,所有函数在需要访问该值时,都会回到环境中找到i的最新值。循环结束后,i变量的值为3。因此,所有三个函数指向相同的值。

为什么是三而不是两是另一个更好的问题,以便更好地理解for循环。

那么,如何实现正确的行为呢?答案是使用另一个闭包,如下面的代码所示:

    function F() { 
      var arr = [], i; 
      for (i = 0; i < 3; i++) { 
        arr[i] = (function (x) { 
          return function () { 
            return x; 
          }; 
        }(i)); 
      } 
      return arr; 
    } 

这将给你期望的结果如下:

    > var arr = F(); 
    > arr[0](); 
    0 
    > arr[1](); 
    1 
    > arr[2](); 
    2 

在这里,您不仅创建一个返回i的函数,还将i变量的当前值传递给另一个立即函数。在这个函数中,i变成了本地值x,并且x每次都有不同的值。

或者,您可以使用一个普通的(而不是立即的)内部函数来实现相同的结果。关键是使用中间函数在每次迭代时将i的值局部化,如下所示:

    function F() { 

      function binder(x) { 
        return function () { 
          return x; 
        }; 
      } 

      var arr = [], i; 
      for (i = 0; i < 3; i++) { 
        arr[i] = binder(i); 
      } 
      return arr; 
    } 

Getter 和 setter

让我们看看使用闭包的另外两个例子。第一个涉及创建 getter 和 setter 函数。想象一下,您有一个变量,它应该包含特定类型的值或特定范围的值。您不想暴露这个变量,因为您不希望代码的任何部分都能够改变它的值。您可以将这个变量保护在一个函数内,并提供两个额外的函数——一个用于获取值,一个用于设置值。设置它的函数可以包含一些逻辑来验证值在分配给受保护的变量之前。让我们简化验证部分(为了保持示例简短)并只接受数字值。

您可以将 getter 和 setter 函数都放在包含secret变量的同一个函数中,以便它们共享相同的作用域:

    var getValue, setValue; 

    (function () { 

      var secret = 0; 

      getValue = function () { 
        return secret; 
      }; 

      setValue = function (v) { 
        if (typeof v === "number") { 
          secret = v; 
        } 
      }; 

    }()); 

在这种情况下,包含所有内容的函数是一个立即函数。它将setValue()getValue()定义为全局函数,而secret变量保持本地和无法直接访问,如下例所示:

    > getValue(); 
    0 
    > setValue(123); 
    > getValue(); 
    123 
    > setValue(false); 
    > getValue(); 
    123 

迭代器

最后一个闭包示例(也是本章的最后一个示例)展示了使用闭包来实现迭代器功能。

您已经知道如何循环遍历一个简单的数组,但可能存在更复杂的数据结构的情况,其中有不同的规则来确定值的顺序。您可以将复杂的下一个逻辑封装到一个易于使用的next()函数中。然后,您可以在需要连续值的每个时间简单地调用next()

对于这个例子,让我们只使用一个简单的数组而不是一个复杂的数据结构。这是一个初始化函数,它接受一个输入数组,并定义一个秘密指针i,它将始终指向数组中的下一个元素:

    function setup(x) { 
      var i = 0; 
      return function () { 
        return x[i++]; 
      }; 
    } 

调用带有数据数组的setup()函数将为您创建next()函数,如下所示:

    > var next = setup(['a', 'b', 'c']); 

从那里开始就很容易和有趣了;一遍又一遍地调用相同的函数给你下一个元素,如下所示:

    > next(); 
    "a" 
    > next(); 
    "b" 
    > next(); 
    "c" 

IIFE 与块

由于 ES5 没有提供块作用域,实现块作用域的一种流行模式是使用立即调用的函数表达式IIFE),例如:

    (function () { 
      var block_scoped=0; 
    }()); 
    console.log(block_scoped); //reference error 

有了 ES6 对块作用域的支持,您可以简单地使用letconst声明。

箭头函数

JavaScript 几乎使用了所有箭头的变体。ES6 引入了一种新的语法来编写函数。我们一直在 JavaScript 中编写函数表达式。在 JavaScript 中写代码像这样是惯用的(此示例是在 jQuery 中):

    $("#submit-btn").click(function (event) { 
      validateForm(); 
      submitMessage(); 
    }); 

这是一个典型的 jQuery 事件处理程序。事件处理程序click()函数接受一个函数作为参数,我们将简单地创建一个内联的匿名函数表达式并将其传递给 click 函数。这种写法匿名函数表达式被称为Lambda 函数。其他几种语言支持这个特性。虽然 lambda 在新语言中更或多或少是标准的,但 JavaScript 负责推广它们的使用。然而,JavaScript 中的 lambda 语法并不是非常简洁。ES6 箭头函数填补了这一空白,并提供了一种简洁的语法来编写函数。

箭头函数提供了比传统函数表达式更简洁的语法;例如,考虑以下代码片段:

    const num = [1,2,3] 
    const squares = num.map(function(n){ 
      return n*n; 
    }); 
    console.log(squares); //[1,4,9] 

箭头函数语法可以简化函数为以下代码行:

    const squares_6 =num.map( n=> n*n) 

正如你所看到的,任何地方都没有functionreturn关键字。如果你的函数只有一个参数,你将会写成identifer => expression

当你需要多个参数时,你需要用括号包裹参数列表:

  • 没有参数() => {...}

  • 一个参数a => {...}

  • 多个参数(a,b) => {...}

箭头函数可以有语句块体和表达式体:

    n => { return n+n}  //statement block 
    n =>n+n            //expression 

两者是等价的,但第二种变体更简洁,更受欢迎。箭头函数始终是匿名的。箭头函数的一个重要方面是,它们不会绑定this关键字的值-该值是从周围作用域中词法推导出来的。由于我们还没有详细讨论this关键字,我们将把讨论推迟到本书的后面部分。

练习

  1. 编写一个函数,将十六进制颜色(例如蓝色(#0000FF))转换为其 RGB 表示形式,rgb(0, 0, 255)。将你的函数命名为getRGB(),并使用以下代码进行测试(提示:将字符串视为字符数组):
        > var a = getRGB("#00FF00"); 
        > a; 
        "rgb(0, 255, 0)" 

  1. 以下每行代码在控制台中打印什么?
        > parseInt(1e1); 
        > parseInt('1e1'); 
        > parseFloat('1e1'); 
        > isFinite(0/10); 
        > isFinite(20/0); 
        > isNaN(parseInt(NaN)); 

  1. 这段代码会弹出什么?
        var a = 1; 

        function f() { 
         function n() { 
            alert(a); 
          } 
          var a = 2; 
          n(); 
        } 

        f(); 

  1. 所有以下示例都会弹出"Boo!"。你能解释为什么吗?
  • 例 1:
        var f = alert; 
        eval('f("Boo!")'); 

  • 例 2:
        var e; 
        var f = alert; 
        eval('e=f')('Boo!'); 

  • 例 3:
        (function(){ 
          return alert;} 
        )()('Boo!'); 

总结

你现在已经完成了与 JavaScript 中函数相关的基本概念的介绍。这为你快速掌握面向对象的 JavaScript 概念和现代 JavaScript 编程中使用的模式奠定了基础。到目前为止,我们一直在避免面向对象的特性,但是当你达到本书的这一部分时,从现在开始会变得更加有趣。让我们花一点时间回顾本章讨论的主题:

  • 如何使用函数声明语法或函数表达式定义和调用函数的基础知识

  • 函数参数及其灵活性

  • 内置函数-parseInt()parseFloat()isNaN()isFinite()eval(),以及四个编码/解码 URL 的函数

  • JavaScript 中的变量作用域-没有花括号作用域,变量只有函数作用域和作用域链

  • 函数作为数据-函数就像你分配给变量的任何其他数据一样,这样会有很多有趣的应用程序,比如:

  • 私有函数和私有变量

  • 匿名函数

  • 回调

  • 立即函数

  • 函数覆盖自身

  • 闭包

  • 箭头函数

第四章:对象

既然你已经掌握了 JavaScript 的原始数据类型、数组和函数,现在是时候兑现本书标题的承诺,谈论对象了。

JavaScript 对经典的面向对象编程有着独特的看法。面向对象编程是最流行的编程范式之一,并且一直是大多数编程语言(如 Java 和 C++)的主要内容。经典 OOP 提出了一些明确定义的概念,大多数语言都采用了这些概念。然而,JavaScript 对此有不同的看法。我们将看看 JavaScript 支持 OOP 的方式。

在本章中,你将学习以下主题:

  • 如何创建和使用对象

  • 构造函数是什么

  • 存在哪些内置的 JavaScript 对象类型以及它们能为你做什么

从数组到对象

正如你已经从第二章中所知道的,原始数据类型、数组、循环和条件,数组只是一个值的列表。每个值都有一个从零开始递增的索引(一个数字键)。考虑以下例子:

    > var myarr = ['red', 'blue', 'yellow', 'purple']; 
    > myarr; 
    ["red", "blue", "yellow", "purple"]. 
    > myarr[0]; 
    "red" 
    > myarr[3]; 
    "purple" 

如果你将索引放在一列,值放在另一列,你将得到一个键/值对的表格,如下所示:

0 红色
1 蓝色
2 黄色
3 紫色

对象类似于数组,但不同之处在于你自己定义键。你不仅限于使用数字索引,还可以使用更友好的键,如first_nameage等。

让我们看一个简单的对象并检查它的部分:

    var hero = { 
      breed: 'Turtle',  
      occupation: 'Ninja' 
    }; 

你可以看到:

  • 指向对象的变量的名称是hero

  • 与用于定义数组的[]不同,你用{}来定义对象

  • 用逗号分隔对象中包含的元素(称为属性)

  • 键/值对由冒号分隔,如 key:value

键(属性的名称)可以选择性地放在引号中。例如,这些键都是相同的:

    var hero = {occupation: 1}; 
    var hero = {"occupation": 1}; 
    var hero = {'occupation': 1}; 

建议不要给属性的名称加引号(这样打字更少),但也有一些情况下你必须使用引号。这里列举了一些情况:

  • 如果属性名称是 JavaScript 中的保留字之一(参见附录 A, 保留字

  • 如果它包含空格或特殊字符(除了字母、数字和_$字符之外的任何字符)

  • 如果它以数字开头

换句话说,如果你为属性选择的名称在 JavaScript 中不是有效的变量名称,那么你需要用引号括起来。

看看这个看起来奇怪的对象:

    var o = { 
      $omething: 1, 
      'yes or no': 'yes', 
      '!@#$%^&*': true 
    }; 

这是一个有效的对象。第二个和第三个属性需要引号;否则,你会得到一个错误。

在本章的后面,你将看到定义对象和数组的其他方法,除了[]{}。然而,首先,让我们介绍一些术语 - 使用[]定义数组称为数组字面量表示法,使用大括号{}定义对象称为对象字面量表示法

元素,属性,方法和成员

当谈论数组时,你说它们包含元素。当谈论对象时,你说它们包含属性。在 JavaScript 中没有什么显著的区别;这只是人们习惯于的术语,可能来自其他编程语言。

对象的属性可以指向一个函数,因为函数只是数据。指向函数的属性也被称为方法。在下面的例子中,talk 是一个方法:

    var dog = { 
      name: 'Benji', 
      talk: function () { 
        alert('Woof, woof!'); 
      } 
    }; 

在前一章中,你已经看到,也可以将函数存储为数组元素并调用它们,但在实践中你不会经常看到这样的代码:

    > var a = []; 
    > a[0] = function (what) { alert(what); }; 
    > a0; 

你也会看到人们使用成员这个词来指代对象的属性,通常是当属性是函数或不是函数都无所谓的时候。

哈希和关联数组

在一些编程语言中,有一个区别:

  • 常规数组,也称为索引枚举数组(键是数字)

  • 关联数组,也称为哈希字典(键是字符串)

JavaScript 使用数组表示索引数组,使用对象表示关联数组。如果你想在 JavaScript 中使用哈希,你会使用对象。

访问对象的属性

有两种访问对象属性的方法:

  • 使用方括号表示法,例如,hero['occupation']

  • 使用点表示法,例如,hero.occupation

点表示法更易于阅读和编写,但并非总是可用。引用属性名称的规则相同。如果属性名称不是有效的变量名称,则不能使用点表示法。

让我们再看一下 hero 对象:

    var hero = { 
      breed: 'Turtle', 
      occupation: 'Ninja' 
    }; 

以下是使用点表示法访问属性的示例:

    > hero.breed; 
    "Turtle" 

让我们看一个使用方括号表示法访问属性的例子:

    > hero['occupation']; 
    "Ninja" 

考虑以下示例,访问一个不存在的属性返回 undefined

    > 'Hair color is ' + hero.hair_color; 
    "Hair color is undefined" 

对象可以包含任何数据,包括其他对象:

    var book = { 
      name: 'Catch-22', 
      published: 1961, 
      author: { 
        firstname: 'Joseph', 
        lastname: 'Heller' 
      } 
    }; 

要访问 book 对象的 author 属性中包含的对象的 firstname 属性,你可以使用以下代码行:

    > book.author.firstname; 
    "Joseph" 

让我们看一个使用方括号表示法的例子:

    > book['author']['lastname']; 
    "Heller" 

即使你混合使用:

    > book.author['lastname']; 
    "Heller" 
    > book['author'].lastname; 
    "Heller" 

另一个需要使用方括号的情况是需要访问的属性名称事先不知道。在运行时,它会动态存储在一个变量中:

    > var key = 'firstname'; 
    > book.author[key]; 
    "Joseph" 

调用对象的方法

你知道方法只是一个恰好是函数的属性,所以你访问方法的方式与访问属性的方式相同-使用点表示法或使用方括号。调用(调用)方法与调用任何其他函数相同-只需在方法名称后添加括号,这实际上是在说执行!:

    > var hero = { 
        breed: 'Turtle', 
        occupation: 'Ninja', 
        say: function () { 
          return 'I am ' + hero.occupation; 
        } 
      }; 
    > hero.say(); 
    "I am Ninja" 

如果有任何要传递给方法的参数,你会像处理普通函数一样进行:

    > hero.say('a', 'b', 'c'); 

由于可以使用类似数组的方括号访问属性,这意味着你也可以使用方括号访问和调用方法:

    > hero['say'](); 

这不是一个常见的做法,除非在编写代码时不知道方法名,而是在运行时定义:

    var method = 'say'; 
    hero[method](); 

注意

除非必须使用点表示法访问方法和属性,并且不要在对象文字中引用属性。

修改属性/方法

JavaScript 允许你随时更改现有对象的属性和方法。这包括添加新属性或删除它们。你可以从一个空白对象开始,然后稍后添加属性。让我们看看你可以如何做到这一点。

没有属性的对象如下所示:

    > var hero = {}; 

注意

“空白”对象

在本节中,你从一个“空白”对象开始,var hero = {}。引号中的“空白”是因为这个对象并不真的是空的和无用的。尽管在这个阶段它没有自己的属性,但它已经继承了一些属性。

稍后你会了解更多关于自有属性与继承属性的知识。因此,在 ES3 中,对象从来不是真正的空白或空的。不过,在 ES5 中,有一种方法可以创建一个完全空白的对象,它不继承任何东西,但我们不要过多地超前。

  1. 以下是访问不存在属性的代码:
        > typeof hero.breed; 
        "undefined" 

  1. 添加两个属性和一个方法:
        > hero.breed = 'turtle'; 
        > hero.name = 'Leonardo'; 
        > hero.sayName = function () { 
            return hero.name;  
          }; 

  1. 调用方法:
        > hero.sayName(); 
        "Leonardo" 

  1. 删除属性:
        > delete hero.name; 
        true 

  1. 如果再次调用该方法,它将不再找到已删除的 name 属性:
        > hero.sayName(); 
        "undefined" 

注意

可变对象

你可以随时更改任何对象,例如添加和删除属性以及更改它们的值。但是,这个规则也有例外。一些内置对象的一些属性是不可更改的(例如 Math.PI,稍后你会看到)。此外,ES5 允许你阻止对对象的更改。你将在附录 C 中了解更多关于它的知识,内置对象

使用 this 值

在上一个例子中,sayName()方法使用了hero.name来访问hero对象的name属性。然而,在方法内部,还有另一种访问方法所属对象的方式。这种方法就是使用特殊值this

    > var hero = { 
        name: 'Rafaelo', 
        sayName: function () { 
          return this.name; 
        } 
      }; 
    > hero.sayName(); 
    "Rafaelo" 

因此,当你说this时,实际上是在说-这个对象或当前对象。

构造函数

还有另一种创建对象的方式-使用构造函数。让我们看一个例子:

    function Hero() { 
      this.occupation = 'Ninja'; 
    } 

为了使用这个函数创建一个对象,你可以使用new操作符,如下所示:

    > var hero = new Hero(); 
    > hero.occupation; 
    "Ninja" 

使用构造函数的好处是它们可以接受参数,在创建新对象时可以使用这些参数。让我们修改构造函数以接受一个参数并将其赋值给name属性:

    function Hero(name) { 
      this.name = name; 
      this.occupation = 'Ninja'; 
      this.whoAreYou = function () { 
        return "I'm " + 
               this.name + 
               " and I'm a " + 
               this.occupation; 
      }; 
    } 

现在,你可以使用相同的构造函数创建不同的对象:

    > var h1 = new Hero('Michelangelo'); 
    > var h2 = new Hero('Donatello'); 
    > h1.whoAreYou(); 
    "I'm Michelangelo and I'm a Ninja" 
    > h2.whoAreYou(); 
    "I'm Donatello and I'm a Ninja" 

注意

按照惯例,你应该将构造函数的第一个字母大写,以便你有一个视觉线索表明它们不打算作为常规函数调用。

如果你调用一个被设计为构造函数的函数但省略了new操作符,这不会报错。但是,它不会给你期望的结果:

    > var h = Hero('Leonardo'); 
    > typeof h; 
    "undefined" 

这里发生了什么?没有new操作符,所以没有创建新对象。函数被像任何其他函数一样调用,所以变量h包含函数返回的值。函数没有返回任何东西(没有return函数),所以实际上返回了undefined,这个值被赋给了变量h

在这种情况下,this指的是什么?它指的是全局对象。

全局对象

你已经学习了一些关于全局变量(以及你应该避免它们)的知识。你也知道 JavaScript 程序运行在一个宿主环境中(例如浏览器)。现在你了解了对象,是时候说出整个真相了,宿主环境提供了一个全局对象,所有全局变量都可以作为全局对象的属性访问。

如果你的宿主环境是 Web 浏览器,全局对象被称为window。另一种访问全局对象的方式(在大多数其他环境中也是如此)是在构造函数之外的全局程序代码中使用this关键字。

举例来说,你可以在任何函数外声明一个全局变量,如下所示:

    > var a = 1; 

然后,你可以以各种方式访问这个全局变量:

  • 作为变量a

  • 作为全局对象的属性,例如window['a']window.a

  • 作为全局对象的属性,称为this

    > var a = 1; 
    > window.a; 
    1 
    > this.a; 
    1 

让我们回到你定义一个构造函数并在没有new操作符的情况下调用它的情况。在这种情况下,this指的是全局对象,并且所有设置为this的属性都成为window的属性。

声明一个构造函数并在没有使用 new 的情况下调用它会返回"undefined"

    > function Hero(name) { 
        this.name = name; 
      } 
    > var h = Hero('Leonardo'); 
    > typeof h; 
    "undefined" 
    > typeof h.name; 
    TypeError: Cannot read property 'name' of undefined 

正如你在Hero函数内部使用了this关键字,一个全局变量(全局对象的属性)叫做name被创建了:

    > name; 
    "Leonardo" 
    > window.name; 
    "Leonardo" 

如果你使用new调用相同的构造函数,那么会返回一个新对象,并且this指向它:

    > var h2 = new Hero('Michelangelo'); 
    > typeof h2; 
    "object" 
    > h2.name; 
    "Michelangelo" 

你在第三章中看到的内置全局函数函数也可以作为window对象的方法来调用。因此,以下两个调用会得到相同的结果:

    > parseInt('101 dalmatians'); 
    101 
    > window.parseInt('101 dalmatians') 
    101 

构造函数属性

当创建一个对象时,会在幕后为其分配一个特殊的属性-constructor属性。它包含了用于创建this对象的构造函数的引用。

继续上一个例子:

    > h2.constructor; 
    function Hero(name) { 
      this.name = name; 
    } 

由于constructor属性包含对函数的引用,你也可以调用这个函数来生成一个新对象。以下代码就像在说:“我不在乎对象h2是如何创建的,但我想要另一个和它一样的对象”:

    > var h3 = new h2.constructor('Rafaello'); 
    > h3.name; 
    "Rafaello" 

如果使用对象文字表示法创建对象,则其构造函数是内置的Object()构造函数(本章后面将更多介绍):

    > var o = {}; 
    > o.constructor; 
    function Object() { [native code] } 
    > typeof o.constructor; 
    "function" 

instanceof 运算符

使用instanceof运算符,您可以测试对象是否是使用特定的constructor函数创建的:

    > function Hero() {} 
    > var h = new Hero(); 
    > var o = {}; 
    > h instanceof Hero; 
    true 
    > h instanceof Object; 
    true 
    > o instanceof Object; 
    true 

请注意,您在函数名称后面不要放括号(不要使用h instanceof Hero())。这是因为您没有调用此函数,而只是通过名称引用它,就像引用任何其他变量一样。

返回对象的函数

除了使用constructor函数和new运算符创建对象之外,您还可以使用普通函数创建对象,而无需使用new运算符。您可以有一个函数进行一些准备工作,并将对象作为返回值。

例如,这是一个简单的factory()函数,用于生成对象:

    function factory(name) { 
      return { 
        name: name 
      }; 
    } 

考虑以下使用factory()函数的示例:

    > var o = factory('one'); 
    > o.name; 
    "one" 
    > o.constructor; 
    function Object() { [native code] } 

实际上,您还可以使用constructor函数和returnthis关键字不同的对象。这意味着您可以修改constructor函数的默认行为。让我们看看如何做到这一点。

这是正常的构造函数场景:

    > function C() { 
        this.a = 1; 
      } 
    > var c = new C(); 
    > c.a; 
    1 

然而,现在,看看这种情况:

    > function C2() { 
        this.a = 1; 
        return {b: 2}; 
      } 
    > var c2 = new C2(); 
    > typeof c2.a; 
    "undefined" 
    > c2.b; 
    2 

这里发生了什么?构造函数没有返回包含属性athis对象,而是返回了另一个包含属性b的对象。只有在返回值是对象的情况下才可能发生这种情况。否则,如果您尝试返回任何不是对象的东西,构造函数将继续其通常的行为并返回this

如果您考虑构造函数内部如何创建对象,您可以想象在函数顶部定义了一个名为this的变量,然后在末尾返回。考虑以下代码:

    function C() { 
      // var this = {}; // pseudo code, you can't do this 
      this.a = 1; 
      // return this; 
    } 

传递对象

当您将对象分配给不同的变量或将其传递给函数时,您只传递了对该对象的引用。因此,如果您对引用进行更改,实际上是修改了原始对象。

这是一个示例,演示了如何将一个对象分配给另一个变量,然后对副本进行更改。结果,原始对象也被更改了:

    > var original = {howmany: 1}; 
    > var mycopy = original; 
    > mycopy.howmany; 
    1 
    > mycopy.howmany = 100; 
    100 
    > original.howmany; 
    100 

将对象传递给函数时也是一样的:

    > var original = {howmany: 100}; 
    > var nullify = function (o) { o.howmany = 0; }; 
    > nullify(original); 
    > original.howmany; 
    0 

比较对象

当您比较对象时,只有在比较两个指向同一对象的引用时才会得到true。如果比较两个不同的对象,这两个对象恰好具有完全相同的方法和属性,结果将是false

让我们创建两个看起来相同的对象:

    > var fido  = {breed: 'dog'}; 
    > var benji = {breed: 'dog'}; 

将它们进行比较会返回false

    > benji === fido; 
    false 
    > benji == fido; 
    false 

您可以创建一个新变量mydog,并将其中一个对象分配给它。这样,变量mydog实际上指向同一个对象:

    > var mydog = benji; 

在这种情况下,benjimydog,因为它们是同一个对象(更改mydog变量的属性将更改benji变量的属性)。比较结果为true

    > mydog === benji; 
    true 

由于fido是一个不同的对象,它与mydog不相等:

    > mydog === fido; 
    false 

WebKit 控制台中的对象

在深入研究 JavaScript 中的内置对象之前,让我们快速谈一下在 WebKit 控制台中使用对象的工作。

在本章的示例中玩耍后,您可能已经注意到对象在控制台中的显示方式。如果您创建一个对象并键入其名称,您将得到一个指向对象的箭头。

对象是可点击的,并展开以显示对象的所有属性列表。如果属性也是对象,则旁边也有一个箭头,因此您也可以展开它。这很方便,因为它可以让您深入了解这个对象的确切内容。考虑以下示例:

WebKit 控制台中的对象

注意

您现在可以忽略__proto__;下一章将更多介绍。

使用 console.log 方法记录

控制台还为您提供了一个名为console的对象和一些方法,例如console.log()console.error(),您可以使用它们在控制台中显示任何您想要的值。

使用 console.log 方法记录日志

console.log()方法在您想要快速测试某些内容时非常方便,以及在您的真实脚本中想要转储一些中间调试信息时非常方便。以下是您可以尝试循环的示例:

    > for (var i = 0; i < 5; i++) { 
        console.log(i);  
      } 
    0 
    1 
    2 
    3 
    4 

ES6 对象字面量

ES6 在使用对象字面量时引入了更简洁的语法。ES6 为属性初始化和函数定义提供了几种简写。ES6 的简写与熟悉的 JSON 语法非常相似。考虑以下代码片段:

    let a = 1 
    let b = 2 
    let val = {a: a, b: b} 
    console.log(val) //{"a":1,"b":2} 

这是分配属性值的典型方式。如果变量的名称和属性键相同,ES6 允许您使用简写语法。上述代码可以写成如下形式:

    let a = 1 
    let b = 2 
    let val = {a, b} 
    console.log(val) //{"a":1,"b":2} 

方法定义也有类似的语法。正如我们所讨论的,方法只是对象的属性,其值是函数。考虑以下示例:

    var obj = { 
      prop: 1, 
      modifier:  function() { 
        console.log(this.prop);   
      } 
    } 

在 ES6 中定义方法的一种简洁方式。您只需删除function关键字和:。在 ES6 中等效的代码如下所示:

    var obj = { 
      prop: 1, 
      modifier () { 
        console.log(this.prop); 
      } 
    } 

ES6 允许您计算属性的键。在 ES6 之前,您只能使用固定的属性名称。以下是一个例子:

    var obj = { 
      prop: 1, 
      modifier: function () { 
        console.log(this.prop);   
      } 
    } 
    obj.prop = 2; 
    obj.modifier(); //2 

正如您所看到的,我们在这种情况下受限于使用固定的键名称:propmodifier。然而,ES6 允许您使用计算属性键。还可以使用由函数返回的值动态创建属性键:

    let vehicle = "car" 
    function vehicleType(){ 
      return "truck" 
    } 
    let car = { 
      [vehicle+"_model"]: "Ford" 
    } 
    let truck= { 
      [vehicleType() + "_model"]: "Mercedez" 
    } 
    console.log(car) //{"car_model":"Ford"} 
    console.log(truck) //{"truck_model":"Mercedez"} 

我们正在使用变量vehicle的值与固定字符串连接,以推导出创建car对象时的属性键。在第二个片段中,我们通过将固定字符串与函数返回的值连接来创建属性。这种计算属性键的方式在创建对象时提供了很大的灵活性,并且可以消除大量样板和重复的代码。

此语法也适用于方法定义:

    let object_type = "Vehicle" 
    let obj = { 
      ["get"+object_type]() { 
        return "Ford" 
      } 
    } 

对象属性和属性

每个对象都有一些属性。每个属性又有一个键和属性。属性的状态存储在这些属性中。所有属性都具有以下属性:

  • 可枚举(布尔值):这表示您是否可以枚举对象的属性。系统属性是不可枚举的,而用户属性是可枚举的。除非有充分的理由,否则应该保持不变。

  • 可配置(布尔值):如果此属性为false,则该属性无法被删除或编辑(它不能更改任何属性)。

您可以使用Object.getOwnPropertyDescriptor()方法来检索对象的自有属性:

    let obj = { 
      age: 25 
    } 
    console.log(Object.getOwnPropertyDescriptor(obj, 'age')); 
    //{"value":25,"writable":true,"enumerable":true,"configurable":true} 

同时,可以使用Object.defineProperty()方法来定义属性:

    let obj = { 
      age: 25 
    } 
    Object.defineProperty(obj, 'age', { configurable: false }) 
    console.log(Object.getOwnPropertyDescriptor(obj, 'age')); 
    //{"value":25,"writable":true,"enumerable":true,"configurable":false} 

虽然你可能永远不会使用这些方法,但了解对象属性和属性是很重要的。在下一节中,我们将讨论一些object方法在某些属性的上下文中是如何使用的。

ES6 对象方法

ES6 引入了一些对象的静态辅助方法。Object.assign是一个辅助方法,用于执行对象的浅复制,取代了流行的混合方法。

使用 Object.assign 复制属性

此方法用于将目标对象的属性复制到源对象中。换句话说,此方法将源对象与目标对象合并,并修改目标对象:

    let a = {} 
    Object.assign(a, { age: 25 }) 
    console.log(a)  //{"age":25} 

Object.assign的第一个参数是要复制源属性的目标对象。同一个目标对象将返回给调用者。现有属性将被覆盖,而不是源对象的一部分的属性将被忽略:

    let a = {age : 23, gender: "male"} 
    Object.assign(a, { age: 25 })    // age overwritten, but gender ignored 
    console.log(a)  //{"age":25, "gender":"male"} 

Object.assign可以接受多个源对象。您可以编写Object.assign(target, source1, source2)。以下是一个例子:

    console.log(Object.assign({a:1, b:2}, {a: 2}, {c: 4}, {b: 3})) 
    //Object { 
    //"a": 2,  
    //"b": 3, 
    //"c": 4 
    // 

在这个片段中,我们正在从多个源对象中分配属性。另外,请注意Object.assign()如何返回目标对象,然后我们将其用在console.log()中。

需要注意的一点是,只有可枚举的自有(非继承的)属性才能使用Object.assign()进行复制。原型链中的属性(在本章后面讨论继承时将会讨论)不会被考虑。我们之前讨论的可枚举属性将帮助您理解这种区别。

在下面的例子中,我们将使用defineProperty()创建一个不可枚举的属性,并验证Object.assign()忽略该属性的事实:

    let a = {age : 23, gender: "male"} 
    Object.defineProperty(a, 'superpowers', {enumberable:false, value: 'ES6'}) 
    console.log(

定义为superpowers的属性的可枚举属性设置为false。在复制属性时,此属性将被忽略。

使用 Object.is 比较值

ES6 提供了一种稍微精确比较值的方式。我们已经讨论了严格相等运算符===。然而,对于NaN-0+0,严格相等运算符的行为是不一致的。这里有一个例子:

    console.log(NaN===NaN) //false 
    console.log(-0===+0) //true 
    //ES6 Object.is 
    console.log(Object.is(NaN,NaN)) //true 
    console.log(Object.is(-0,+0)) //false 

除了这两种情况,Object.is()可以安全地替换为===运算符。

解构

编码时,您将一直使用对象和数组。JavaScript 对象和数组的表示方式类似于 JSON 格式。您将定义对象和数组,然后从中检索元素。ES6 提供了一种方便的语法,显著改进了我们从对象和数组中访问属性/成员的方式。让我们考虑一个您经常会写的典型代码:

    var config = { 
      server: 'localhost', 
      port: '8080' 
    } 
    var server = config.server; 
    var port = config.port; 

在这里,我们从config对象中提取了serverport的值,并将它们分配给本地变量。非常简单明了!然而,当这个对象有一堆属性,其中一些是嵌套的,这个简单的操作可能会变得非常乏味。

ES6 解构语法允许在赋值语句的左侧使用对象字面量。在下面的例子中,我们将定义一个带有几个属性的对象config。稍后,我们将使用解构来将对象config的值分配给赋值语句左侧的各个属性:

    let config = { 
      server: 'localhost', 
      port: '8080', 
      timeout: 900, 
    } 
    let {server,port} = config  
    console.log(server, port) //"localhost" "8080" 

如您所见,serverport是本地变量,它们从config对象中获取了属性,因为属性的名称与本地变量的名称相同。您还可以在将它们分配给本地变量时挑选特定的属性。这里有一个例子:

    let {timeout : t} =config 
    console.log(t) //900 

在这里,我们只从config对象中挑选timeout并将其赋值给一个本地变量t

您还可以使用解构语法将值分配给已声明的变量。在这种情况下,您必须在赋值周围加上括号:

    let config = { 
      server: 'localhost', 
      port: '8080', 
      timeout: 900, 
    } 
    let server = '127.0.0.1'; 
    let port = '80'; 
    ({server,port} = config) //assignment surrounded by () 
    console.log(server, port) //"localhost" "8080" 

由于解构表达式评估为表达式的右侧,因此可以在期望值的任何位置使用它。例如,在函数调用中,如下所示:

    let config = { 
      server: 'localhost', 
      port: '8080', 
      timeout: 900, 
    } 
    let server='127.0.0.1'; 
    let port ='80'; 
    let timeout ='100'; 

    function startServer(configValue){ 
      console.log(configValue) 
    } 
    startServer({server,port,timeout} = config) 

如果您指定一个在对象中不存在的属性名称的本地变量,那么本地变量将获得一个undefined值。然而,在解构赋值中使用变量时,您可以选择指定默认值:

    let config = { 
      server: 'localhost', 
      port: '8080' 
    } 
    let {server,port,timeout=0} = config 
    console.log(timeout) 

在这个例子中,对于不存在的属性timeout,我们提供了一个默认值,以防止将undefined值分配给本地变量。

解构也适用于数组,并且语法与对象的语法非常相似。我们只需要用array:literals替换对象字面量语法:

    const arr = ['a','b'] 
    const [x,y] = arr 
    console.log (x,y) /"a" "b" 

如您所见,这与我们之前看到的完全相同的语法。我们定义了一个数组arr,然后使用解构语法将该数组的元素分配给两个本地变量xy。在这里,赋值是基于数组中元素的顺序进行的。由于您只关心元素的位置,如果需要,可以跳过其中一些元素。这里有一个例子:

    const days = ['Thursday','Friday','Saturday','Sunday'] 
    const [,,sat,sun] = days 
    console.log (sat,sun) //"Saturday" "Sunday" 

在这里,我们知道我们需要位置 2 和 3 的元素(数组的索引从 0 开始),因此,我们忽略位置 0 和 1 的元素。数组解构可以在交换两个变量的值时消除对temp变量的使用。考虑以下内容:

    let a=1, b=2; 
    [b,a] = [a,b] 
    console.log(a,b) //2 1 

您可以使用剩余运算符(...)来提取剩余的元素并将它们分配给数组。剩余运算符只能在解构期间作为最后一个运算符使用:

    const [x, ...y] = ['a', 'b', 'c']; // x='a'; y=['b', 'c'] 

内置对象

在本章的前面,您遇到了Object()构造函数。当您使用对象文字表示法创建对象并访问它们的constructor属性时,它将返回。Object()是内置构造函数之一;还有其他一些,在本章的其余部分中您将看到它们全部。

内置对象可以分为三组:

  • 数据包装对象:这些是ObjectArrayFunctionBooleanNumberString。这些对象对应于 JavaScript 中的不同数据类型。对于typeof返回的每个不同值(在第二章中讨论),都有一个数据包装对象,除了undefinednull

  • 实用对象:这些是MathDateRegExp,可能会派上用场。

  • 错误对象:这些包括通用的Error对象,以及其他更具体的对象,可以帮助您的程序在发生意外情况时恢复其工作状态。

本章将讨论内置对象的少数方法。有关完整的参考信息,请参阅附录 C,内置对象

如果您对内置对象和内置构造函数感到困惑,那么它们是相同的。一会儿,您将看到函数,因此构造函数也是对象。

对象

对象是所有 JavaScript 对象的父对象,这意味着您创建的每个对象都继承自它。要创建一个新的空对象,可以使用文字表示法或Object()构造函数。以下两行是等效的:

    > var o = {}; 
    > var o = new Object(); 

如前所述,空(或空白)对象并不是完全无用的,因为它已经包含了几个继承的方法和属性。在本书中,空表示像{}这样的没有自己属性的对象,除了它自动获得的属性。让我们看看即使是空白对象已经具有的一些属性:

  • o.constructor属性返回对构造函数的引用

  • o.toString()是一个返回对象的字符串表示的方法

  • o.valueOf()返回对象的单个值表示;通常,这就是对象本身

让我们看看这些方法的实际应用。首先,创建一个对象:

    > var o = new Object(); 

调用toString()返回对象的字符串表示:

    > o.toString(); 
    "[object Object]" 

当 JavaScript 在字符串上下文中使用对象时,将在内部调用toString()方法。例如,alert()仅适用于字符串,因此如果调用alert()函数并传递一个对象,则toString()方法将在幕后调用。这两行产生相同的结果:

    > alert(o); 
    > alert(o.toString()); 

另一种字符串上下文是字符串连接。如果尝试将对象与字符串连接,将首先调用对象的toString()方法:

    > "An object: " + o; 
    "An object: [object Object]" 

valueOf()方法是所有对象提供的另一种方法。对于简单对象(其构造函数为Object()),valueOf()方法返回对象本身:

    > o.valueOf() === o; 
    true 

总结一下:

  • 您可以使用var o = {};(对象文字表示法,首选方法)或var o = new Object();来创建对象

  • 任何对象,无论多么复杂,都继承自Object对象,因此提供诸如toString()之类的方法和构造函数之类的属性

数组

Array()是一个内置函数,您可以将其用作构造函数来创建数组:

    > var a = new Array(); 

这相当于数组文字表示法:

    > var a = []; 

无论数组如何创建,都可以像通常一样向其添加元素:

    > a[0] = 1; 
    > a[1] = 2; 
    > a; 
    [1, 2] 

在使用 Array() 构造函数时,还可以传递要分配给新数组元素的值:

    > var a = new Array(1, 2, 3, 'four'); 
    > a; 
    [1, 2, 3, "four"] 

一个例外是当将单个数字传递给构造函数时。在这种情况下,该数字被视为数组的长度:

    > var a2 = new Array(5); 
    > a2; 
     [undefined x 5] 

由于数组是用构造函数创建的,这是否意味着数组实际上是对象?是的,您可以使用 typeof 运算符来验证这一点:

    > typeof [1, 2, 3]; 
    "object" 

由于数组是对象,这意味着它们继承了父对象的属性和方法:

    > var a = [1, 2, 3, 'four']; 
    > a.toString(); 
    "1,2,3,four" 
    > a.valueOf(); 
    [1, 2, 3, "four"] 
    > a.constructor; 
    function Array() { [native code] } 

数组是对象,但是一种特殊类型的对象,因为:

  • 它们的属性名称会自动使用从 0 开始的数字进行分配。

  • 它们有一个包含数组中元素数量的 length 属性。

  • 除了从父对象继承的方法之外,它们还有更多内置方法。

让我们来看看数组和对象之间的区别,首先创建空数组 a 和空对象 o

    > var a = [], o = {}; 

数组对象自动为它们定义了一个 length 属性,而普通对象没有:

    > a.length; 
    0 
    > typeof o.length; 
    "undefined" 

向数组和对象都可以添加数字和非数字属性是可以的:

    > a[0] = 1;  
    > o[0] = 1; 
    > a.prop = 2; 
    > o.prop = 2; 

length 属性始终与数字属性的数量保持同步,而忽略非数字属性:

    > a.length; 
    1 

length 属性也可以由您设置。将其设置为大于数组中当前项目数的值会为额外的元素腾出空间。如果尝试访问这些不存在的元素,将得到值 undefined

    > a.length = 5; 
    5 
    > a; 
    [1, undefined x 4] 

length 属性设置为较小的值会移除尾随元素:

    > a.length = 2; 
    2 
    > a; 
    [1, undefined x 1] 

一些数组方法

除了从父对象继承的方法之外,数组对象还具有专门用于处理数组的方法,例如 sort()join()slice() 等(有关完整列表,请参见 附录 C,“内置对象”)。

让我们拿一个数组来尝试一些这些方法:

    > var a = [3, 5, 1, 7, 'test']; 

push() 方法将一个新元素追加到数组的末尾。pop() 方法移除最后一个元素。a.push('new') 方法的作用类似于 a[a.length] = 'new',而 a.pop() 类似于 a.length-

push() 方法返回更改后数组的长度,而 pop() 返回移除的元素:

    > a.push('new'); 
    6 
    > a; 
    [3, 5, 1, 7, "test", "new"] 
    > a.pop(); 
    "new" 
    > a; 
    [3, 5, 1, 7, "test"] 

sort() 方法对数组进行排序并返回它。在下一个例子中,排序后,ab 都指向同一个数组:

    > var b = a.sort(); 
    > b; 
    [1, 3, 5, 7, "test"] 
    > a === b; 
    true 

join() 方法返回一个包含数组中所有元素值的字符串,这些值使用传递给 join() 的字符串参数粘合在一起:

    > a.join(' is not '); 
    "1 is not 3 is not 5 is not 7 is not test" 

slice() 方法返回一个数组的一部分,而不修改源数组。slice() 的第一个参数是起始索引(从零开始),第二个是结束索引(两个索引都是从零开始)。起始索引包括在内,而结束索引不包括在内。看下面的例子:

    > b = a.slice(1, 3); 
    [3, 5] 
    > b = a.slice(0, 1); 
    [1] 
    > b = a.slice(0, 2); 
    [1, 3] 

在所有切片之后,源数组仍然是相同的:

    > a; 
    [1, 3, 5, 7, "test"] 

splice() 方法修改源数组。它移除一个片段,返回它,并可选择用新元素填充空白。前两个参数定义要移除的片段的起始索引和长度(元素数量);其他参数传递新值:

    > b = a.splice(1, 2, 100, 101, 102); 
    [3, 5] 
    > a; 
    [1, 100, 101, 102, 7, "test"] 

用新元素填充空白是可选的,所以您可以跳过它:

    > a.splice(1, 3);  
    [100, 101, 102] 
    > a; 
    [1, 7, "test"] 

ES6 数组方法

数组获得了一堆有用的方法。像 lodashunderscore 这样的库提供了语言中缺少的功能。有了新的辅助方法,数组的创建和操作变得更加功能化和易于编码。

Array.from

在 JavaScript 中,将类似数组的值转换为数组一直是一个挑战。人们已经使用了几种技巧并编写了库,只是为了让您有效地处理数组。

ES6 引入了一个非常有用的方法,可以将类似数组的对象和可迭代值转换为数组。类似数组的值是具有长度属性和索引元素的对象。每个函数都有一个隐式的arguments变量,其中包含传递给函数的所有参数的列表。这个变量是一个类似数组的对象。在 ES6 之前,我们将arguments对象转换为数组的唯一方法是遍历它并将值复制到一个新数组中:

    function toArray(args) { 
        var result = []; 
        for (var i = 0, len = args.length; i < len; i++) { 
            result.push(args[i]); 
        } 
        return result; 
    } 
    function doSomething() { 
        var args = toArray(arguments); 
        console.log(args) 
    } 
    doSomething("hellow", "world") 
    //Array [ 
    //  "hellow", 
    //  "world" 
    //] 

在这里,我们正在创建一个新数组,以复制arguments对象的所有元素。这是浪费的,需要大量不必要的编码。Array.from()是将类似数组的对象转换为数组的简洁方式。我们可以使用Array.from()将这个例子转换为更简洁的一个:

    function doSomething() { 
        console.log(Array.from(arguments)) 
    } 
    doSomething("hellow", "world") 
    //Array [ 
    //  "hellow", 
    //  "world" 
    //] 

在调用Array.from()时,您可以通过提供映射函数来提供自己的映射方案。这个函数在对象的所有元素上被调用并进行转换。这是许多常见用例的一个有用构造,例如:

    function doSomething() { 
       console.log(Array.from(arguments, function(elem) 
      { return elem + " mapped"; })); 
    } 

在这个例子中,我们正在使用Array.from解构arguments对象,并对arguments对象中的每个元素调用一个函数。

使用 Array.of 创建数组

使用Array()构造函数创建数组会引起一些问题。构造函数的行为基于参数的数量和类型而有所不同。当您将单个数值传递给Array()构造函数时,将创建一个包含未定义元素的数组,其长度的值被分配给参数的值:

    let arr = new Array(2) 
    console.log(arr) //[undefined, undefined] 
    console.log(arr.length) //2 

另一方面,如果您只传递一个非数值值,它将成为数组中的唯一项:

    let arr = new Array("2") 
    console.log(arr) //["2"] 
    console.log(arr.length) //1 

这还不是全部。如果传递多个值,它们将成为数组的元素:

    let arr = new Array(1,"2",{obj: "3"}) 
    console.log(arr.length) //3 

因此,显然,需要有更好的方法来创建数组,以避免混淆。ES6 引入了Array.of方法,它的工作方式类似于Array()构造函数,但保证了一种标准行为。Array.of从其参数创建一个数组,而不管它们的数量和类型:

    let arr = Array.of(1,"2",{obj: "3"}) 
    console.log(arr.length) //3 

Array.prototype 方法

ES6 引入了几种有趣的方法作为数组实例的一部分。这些方法有助于数组迭代和搜索数组中的元素,这两种操作都是非常频繁和有用的。

以下是用于迭代数组的方法:

  • Array.prototype.entries()

  • Array.prototype.values()

  • Array.prorotype.keys()

所有三种方法都返回一个迭代器。这个迭代器可以用于使用Array.from()创建数组,并且可以在 for 循环中用于迭代:

    let arr = ['a','b','c'] 
    for (const index of arr.keys()){ 
      console.log(index) //0 1 2 
    } 
    for (const value of arr.values()){ 
      console.log(value) //a b c 
    } 
    for (const [index,value] of arr.entries()){ 
      console.log(index,value)  
    } 
    //0 "a" 
    //1 "b" 
    //2 "c" 

同样,有新的方法用于在数组中搜索。在数组中查找元素通常涉及迭代整个列表,并将它们与一个值进行比较,因为没有内置的方法来实现这一点。虽然indexOf()lastIndexOf()有助于找到单个值,但没有办法根据复杂条件找到元素。使用 ES6,以下内置方法帮助使用this关键字。

  • Array.prototype.find

  • Array.prototype.findIndex

这两种方法都接受两个参数-第一个是callback函数(其中包含谓词条件),第二个是可选的this关键字。callback接受三个参数:数组元素,该元素的索引和数组。如果元素与谓词匹配,则callback返回true

    let numbers = [1,2,3,4,5,6,7,8,9,10]; 
    console.log(numbers.find(n => n > 5)); //6 
    console.log(numbers.findIndex(n => n > 5)); //5 

函数

您已经知道函数是一种特殊的数据类型。然而,事实证明,函数不仅仅是如此:函数实际上是对象。有一个内置的constructor函数叫做Function(),它允许以一种替代的方式(但不一定推荐)创建函数。

以下示例显示了定义函数的三种方法:

    > function sum(a, b) { // function declaration 
        return a + b; 
      } 
    > sum(1, 2); 
    3 
    > var sum = function (a, b) { // function expression 
        return a + b; 
      }; 
    > sum(1, 2) 
    3 
    > var sum = new Function('a', 'b', 'return a + b;'); 
    > sum(1, 2) 
    3 

当使用Function()构造函数时,首先传递参数名称(作为字符串),然后传递函数主体的源代码(再次作为字符串)。JavaScript 引擎需要评估您传递的源代码并为您创建新的函数。这种源代码评估遭受与eval()函数相同的缺点,因此在可能的情况下应避免使用Function()构造函数定义函数。

如果您使用Function()构造函数创建具有许多参数的函数,请记住参数可以作为单个逗号分隔的列表传递;因此,例如,这些是相同的:

    > var first = new Function( 
        'a, b, c, d', 
        'return arguments;' 
      ); 
    > first(1, 2, 3, 4); 
           [1, 2, 3, 4] 
    > var second = new Function( 
        'a, b, c', 
        'd', 
        'return arguments;' 
       ); 
    > second(1, 2, 3, 4); 
           [1, 2, 3, 4] 
    > var third = new Function( 
        'a', 
        'b', 
        'c', 
        'd', 
        'return arguments;' 
      ); 
    > third(1, 2, 3, 4);  
          [1, 2, 3, 4] 

注意

不要使用Function()构造函数。与eval()setTimeout()(本书后面讨论)一样,始终尝试避免将 JavaScript 代码作为字符串传递。

函数对象的属性

与任何其他对象一样,函数都有一个constructor属性,其中包含对Function()构造函数的引用。无论您使用哪种语法创建函数,这都是正确的:

    > function myfunc(a) { 
        return a;  
      } 
    > myfunc.constructor; 
    function Function() { [native code] } 

函数还有一个length属性,其中包含函数期望的形式参数的数量:

    > function myfunc(a, b, c) { 
        return true; 
      } 
    > myfunc.length; 
       3 

使用 prototype 属性

函数对象最广泛使用的属性之一是prototype属性。您将在下一章中详细讨论这个属性,但现在,让我们先说一下:

  • function对象的prototype属性指向另一个对象

  • 只有当您将此function用作构造函数时,其优势才会显现

  • 使用此function创建的所有对象都保留对prototype属性的引用,并且可以将其属性用作自己的属性

让我们看一个快速的例子来演示prototype属性。拿一个简单的对象,它有一个属性名称和一个say()方法:

    var ninja = { 
      name: 'Ninja', 
      say: function () { 
        return 'I am a ' + this.name; 
      } 
    }; 

当您创建一个函数(即使没有主体),您可以验证它自动具有指向新对象的prototype属性:

    > function F() {} 
    > typeof F.prototype; 
    "object" 

当您修改prototype属性时,情况变得有趣。您可以向其中添加属性,或者您可以用任何其他对象替换默认对象。让我们将ninja分配给prototype

    > F.prototype = ninja; 

现在,这就是魔术发生的地方,使用F()函数作为constructor函数,您可以创建一个新对象baby_ninja,它将可以访问F.prototype的属性(指向ninja)就像它自己的属性一样:

    > var baby_ninja = new F(); 
    > baby_ninja.name; 
    "Ninja" 
    > baby_ninja.say(); 
    "I am a Ninja" 

以后会有更多关于这个主题的内容。事实上,下一章就是关于prototype属性的。

函数对象的方法

函数对象作为顶级父对象的后代,获得默认方法,例如toString()。当在函数上调用toString()方法时,它将返回函数的源代码:

    > function myfunc(a, b, c) { 
        return a + b + c; 
      } 
    > myfunc.toString(); 
    "function myfunc(a, b, c) { 
      return a + b + c; 
    }" 

如果您尝试窥探内置函数的源代码,您将得到[native code]字符串,而不是函数的主体:

    > parseInt.toString(); 
    "function parseInt() { [native code] }" 

正如您所看到的,您可以使用toString()来区分原生方法和开发者定义的方法。

注意

函数的toString()的行为取决于环境,并且在浏览器之间在间距和换行方面有所不同。

调用和应用

函数对象具有call()apply()方法。您可以使用它们来调用函数并传递任何参数给它。

这些方法还允许您的对象从其他对象中借用方法并将其作为自己的方法调用。这是一种重用代码的简单而强大的方式。

假设您有一个some_obj对象,其中包含say()方法:

    var some_obj = { 
      name: 'Ninja', 
      say: function (who) { 
        return 'Haya ' + who + ', I am a ' + this.name; 
      } 
   }; 

您可以调用say()方法,它在内部使用this.name来访问自己的名称属性:

    > some_obj.say('Dude'); 
    "Haya Dude, I am a Ninja" 

现在,让我们创建一个简单的对象my_obj,它只有一个名称属性:

    > var my_obj = {name: 'Scripting guru'}; 

my_obj非常喜欢some_obj对象的say()方法,以至于它想将其作为自己的方法调用。这可以使用say()函数对象的call()方法实现:

    > some_obj.say.call(my_obj, 'Dude'); 
    "Haya Dude, I am a Scripting guru" 

成功了!但这里发生了什么?您通过传递两个参数-my_obj对象和Dude字符串来调用say()函数对象的call()方法。结果是,当调用say()时,它包含的 this 值的引用指向my_obj。这样,this.name不会返回Ninja,而是返回Scripting guru

如果在调用call()方法时有更多参数要传递,只需继续添加它们:

    some_obj.someMethod.call(my_obj, 'a', 'b', 'c'); 

如果您没有将对象作为call()的第一个参数传递,或者传递null,则假定为全局对象。

apply()方法的工作方式与call()相同,但不同之处在于要传递给其他对象方法的所有参数都作为数组传递。以下两行是等效的:

    some_obj.someMethod.apply(my_obj, ['a', 'b', 'c']); 
    some_obj.someMethod.call(my_obj, 'a', 'b', 'c'); 

继续上一个示例,您可以使用以下代码行:

    > some_obj.say.apply(my_obj, ['Dude']); 
    "Haya Dude, I am a Scripting guru" 

重新访问 arguments 对象

在上一章中,您已经看到了如何从函数内部访问称为arguments的东西,其中包含传递给函数的所有参数的值:

    > function f() { 
        return arguments; 
      } 
    > f(1, 2, 3); 
    [1, 2, 3] 

arguments看起来像一个数组,但实际上它是一个类似数组的对象。它看起来像一个数组,因为它包含索引元素和length属性。然而,相似之处就在这里,因为 arguments 不提供任何数组方法,比如sort()slice()

但是,您可以将arguments转换为数组,并从所有数组好处中受益。练习您新学到的call()方法,您可以这样做:

    > function f() { 
        var args = [].slice.call(arguments); 
        return args.reverse(); 
      } 

    > f(1, 2, 3, 4); 
     [4, 3, 2, 1] 

正如您所看到的,您可以使用[].slice或更冗长的Array.prototype.slice来借用slice()

箭头函数中的词法 this

我们在上一章中详细讨论了 ES6 箭头函数和语法。然而,箭头函数的一个重要方面是它们的行为与普通函数不同。差异是微妙但重要的。箭头函数没有自己的this值。箭头函数中的this值是从封闭(词法)范围继承的。

函数有一个特殊的变量this,它指的是调用该方法的对象。由于this的值是根据函数调用动态给出的,有时被称为动态this。函数在两个范围中执行-词法和动态。词法范围是包围函数范围的范围,动态范围是调用函数的范围(通常是一个对象)。

在 JavaScript 中,传统函数扮演着几种角色。它们是非方法函数(也称为子例程或函数)、方法(对象的一部分)和构造函数。当函数执行子例程的职责时,由于动态this,存在一个小问题。由于子例程不是在对象上调用的,因此在严格模式下this的值为未定义,否则设置为全局范围。这使得编写回调变得困难。考虑以下示例:

    var greeter = { 
      default: "Hello ", 
      greet: function (names){ 
        names.forEach(function(name) { 
    console.log(this.default + name); //Cannot read property 
      'default' of undefined 
       }) 
      } 
    }     
    console.log(greeter.greet(['world', 'heaven'])) 

我们正在将一个子例程传递给names数组上的forEach()函数。这个子例程的this值是未定义的,不幸的是,它无法访问外部方法greetthis。显然,这个子例程需要一个词法this,从greet方法的周围范围派生this。传统上,为了解决这个限制,我们将词法this分配给一个变量,然后通过闭包使子例程可以访问它。

我们可以按照以下方式修复之前的示例:

    var greeter = { 
      default: "Hello ", 
      greet: function (names){ 
        let that = this 
        names.forEach(function(name) { 
          console.log(that.default + name);  
       }) 
      } 
    }     
    console.log(greeter.greet(['world', 'heaven'])) 

这是一个合理的黑客来模拟词法this。然而,这种黑客的问题是它为编写或审查this代码的人创建了太多噪音。首先,您必须了解this行为的怪癖。即使您很好地理解了this的行为,您也需要不断地留意代码中的这种黑客。

箭头函数具有词法this,不需要这样的黑客。由于this,它们更适合作为子例程。我们可以使用箭头函数将前面的示例转换为使用词法this

    var greeter = { 
      default: "Hello ", 
      greet: function (names){ 
        names.forEach(name=> { 
          console.log(this.default + name);   //lexical 'this' 
           available for this subroutine 
       }) 
     } 
    }     
    console.log(greeter.greet(['world', 'heaven'])) 

推断对象类型

您可以看到,您有一个类似数组的参数对象,看起来非常像一个数组对象。您如何可靠地区分这两者?此外,当与数组一起使用时,typeof返回一个对象。因此,您如何区分对象和数组之间的区别?

银弹是Object对象的toString()方法。它为您提供了用于创建给定对象的内部类名称:

    > Object.prototype.toString.call({}); 
    "[object Object]" 
    > Object.prototype.toString.call([]); 
    "[object Array]" 

您必须调用Object构造函数原型中定义的原始toString()方法。否则,如果调用Array函数的toString(),它将给出不同的结果,因为它已被重写,用于数组对象的特定目的:

    > [1, 2, 3].toString(); 
    "1,2,3" 

前面的代码与以下代码相同:

    > Array.prototype.toString.call([1, 2, 3]); 
    "1,2,3" 

让我们再来玩一下toString()。创建一个方便的参考以节省输入:

    > var toStr = Object.prototype.toString; 

以下示例显示了如何区分数组和类似数组对象arguments

    > (function () { 
        return toStr.call(arguments); 
      }()); 
    "[object Arguments]" 

甚至可以检查 DOM 元素:

    > toStr.call(document.body); 
    "[object HTMLBodyElement]" 

布尔

您在 JavaScript 中内置对象的旅程继续进行,接下来的三个对象都相当简单。它们是布尔值、数字和字符串。它们只是包装了原始数据类型。

您已经从第二章原始数据类型、数组、循环和条件中了解了很多关于布尔值。现在,让我们来认识Boolean()构造函数:

    > var b = new Boolean(); 

重要的是要注意,这将创建一个新对象b,而不是原始的布尔值。要获取原始值,可以调用valueOf()方法(从Object类和自定义继承):

    > var b = new Boolean(); 
    > typeof b; 
    "object" 
    > typeof b.valueOf(); 
    "boolean" 
    > b.valueOf(); 
    false 

总的来说,使用Boolean()构造函数创建的对象并不太有用,因为它们除了继承的方法或属性之外没有提供任何其他方法。

Boolean()函数在没有new的情况下作为普通函数调用时,将非布尔值转换为布尔值(这类似于使用双重否定!!值):

    > Boolean("test"); 
    true 
    > Boolean(""); 
    false 
    > Boolean({}); 
    true 

除了六个false值,JavaScript 中的其他所有内容都是true,包括所有对象。这也意味着使用new Boolean()创建的所有布尔对象也都是true,因为它们是对象:

    > Boolean(new Boolean(false)); 
    true 

这可能会令人困惑,由于布尔对象没有提供任何特殊方法,最好只使用常规原始布尔值。

数字

Boolean()类似,Number()函数可以用作:

  • 一个constructor函数(使用new)来创建对象。

  • 一个普通函数,用于尝试将任何值转换为数字。这类似于使用parseInt()parseFloat()

    > var n = Number('12.12'); 
    > n; 
    12.12 
    > typeof n; 
    "number" 
    > var n = new Number('12.12'); 
    > typeof n; 
    "object" 

由于函数也是对象,它们也可以有属性。Number()函数具有内置的常量属性,您无法修改:

    > Number.MAX_VALUE; 
    1.7976931348623157e+308 
    > Number.MIN_VALUE; 
    5e-324 
    > Number.POSITIVE_INFINITY; 
    Infinity 
    > Number.NEGATIVE_INFINITY; 
    -Infinity 
    > Number.NaN; 
    NaN 

数字对象提供了三种方法-toFixed()toPrecision()toExponential()(有关更多详细信息,请参见附录 C,内置对象):

    > var n = new Number(123.456); 
    > n.toFixed(1); 
    "123.5" 

请注意,您可以在不显式创建Number对象的情况下使用这些方法。在这种情况下,Number对象会在幕后为您创建(并销毁):

    > (12345).toExponential(); 
    "1.2345e+4" 

与所有对象一样,Number对象也提供了toString()方法。当与Number对象一起使用时,此方法接受一个可选的基数参数(默认为 10):

    > var n = new Number(255); 
    > n.toString(); 
    "255" 
    > n.toString(10); 
    "255" 
    > n.toString(16); 
    "ff" 
    > (3).toString(2); 
    "11" 
    > (3).toString(10); 
    "3" 

字符串

您可以使用String()构造函数来创建字符串对象。字符串对象提供了方便的文本操作方法。

以下是一个示例,显示了String对象和primitive字符串数据类型之间的区别:

    > var primitive = 'Hello'; 
    > typeof primitive; 
    "string" 
    > var obj = new String('world'); 
    > typeof obj; 
    "object" 

String对象类似于字符数组。字符串对象为每个字符都有一个索引属性(在 ES5 中引入,但在许多浏览器中长期受支持,除了旧的 IE),它们还有一个length属性。

    > obj[0]; 
    "w" 
    > obj[4]; 
    "d" 
    > obj.length; 
    5 

要从String对象中提取primitive值,可以使用从Object继承的valueOf()toString()方法。您可能永远不需要这样做,因为如果在primitive字符串上下文中使用对象,则会在幕后调用toString()

    > obj.valueOf(); 
    "world" 
    > obj.toString(); 
    "world" 
    > obj + ""; 
    "world" 

primitive 字符串不是对象,因此它们没有任何方法或属性。但是,JavaScript 还为您提供了将 primitive 字符串视为对象的语法(就像您已经看到的原始数字一样)。

在下面的示例中,每当您将 primitive 字符串视为对象时,都会在后台创建(然后销毁)String 对象:

    > "potato".length; 
    6 
    > "tomato"[0]; 
    "t" 
    > "potatoes"["potatoes".length - 1]; 
    "s" 

以下是一个最终示例,用于说明 primitive 字符串和 String 对象之间的区别。在此示例中,我们将它们转换为布尔值。空字符串是一个假值,但任何字符串对象都是真值(因为所有对象都是真值):

    > Boolean(""); 
    false 
    > Boolean(new String("")); 
    true 

Number()Boolean() 类似,如果您在没有 new 的情况下使用 String() 函数,它会将参数转换为原始值:

    > String(1); 
    "1" 

如果您将对象传递给 String(),则首先将调用该对象的 toString() 方法:

    > String({p: 1}); 
       "[object Object]" 
    > String([1, 2, 3]); 
       "1,2,3" 
    > String([1, 2, 3]) === [1, 2, 3].toString(); 
       true 

字符串对象的一些方法

让我们尝试一下您可以在字符串对象上调用的一些方法(请参见附录 C,“内置对象”,获取完整列表)。

首先创建一个字符串对象:

    > var s = new String("Couch potato"); 

toUpperCase()toLowerCase() 方法可以转换字符串的大小写:

    > s.toUpperCase(); 
    "COUCH POTATO" 
    > s.toLowerCase(); 
    "couch potato" 

charAt() 方法告诉您在指定位置找到的字符,这与使用方括号(将字符串视为字符数组)相同:

    > s.charAt(0); 
    "C" 
    > s[0]; 
    "C" 

如果您向 charAt() 传递一个不存在的位置,您将得到一个空字符串:

    > s.charAt(101); 
    "" 

indexOf() 方法允许您在字符串中进行搜索。如果找到匹配项,该方法将返回找到第一个匹配项的位置。位置计数从 0 开始,因此 Couch 中的第二个字符是位置 1 处的 o

    > s.indexOf('o'); 
    1 

您可以选择指定从哪里(在什么位置)开始搜索。以下示例找到第二个 o,因为 indexOf() 被指示从位置 2 开始搜索:

    > s.indexOf('o', 2); 
    7 

lastIndexOf() 从字符串的末尾开始搜索(但是匹配的位置仍然从开头计数):

    > s.lastIndexOf('o'); 
    11 

您可以搜索字符,也可以搜索字符串,搜索区分大小写:

    > s.indexOf('Couch'); 
    0 

如果没有匹配项,该函数将返回位置 -1

    > s.indexOf('couch'); 
    -1 

对于不区分大小写的搜索,您可以先将字符串转换为小写,然后再搜索:

    > s.toLowerCase().indexOf('couch'.toLowerCase()); 
    0 

如果得到 0,这意味着字符串的匹配部分从位置 0 开始。这可能会在使用 if 时引起混淆,因为 if 将位置 0 转换为布尔值 false。因此,尽管这在语法上是正确的,但在逻辑上是错误的:

    if (s.indexOf('Couch')) {...} 

检查字符串是否包含另一个字符串的正确方法是将 indexOf() 的结果与数字 -1 进行比较:

    if (s.indexOf('Couch') !== -1) {...} 

slice()substring() 在指定开始和结束位置时返回字符串的一部分:

    > s.slice(1, 5); 
    "ouch" 
    > s.substring(1, 5); 
    "ouch" 

请注意,您传递的第二个参数是结束位置,而不是片段的长度。这两种方法之间的区别在于它们如何处理负参数。substring() 将它们视为零,而 slice() 将它们添加到字符串的长度。因此,如果您将参数 (1, -1) 传递给这两种方法,它等同于 substring(1,0)slice(1,s.length-1)

    > s.slice(1, -1); 
    "ouch potat" 
    > s.substring(1, -1); 
    "C" 

还有一个非标准的 substr() 方法,但您应该尽量避免使用它,而使用 substring()

split() 方法使用您传递的另一个字符串作为分隔符从字符串创建一个数组:

    > s.split(" "); 
    ["Couch", "potato"] 

split() 方法是 join() 方法的相反,它从数组创建一个字符串:

    > s.split(' ').join(' '); 
    "Couch potato" 

concat() 将字符串粘合在一起,就像 + 运算符对 primitive 字符串一样:

    > s.concat("es"); 
    "Couch potatoes" 

请注意,虽然前面讨论的一些方法返回新的 primitive 字符串,但它们都不会修改源字符串。在之前列出的所有方法调用之后,初始字符串仍然是相同的:

    > s.valueOf(); 
    "Couch potato" 

你已经知道如何使用indexOf()lastIndexOf()在字符串中进行搜索,但还有更强大的方法(search()match()replace())可以将正则表达式作为参数。稍后你会看到这些方法在RegExp()构造函数中。

在这一点上,你已经完成了所有的数据包装对象,所以让我们继续讨论实用对象MathDateRegExp

Math

Math与你之前看到的其他内置全局对象有些不同。它不是一个函数,因此不能与new一起用来创建对象。Math是一个内置的全局对象,为数学运算提供了许多方法和属性。

Math对象的属性是常数,所以你不能改变它们的值。它们的名称都是大写的,以强调它们与普通属性的区别(类似于Number()构造函数的常量属性)。让我们看一些这些常量属性:

  • 常数 PI:
    > Math.PI; 
      3.141592653589793 

  • 2 的平方根:
    > Math.SQRT2; 
      1.4142135623730951 

  • 欧拉常数:
    > Math.E; 
      2.718281828459045 

  • 2 的自然对数:
    > Math.LN2; 
      0.6931471805599453 

  • 10 的自然对数:
    > Math.LN10; 
      2.302585092994046 

现在,你知道了如何在下一次朋友们(不管出于什么原因)开始想知道“e的值是多少?我记不清了。”时给他们留下深刻印象。只需在控制台中输入Math.E,你就有了答案。

让我们来看一下Math对象提供的一些方法(完整列表在附录 C 中,“内置对象”)。

生成随机数:

    > Math.random(); 
    0.3649461670235814 

random()函数返回一个在01之间的数字,所以如果你想要一个在0100之间的数字,你可以使用以下代码:

    > 100 * Math.random(); 

对于任意两个值之间的数字,使用公式((max-min) * Math.random())+min。例如,可以使用以下公式获得 2 到 10 之间的随机数:

    > 8 * Math.random() + 2; 
    9.175650496668485 

如果你只需要一个整数,你可以使用以下其中一种取整方法:

  • floor()向下取整

  • ceil()向上取整

  • round()四舍五入

例如,要获得01,你可以使用以下代码:

    > Math.round(Math.random()); 

如果你需要一组数字中的最低或最高值,你可以使用min()max()方法。所以,如果你在页面上有一个要求有效月份的表单,你可以确保你总是使用合理的数据(一个值在112之间):

    > Math.min(Math.max(1, input), 12); 

Math对象还提供了执行数学运算的能力,而这些运算没有专门的运算符。这意味着你可以使用pow()进行乘方运算,使用sqrt()找到平方根,并执行所有的三角函数运算-sin()cos()atan()等等。

例如,要计算28次方,你可以使用以下代码:

    > Math.pow(2, 8); 
    256 

要计算9的平方根,你可以使用以下代码:

    > Math.sqrt(9); 
    3 

日期

Date()是一个创建日期对象的构造函数。您可以通过传递来创建一个新对象:

  • 无(默认为今天的日期)

  • 类似日期的字符串

  • 分别为天、月、时间等提供值

  • 时间戳

这是一个使用今天的日期/时间实例化的对象(使用浏览器的时区):

    > new Date(); 
    Wed Feb 27 2013 23:49:28 GMT-0800 (PST) 

控制台显示了在Date对象上调用toString()方法的结果,所以你会得到这个长字符串Wed Feb 27 2013 23:49:28 GMT-0800 (PST)作为日期对象的表示。

以下是使用字符串初始化Date对象的一些示例。请注意你可以使用多种不同的格式来指定日期:

    > new Date('2015 11 12'); 
    Thu Nov 12 2015 00:00:00 GMT-0800 (PST) 
    > new Date('1 1 2016'); 
    Fri Jan 01 2016 00:00:00 GMT-0800 (PST) 
    > new Date('1 mar 2016 5:30'); 
    Tue Mar 01 2016 05:30:00 GMT-0800 (PST) 

Date构造函数可以从不同的字符串中找出一个日期,但这并不是一个定义精确日期的可靠方法,例如,当将用户输入传递给构造函数时。更好的方法是向Date()构造函数传递表示:

  • 月 - 0(一月)到 11(十二月)

  • 日 - 1 到 31

  • 小时 - 0 到 23

  • 分钟 - 0 到 59

  • 秒 - 0 到 59

  • 毫秒 - 0 到 999

让我们看一些例子。

通过编写以下代码来传递所有参数:

    > new Date(2015, 0, 1, 17, 05, 03, 120); 
    Tue Jan 01 2015 17:05:03 GMT-0800 (PST) 

通过编写以下代码来传递日期和小时:

    > new Date(2015, 0, 1, 17); 
    Tue Jan 01 2015 17:00:00 GMT-0800 (PST) 

要注意月份是从 0 开始的,所以 1 代表二月:

    > new Date(2016, 1, 28); 
    Sun Feb 28 2016 00:00:00 GMT-0800 (PST) 

如果传递的值大于允许的值,您的日期将向前溢出。由于 2016 年没有 2 月 30 日,这意味着它必须是 3 月 1 日(2016 年是闰年):

    > new Date(2016, 1, 29); 
    Mon Feb 29 2016 00:00:00 GMT-0800 (PST) 
    > new Date(2016, 1, 30); 
    Tue Mar 01 2016 00:00:00 GMT-0800 (PST) 

同样,12 月 32 日变成了下一年的 1 月 1 日:

    > new Date(2012, 11, 31); 
    Mon Dec 31 2012 00:00:00 GMT-0800 (PST) 
    > new Date(2012, 11, 32); 
    Tue Jan 01 2013 00:00:00 GMT-0800 (PST) 

最后,日期对象可以使用时间戳(自 UNIX 纪元以来的毫秒数,其中 0 毫秒是 1970 年 1 月 1 日)进行初始化:

    > new Date(1357027200000); 
    Tue Jan 01 2013 00:00:00 GMT-0800 (PST) 

如果调用Date()而不使用new,则会得到一个表示当前日期的字符串,无论是否传递任何参数。以下示例给出了当前时间(在运行此示例时的当前时间):

    > Date(); 
    Wed Feb 27 2013 23:51:46 GMT-0800 (PST) 
    > Date(1, 2, 3, "it doesn't matter"); 
    Wed Feb 27 2013 23:51:52 GMT-0800 (PST) 
    > typeof Date(); 
    "string" 
    > typeof new Date(); 
    "object" 

用于处理日期对象的方法

创建日期对象后,您可以在该对象上调用许多方法。大多数方法可以分为set*()get*()方法,例如getMonth()setMonth()getHours()setHours()等。让我们看一些例子。

通过编写以下代码创建日期对象:

    > var d = new Date(2015, 1, 1); 
    > d.toString(); 
    Sun Feb 01 2015 00:00:00 GMT-0800 (PST) 

将月份设置为三月(月份从 0 开始):

    > d.setMonth(2); 
    1425196800000 
    > d.toString(); 
    Sun Mar 01 2015 00:00:00 GMT-0800 (PST) 

通过编写以下代码来获取月份:

    > d.getMonth(); 
    2 

除了日期对象的所有方法外,Date()函数/对象还有两种方法(ES5 中添加了一种方法),它们是Date()函数/对象的属性。这些不需要date对象;它们的工作方式就像Math对象的方法一样。在基于类的语言中,这样的方法被称为静态方法,因为它们不需要实例。

Date.parse()方法接受一个字符串并返回一个时间戳:

    > Date.parse('Jan 11, 2018'); 
    1515657600000 

Date.UTC()方法接受年、月、日等所有参数,并在世界标准时间UT)中生成一个时间戳:

    > Date.UTC(2018, 0, 11); 
    1515628800000 

由于new Date()构造函数可以接受时间戳,因此可以将Date.UTC()的结果传递给它。使用以下示例,您可以看到UTC()如何与世界标准时间一起工作,而new Date()与本地时间一起工作:

    > new Date(Date.UTC(2018, 0, 11)); 
    Wed Jan 10 2018 16:00:00 GMT-0800 (PST) 
    > new Date(2018, 0, 11); 
    Thu Jan 11 2018 00:00:00 GMT-0800 (PST) 

Date构造函数的 ES5 新增方法是now(),它返回当前时间戳。它提供了一个更方便的方法来获取时间戳,而不是像在 ES3 中那样在Date对象上使用getTime()方法:

    > Date.now(); 
    1362038353044 
    > Date.now() === new Date().getTime(); 
    true 

你可以将日期的内部表示想象成一个整数时间戳,而所有其他方法都是在其上的糖。因此,valueOf()是一个时间戳是有意义的:

    > new Date().valueOf(); 
    1362418306432 

此外,日期可以通过+运算符转换为整数:

    > +new Date(); 
    1362418318311 

计算生日

让我们看一个最后的Date对象的工作示例。我很好奇我的生日在 2016 年是星期几:

    > var d = new Date(2016, 5, 20); 
    > d.getDay(); 
    1 

从 0 开始计数(星期日),1 代表星期一。是这样吗?

    > d.toDateString(); 
    "Mon Jun 20 2016" 

好的,知道了,但是星期一不一定是举办派对的最佳日子。那么,我们来看看一个循环,展示从 2016 年到 3016 年 6 月 20 日是星期五的次数,或者更好的是,让我们看看一周中所有日期的分布。毕竟,随着 DNA 技术的进步,我们都将在 3016 年活得好好的。

首先,让我们用七个元素初始化一个数组,每个元素代表一周的一天。这些将被用作计数器。然后,当循环到 3016 年时,让我们递增计数器:

    var stats = [0, 0, 0, 0, 0, 0, 0]; 

以下是循环:

    for (var i = 2016; i < 3016; i++) { 
       stats[new Date(i, 5, 20).getDay()]++; 
    } 

这是结果:

    > stats; 
    [140, 146, 140, 145, 142, 142, 145] 

142 个星期五和 145 个星期六。哇哦!

RegExp

正则表达式提供了一种强大的搜索和操作文本的方法。不同的语言有不同的正则表达式语法实现(考虑方言)。JavaScript 使用 Perl 5 语法。

人们通常将正则表达式缩写为 regex 或 regexp。

正则表达式由以下部分组成:

  • 用于匹配文本的模式

  • 零个或多个修饰符(也称为标志),提供有关应如何使用模式的更多指令

模式可以是简单的文字文本,以便逐字匹配,但这种情况很少见,而且在这种情况下,最好使用indexOf()。大多数情况下,模式更复杂,可能难以理解。掌握正则表达式的模式是一个庞大的主题,在这里不会详细讨论。相反,你将看到 JavaScript 在语法、对象和方法方面提供了什么来支持正则表达式的使用。你也可以参考附录 D,正则表达式,在你编写模式时帮助你。

JavaScript 提供了RegExp()构造函数,允许你创建正则表达式对象:

    > var re = new RegExp("j.*t");  

还有更方便的正则表达式字面量表示法

    > var re = /j.*t/; 

在上面的例子中,j.*t是正则表达式模式。它表示"匹配任何以j开头,以t结尾,并且中间有零个或多个字符的字符串"。星号(*)表示"前面的零个或多个,"点(.)表示"任何字符"。传递给RegExp()构造函数时,模式需要用引号括起来。

RegExp 对象的属性

正则表达式对象有以下属性:

  • global:如果这个属性是false,也就是默认值,那么当找到第一个匹配时搜索就会停止。如果你想要所有的匹配,就把它设置为true

  • ignoreCase:当匹配不区分大小写时,这个属性默认为false(意味着默认是区分大小写的匹配)。

  • multiline:搜索可能跨越多行的匹配,默认为false

  • lastIndex:开始搜索的位置;默认为0

  • source:这包含了RegExp模式。

除了lastIndex之外,这些属性都不能在对象创建后被改变。

在上面的列表中,前三个项目代表了正则表达式修饰符。如果你使用构造函数创建一个正则表达式对象,你可以将以下任意组合的字符作为第二个参数传递:

  • g代表global

  • i代表ignoreCase

  • m代表multiline

这些字母可以以任何顺序出现。如果传递了一个字母,相应的修饰符属性就会被设置为true。在下面的例子中,所有修饰符都被设置为true

    > var re = new RegExp('j.*t', 'gmi'); 

让我们验证一下:

    > re.global; 
    true 

一旦设置,修饰符就不能被改变:

    > re.global = false; 
    > re.global; 
    true 

要使用正则表达式字面量设置任何修饰符,你需要在闭合斜杠后添加它们:

    > var re = /j.*t/ig; 
    > re.global; 
    true 

RegExp 对象的方法

正则表达式对象提供了两种方法来查找匹配-test()exec()。它们都接受一个字符串参数。test()方法返回一个布尔值(当有匹配时为true,否则为false),而exec()返回一个匹配字符串的数组。显然,exec()做了更多的工作,所以只有在你真的需要对匹配做一些操作时才使用test()。人们经常使用正则表达式来验证数据。在这种情况下,test()应该足够了。

在下面的例子中,由于大写的J,没有匹配:

    > /j.*t/.test("Javascript"); 
    false 

一个不区分大小写的测试会得到一个积极的结果:

    > /j.*t/i.test("Javascript"); 
    true 

使用exec()进行相同的测试会返回一个数组,你可以像下面这样访问第一个元素:

    > /j.*t/i.exec("Javascript")[0]; 
    "Javascript" 

接受正则表达式作为参数的字符串方法

在本章的前面,你学习了字符串对象以及如何使用indexOf()lastIndexOf()方法在文本中进行搜索。使用这些方法,你只能指定文字字符串模式进行搜索。更强大的解决方案是使用正则表达式来查找文本。字符串对象为你提供了这种能力。

字符串对象提供了以下接受正则表达式对象作为参数的方法:

  • match():返回一个匹配的数组

  • search():返回第一个匹配的位置

  • replace():允许你用另一个字符串替换匹配的文本

  • split():在将字符串分割成数组元素时接受一个正则表达式

search()和 match()

让我们看一些使用search()match()方法的例子。首先,你创建一个字符串对象:

    > var s = new String('HelloJavaScriptWorld'); 

使用match(),你得到一个只包含第一个匹配的数组:

    > s.match(/a/); 
    ["a"] 

使用g修饰符,你执行全局搜索,所以结果数组包含两个元素:

    > s.match(/a/g); 
    ["a", "a"] 

不区分大小写的匹配如下:

    > s.match(/j.*a/i); 
    ["Java"] 

search()方法给出了匹配字符串的位置:

    > s.search(/j.*a/i); 
    5 

replace()

replace()方法允许你用其他字符串替换匹配的文本。以下示例删除所有大写字母(用空字符串替换它们):

    > s.replace(/[A-Z]/g, ''); 
    "elloavacriptorld" 

如果你省略g修饰符,你只会替换第一个匹配:

    > s.replace(/[A-Z]/, ''); 
    "elloJavaScriptWorld" 

当找到匹配时,如果你想在替换字符串中包含匹配的文本,你可以使用$&来访问它。下面是如何在保留匹配的同时在匹配前添加下划线:

    > s.replace(/[A-Z]/g, "_$&"); 
    "_Hello_Java_Script_World" 

当正则表达式包含组(用括号表示),每个组的匹配都可以作为$1表示第一个组,$2表示第二个组,依此类推:

    > s.replace(/([A-Z])/g, "_$1"); 
    "_Hello_Java_Script_World" 

想象一下,你的网页上有一个注册表单,要求输入电子邮件地址、用户名和密码。用户输入他们的电子邮件 ID,然后,你的 JavaScript 开始并建议用户名,从电子邮件地址中获取:

    > var email = "stoyan@phpied.com"; 
    > var username = email.replace(/(.*)@.*/, "$1"); 
    > username; 
    "stoyan" 

替换回调

在指定替换时,你也可以传递一个返回字符串的函数。这使你能够在指定替换之前实现任何特殊逻辑:

    > function replaceCallback(match) { 
       return "_" + match.toLowerCase(); 
      } 

    > s.replace(/[A-Z]/g, replaceCallback); 
    "_hello_java_script_world" 

回调函数接收多个参数(前面的例子忽略了除第一个参数之外的所有参数):

  • 第一个参数是match

  • 最后是被搜索的字符串

  • 倒数第二个是match的位置

  • 其余的参数包含你的正则表达式模式中任何组匹配的任何字符串

让我们测试一下。首先,让我们创建一个变量来存储传递给回调函数的整个参数数组:

    > var glob; 

接下来,定义一个具有三个组并匹配格式为something@something.something的电子邮件地址的正则表达式:

    > var re = /(.*)@(.*)\.(.*)/; 

最后,让我们定义一个回调函数,将参数存储在glob中,然后返回替换:

    var callback = function () { 
      glob = arguments; 
      return arguments[1] + ' at ' + 
        arguments[2] + ' dot ' +  arguments[3]; 
    }; 

现在,执行一个测试:

    > "stoyan@phpied.com".replace(re, callback); 
    "stoyan at phpied dot com" 

这是回调函数接收到的参数:

    > glob; 
    ["stoyan@phpied.com", "stoyan", "phpied", "com", 0,  
    "stoyan@phpied.com"] 

split()

你已经知道split()方法,它可以从输入字符串和分隔符字符串创建一个数组。让我们取一个逗号分隔的值的字符串并将其拆分:

    > var csv = 'one, two,three ,four'; 
    > csv.split(','); 
    ["one", " two", "three ", "four"] 

因为输入字符串恰好在逗号之前和之后有随机不一致的空格,所以数组结果也有空格。使用正则表达式,你可以使用\s*来修复这个问题,它表示零个或多个空格:

    > csv.split(/\s*,\s*/); 
    ["one", "two", "three", "four"] 

当期望一个正则表达式时传递一个字符串

最后要注意的一点是,你刚刚看到的这四种方法(split()match()search()replace())也可以接受字符串而不是正则表达式。在这种情况下,字符串参数被用来产生一个新的正则表达式,就好像它被传递给new RegExp()一样。

将字符串传递给replace的示例如下所示:

    > "test".replace('t', 'r'); 
    "rest" 

前面的代码行与下面的代码行相同:

    > "test".replace(new RegExp('t'), 'r'); 
    "rest" 

当你传递一个字符串时,你不能像使用普通构造函数或正则表达式字面量那样设置修饰符。当使用字符串而不是正则表达式对象进行字符串替换时,存在一个常见的错误来源,这是因为g修饰符默认为false。结果是只有第一个字符串被替换,这与大多数其他语言不一致,有点令人困惑。这里有一个例子:

    > "pool".replace('o', '*'); 
    "p*ol" 

很可能,你想要替换所有出现的:

    > "pool".replace(/o/g, '*'); 
    "p**l" 

错误对象

错误会发生,有必要有机制来让你的代码意识到发生了错误,并以一种优雅的方式从中恢复。JavaScript 提供了trycatchfinally语句来帮助你处理错误。如果发生错误,将抛出一个错误对象。错误对象是使用这些内置构造函数之一创建的-EvalErrorRangeErrorReferenceErrorSyntaxErrorTypeErrorURIError。所有这些构造函数都继承自Error

让我们只是引发一个错误,看看会发生什么。引发错误的简单方法是什么?只需调用一个不存在的函数。在控制台中键入以下内容:

    > iDontExist(); 

您将得到类似以下的内容:

错误对象

错误的显示在不同的浏览器和其他主机环境中可能会有很大的差异。事实上,大多数最近的浏览器倾向于隐藏错误。但是,您不能假设所有用户都已禁用错误显示,并且您有责任确保他们的体验是无错误的。之前的错误传播给用户,因为代码没有尝试捕获(catch)此错误。代码没有预期错误,也没有准备好处理它。幸运的是,捕获错误是微不足道的。您只需要try语句,后跟catch语句。

这段代码会隐藏错误:

    try { 
      iDontExist(); 
    } catch (e) { 
      // do nothing 
     } 

在这里你有:

  • try语句后跟一块代码。

  • catch语句后跟括号中的变量名,然后是另一个代码块。

可以有一个可选的finally语句(在此示例中未使用),后跟一块代码,无论是否出现错误都会执行。

在上一个例子中,紧随catch语句之后的代码块什么也没做。然而,这是你放置代码的地方,可以帮助从错误中恢复,或者至少向用户提供反馈,表明你的应用程序知道存在特殊条件。

catch语句括号中的变量e包含一个错误对象。与任何其他对象一样,它包含属性和方法。不幸的是,不同的浏览器以不同的方式实现这些方法和属性,但有两个属性是一致实现的-e.namee.message

现在让我们尝试这段代码:

    try { 
      iDontExist(); 
    } catch (e) { 
      alert(e.name + ': ' + e.message); 
    } finally { 
      alert('Finally!'); 
    } 

这将显示一个alert(),显示e.namee.message,然后另一个alert()显示Finally!

在 Firefox 和 Chrome 中,第一个警报将显示ReferenceError: iDontExist is not defined。在 Internet Explorer 中,它将是TypeError: Object expected。这告诉我们两件事:

  • e.name方法包含用于创建错误对象的构造函数的名称

  • 由于错误对象在主机环境(浏览器)中不一致,根据错误类型(e.name的值)使您的代码以不同方式行事可能有些棘手

您还可以使用new Error()或任何其他错误构造函数自己创建错误对象,然后使用throw语句告诉 JavaScript 引擎存在错误条件。

例如,想象一种情况,您调用maybeExists()函数,然后进行计算。您希望以一种一致的方式捕获所有错误,无论是maybeExists()不存在还是您的计算发现了问题。考虑以下代码:

    try { 
      var total = maybeExists(); 
      if (total === 0) { 
        throw new Error('Division by zero!'); 
      } else { 
        alert(50 / total); 
      } 
    } catch (e) { 
       alert(e.name + ': ' + e.message); 
     } finally { 
      alert('Finally!'); 
    } 

这段代码将根据maybeExists()是否定义以及它返回的值而显示不同的消息:

  • 如果maybeExists()不存在,Firefox 中会得到ReferenceError: maybeExists() is not defined,IE 中会得到TypeError: Object expected

  • 如果maybeExists()返回0,您将得到Error: Division by zero!

  • 如果maybeExists()返回2,您将得到一个显示25的警报

在所有情况下,都会有第二个警报,显示Finally!

不要抛出通用错误,thrownewError('Divisionbyzero!'),如果选择,可以更具体,例如抛出thrownewRangeError('Divisionbyzero!')。或者,您不需要构造函数;您可以简单地抛出一个普通对象:

    throw { 
      name: "MyError", 
      message: "OMG! Something terrible has happened" 
    } 

这使您可以跨浏览器控制错误名称。

练习

让我们解决以下练习:

  1. 看看以下代码:
        function F() { 
          function C() { 
           return this; 
          } 
          return C(); 
        } 
        var o = new F(); 

this的值是指全局对象还是对象o

  1. 执行此代码片段的结果是什么?
        function C(){  
          this.a = 1; 
          return false; 
        } 
        console.log(typeof new C()); 

  1. 执行以下代码片段的结果是什么?
        > c = [1, 2, [1, 2]]; 
        > c.sort(); 
        > c.join('--');  
        > console.log(c);  

  1. 想象一下String()构造函数不存在。创建一个名为MyString()的构造函数,尽可能接近String()的行为。你不允许使用任何内置的字符串方法或属性,并且记住String()不存在。你可以使用这段代码来测试你的构造函数:
        > var s = new MyString('hello'); 
        > s.length; 
              5 
        > s[0]; 
              "h" 
        > s.toString(); 
              "hello" 
        > s.valueOf(); 
              "hello" 
        > s.charAt(1); 
              "e" 
        > s.charAt('2'); 
              "l" 
        > s.charAt('e'); 
              "h" 
        > s.concat(' world!'); 
              "hello world!" 
        > s.slice(1, 3); 
              "el" 
        > s.slice(0, -1); 
              "hell" 
        > s.split('e'); 
              ["h", "llo"] 
        > s.split('l'); 
              ["he", "", "o"] 

注意

你可以使用for循环来遍历输入字符串,将其视为数组。

  1. 更新你的MyString()构造函数,包括一个reverse()方法。

注意

尝试利用数组具有reverse()方法的事实。

  1. 想象一下Array()和数组文字表示法不存在。创建一个名为MyArray()的构造函数,其行为尽可能接近Array()。使用以下代码进行测试:
        > var a = new MyArray(1, 2, 3, "test"); 
        > a.toString(); 
              "1,2,3,test" 
        > a.length; 
              4 
        > a[a.length - 1]; 
              "test" 
        > a.push('boo'); 
              5 
        > a.toString(); 
              "1,2,3,test,boo" 
        > a.pop(); 
              "boo" 
        > a.toString(); 
              "1,2,3,test" 
        > a.join(','); 
              "1,2,3,test" 
        > a.join(' isn't '); 
              "1 isn't 2 isn't 3 isn't test" 

  • 如果你觉得这个练习有趣,不要停留在join()方法上;尽可能多地使用其他方法。
  1. 想象一下Math不存在。创建一个MyMath对象,还提供以下额外的方法:
  • MyMath.rand(min, max, inclusive): 这将在minmax之间生成一个随机数,如果inclusivetrue(默认)

  • MyMath.min(array): 这将返回给定数组中的最小数

  • MyMath.max(array): 这将返回给定数组中的最大数

总结

在第二章中,原始数据类型、数组、循环和条件,你看到有五种原始数据类型(numberstringBooleannullundefined),我们也说过,不是原始数据的一切都是对象。现在,你也知道:

  • 对象就像数组,但你要指定键

  • 对象包含属性

  • 属性可以是函数(函数是数据;记住var f = function () {};)。函数是属性的也称为方法

  • 数组实际上是具有预定义数值属性和自动递增length属性的对象

  • 数组对象有许多方便的方法(如sort()slice()

  • 函数也是对象,它们有属性(如lengthprototype)和方法(如call()apply()

关于五种原始数据类型,除了undefinednull之外,其他三种都有相应的构造函数-Number()String()Boolean()。使用这些,你可以创建包含用于处理原始数据元素的方法的对象,称为包装对象。

Number()String()Boolean()可以被调用:

  • 使用new运算符,创建新对象。

  • 没有new运算符,将任何值转换为相应的原始数据类型。

你现在熟悉的其他内置构造函数包括Object()Array()Function()Date()RegExp()Error()。你也熟悉Math-一个不是构造函数的全局对象。

现在,你可以看到对象在 JavaScript 编程中起着核心作用,因为几乎一切都是对象,或者可以被对象包装。

最后,让我们总结一下你现在熟悉的文字表示法:

名称 文字 构造函数 示例
对象 {} new Object() {prop: 1}
数组 [] new Array() [1,2,3,'test']
正则表达式 /pattern/modifiers new RegExp('pattern', 'modifiers') /java.*/img

第五章:ES6 迭代器和生成器

到目前为止,我们已经讨论了 JavaScript 的语言构造,而没有看任何特定的语言版本。然而,在本章中,我们将主要关注 ES6 中引入的一些语言特性。这些特性对你编写 JavaScript 代码有很大的影响。它们不仅显著改进了语言,还为 JavaScript 程序员提供了迄今为止无法使用的几个函数式编程构造。

在本章中,我们将看一下 ES6 中新引入的迭代器和生成器。有了这些知识,我们将继续详细了解增强的集合构造。

For...of 循环

for...of循环是在 ES6 中引入的,与可迭代对象和迭代器构造一起。这个新的循环构造替代了 ES5 中的for...infor...each循环构造。由于for...of循环支持迭代协议,它可以用于内置对象,比如数组、字符串、映射、集合等,以及可迭代的自定义对象。考虑下面的代码片段作为一个例子:

    const iter = ['a', 'b']; 
    for (const i of iter) { 
      console.log(i); 
    } 
    "a" 
    "b" 

for...of循环适用于可迭代对象和内置对象,比如数组是可迭代的。如果你注意到,我们在定义循环变量时使用的是const而不是var。这是一个好的做法,因为当你使用const时,会创建一个新的变量绑定和存储空间。当你不打算在块内修改循环变量的值时,应该在for...of循环中使用const而不是var声明。

其他集合也支持for...of循环。例如,字符串是 Unicode 字符序列,for...of循环也可以正常工作:

    for (let c of "String"){ 
      console.log(c); 
    } 
    //"s" "t" "r" "i" "n" "g" 

for...infor...of循环的主要区别在于for...in循环遍历对象的所有可枚举属性。for...of循环有一个特定的目的,那就是根据对象定义的可迭代协议来遵循迭代行为。

迭代器和可迭代对象

ES6 引入了一种新的迭代数据的机制。遍历数据列表并对其进行操作是一种非常常见的操作。ES6 增强了迭代构造。这个变化涉及到两个主要概念——迭代器和可迭代对象。

迭代器

JavaScript 迭代器是一个公开next()方法的对象。这个方法以一个对象的形式返回集合中的下一个项,这个对象有两个属性——donevalue。在下面的例子中,我们将通过公开next()方法从数组中返回一个迭代器:

    //Take an array and return an iterator 
    function iter(array){ 
      var nextId= 0; 
      return { 
        next: function() { 
          if(nextId < array.length) { 
            return {value: array[nextId++], done: false}; 
          } else { 
            return {done: true}; 
          } 
        } 
      } 
    } 
    var it = iter(['Hello', 'Iterators']); 
    console.log(it.next().value); // 'Hello' 
    console.log(it.next().value); // 'Iterators' 
    console.log(it.next().done);  // true 

在上面的例子中,我们会不断通过next()方法返回数组中的元素,直到没有元素可返回为止,这时我们将返回donetrue,表示迭代没有更多的值。通过next()方法重复访问迭代器中的元素。

可迭代对象

可迭代对象是定义了其迭代行为或内部迭代的对象。这样的对象可以在 ES6 中引入的for...of循环中使用。内置类型,比如数组和字符串,定义了默认的迭代行为。为了使对象可迭代,它必须实现@@iterator方法,也就是说对象必须有一个以'Symbol.iterator'为键的属性。

如果一个对象实现了一个键为'Symbol.iterator'的方法,那么它就变成了可迭代对象。这个方法必须通过next()方法返回一个迭代器。让我们看下面的例子来澄清这一点:

    //An iterable object 
    //1\. Has a method with key has 'Symbol.iterator' 
    //2\. This method returns an iterator via method 'next' 
    let iter = { 
      0: 'Hello', 
      1: 'World of ', 
      2: 'Iterators', 
      length: 3, 
      [Symbol.iterator]() { 
        let index = 0; 
        return { 
          next: () => { 
            let value = this[index]; 
            let done = index >= this.length; 
            index++; 
            return { value, done }; 
          } 
        }; 
      } 
    }; 
    for (let i of iter) { 
      console.log(i);  
    } 
    "Hello" 
    "World of " 
    "Iterators" 

让我们把这个例子分解成更小的部分。我们正在创建一个可迭代对象。我们将使用对象字面语法创建一个iter对象,这是我们已经熟悉的。这个对象的一个特殊方面是[Symbol.iterator]方法。这个方法的定义使用了计算属性和 ES6 的简写方法定义语法,这是我们在上一章已经讨论过的。由于这个对象包含了[Symbol.iterator]方法,这个对象是可迭代的,或者说它遵循可迭代协议。这个方法还通过暴露next()方法返回迭代器对象,定义了迭代行为。现在这个对象可以与for...of循环一起使用。

生成器

与迭代器和可迭代对象密切相关,生成器是 ES6 中最受关注的功能之一。生成器函数返回一个生成器对象;这个术语起初听起来很令人困惑。当你编写一个函数时,你也本能地理解它的行为-函数开始执行,逐行执行,并在执行最后一行时结束执行。一旦函数以这种方式被线性执行,随后跟随函数的代码将被执行。

在支持多线程的语言中,这种执行流程可以被中断,部分完成的任务可以在不同的线程、进程和通道之间共享。JavaScript 是单线程的,你目前不需要处理多线程的挑战。

然而,生成器函数可以被暂停和稍后恢复。这里的重要思想是,生成器函数选择暂停自己,它不能被任何外部代码暂停。在执行期间,函数使用yield关键字来暂停。一旦生成器函数被暂停,它只能被函数外的代码恢复。

你可以暂停和恢复生成器函数多次。使用生成器函数,一个常见的模式是编写无限循环,并在需要时暂停和恢复它们。这样做有利有弊,但这种模式已经变得流行起来。

另一个重要的理解点是,生成器函数还允许双向消息传递,进出函数。每当你使用yield关键字暂停函数时,消息就会从生成器函数中发送出去,当函数恢复时,消息就会传回生成器函数。

让我们看下面的例子来澄清生成器函数的工作原理:

    function* generatorFunc() { 
      console.log('1'); //-----------> A 
      yield;            //-----------> B 
      console.log('2'); //-----------> C 
    } 
    const generatorObj = generatorFunc(); 
    console.log(generatorObj.next());   
    //"1" 
    //Object { 
    // "done": false, 
    // "value": undefined 
    //} 

这是一个非常简单的生成器函数。然而,有几个有趣的方面需要仔细理解。

首先,注意关键字 function 后面紧跟着一个星号*,这是表示该函数是一个生成器函数的语法。在函数名之前紧跟着星号也是可以的。以下两种声明都是有效的:

    function *f(){ }  
    function* f(){ } 

在函数内部,真正的魔力在于yield关键字。当遇到yield关键字时,函数会暂停自己。在我们继续之前,让我们看看函数是如何被调用的:

    const generatorObj = generatorFunc(); 
    generatorObj.next();  //"1" 

当我们调用生成器函数时,它不像普通函数一样被执行,而是返回一个生成器对象。你可以使用这个生成器对象来控制生成器函数的执行。生成器对象上的next()方法会恢复函数的执行。

当我们第一次调用next()时,执行会一直进行到函数的第一行(标记为'A'),当遇到yield关键字时暂停。如果我们再次调用next()函数,它将从上次暂停执行的地方继续执行到下一行:

    console.log(generatorObj.next());   
    //"2" 
    //Object { 
    // "done": true, 
    // "value": undefined 
    //} 

一旦整个函数体被执行,对生成器对象的任何next()调用都没有效果。我们谈到生成器函数允许双向消息传递。这是如何工作的?在前面的例子中,你可以看到每当我们恢复生成器函数时,我们会收到一个包含两个值donevalue的对象;在我们的例子中,我们收到的值是undefined。这是因为我们没有用yield关键字返回任何值。当你用yield关键字返回一个值时,调用函数会接收到它。考虑以下例子:

    function* logger() { 
      console.log('start') 
      console.log(yield) 
      console.log(yield) 
      console.log(yield) 
      return('end') 
    } 

    var genObj = logger(); 

    // the first call of next executes from the 
      start of the function until the first yield statement 
    console.log(genObj.next())         
    // "start", Object {"done": false,"value": undefined} 
    console.log(genObj.next('Save'))   
    // "Save", Object {"done": false,"value": undefined} 
    console.log(genObj.next('Our'))    
    // "Our", Object {"done": false,"value": undefined} 
    console.log(genObj.next('Souls'))  
    // "Souls",Object {"done": true,"value": "end"} 

让我们一步一步地追踪这个例子的执行流程。生成器函数有三个暂停或 yield。我们可以通过编写以下代码来创建生成器对象:

    var genObj = logger(); 

我们将通过调用next方法开始执行生成器函数;这个方法开始执行,直到第一个yield。如果你注意到,在第一次调用中我们没有向next()方法传递任何值。这个next()方法的目的只是启动生成器函数。我们将再次调用next()方法,但这次传递一个"Save"值作为参数。当函数执行恢复时,yield接收到这个值,我们可以在控制台上看到打印出的值:

    "Save", Object {"done": false,"value": undefined} 

我们将再次使用两个不同的值调用next()方法,输出与前面的代码类似。当我们最后一次调用next()方法时,执行结束,生成器函数将返回一个end值给调用代码。在执行结束时,你会看到done设置为truevalue赋值为函数返回的值,即end

    "Souls",Object {"done": true,"value": "end"} 

重要的是要注意,第一个next()方法的目的是启动生成器函数的执行-它将我们带到第一个yield关键字,因此,传递给第一个next()方法的任何值都将被忽略。

到目前为止的讨论表明,生成器对象符合迭代器的约定:

    function* logger() { 
      yield 'a' 
      yield 'b' 
    } 
    var genObj = logger(); 
    //the generator object is built using generator function 
    console.log(typeof genObj[Symbol.iterator] === 'function')    //true 
    // it is an iterable 
    console.log(typeof genObj.next === 'function') //true 
    // and an iterator (has a next() method) 
    console.log(genObj[Symbol.iterator]() === genObj) //true 

这个例子证实了生成器函数也符合可迭代的约定。

迭代生成器

生成器是迭代器,像所有支持可迭代的 ES6 构造一样,它们可以用于迭代生成器。

第一种方法是使用for...of循环,如下面的代码所示:

    function* logger() { 
      yield 'a' 
      yield 'b' 
    } 
    for (const i of logger()) { 
      console.log(i) 
    } 
    //"a" "b" 

我们在这里没有创建生成器对象。For...of循环支持可迭代对象,生成器自然适用于这个循环。

扩展运算符可以用于将可迭代对象转换为数组。考虑以下示例:

    function* logger() { 
      yield 'a' 
      yield 'b' 
    } 
    const arr = [...logger()] 
    console.log(arr) //["a","b"] 

最后,你可以使用解构语法与生成器,如下所示:

    function* logger() { 
      yield 'a' 
      yield 'b' 
    } 
    const [x,y] = logger() 
    console.log(x,y) //"a" "b" 

生成器在异步编程中扮演着重要的角色。接下来,我们将看一下 ES6 中的异步编程和 Promise。JavaScript 和 Node.js 提供了一个很好的环境来编写异步程序。生成器可以帮助你编写协作式的多任务函数。

集合

ES6 引入了四种数据结构-MapWeakMapSetWeakSet。与 Python 和 Ruby 等其他语言相比,JavaScript 的标准库非常薄弱,无法支持哈希或 Map 数据结构或字典。人们发明了一些方法来模拟Map的行为,通过将字符串键与对象进行映射。这些方法会产生一些副作用。语言对这些数据结构的支持是非常必要的。

ES6 支持标准的字典数据结构;我们将在下一节更详细地了解这些内容。

Map

Map允许将任意值作为keykeys映射到值。Map 允许快速访问值。让我们看一些 Map 的例子:

    const m = new Map(); //Creates an empty Map 
    m.set('first', 1);   //Set a value associated with a key 
    console.log(m.get('first'));  //Get a value using the key 

我们将使用构造函数创建一个空的Map。你可以使用set()方法向Map添加一个条目,将键与值关联起来,并覆盖具有相同键的任何现有条目。它的对应方法get()获取与键关联的值,如果在映射中没有这样的条目,则返回undefined

Map 还有其他可用的辅助方法,如下所示:

    console.log(m.has('first')); //Checks for existence of a key 
    //true 
    m.delete('first'); 
    console.log(m.has('first')); //false 

    m.set('foo', 1); 
    m.set('bar', 0); 

    console.log(m.size); //2 
    m.clear(); //clears the entire map 
    console.log(m.size); //0 

您也可以使用以下可迭代的[key, value]对来创建Map

    const m2 = new Map([ 
        [ 1, 'one' ], 
        [ 2, 'two' ], 
        [ 3, 'three' ], 
    ]); 

您可以使用链式set()方法来获得紧凑的语法,如下所示:

    const m3 = new Map().set(1, 'one').set(2, 'two').set(3, 'three'); 

我们可以使用任何值作为键。对于对象,键只能是字符串,但是对于集合,这种限制被移除了。我们也可以使用对象作为键,尽管这种用法并不是很流行:

    const obj = {} 
    const m2 = new Map([ 
      [ 1, 'one' ], 
      [ "two", 'two' ], 
      [ obj, 'three' ], 
    ]); 
    console.log(m2.has(obj)); //true 

遍历 Map

要记住的一件重要的事情是,对于 Map 来说,顺序很重要。Map 保留了添加元素的顺序。

有三种可迭代对象可用于遍历Map,即keysvaluesentries

keys()方法返回Map键的可迭代对象,如下所示:

    const m = new Map([ 
      [ 1, 'one' ], 
      [ 2, 'two' ], 
      [ 3, 'three' ], 
    ]); 
    for (const k of m.keys()){ 
      console.log(k);  
    } 
    //1 2 3 

同样,values()方法返回Map值的可迭代对象,如下面的示例所示:

    for (const v of m.values()){ 
      console.log(v);  
    } 
    //"one" 
    //"two" 
    //"three" 

entries()方法以[key,value]对的形式返回Map的条目,如下面的代码所示:

    for (const entry of m.entries()) { 
      console.log(entry[0], entry[1]); 
    } 
    //1 "one" 
    //2 "two" 
    //3 "three" 

您可以使用解构来使其更简洁,如下所示:

    for (const [key, value] of m.entries()) { 
      console.log(key, value); 
    } 
    //1 "one" 
    //2 "two" 
    //3 "three" 

更简洁的是:

    for (const [key, value] of m) { 
      console.log(key, value); 
    } 
    //1 "one" 
    //2 "two" 
    //3 "three" 

将 Map 转换为数组

如果要将Map转换为数组,则扩展运算符(...)非常方便:

    const m = new Map([ 
      [ 1, 'one' ], 
      [ 2, 'two' ], 
      [ 3, 'three' ], 
    ]); 
    const keys = [...m.keys()] 
    console.log(keys) 
    //Array [ 
    //1, 
    //2, 
    //3 
    //] 

由于 Map 是可迭代的,您可以使用扩展运算符将整个Map转换为数组:

    const m = new Map([ 
      [ 1, 'one' ], 
      [ 2, 'two' ], 
      [ 3, 'three' ], 
    ]); 
    const arr = [...m] 
    console.log(arr) 
    //Array [ 
    //[1,"one"], 
    //[2,"two"], 
    //[3,"three"] 
    //] 

Set

Set是一个值的集合。您可以向其中添加和删除值。尽管这听起来与数组类似,但集合不允许相同的值出现两次。Set中的值可以是任何类型。到目前为止,您一定在想这与数组有何不同?Set旨在快速执行一项操作-成员测试。相对而言,数组在这方面较慢。Set操作类似于Map操作:

    const s = new Set(); 
    s.add('first'); 
    s.has('first'); // true 
    s.delete('first'); //true 
    s.has('first'); //false 

与 Map 类似,您可以通过迭代器创建一个Set

    const colors = new Set(['red', white, 'blue']); 

当您向Set添加一个值,并且该值已经存在时,不会发生任何事情。同样,如果您从Set中删除一个值,并且该值一开始不存在,也不会发生任何事情。没有办法捕获这种情况。

WeakMap 和 WeakSet

WeakMapWeakSet的 API 与MapSet类似,但受到限制,并且它们大部分工作方式与它们的强大对应物相似。不过,也有一些区别,如下所示:

  • WeakMap仅支持newhas()get()set()delete()方法

  • WeakSet仅支持newhas()add()delete()方法

  • WeakMap的键必须是对象

  • WeakSet的值必须是对象

  • 您无法迭代WeakMap;您可以通过其键访问值的唯一方式

  • 您无法迭代WeakSet

  • 您无法清除WeakMapWeakSet

首先让我们了解WeakMapMapWeakMap之间的区别在于WeakMap允许自身被垃圾回收。WeakMap中的键是弱引用的。当垃圾回收器进行引用计数时(一种查看所有存活引用的技术),WeakMap的键不会被计算,并且在可能的情况下会被垃圾回收。

当您无法控制在 Map 中保存的对象的生命周期时,WeakMaps非常有用。使用WeakMaps时不需要担心内存泄漏,因为即使对象的生命周期很长,它们也不会占用内存。

WeakSet也适用相同的实现细节。然而,由于无法迭代WeakSet,因此WeakSet的用例并不多。

总结

在本章中,我们详细研究了 ES6 生成器。生成器是 ES6 最受期待的功能之一。暂停和恢复函数执行的能力打开了许多关于协作编程的可能性。生成器的主要优势在于它们提供了单线程、同步代码风格,同时隐藏了异步的本质。这使我们更容易以非常自然的方式表达程序步骤/语句的流程,而无需同时导航异步语法和陷阱。我们通过生成器实现了关注点的分离。

生成器与迭代器和可迭代对象的约定密切相关。这些是 ES6 中受欢迎的添加,显著增强了语言提供的数据结构。迭代器提供了一种简单的方法来返回(可能是无界的)值序列。@@iterator符号用于为对象定义默认迭代器,使其成为可迭代对象。

迭代器最重要的用例是当我们想在消耗可迭代对象的结构中使用它时,比如for...of循环。在本章中,我们还研究了 ES6 中引入的新循环结构for...offor...of与许多原生对象一起工作,因为它们定义了默认的@@iterator方法。我们还研究了 ES6 集合的新添加,如 Map、Set、WeakMap 和 Weak Set。这些集合有额外的迭代器方法-.entries().values().keys()

下一章将详细研究 JavaScript 原型。

第六章:原型

在本章中,您将学习函数对象的prototype属性。理解prototype的工作原理是学习 JavaScript 语言的重要部分。毕竟,JavaScript 经常被归类为具有基于原型的对象模型。原型并不特别困难,但它是一个新概念,因此有时可能需要一点时间才能理解。就像闭包(见第三章,“函数”)一样,原型是 JavaScript 中的一些东西,一旦理解,就会显得如此明显并且合乎逻辑。与本书的其余部分一样,强烈建议您输入并尝试这些示例-这样学习和记忆概念会更容易。

在本章中,我们将涵盖以下主题:

  • 每个函数都有一个prototype属性,它包含一个对象

  • 向原型对象添加属性

  • 使用添加到原型的属性

  • 自有属性和原型属性之间的区别

  • __proto__属性,每个对象都保留着与其原型的秘密链接

  • 诸如isPrototypeOf()hasOwnProperty()propertyIsEnumerable()之类的方法

  • 增强内置对象,例如数组或字符串,以及为什么这可能是一个坏主意

原型属性

JavaScript 中的函数是对象,它们包含方法和属性。您已经熟悉的一些方法是apply()call(),其他一些属性是lengthconstructor。函数对象的另一个属性是prototype

如果您定义一个简单的函数foo(),您可以像处理其他对象一样访问它的属性。考虑以下代码:

    > function foo(a, b) { 
        return a * b; 
      } 
    > foo.length; 
    2 
    > foo.constructor; 
    function Function() { [native code] } 

prototype属性是在定义函数时立即可用的属性。它的初始值是一个空对象:

    > typeof foo.prototype; 
    "object" 

就好像您自己添加了这个属性一样,如下所示:

    > foo.prototype = {}; 

您可以用属性和方法增强这个空对象。它们不会对foo()函数本身产生任何影响;它们只会在您将foo()作为构造函数调用时使用。

使用原型添加方法和属性

在上一章中,您学习了如何定义构造函数,以便用于创建(构造)新对象。主要思想是,在使用new调用的函数内部,您将可以访问this值,它指的是构造函数返回的对象。增强,即向this添加方法和属性,是您可以向正在构造的对象添加功能的方法。

让我们来看看构造函数Gadget(),它使用this向创建的对象添加了两个属性和一个方法,如下所示:

    function Gadget(name, color) { 
      this.name = name; 
      this.color = color; 
      this.whatAreYou = function () { 
        return 'I am a ' + this.color + ' ' + this.name; 
      }; 
    } 

向构造函数的prototype属性添加方法和属性是另一种为该构造函数产生的对象添加功能的方法。让我们添加两个属性pricerating,以及一个getInfo()方法。由于prototype已经指向一个对象,您可以继续向其添加属性和方法,如下所示:

    Gadget.prototype.price = 100; 
    Gadget.prototype.rating = 3; 
    Gadget.prototype.getInfo = function () { 
      return 'Rating: ' + this.rating + 
             ', price: ' + this.price; 
    }; 

或者,您可以完全覆盖prototype对象,用您选择的对象替换它,如下例所示:

    Gadget.prototype = { 
      price: 100, 
      rating: ...  /* and so on... */ 
    }; 

使用原型的方法和属性

您添加到prototype的所有方法和属性在使用构造函数创建新对象时立即可用。如果您使用Gadget()构造函数创建一个newtoy对象,您可以访问已经定义的所有方法和属性,如下所示:

    > var newtoy = new Gadget('webcam', 'black'); 
    > newtoy.name; 
    "webcam" 
    > newtoy.color; 
    "black" 
    > newtoy.whatAreYou(); 
    "I am a black webcam" 
    > newtoy.price; 
    100 
    > newtoy.rating; 
    3 
    > newtoy.getInfo(); 
    "Rating: 3, price: 100" 

重要的是要注意prototype是活动的。在 JavaScript 中,对象是按引用传递的,因此prototype不会随着每个新对象实例的创建而复制。这在实践中意味着什么?这意味着你可以随时修改prototype,所有的对象,甚至是在修改之前创建的对象,都会看到这些变化。

让我们通过向prototype添加一个新方法来继续示例:

    Gadget.prototype.get = function (what) { 
      return this[what]; 
    }; 

即使newtoy对象在get()方法定义之前创建,newtoy对象仍然可以访问新方法,如下所示:

    > newtoy.get('price'); 
    100 
    > newtoy.get('color'); 
    "black" 

自有属性与原型属性

在上面的例子中,getInfo()被内部使用来访问对象的属性。它也可以使用Gadget.prototype来实现相同的输出,如下所示:

    Gadget.prototype.getInfo = function () { 
      return 'Rating: ' + Gadget.prototype.rating + 
             ', price: ' + Gadget.prototype.price; 
    }; 

有什么区别?为了回答这个问题,让我们详细研究一下prototype的工作原理。

让我们再次看看newtoy对象:

    var newtoy = new Gadget('webcam', 'black'); 

当你尝试访问newtoy的属性,比如newtoy.name,JavaScript 引擎会查找对象的所有属性,寻找名为name的属性,如果找到,就返回它的值,如下所示:

    > newtoy.name; 
    "webcam" 

如果你尝试访问rating属性会发生什么?JavaScript 引擎会检查newtoy对象的所有属性,没有找到名为rating的属性。然后,脚本引擎会识别用于创建此对象的构造函数的prototype(与newtoy.constructor.prototype相同)。如果在prototype对象中找到属性,则使用该属性:

    > newtoy.rating; 
    3 

你可以做同样的事情并直接访问prototype。每个对象都有一个constructor属性,它是指向创建对象的函数的引用,所以在这种情况下看看下面的代码:

    > newtoy.constructor === Gadget; 
    true 
    > newtoy.constructor.prototype.rating; 
    3 

现在,让我们进一步查找。每个对象都有一个构造函数。prototype是一个对象,所以它也必须有一个构造函数,而构造函数又有一个prototype。你可以沿着原型链向上查找,最终会得到内置的Object()对象,它是最高级的父对象。实际上,这意味着如果你尝试newtoy.toString(),而newtoy没有自己的toString()方法,它的prototype也没有,最终你会得到对象的toString()方法:

    > newtoy.toString(); 
    "[object Object]" 

用自有属性覆盖原型的属性

正如前面的讨论所示,如果你的对象没有自己的某个属性,它可以使用原型链上的属性。如果对象有自己的属性,原型也有一个同名的属性会发生什么?那么自有属性优先于原型的属性。

考虑这样一个情景,一个属性名称既存在于自有属性中,又存在于prototype对象的属性中:

    > function Gadget(name) { 
        this.name = name; 
      } 
    > Gadget.prototype.name = 'mirror'; 

创建一个新对象并访问它的name属性会给你对象自己的name属性,如下所示:

    > var toy = new Gadget('camera'); 
    > toy.name; 
    "camera" 

可以使用hasOwnProperty()来确定属性的定义位置,如下所示:

    > toy.hasOwnProperty('name'); 
    true 

如果删除toy对象自有的name属性,原型的具有相同名称的属性将显示出来:

    > delete toy.name; 
    true 
    > toy.name; 
    "mirror" 
    > toy.hasOwnProperty('name'); 
    false 

当然,你总是可以重新创建对象的自有属性,如下所示:

    > toy.name = 'camera'; 
    > toy.name; 
    "camera" 

你可以使用hasOwnProperty()方法来查找你感兴趣的特定属性的来源。前面提到了toString()方法。它是从哪里来的?

    > toy.toString(); 
    "[object Object]" 
    > toy.hasOwnProperty('toString'); 
    false 
    > toy.constructor.hasOwnProperty('toString'); 
    false 
    > toy.constructor.prototype.hasOwnProperty('toString'); 
    false 
    > Object.hasOwnProperty('toString'); 
    false 
    > Object.prototype.hasOwnProperty('toString'); 
    true 

枚举属性

如果你想列出对象的所有属性,可以使用for...in循环。在第二章中,基本数据类型、数组、循环和条件,你看到你也可以使用for...in循环遍历数组的所有元素,但正如在那里提到的,for更适合数组,for...in更适合对象。让我们以构造一个查询字符串的 URL 为例:

    var params = { 
      productid: 666, 
      section: 'products' 
    }; 

    var url = 'http://example.org/page.php?', 
        i, 
        query = []; 

    for (i in params) { 
        query.push(i + '=' + params[i]); 
    } 

    url += query.join('&'); 

这将产生以下url字符串:

http://example.org/page.php?productid=666&section=products

以下是一些需要注意的细节:

  • 并非所有属性都会出现在for...in循环中。例如,长度(对于数组)和构造函数属性不会显示出来。能够显示出来的属性被称为可枚举。你可以通过每个对象提供的propertyIsEnumerable()方法来检查哪些属性是可枚举的。在 ES5 中,你可以指定哪些属性是可枚举的,而在 ES3 中你没有这种控制。

  • 通过原型链传递的原型也会显示出来,只要它们是可枚举的。你可以使用hasOwnProperty()方法来检查属性是对象自己的属性还是原型的属性。

  • propertyIsEnumerable()方法对于原型的所有属性都返回false,即使它们是可枚举的并出现在for...in循环中。

让我们看看这些方法的实际应用。看看这个简化版本的Gadget()

    function Gadget(name, color) { 
      this.name = name; 
      this.color = color; 
      this.getName = function () { 
        return this.name; 
      }; 
    } 
    Gadget.prototype.price = 100; 
    Gadget.prototype.rating = 3; 

创建一个新对象如下:

    var newtoy = new Gadget('webcam', 'black'); 

现在,如果你使用for...in循环进行循环,你可以看到对象的所有属性,包括来自原型的属性:

    for (var prop in newtoy) {  
      console.log(prop + ' = ' + newtoy[prop]);  
    } 

结果还包括对象的方法,因为方法只是恰好是函数的属性:

    name = webcam 
    color = black 
    getName = function () { 
      return this.name; 
    } 
    price = 100 
    rating = 3 

如果你想区分对象自己的属性和原型的属性,使用hasOwnProperty()。首先尝试以下操作:

    > newtoy.hasOwnProperty('name'); 
    true 
    > newtoy.hasOwnProperty('price'); 
    false 

让我们再次循环,但这次只显示对象自己的属性:

    for (var prop in newtoy) {  
      if (newtoy.hasOwnProperty(prop)) { 
        console.log(prop + '=' + newtoy[prop]);  
      } 
    } 

结果如下:

    name=webcam 
    color=black 
    getName = function () { 
      return this.name; 
    } 

现在,让我们尝试propertyIsEnumerable()。这个方法对于对象自己的非内置属性返回true,例如:

    > newtoy.propertyIsEnumerable('name'); 
    true 

大多数内置属性和方法都不可枚举:

    > newtoy.propertyIsEnumerable('constructor'); 
    false 

原型链中传递下来的任何属性都不可枚举:

    > newtoy.propertyIsEnumerable('price'); 
    false 

然而,要注意的是,如果你到达prototype中包含的对象并调用它的propertyIsEnumerable()方法,这样的属性是可枚举的。考虑以下代码:

    > newtoy.constructor.prototype.propertyIsEnumerable('price'); 
    true 

使用isPrototypeOf()方法

对象也有isPrototypeOf()方法。这个方法告诉你特定的对象是否被用作另一个对象的原型。

让我们来看一个名为monkey的简单对象:

    var monkey = {    
      hair: true,    
      feeds: 'bananas',    
      breathes: 'air'  
    }; 

现在,让我们创建一个Human()构造函数,并将其prototype属性指向monkey

    function Human(name) { 
      this.name = name; 
    } 
    Human.prototype = monkey; 

现在,如果你创建一个名为george的新Human对象,并询问monkeygeorge的原型吗?你会得到true

    > var george = new Human('George');  
    > monkey.isPrototypeOf(george); 
    true 

请注意,你必须知道或怀疑原型是谁,然后问你的原型是否是monkey?以确认你的怀疑。但是,如果你什么都不怀疑,你一无所知呢?你能否询问对象告诉你它的原型?答案是,在大多数浏览器中你不能,但在大多数浏览器中你可以。大多数最新的浏览器已经实现了 ES5 的一个补充,叫做Object.getPrototypeOf()

    > Object.getPrototypeOf(george).feeds; 
    "bananas" 
    > Object.getPrototypeOf(george) === monkey; 
    true 

对于一些没有getPrototypeOf()的 ES5 之前的环境,你可以使用特殊属性__proto__

秘密的__proto__链接

正如你已经知道的,当你尝试访问当前对象中不存在的属性时,会查找prototype属性。

考虑另一个名为monkey的对象,并在使用Human()构造函数创建对象时将其用作原型:

    > var monkey = { 
        feeds: 'bananas', 
        breathes: 'air' 
      }; 
    > function Human() {}  
    > Human.prototype = monkey; 

现在,让我们创建一个developer对象,并给它以下属性:

    > var developer = new Human(); 
    > developer.feeds = 'pizza'; 
    > developer.hacks = 'JavaScript'; 

现在,让我们访问这些属性(例如,hacksdeveloper对象的一个属性):

    > developer.hacks; 
    "JavaScript" 

feeds属性也可以在对象中找到,如下:

    > developer.feeds; 
    "pizza" 

breathes属性并不存在于developer对象的属性中,所以会查找原型,就好像有一个秘密链接或通道通向prototype对象:

    > developer.breathes; 
    "air" 

在大多数现代 JavaScript 环境中,秘密链接被暴露为__proto__属性,即proto一词前后各有两个下划线:

    > developer.__proto__ === monkey; 
    true 

你可以使用这个秘密属性进行学习,但在你的真实脚本中使用它并不是一个好主意,因为它并不在所有浏览器中都存在(特别是 IE),所以你的脚本不具备可移植性。

请注意,__proto__prototype不同,__proto__是实例(对象)的属性,而prototype是用于创建这些对象的构造函数的属性:

    > typeof developer.__proto__; 
    "object" 
    > typeof developer.prototype; 
    "undefined" 
    > typeof developer.constructor.prototype; 
    "object" 

再次强调,你应该只在学习或调试目的时使用__proto__。或者,如果你足够幸运,你的代码只需要在符合 ES5 的环境中工作,你可以使用Object.getPrototypeOf()

增强内置对象

由内置构造函数创建的对象,如ArrayString,甚至ObjectFunction,都可以通过原型进行增强。这意味着你可以向Array原型添加新方法,以便让它们对所有数组可用。让我们看看如何做到这一点。

在 PHP 中,有一个名为in_array()的函数,它告诉你一个值是否存在于数组中。在 JavaScript 中,没有inArray()方法,尽管在 ES5 中有indexOf(),你可以用它来达到相同的目的。因此,让我们实现它并添加到Array.prototype中,如下所示:

    Array.prototype.inArray = function (needle) { 
      for (var i = 0, len = this.length; i < len; i++) { 
        if (this[i] === needle) { 
          return true; 
        } 
      } 
      return false; 
    }; 

现在,所有数组都可以访问这个新方法。让我们测试以下代码:

    > var colors = ['red', 'green', 'blue']; 
    > colors.inArray('red'); 
    true 
    > colors.inArray('yellow'); 
    false 

这很简单!让我们再做一次。想象一下,你的应用程序经常需要将单词倒过来拼写,你觉得字符串对象应该有一个内置的reverse()方法。毕竟,数组有reverse()。你可以通过借用Array.prototype.reverse()来轻松地向String原型添加一个reverse()方法(在第四章的结尾有一个类似的练习,对象):

    String.prototype.reverse = function () { 
      return Array.prototype.reverse. 
               apply(this.split('')).join(''); 
    }; 

这段代码使用split()方法从字符串创建一个数组,然后在这个数组上调用reverse()方法,产生一个反转的数组。然后使用join()方法将结果数组转换回字符串。让我们测试一下新方法:

    > "bumblebee".reverse(); 
      "eebelbmub"

增强内置对象 - 讨论

通过原型增强内置对象是一种强大的技术,你可以用它来塑造 JavaScript 的任何方式。但是,由于它的强大,你在使用这种方法之前应该仔细考虑你的选择。

原因是一旦你了解了 JavaScript,你期望它以相同的方式工作,无论你使用的是哪个第三方库或小部件。修改核心对象可能会让用户和代码维护者感到困惑,并产生意外的错误。

JavaScript 不断发展,浏览器供应商不断支持更多功能。今天你认为缺少的方法并决定添加到核心原型中的方法,明天可能就成为内置方法。在这种情况下,你的方法就不再需要了。此外,如果你已经编写了大量使用该方法的代码,并且你的方法与新的内置实现略有不同,会怎么样呢?

增强内置原型的最常见和可接受的用例是为旧浏览器添加对新功能的支持(这些功能已经由 ECMAScript 委员会标准化并在新浏览器中实现)。一个例子是在旧版本的 IE 中添加 ES5 方法。这些扩展被称为shimspolyfills

在增强原型时,你首先要检查方法是否存在,然后再自己实现。这样,如果浏览器中存在原生实现,你就可以使用它。例如,让我们为字符串添加trim()方法,这是 ES5 中存在的方法,但在旧浏览器中缺少:

    if (typeof String.prototype.trim !== 'function') { 
      String.prototype.trim = function () { 
        return this.replace(/^\s+|\s+$/g,''); 
      }; 
    } 
    > " hello ".trim(); 
    "hello" 

提示

最佳实践

如果你决定增强内置对象或其原型以添加新属性,首先要检查新属性是否存在。

原型陷阱

处理原型时需要考虑的两个重要行为是:

  • 原型链是活跃的,除非你完全替换了prototype对象

  • prototype.constructor方法不可靠

让我们创建一个简单的构造函数和两个对象:

    > function Dog() { 
        this.tail = true; 
      } 
    > var benji = new Dog(); 
    > var rusty = new Dog(); 

即使您已经创建了benjirusty对象,您仍然可以向Dog()的原型添加属性,现有对象将可以访问新属性。让我们加入say()方法:

    > Dog.prototype.say = function () { 
        return 'Woof!'; 
      }; 

两个对象都可以访问新方法:

    > benji.say(); 
    "Woof!" 
     rusty.say(); 
    "Woof!" 

到目前为止,如果您咨询您的对象,询问它们是使用哪个构造函数创建的,它们将正确报告:

    > benji.constructor === Dog; 
    true 
    > rusty.constructor === Dog; 
    true 

现在,让我们完全用全新的对象覆盖prototype对象:

    > Dog.prototype = { 
        paws: 4, 
        hair: true 
      }; 

事实证明,旧对象无法访问新原型的属性;它们仍然保留指向旧原型对象的秘密链接,如下所示:

    > typeof benji.paws; 
    "undefined" 
    > benji.say(); 
    "Woof!" 
    > typeof benji.__proto__.say; 
    "function" 
    > typeof benji.__proto__.paws; 
    "undefined" 

从现在开始创建的任何新对象都将使用更新后的原型,如下所示:

    > var lucy = new Dog(); 
    > lucy.say(); 
    TypeError: lucy.say is not a function 
    > lucy.paws; 
    4 

秘密的__proto__链接指向新的原型对象,如下面的代码行所示:

    > typeof lucy.__proto__.say; 
    "undefined" 
    > typeof lucy.__proto__.paws; 
    "number" 

现在新对象的constructor属性不再正确报告。您期望它指向Dog(),但实际上它指向Object(),如下例所示:

    > lucy.constructor; 
    function Object() { [native code] } 
    > benji.constructor; 
    function Dog() { 
      this.tail = true; 
    } 

在完全覆盖原型后,您可以通过重置constructor属性轻松防止混淆,如下所示:

    > function Dog() {} 
    > Dog.prototype = {}; 
    > new Dog().constructor === Dog; 
    false 
    > Dog.prototype.constructor = Dog; 
    > new Dog().constructor === Dog; 
    true 

提示

最佳实践

当您覆盖原型时,请记得重置constructor属性。

练习

让我们练习以下练习:

  1. 创建一个名为shape的对象,该对象具有类型property和一个getType()方法。

  2. 定义一个Triangle()构造函数,其原型是shape。使用Triangle()创建的对象应该有三个自有属性-abc,表示三角形的边长。

  3. 在原型中添加一个名为getPerimeter()的新方法。

  4. 使用以下代码测试您的实现:

        > var t = new Triangle(1, 2, 3); 
        > t.constructor === Triangle; 
               true 
        > shape.isPrototypeOf(t); 
               true 
        > t.getPerimeter(); 
               6 
        > t.getType(); 
               "triangle" 

  1. 循环遍历t,仅显示您自己的属性和方法,而不是原型的。

  2. 使以下代码工作:

        > [1, 2, 3, 4, 5, 6, 7, 8, 9].shuffle(); 
          [2, 4, 1, 8, 9, 6, 5, 3, 7] 

总结

让我们总结一下您在本章学到的最重要的主题:

  • 所有函数都有一个名为prototype的属性。最初,它包含一个空对象-一个没有任何自有属性的对象。

  • 您可以向prototype对象添加属性和方法。您甚至可以完全替换它为您选择的对象。

  • 当您使用函数作为构造函数创建对象(使用new)时,对象会得到一个指向构造函数原型的秘密链接,并且可以访问原型的属性。

  • 对象的自有属性优先于具有相同名称的原型属性。

  • 使用hasOwnProperty()方法区分对象的自有属性和prototype属性。

  • 存在原型链。当您执行foo.bar时,如果您的foo对象没有名为bar的属性,JavaScript 解释器将在原型中查找bar属性。如果找不到,则会继续在原型的原型中查找,然后在原型的原型的原型中查找,一直到Object.prototype

  • 您可以增强内置构造函数的原型,并且所有对象都将看到您的添加。将一个函数分配给Array.prototype.flip,所有数组将立即获得一个flip()方法,就像[1,2,3].flip()一样。但是,请检查您要添加的方法/属性是否已经存在,以便为您的脚本未来保值。

第七章:继承

如果您回到第一章 面向对象的 JavaScript,并回顾面向对象编程部分,您会发现您已经知道如何将大部分应用到 JavaScript 中。您知道对象、方法和属性是什么。您知道 ES5 中没有类,尽管您可以使用构造函数来实现它们。ES6 引入了类的概念;我们将在下一章详细了解 ES6 类的工作原理。封装?是的,对象封装了数据和处理数据的方法(方法)。聚合?当然,一个对象可以包含其他对象。事实上,这几乎总是这种情况,因为方法是函数,函数也是对象。

现在,让我们专注于继承部分。这是最有趣的特性之一,因为它允许您重用现有的代码,从而促进懒惰,这很可能是最初吸引人类物种进行计算机编程的原因。

JavaScript 是一种动态语言,通常有多种方法可以实现任何给定的任务。继承也不例外。在本章中,您将看到一些常见的实现继承的模式。对这些模式有很好的理解将帮助您选择合适的模式,或者根据您的任务、项目或风格选择合适的混合模式。

原型链

让我们从实现继承的默认方式开始 - 通过原型进行继承链。

正如您已经知道的,每个函数都有一个prototype属性,指向一个对象。当使用new运算符调用函数时,将创建并返回一个对象。这个新对象有一个指向prototype对象的秘密链接。秘密链接(在某些环境中称为__proto__)允许使用prototype对象的方法和属性,就好像它们属于新创建的对象一样。

prototype对象只是一个普通对象,因此它也有指向它的原型的秘密链接。因此,创建了一个称为原型链的链:

原型链

在这个示例中,对象A包含许多属性。其中一个属性是隐藏的__proto__属性,它指向另一个对象BB__proto__属性指向C。这个链以Object.prototype对象结束,这是祖父,每个对象都从它继承。

这些都是很好知道的,但它如何帮助你呢?实际的一面是,当对象A缺少一个属性,但B有它时,A仍然可以访问这个属性作为它自己的。如果B也没有所需的属性,但C有,同样适用。这就是继承发生的方式 - 一个对象可以访问沿着继承链找到的任何属性。

在本章中,您将看到使用以下层次结构的不同示例 - 一个通用的Shape父类被一个2D shape继承,然后被任意数量的特定的二维形状继承,比如三角形、矩形等等。

原型链示例

原型链是实现继承的默认方式。为了实现层次结构,让我们定义三个构造函数:

    function Shape(){ 
    this.name = 'Shape'; 
    this.toString = function () { 
        return this.name; 
      }; 
    } 

    function TwoDShape(){ 
      this.name = '2D shape'; 
    } 

    function Triangle(side, height){ 
      this.name = 'Triangle'; 
      this.side = side; 
      this.height = height; 
      this.getArea = function () { 
        return this.side * this.height / 2; 
      }; 
    } 

执行继承魔术的代码如下:

    TwoDShape.prototype = new Shape(); 
    Triangle.prototype = new TwoDShape(); 

这里发生了什么?您获取了TwoDShapeprototype属性中包含的对象,并且不是增加个别属性,而是完全用另一个对象覆盖它,该对象是通过使用new调用Shape()构造函数创建的。对Triangle也可以遵循相同的过程-它的原型被new TwoDShape()创建的对象所取代。重要的是要记住 JavaScript 使用对象而不是类。您需要使用new Shape()构造函数创建一个实例,然后才能继承其属性;您不直接从Shape()继承。此外,在继承后,您可以修改Shape()构造函数,覆盖它,甚至删除它,这不会对TwoDShape产生影响,因为您只需要一个实例来继承。

正如您从上一章中所知道的,重写原型(而不仅仅是向其添加属性)会对constructor属性产生副作用。因此,在继承后重置constructor属性是一个好主意。考虑以下示例:

    TwoDShape.prototype.constructor = TwoDShape; 
    Triangle.prototype.constructor = Triangle; 

现在,让我们测试一下到目前为止发生了什么。创建一个Triangle对象并调用其自己的getArea()方法可以正常工作:

    >var my = new Triangle(5, 10); 
    >my.getArea(); 
    25 

尽管my对象没有自己的toString()方法,但它继承了一个,您可以调用它。请注意,继承的方法toString()this对象绑定到my

    >my.toString(); 
    "Triangle" 

当您调用my.toString()时,考虑一下 JavaScript 引擎的操作:

  • 它循环遍历my的所有属性,并没有找到名为toString()的方法。

  • 它查看my.__proto__指向的对象,这个对象是在继承过程中创建的new TwoDShape()实例。

  • 现在,JavaScript 引擎循环遍历TwoDShape的实例,并没有找到toString()方法。然后它检查该对象的__proto__。这一次,__proto__指向由new Shape()创建的实例。

  • 检查new Shape()的实例,最终找到了toString()

  • 此方法在my的上下文中被调用,这意味着this指向my

如果您问my,你的构造函数是谁?,它会正确报告,因为在继承后重置了constructor属性:

    >my.constructor === Triangle; 
    true 

使用instanceof运算符,您可以验证my是所有三个构造函数的实例:

    > my instanceof Shape; 
    true 
    > my instanceofTwoDShape; 
    true 
    > my instanceof Triangle; 
    true 
    > my instanceof Array; 
    false 

当您通过传递my调用isPrototypeOf()时,会发生相同的情况:

    >Shape.prototype.isPrototypeOf(my); 
    true 
    >TwoDShape.prototype.isPrototypeOf(my); 
    true 
    >Triangle.prototype.isPrototypeOf(my); 
    true 
    >String.prototype.isPrototypeOf(my); 
    false 

您还可以使用其他两个构造函数创建对象。使用new TwoDShape()创建的对象也会继承自Shape()继承的toString()方法:

    >var td = new TwoDShape(); 
    >td.constructor === TwoDShape; 
    true 
    >td.toString(); 
    "2D shape" 
    >var s = new Shape(); 
    >s.constructor === Shape; 
    true 

将共享属性移动到原型

当您使用构造函数创建对象时,使用this添加自己的属性。在属性跨实例不变的情况下,这可能效率低下。在前面的示例中,Shape()定义如下:

    function Shape(){ 
    this.name = 'Shape'; 
    } 

这意味着每次使用new Shape()创建新对象时,都会创建一个新的name属性并将其存储在内存中的某个位置。另一种选择是将name属性添加到原型中,并在所有实例之间共享:

    function Shape() {} 
    Shape.prototype.name = 'Shape'; 

现在,每次使用new Shape()创建对象时,该对象都不会获得自己的name属性,而是使用添加到原型中的属性。这更有效,但您应该只对不会从一个实例更改为另一个实例的属性使用它。方法非常适合这种共享。

通过将所有方法和适当的属性添加到prototype来改进前面的示例。在Shape()TwoDShape()的情况下,一切都是共享的:

    // constructor 
    function Shape() {} 

    // augment prototype 
    Shape.prototype.name = 'Shape'; 
    Shape.prototype.toString = function () { 
      return this.name; 
    }; 

    // another constructor 
    function TwoDShape() {} 

    // take care of inheritance 
    TwoDShape.prototype = new Shape(); 
    TwoDShape.prototype.constructor = TwoDShape; 

    // augment prototype 
    TwoDShape.prototype.name = '2D shape'; 

如您所见,您必须先处理继承,然后再增加原型。否则,当您继承时,添加到TwoDShape.prototype的任何内容都会被清除。

Triangle构造函数有点不同,因为它创建的每个对象都是一个新的三角形,可能具有不同的尺寸。因此,最好将sideheight作为自有属性,并共享其余部分。例如,getArea()方法是相同的,无论每个三角形的实际尺寸如何。同样,首先进行继承,然后增加原型:

    function Triangle(side, height) { 
    this.side = side; 
    this.height = height; 
    } 
    // take care of inheritance 
    Triangle.prototype = new TwoDShape(); 
    Triangle.prototype.constructor = Triangle; 

    // augment prototype 
    Triangle.prototype.name = 'Triangle'; 
    Triangle.prototype.getArea = function () { 
    return this.side * this.height / 2; 
    }; 

所有先前的测试代码都完全相同。这是一个例子:

    >var my = new Triangle(5, 10); 
    >my.getArea(); 
    25 
    >my.toString(); 
    "Triangle" 

调用my.toString()时,只有一个微小的幕后差异。不同之处在于在找到Shape.prototype之前,需要进行一次额外的查找,而不是在new Shape()实例中,就像在先前的示例中一样。

您还可以使用hasOwnProperty()来查看自有属性与原型链中的属性之间的差异:

    >my.hasOwnProperty('side'); 
    true 
    >my.hasOwnProperty('name'); 
    false 

先前示例中对isPrototypeOf()instanceof运算符的调用方式完全相同:

    >TwoDShape.prototype.isPrototypeOf(my); 
    true 
    > my instanceof Shape; 
    true 

只继承原型

如前所述,出于效率考虑,应将可重复使用的属性和方法添加到原型中。如果这样做,那么只继承原型是一个好主意,因为所有可重复使用的代码都在那里。这意味着继承Shape.prototype对象比继承使用new Shape()创建的对象更好。毕竟,new Shape()只会给出自有形状属性,这些属性不打算被重复使用(否则,它们将在原型中)。通过这样做,您可以获得更高的效率:

  • 不仅仅为了继承而创建新对象

  • 在运行时查找toString()时减少查找次数

例如,这是更新后的代码;更改部分已突出显示:

    function Shape() {} 
    // augment prototype 
    Shape.prototype.name = 'Shape'; 
    Shape.prototype.toString = function () { 
      return this.name; 
    }; 

    function TwoDShape() {} 
    // take care of inheritance 
    TwoDShape.prototype = Shape.prototype; 
    TwoDShape.prototype.constructor = TwoDShape; 
    // augment prototype 
    TwoDShape.prototype.name = '2D shape'; 

    function Triangle(side, height) { 
      this.side = side; 
      this.height = height; 
    } 

    // take care of inheritance 
    Triangle.prototype = TwoDShape.prototype; 
    Triangle.prototype.constructor = Triangle; 
    // augment prototype 
    Triangle.prototype.name = 'Triangle'; 
    Triangle.prototype.getArea = function () { 
      return this.side * this.height / 2; 
    }; 

测试代码给出了相同的结果:

    >var my = new Triangle(5, 10); 
    >my.getArea(); 
    25 
    >my.toString(); 
    "Triangle" 

调用my.toString()时查找的差异是什么?首先,像往常一样,JavaScript 引擎会查找my对象本身的toString()方法。引擎找不到这样的方法,所以它会检查原型。原型指向与TwoDShape的原型和Shape.prototype指向的相同对象。请记住,对象不是按值复制的,而是按引用复制的。因此,查找只是一个两步过程,而不是四步(在上一个示例中)或三步(在第一个示例中)。

简单地复制原型更有效,但会产生副作用,因为所有子代和父代的原型都指向相同的对象,当子代修改原型时,父代和兄弟也会得到更改。

看看下面这行:

    Triangle.prototype.name = 'Triangle'; 

它更改了name属性,因此实际上也更改了Shape.prototype.name。如果使用new Shape()创建实例,其name属性会显示为"Triangle"

    >var s = new Shape(); 
    >s.name; 
    "Triangle" 

这种方法更有效,但可能不适用于所有用例。

临时构造函数 - new F()

解决先前概述的问题的一个解决方案是,所有原型都指向相同对象,父代获取子代的属性,是使用中介来打破链条。中介是一个临时构造函数的形式。创建一个空函数F()并将其prototype设置为父构造函数的原型,允许您调用new F()并创建没有自有属性但从父代prototype继承一切的对象。

让我们看一下修改后的代码:

    function Shape() {} 
    // augment prototype 
    Shape.prototype.name = 'Shape'; 
    Shape.prototype.toString = function () { 
    return this.name; 
    }; 

    function TwoDShape() {} 
    // take care of inheritance 
    var F = function () {}; 
    F.prototype = Shape.prototype; 
    TwoDShape.prototype = new F(); 
    TwoDShape.prototype.constructor = TwoDShape; 
    // augment prototype 
    TwoDShape.prototype.name = '2D shape'; 

    function Triangle(side, height) { 
    this.side = side; 
    this.height = height; 
    } 

    // take care of inheritance 
    var F = function () {}; 
    F.prototype = TwoDShape.prototype; 
    Triangle.prototype = new F(); 
    Triangle.prototype.constructor = Triangle; 
    // augment prototype 
    Triangle.prototype.name = 'Triangle'; 
    Triangle.prototype.getArea = function () { 
    return this.side * this.height / 2; 
    }; 

创建my三角形并测试方法:

    >var my = new Triangle(5, 10); 
    >my.getArea(); 
    25 
    >my.toString(); 
    "Triangle" 

使用这种方法,原型链保持不变:

    >my.__proto__ === Triangle.prototype; 
    true 
    >my.__proto__.constructor === Triangle; 
    true 
    >my.__proto__.__proto__ === TwoDShape.prototype; 
    true 
    >my.__proto__.__proto__.__proto__.constructor === Shape; 
    true 

此外,父代的属性不会被子代覆盖:

    >var s = new Shape(); 
    >s.name; 
    "Shape" 
    >"I am a " + new TwoDShape(); // calling toString() 
    "I am a 2D shape" 

同时,这种方法支持只继承原型应该继承的属性和方法的想法,而不应该继承自有属性。这背后的原理是,自有属性可能太具体,无法重复使用。

Uber - 从子对象访问父对象

经典的面向对象语言通常有一个特殊的语法,让你可以访问父类,也称为超类。当子类想要一个方法,做父类方法的所有事情,再加上一些额外的东西时,这可能很方便。在这种情况下,子类用相同的名称调用父类的方法,并处理结果。

在 JavaScript 中,没有这样的特殊语法,但实现相同的功能非常简单。让我们重写上一个例子,同时处理继承,并创建一个指向父类prototype对象的uber属性:

    function Shape() {} 
    // augment prototype 
    Shape.prototype.name = 'Shape'; 
    Shape.prototype.toString = function () { 
    var const = this.constructor; 
    returnconst.uber 
        ? this.const.uber.toString() + ', ' + this.name 
        : this.name; 
    }; 

    function TwoDShape() {} 
    // take care of inheritance 
    var F = function () {}; 
    F.prototype = Shape.prototype; 
    TwoDShape.prototype = new F(); 
    TwoDShape.prototype.constructor = TwoDShape; 
    TwoDShape.uber = Shape.prototype; 
    // augment prototype 
    TwoDShape.prototype.name = '2D shape'; 

    function Triangle(side, height) { 
    this.side = side; 
    this.height = height; 
    } 

    // take care of inheritance 
    var F = function () {}; 
    F.prototype = TwoDShape.prototype; 
    Triangle.prototype = new F(); 
    Triangle.prototype.constructor = Triangle; 
    Triangle.uber = TwoDShape.prototype; 
    // augment prototype 
    Triangle.prototype.name = 'Triangle'; 
    Triangle.prototype.getArea = function () { 
    return this.side * this.height / 2; 
    }; 

这里的新东西是:

  • 一个新的uber属性指向父类的prototype

  • 更新的toString()方法

以前,toString()只返回this.name。现在,除此之外,还有一个检查来看this.constructor.uber是否存在,如果存在,首先调用它的toString()this.constructor是函数本身,this.constructor.uber指向父类的prototype。结果是,当你为Triangle实例调用toString()时,所有原型链上的toString()方法都会被调用:

    >var my = new Triangle(5, 10); 
    >my.toString(); 
    "Shape, 2D shape, Triangle" 

uber属性的名称本来可以是 superclass,但这会暗示 JavaScript 有类。理想情况下,它本来可以是 super(就像 Java 中一样),但 super 在 JavaScript 中是一个保留字。Douglas Crockford 建议的德语单词 uber 的意思与 super 差不多,你不得不承认,听起来非常酷。

将继承部分隔离成一个函数

让我们将上一个例子中处理所有继承细节的代码移到一个可重用的extend()函数中:

    function extend(Child, Parent) { 
    var F = function () {}; 
    F.prototype = Parent.prototype; 
    Child.prototype = new F(); 
    Child.prototype.constructor = Child; 
    Child.uber = Parent.prototype; 
    } 

使用这个函数(或你自己定制的版本)可以帮助你保持代码在重复的继承相关任务方面的整洁。这样,你可以通过简单地使用以下两行代码来继承:

    extend(TwoDShape, Shape); 
    extend(Triangle, TwoDShape); 

让我们看一个完整的例子:

    // inheritance helper 
    function extend(Child, Parent) { 
      var F = function () {}; 
      F.prototype = Parent.prototype; 
      Child.prototype = new F(); 
      Child.prototype.constructor = Child; 
      Child.uber = Parent.prototype; 
    } 

    // define -> augment 
    function Shape() {} 
    Shape.prototype.name = 'Shape'; 
    Shape.prototype.toString = function () { 
      return this.constructor.uber 
        ? this.constructor.uber.toString() + ', ' + this.name 
        : this.name; 
    }; 

    // define -> inherit -> augment 
    function TwoDShape() {} 
    extend(TwoDShape, Shape); 
    TwoDShape.prototype.name = '2D shape'; 

    // define 
    function Triangle(side, height) { 
      this.side = side; 
      this.height = height; 
    } 
    // inherit 
    extend(Triangle, TwoDShape); 
    // augment 
    Triangle.prototype.name = 'Triangle'; 
    Triangle.prototype.getArea = function () { 
      return this.side * this.height / 2; 
    }; 

让我们测试以下代码:

    > new Triangle().toString(); 
    "Shape, 2D shape, Triangle" 

复制属性

现在,让我们尝试一个稍微不同的方法。由于继承都是关于重用代码,你能否简单地从一个对象复制你喜欢的属性到另一个对象?或者从父类到子类?保持与前面的extend()函数相同的接口,你可以创建一个extend2()函数,它接受两个构造函数,并将父类的prototype的所有属性复制到子类的prototype。当然,这也会复制方法,因为方法只是恰好是函数的属性:

    function extend2(Child, Parent) { 
      var p = Parent.prototype; 
      var c = Child.prototype; 
      for (var i in p) { 
        c[i] = p[i]; 
      } 
      c.uber = p; 
    } 

正如你所看到的,通过属性的简单循环就可以完成。与前面的例子一样,如果你想要从子类方便地访问父类的方法,可以设置一个uber属性。不过与前面的例子不同的是,在这里,不需要重置Child.prototype.constructor,因为这里子类的prototype是增强的,而不是完全被覆盖。所以,constructor属性指向初始值。

与前一种方法相比,这种方法有点低效,因为子类的prototype的属性被复制,而不是在执行过程中通过原型链查找。请记住,这只对包含原始类型的属性有效。所有对象(包括函数和数组)都不会被复制,因为这些只是通过引用传递的。

让我们看一个使用两个构造函数Shape()TwoDShape()的例子。Shape()函数的prototype对象包含一个原始属性name和一个非原始属性toString()

    var Shape = function () {}; 
    var TwoDShape = function () {}; 
    Shape.prototype.name = 'Shape'; 
    Shape.prototype.toString = function () { 
      return this.uber 
        ? this.uber.toString() + ', ' + this.name 
        : this.name; 
    }; 

如果你用extend()继承,用TwoDShape()创建的对象和它的原型都不会有自己的name属性,但它们可以访问继承的那个:

    > extend(TwoDShape, Shape); 
    >var td = new TwoDShape(); 
    >td.name; 
    "Shape" 
    >TwoDShape.prototype.name; 
    "Shape" 
    >td.__proto__.name; 
    "Shape" 
    >td.hasOwnProperty('name'); 
    false 
    > td.__proto__.hasOwnProperty('name'); 
    false 

然而,如果你用extend2()继承,TwoDShape()的原型会得到自己的name属性的副本。它还会得到自己的toString()的副本,但这只是一个引用,所以函数不会被重新创建第二次:

    >extend2(TwoDShape, Shape); 
    >var td = new TwoDShape(); 
    > td.__proto__.hasOwnProperty('name'); 
    true 
    > td.__proto__.hasOwnProperty('toString'); 
    true 
    > td.__proto__.toString === Shape.prototype.toString; 
    true 

如您所见,两个toString()方法是相同的函数对象。这是好的,因为这意味着不会创建不必要的方法副本。

因此,您可以说extend2()extend()效率低,因为它重新创建了原型的属性。然而,这并不是很糟糕,因为只有原始数据类型被复制。此外,在原型链查找期间,这对于减少链条链接是有益的,因为在找到属性之前需要跟随的链条更少。

再次看一下uber属性。这次,出于变化的原因,它设置在Parent对象的原型p上,而不是Parent构造函数上。这就是为什么toString()使用它作为this.uber而不是this.constructor.uber。这只是一个说明,您可以以任何您认为合适的方式塑造您喜欢的继承模式。让我们来测试一下:

    >td.toString(); 
    "Shape, Shape" 

TwoDShape没有重新定义name属性,因此会重复。它可以随时这样做,而且(原型链是活动的)所有实例都会看到更新:

    >TwoDShape.prototype.name = "2D shape"; 
    >td.toString(); 
    "Shape, 2D shape" 

引用复制时要注意

对象(包括函数和数组)被引用复制的事实有时可能会导致您意想不到的结果。

让我们创建两个构造函数,并向第一个的原型添加属性:

    > function Papa() {} 
    >function Wee() {} 
    >Papa.prototype.name = 'Bear';  
    >Papa.prototype.owns = ["porridge", "chair", "bed"]; 

现在,让我们让WeePapa继承(extend()extend2()都可以):

    >extend2(Wee, Papa); 

使用extend2()Wee函数的原型继承了Papa.prototype的属性作为自己的属性:

    >Wee.prototype.hasOwnProperty('name'); 
    true 
    >Wee.prototype.hasOwnProperty('owns'); 
    true 

name属性是原始的,因此会创建一个新的副本。owns属性是一个数组对象,因此它是引用复制的:

    >Wee.prototype.owns; 
    ["porridge", "chair", "bed"] 
    >Wee.prototype.owns=== Papa.prototype.owns; 
    true 

更改Wee函数的name副本不会影响Papa

    >Wee.prototype.name += ', Little Bear'; 
    "Bear, Little Bear" 
    >Papa.prototype.name; 
    "Bear" 

然而,更改Wee函数的owns属性会影响Papa,因为两个属性指向内存中的同一个数组:

    >Wee.prototype.owns.pop(); 
    "bed" 
    >Papa.prototype.owns; 
    ["porridge", "chair"] 

当您完全用另一个对象(而不是修改现有对象)覆盖Wee函数的owns副本时,情况就不同了。在这种情况下,Papa.owns继续指向旧对象,而Wee.owns指向新对象:

    >Wee.prototype.owns= ["empty bowl", "broken chair"]; 
    >Papa.prototype.owns.push('bed'); 
    >Papa.prototype.owns; 
    ["porridge", "chair", "bed"] 

将对象视为在内存中创建和存储的东西。变量和属性仅仅指向这个位置,因此当您将全新的对象分配给Wee.prototype.owns时,您实质上是在说-嘿,忘记这个旧对象,把你的指针移到这个新对象上。

以下图表说明了如果你想象内存是一堆对象(就像一堵砖墙),你指向(引用)其中一些对象会发生什么:

  • 创建了一个新对象,并且A指向它。

  • 创建了一个新变量B,并且使其等于A,这意味着它现在指向A指向的相同位置。

  • 使用B句柄(指针)更改了属性颜色。砖头现在是白色的。A.color === "white"的检查将返回 true。

  • 创建了一个新对象,并且B变量/指针被回收,指向了新对象。AB现在指向内存堆的不同部分。它们没有共同之处,对其中一个的更改不会影响另一个:

引用复制时要注意

如果您想解决对象被引用复制的问题,请考虑深复制,本章后面会有描述。

对象从对象继承

到目前为止,本章中的所有示例都假设您使用构造函数创建对象,并且希望使用一个构造函数创建的对象继承来自另一个构造函数的属性。但是,您也可以创建对象而不使用构造函数的帮助,只使用对象文字,这实际上是更少的输入。那么,如何继承这些呢?

在 Java 或 PHP 中,你定义类并让它们继承自其他类。这就是为什么你会看到经典这个词,因为面向对象的功能来自于类的使用。在 JavaScript 中,没有类,所以来自经典背景的程序员会使用构造函数,因为构造函数是他们习惯的最接近的东西。此外,JavaScript 提供了new运算符,这可能会进一步暗示 JavaScript 类似于 Java。事实上,最终一切都归结为对象。本章的第一个例子就是用这种语法:

    Child.prototype = new Parent(); 

在这里,Child构造函数(或类,如果你愿意的话)继承自Parent。然而,这是通过使用new Parent()创建一个对象并从中继承的。这也被称为伪经典继承模式,因为它类似于经典继承,尽管实际上并不是(没有涉及类)。

那么,为什么不摆脱中间人(构造函数/类),让对象直接继承对象呢?在extend2()中,父prototype对象的属性被复制为子prototype对象的属性。这两个原型本质上只是对象。忘记原型和构造函数,你可以简单地将一个对象的所有属性复制到另一个对象中。

你已经知道对象可以作为一个空白画布开始,没有任何自己的属性,使用var o = {};,然后稍后再添加属性。然而,你可以通过复制现有对象的所有属性来开始,而不是从头开始。这里有一个函数可以做到这一点:它接受一个对象并返回一个新的副本:

    function extendCopy(p) { 
      var c = {}; 
      for (var i in p) { 
        c[i] = p[i]; 
      } 
      c.uber = p; 
      return c; 
    } 

简单地复制所有属性是一种简单的模式,它被广泛使用。让我们看看这个函数的作用。你首先有一个基础对象:

    var shape = { 
    name: 'Shape', 
    toString: function () { 
    return this.name; 
    } 
    }; 

为了创建一个建立在旧对象基础上的新对象,你可以调用extendCopy()函数,它会返回一个新对象。然后,你可以用额外的功能来增强新对象:

    var twoDee = extendCopy(shape); 
    twoDee.name = '2D shape'; 
    twoDee.toString = function () { 
    return this.uber.toString() + ', ' + this.name; 
    }; 

这里有一个继承2D 形状对象的三角形对象:

    var triangle = extendCopy(twoDee); 
    triangle.name = 'Triangle'; 
    triangle.getArea = function () { 
    return this.side * this.height / 2; 
    }; 

例如,使用三角形:

    >triangle.side = 5; 
    >triangle.height = 10; 
    >triangle.getArea(); 
    25 
    >triangle.toString(); 
    "Shape, 2D shape, Triangle" 

这种方法可能的一个缺点是初始化新的triangle对象的方式有点冗长,你需要手动设置sideheight的值,而不是将它们作为值传递给构造函数。然而,这很容易通过一个名为init()(或者如果你来自 PHP,叫__construct())的函数来解决,它充当构造函数并接受初始化参数。另外,让extendCopy()接受两个参数,一个是要继承的对象,另一个是要添加到副本中的属性的对象字面量。换句话说,就是合并两个对象。

深拷贝

之前讨论的extendCopy()函数创建了一个被称为浅拷贝的对象,就像之前的extend2()一样。浅拷贝的相反就是深拷贝。如前所述(在本章的通过引用复制时要注意部分),当你复制对象时,你只复制指向存储对象的内存位置的指针。这就是浅拷贝的情况。如果你在副本中修改一个对象,你也会修改原始对象。深拷贝避免了这个问题。

深拷贝的实现方式与浅拷贝相同-你遍历属性并逐个复制它们。然而,当你遇到指向对象的属性时,你会再次调用deepcopy函数:

    function deepCopy(p, c) { 
      c = c || {}; 
      for (var i in p) { 
        if (p.hasOwnProperty(i)) { 
          if (typeof p[i] === 'object') { 
            c[i] = Array.isArray(p[i]) ? [] : {}; 
    deepCopy(p[i], c[i]); 
          } else { 
            c[i] = p[i]; 
          } 
        } 
      } 
      return c; 
    } 

让我们创建一个具有数组和子对象作为属性的对象:

    var parent = { 
      numbers: [1, 2, 3], 
      letters: ['a', 'b', 'c'], 
      obj: { 
        prop: 1 
      }, 
      bool: true 
    }; 

让我们通过创建一个深拷贝和一个浅拷贝来测试一下。与浅拷贝不同,当你更新深拷贝的numbers属性时,原始对象不会受到影响:

    >var mydeep = deepCopy(parent); 
    >var myshallow = extendCopy(parent); 
    >mydeep.numbers.push(4,5,6); 
    6 
    >mydeep.numbers; 
    [1, 2, 3, 4, 5, 6] 
    >parent.numbers; 
    [1, 2, 3] 
    >myshallow.numbers.push(10); 
    4 
    >myshallow.numbers; 
    [1, 2, 3, 10] 
    >parent.numbers; 
    [1, 2, 3, 10] 
    >mydeep.numbers; 
    [1, 2, 3, 4, 5, 6] 

关于deepCopy()函数的两个注意事项:

  • 使用hasOwnProperty()过滤非自有属性总是一个好主意,以确保你不会带上别人对核心原型的添加。

  • Array.isArray()自 ES5 以来存在,因为否则很难区分真实数组和对象。最佳的跨浏览器解决方案(如果需要在 ES3 浏览器中定义isArray())看起来有点奇怪,但它有效:

    if (Array.isArray !== "function") { 
    Array.isArray = function (candidate) { 
        return  
    Object.prototype.toString.call(candidate) ===  
    '[object Array]'; 
    }; 
    } 

使用 object()方法

基于对象从对象继承的思想,Douglas Crockford 提倡使用一个接受对象并返回一个具有父对象作为原型的新对象的object()函数:

    function object(o) { 
    function F() {} 
    F.prototype = o; 
    return new F(); 
    } 

如果您需要访问uber属性,可以修改object()函数如下:

    function object(o) { 
    var n; 
    function F() {} 
    F.prototype = o; 
    n = new F(); 
    n.uber = o; 
    return n; 
    } 

使用this函数与使用extendCopy()相同,您可以从诸如twoDee之类的对象创建一个新对象,然后继续增强新对象:

    var triangle = object(twoDee); 
    triangle.name = 'Triangle'; 
    triangle.getArea = function () { 
    return this.side * this.height / 2; 
    }; 

新的三角形仍然表现得一样:

    >triangle.toString(); 
    "Shape, 2D shape, Triangle" 

这种模式也被称为原型继承,因为您使用父对象作为子对象的原型。它也被 ES5 采用并构建,称为Object.create()。这里是一个例子:

    >var square = Object.create(triangle); 

使用原型继承和复制属性的混合

当您使用继承时,您很可能希望使用已经存在的功能,然后在此基础上构建。这意味着通过从现有对象继承创建一个新对象,然后添加其他方法和属性。您可以使用刚刚讨论的最后两种方法的组合来一次性完成这个功能。

您可以:

  • 使用原型继承来使用现有对象作为新对象的原型

  • 将另一个对象的所有属性复制到新创建的对象中:

    function objectPlus(o, stuff) { 
      var n; 
      function F() {} 
      F.prototype = o; 
      n = new F(); 
      n.uber = o; 

     for (var i in stuff) { 
        n[i] = stuff[i]; 
        } 
      return n; 
    } 

此函数接受一个要继承的对象o和另一个具有要复制的附加方法和属性的对象stuff。让我们看看这个实际操作。

从基本shape对象开始:

    var shape = { 
    name: 'Shape', 
    toString: function () { 
    return this.name; 
    } 
    }; 

通过继承形状并添加更多属性来创建一个 2D 对象。附加属性只是使用对象文字创建的:

    var twoDee = objectPlus(shape, { 
      name: '2D shape', 
      toString: function () { 
        return this.uber.toString() + ', ' + this.name; 
      } 
    }); 

现在,让我们创建一个从 2D 继承并添加更多属性的triangle对象:

    var triangle = objectPlus(twoDee, { 
      name: 'Triangle', 
      getArea: function () { return this.side * this.height / 2; 
     }, 
      side: 0, 
      height: 0 
    }); 

您可以通过创建具有定义的sideheight的具体三角形my来测试所有这些工作:

    var my = objectPlus(triangle, { 
      side: 4, height: 4 
    }); 
    >my.getArea(); 
    8 
    >my.toString(); 
    "Shape, 2D shape, Triangle, Triangle" 

在执行toString()时,这里的区别在于Triangle名称重复了两次。这是因为具体实例是通过继承triangle创建的,所以还有一层继承。您可以给新实例一个名称:

    >objectPlus(triangle, { 
      side: 4,  
      height: 4, 
      name: 'My 4x4' 
    }).toString(); 
    "Shape, 2D shape, Triangle, My 4x4" 

这个objectPlus()甚至更接近 ES5 的Object.create();只是 ES5 的Object.create()使用称为属性描述符的东西来获取附加属性(第二个参数)(在附录 C 中讨论,内置对象)。

多重继承

多重继承是指一个子对象从多个父对象继承。一些面向对象的语言原生支持多重继承,而一些不支持。您可以从两方面进行论证,即多重继承很方便,或者它是不必要的,使应用程序设计复杂化,并且最好使用继承链。在漫长而寒冷的冬夜中留下多重继承的利弊讨论,让我们看看您如何在 JavaScript 中实际操作。

实现可以简单地将继承的概念扩展为从无限数量的输入对象继承。

让我们创建一个接受任意数量输入对象的multi()函数。您可以在另一个循环中包装复制属性的循环,该循环通过作为函数arguments传递的所有对象:

    function multi() { 
      var n = {}, stuff, j = 0, len = arguments.length; 
      for (j = 0; j <len; j++) { 
        stuff = arguments[j]; 
        for (var i in stuff) { 
          if (stuff.hasOwnProperty(i)) { 
            n[i] = stuff[i]; 
          } 
        } 
      } 
      return n; 
    } 

让我们通过创建三个对象-shapetwoDee和第三个未命名的对象来测试这一点。然后,创建一个triangle对象意味着调用multi()并传递所有三个对象:

    var shape = { 
      name: 'Shape', 
      toString: function () { 
        return this.name; 
      } 
    }; 

    var twoDee = { 
      name: '2D shape', 
      dimensions: 2 
    }; 

    var triangle = multi(shape, twoDee, { 
      name: 'Triangle', 
      getArea: function () { 
        return this.side * this.height / 2; 
      }, 
      side: 5, 
      height: 10 
    }); 

这样行得通吗?让我们看看。getArea()方法应该是自己的属性,dimensions应该来自twoDeetoString()应该来自shape

    >triangle.getArea(); 
    25 
    >triangle.dimensions; 
    2 
    >triangle.toString(); 
    "Triangle" 

请记住,multi()按照输入对象出现的顺序循环,如果发生两个对象具有相同的属性,最后一个将获胜。

混合

你可能会遇到混入这个术语。把混入想象成一个提供一些有用功能的对象,但不是用来被子对象继承和扩展的。前面概述的多重继承方法可以被认为是混入思想的一种实现。当你创建一个新对象时,你可以选择任何其他对象混入到你的新对象中。通过将它们全部传递给multi(),你可以获得它们所有的功能,而不使它们成为继承树的一部分。

寄生式继承

如果你喜欢在 JavaScript 中有各种不同的实现继承的方式,并且渴望了解更多,这里有另一种方式。这种模式,由道格拉斯·克罗克福德提出,被称为寄生式继承。它是关于一个函数通过从另一个对象中获取所有功能来创建对象,增强新对象,并返回它,假装它已经完成了所有工作。

这是一个普通对象,用对象字面量定义,并不知道它很快就会成为寄生关系的受害者:

    var twoD = { 
      name: '2D shape', 
      dimensions: 2 
    }; 

一个创建triangle对象的函数可以:

  • 使用twoD对象作为一个名为that的对象的原型(为了方便类似于this)。这可以以你之前看到的任何方式来完成,例如使用object()函数或复制所有属性。

  • 增加更多属性。

  • 返回that

        function triangle(s, h) { 
          var that = object(twoD); 
          that.name ='Triangle'; 
          that.getArea = function () { 
            return this.side * this.height / 2; 
          }; 
          that.side = s; 
          that.height = h; 
          return that; 
        } 

因为triangle()是一个普通函数,而不是构造函数,所以它不需要new运算符。然而,因为它返回一个对象,所以错误地使用new调用它也是可以的:

    >var t = triangle(5, 10); 
   >t.dimensions; 
    2 
    >var t2 = new triangle(5,5); 
    >t2.getArea(); 
    12.5 

请注意that只是一个名称,它没有特殊的含义,就像this一样。

借用构造函数

实现继承的另一种方式(本章最后一种,我保证)再次与构造函数有关,而不是直接与对象有关。在这种模式中,子级的构造函数使用call()apply()方法调用父级的构造函数。这可以称为偷取构造函数通过借用构造函数继承,如果你想更加微妙一些的话。

call()apply()方法在第四章对象中已经讨论过,但这里是一个复习;它们允许你调用一个函数并传递一个对象,该函数应该将其this值绑定到该对象。因此,为了继承目的,子构造函数调用父构造函数,并将子级新创建的this对象绑定为父级的this

让我们有这个父构造函数Shape()

    function Shape(id) { 
      this.id = id; 
    } 
    Shape.prototype.name = 'Shape'; 
    Shape.prototype.toString = function () { 
      return this.name; 
    }; 

现在,让我们定义Triangle(),它使用apply()来调用Shape()构造函数,传递this(使用new Triangle()创建的实例)和任何额外的参数:

   function Triangle() { 
    Shape.apply(this, arguments); 
    } 
    Triangle.prototype.name = 'Triangle'; 

请注意,Triangle()Shape()都向它们的原型添加了一些额外的属性。

现在,让我们通过创建一个新的triangle对象来测试一下:

    >var t = new Triangle(101); 
    >t.name; 
    "Triangle" 

新的triangle对象从父级继承了id属性,但它没有继承任何添加到父级prototype的东西:

    >t.id; 
    101 
    >t.toString(); 
    "[object Object]" 

三角形未能获取Shape函数的原型属性,因为从未创建过new Shape()实例,所以原型从未被使用。然而,你在本章的开头看到了如何做到这一点。你可以重新定义Triangle如下:

    function Triangle() { 
      Shape.apply(this, arguments); 
    } 
    Triangle.prototype = new Shape(); 
    Triangle.prototype.name = 'Triangle'; 

在这种继承模式中,父级的自有属性被重新创建为子级的自有属性。如果子级继承了数组或其他对象,则它是一个全新的值(不是引用),对其进行修改不会影响父级。

缺点是父级的构造函数被调用两次——一次用apply()继承自有属性,一次用new继承原型。事实上,父级的自有属性被继承了两次。让我们看一个简化的情景:

    function Shape(id) { 
      this.id = id; 
    } 
    function Triangle() { 
      Shape.apply(this, arguments); 
    } 
    Triangle.prototype = new Shape(101); 

在这里,我们将创建一个新实例:

    >var t = new Triangle(202); 
    >t.id; 
    202 

有一个自有属性id,但也有一个通过原型链传递下来的,准备好发挥作用的属性:

    >t.__proto__.id; 
    101 
    > delete t.id; 
    true 
    >t.id; 
    101 

借用构造函数并复制其原型

通过调用两次构造函数执行的双重工作问题可以很容易地得到纠正。您可以在父构造函数上调用apply()来获取所有自有属性,然后使用简单的迭代(或者如前所述的extend2())复制原型的属性:

    function Shape(id) { 
      this.id = id; 
    } 
    Shape.prototype.name = 'Shape'; 
    Shape.prototype.toString = function () { 
      return this.name; 
    }; 

    function Triangle() { 
      Shape.apply(this, arguments); 
    } 
    extend2(Triangle, Shape); 
    Triangle.prototype.name = 'Triangle'; 

让我们测试以下代码:

   >var t = new Triangle(101); 
    >t.toString(); 
    "Triangle" 
    >t.id; 
    101 

没有双重继承:

    >typeoft.__proto__.id; 
    "undefined" 

extend2()方法也可以访问uber(如果需要的话):

    >t.uber.name; 
    "Shape" 

案例研究-绘制形状

让我们以一个更实际的例子来完成本章,使用继承。任务是能够计算不同形状的面积和周长,并绘制它们,同时尽可能多地重用代码。

分析

让我们有一个包含所有共同部分的Shape构造函数。然后,让我们有TriangleRectangleSquare构造函数,它们都继承自Shape。正方形实际上是具有相同边长的矩形,因此在构建Square时让我们重用Rectangle

为了定义一个形状,您需要带有xy坐标的点。通用形状可以有任意数量的点。三角形由三个点定义,矩形(为了简单起见)由一个点和边长定义。任何形状的周长是其边长的总和。计算面积是形状特定的,将由每个形状实现。

Shape中的共同功能将是:

  • 一个draw()方法,可以根据给定的点绘制任何形状

  • 一个getParameter()方法

  • 包含一个points数组的属性

  • 其他所需的方法和属性

对于绘图部分,让我们使用<canvas>标签。它在早期 IE 中不受支持,但嘿,这只是一个练习。

让我们再添加两个辅助构造函数-PointLine。在定义形状时,Point将会有所帮助。Line将使计算更容易,因为它可以给出连接任意两个给定点的线段长度。

您可以在www.phpied.com/files/canvas/上玩一个可工作的示例。只需打开控制台,然后开始创建新形状,就像您马上会看到的那样。

实施

让我们从在空白 HTML 页面中添加一个canvas标签开始:

    <canvas height="600" width="800" id="canvas" /> 

然后,将 JavaScript 代码放在<script>标签内:

    <script> 
    // ... code goes here 
    </script> 

现在,让我们来看看 JavaScript 部分的内容。首先是辅助Point构造函数。它比以下内容更简单就不能再简单了:

    function Point(x, y) { 
      this.x = x; 
      this.y = y; 
    } 

请记住,canvas上的点的坐标从x=0y=0开始,这是左上角。右下角将是x=800y=600

实施

接下来是Line构造函数。它接受两个点,并使用毕达哥拉斯定理a² + b² = c²(想象一个直角三角形,斜边连接两个给定点)来计算它们之间的线段长度:

    function Line(p1, p2) { 
      this.p1 = p1; 
      this.p2 = p2; 
      this.length = Math.sqrt( 
      Math.pow(p1.x - p2.x, 2) + 
      Math.pow(p1.y - p2.y, 2) 
      ); 
    } 

接下来是Shape构造函数。形状将有它们的点(以及连接它们的线)作为自有属性。构造函数还调用一个初始化方法init(),该方法将在原型中定义:

    function Shape() { 
      this.points = []; 
      this.lines= []; 
      this.init(); 
    } 

现在,重要的部分-Shape.prototype的方法。让我们使用对象文字表示法定义所有这些方法。参考注释以了解每个方法的指导方针:

    Shape.prototype = { 
      // reset pointer to constructor 
      constructor: Shape, 

      // initialization, sets this.context to point 
      // to the context if the canvas object 
      init: function () { 
        if (this.context === undefined) { 
          var canvas = document.getElementById('canvas'); 
          Shape.prototype.context = canvas.getContext('2d'); 
        } 
      }, 

      // method that draws a shape by looping through this.points 
      draw: function () { 
        var i, ctx = this.context; 
        ctx.strokeStyle = this.getColor(); 
        ctx.beginPath(); 
        ctx.moveTo(this.points[0].x, this.points[0].y); 
        for (i = 1; i<this.points.length; i++) { 
          ctx.lineTo(this.points[i].x, this.points[i].y); 
        } 
        ctx.closePath(); 
        ctx.stroke(); 
      }, 

      // method that generates a random color 
      getColor: function () { 
        var i, rgb = []; 
        for (i = 0; i< 3; i++) { 
          rgb[i] = Math.round(255 * Math.random()); 
        } 
        return 'rgb(' + rgb.join(',') + ')'; 
      }, 

      // method that loops through the points array, 
      // creates Line instances and adds them to this.lines 
     getLines: function () { 
        if (this.lines.length> 0) { 
          return this.lines; 
        } 
        var i, lines = []; 
        for (i = 0; i<this.points.length; i++) { 
          lines[i] = new Line(this.points[i],  
          this.points[i + 1] || this.points[0]); 
        } 
        this.lines = lines; 
        return lines; 
      }, 

     // shell method, to be implemented by children 
      getArea: function () {}, 

      // sums the lengths of all lines 
      getPerimeter: function () { 
        var i, perim = 0, lines = this.getLines(); 
        for (i = 0; i<lines.length; i++) { 
          perim += lines[i].length; 
        } 
        return perim; 
      } 
    }; 

现在,是子构造函数。首先是Triangle

    function Triangle(a, b, c) { 
      this.points = [a, b, c]; 

      this.getArea = function () { 
        var p = this.getPerimeter(), 
        s = p / 2; 
        return Math.sqrt( s * (s - this.lines[0].length) * 
          (s - this.lines[1].length) * (s - this.lines[2].length)); 
      }; 
    } 

Triangle构造函数接受三个点对象,并将它们分配给this.points(它自己的点集合)。然后,它实现getArea()方法,使用海伦公式:

    Area = s(s-a)(s-b)(s-c) 

s是半周长(周长除以二)。

接下来是Rectangle构造函数。它接收一个点(左上角的点)和两边的长度。然后,它从那一个点开始填充它的points数组:

    function Rectangle(p, side_a, side_b){ 
    this.points = [ 
    p, 
    new Point(p.x + side_a, p.y),// top right 
    new Point(p.x + side_a, p.y + side_b), // bottom right 
    new Point(p.x, p.y + side_b)// bottom left 
    ]; 
    this.getArea = function () { 
    return side_a * side_b; 
    }; 
    } 

最后一个子构造函数是Square。正方形是矩形的一种特殊情况,因此重用Rectangle是有意义的。这里最容易做的事情是借用构造函数:

    function Square(p, side){ 
      Rectangle.call(this, p, side, side); 
    } 

现在所有的构造函数都完成了,让我们来处理继承。任何伪经典模式(与对象不同,使用构造函数的模式)都可以。让我们尝试使用修改和简化的原型链模式(本章描述的第一种方法)。这种模式要求创建父类的新实例,并将其设置为子类的原型。在这种情况下,不需要为每个子类创建一个新实例-它们都可以共享它:

    (function () { 
    var s = new Shape(); 
    Triangle.prototype = s; 
    Rectangle.prototype = s; 
    Square.prototype = s; 
    })(); 

测试

让我们通过绘制形状来测试这一点。首先,为三角形定义三个点:

    >var p1 = new Point(100, 100); 
    >var p2 = new Point(300, 100); 
    >var p3 = new Point(200, 0); 

现在你可以通过将三个点传递给Triangle构造函数来创建一个三角形:

    >var t = new Triangle(p1, p2, p3); 

你可以调用绘制三角形的方法在canvas上,并获得它的面积和周长:

    >t.draw(); 
    >t.getPerimeter(); 
    482.842712474619 
    >t.getArea(); 
    10000.000000000002 

现在让我们来玩一个矩形实例:

    >var r = new Rectangle(new Point(200, 200), 50, 100); 
    >r.draw(); 
    >r.getArea(); 
    5000 
    >r.getPerimeter(); 
    300 

最后,让我们玩一个正方形:

    >var s = new Square(new Point(130, 130), 50); 
    >s.draw(); 
    >s.getArea(); 
    2500 
    >s.getPerimeter(); 
    200 

画这些形状很有趣。你也可以像下面的例子一样懒惰,重用三角形的点来画另一个正方形:

    > new Square(p1, 200).draw(); 

测试的结果将类似于以下内容:

测试

练习

让我们做以下练习:

  1. 使用原型继承模式实现多重继承,而不是属性复制。以下是一个例子:
        var my = objectMulti(obj, another_obj, a_third, { 
        additional: "properties" 
        }); 

additional属性应该是自有属性;其余的属性应该混合到原型中。

  1. 使用canvas示例进行练习。尝试不同的东西。以下是一些例子:
  • 画几个三角形、正方形和矩形。

  • 为更多的形状添加构造函数,比如TrapezoidRhombusKitePentagon。如果你想了解更多关于canvas标签的知识,也可以创建一个Circle构造函数。它需要覆盖父类的draw()方法。

  • 你能想到另一种方法来解决这个问题并使用另一种类型的继承吗?

  • 选择一个使用uber作为子类访问其父类的方法。添加功能,使父类可以跟踪他们的子类,也许使用包含children数组的属性?

总结

在本章中,你学到了实现继承的许多方式(模式),以下表格对它们进行了总结。不同类型大致可以分为以下几类:

  • 适用于构造函数的模式

  • 适用于对象的模式

你也可以根据它们是否:

  • 使用原型

  • 复制属性

  • 两者都做(复制原型的属性):

# 名称 示例 分类 备注
1 原型链(伪经典)
Child.prototype = new Parent();   

|

  • 适用于构造函数

  • 使用原型链

|

  • 默认机制

  • 提示-将所有希望被重用的属性/方法移动到原型中,并将不可重用的内容添加为自有属性

|

2 仅继承原型
Child.prototype = Parent.prototype;   

|

  • 适用于构造函数

  • 复制原型(没有原型链,因为所有的对象共享同一个原型对象)

|

  • 更高效;不会仅为了继承而创建新实例

  • 运行时原型链查找;速度快,因为没有链

  • 缺点:子类可以修改父类的功能

|

3 临时构造函数
function extend(Child, Parent) {   
 var F = function(){};   
 F.prototype = Parent.prototype;   
Child.prototype = new F();   
Child.prototype.constructor = Child;   
Child.uber = Parent.prototype;   
}   

|

  • 适用于构造函数

  • 使用原型链

|

  • 与#1 不同,它只继承原型的属性;自有属性(在构造函数内部使用 this 创建的属性)不会被继承。

  • 提供方便的访问父类的方法(通过uber

|

4 复制prototype属性
function extend2(Child, Parent) {   
var p = Parent.prototype;   
var c = Child.prototype;   
 for (var i in p) {   
 c[i] = p[i];   
 }   
c.uber = p;   
}   

|

  • 适用于构造函数

  • 复制属性

  • 使用原型链

|

  • 父类原型的所有属性都成为子类原型的属性

  • 不需要仅为了继承目的创建一个新对象

  • 更短的原型链

|

5 复制所有属性(浅复制)
function extendCopy(p) {   
var c = {};    
 for (var i in p) {   
 c[i] = p[i];   
 }   
c.uber = p;   
 return c;   
}   

|

  • 适用于对象

  • 复制属性

|

  • 简单

  • 不使用原型

|

6 深复制 与前一个相同,但是递归到对象中
  • 适用于对象

  • 复制属性

|

  • 与#5 相同,但是克隆对象和数组

|

7 原型继承
function object(o){   
 function F() {}   
F.prototype = o;   
 return new F();   
}   

|

  • 适用于对象

  • 使用原型链

|

  • 没有伪类,对象从对象继承

  • 利用原型的好处

|

8 扩展和增强
function objectPlus(o, stuff) {   
var n;   
 function F() {}   
F.prototype = o;   
 n = new F();   
n.uber = o;   
 for (var i in stuff) {   
 n[i] = stuff[i];   
 }   
 return n;   
}   

|

  • 适用于对象

  • 使用原型链

  • 复制属性

|

  • 原型继承(#7)和复制属性(#5)的混合

  • 一次函数调用同时继承和扩展

|

9 多重继承
function multi() {   
var n = {}, stuff, j = 0,   
len = arguments.length;   
 for (j = 0; j <len; j++) {   
 stuff = arguments[j];   
 for (var i in stuff) {   
 n[i] = stuff[i];   
 }   
 }   
 return n;   
}    

|

  • 适用于对象

  • 复制属性

|

  • 一种混合风格的实现

  • 按出现顺序复制所有父对象的所有属性

|

10 寄生继承
function parasite(victim) {   
var that = object(victim);   
that.more = 1;   
 return that;   
}   

|

  • 适用于对象

  • 使用原型链

|

  • 类似构造函数的函数创建对象

  • 复制一个对象,并增强并返回副本

|

11 借用构造函数
function Child() {   
Parent.apply(this, arguments);   
}   

|

  • 适用于构造函数

|

  • 仅继承自有属性

  • 可以与#1 结合,也可以继承原型

  • 处理子类继承对象属性(因此通过引用传递)时的便捷方式

|

12 借用构造函数并复制原型
function Child() {   
Parent.apply(this, arguments);   
}   

extend2(Child, Parent);   

|

  • 适用于构造函数

  • 使用原型链

  • 复制属性

|

  • #11 和#4 的组合

  • 允许您继承自有属性和原型属性,而不需要两次调用父构造函数。

|

有这么多选择,你一定想知道哪一个是正确的。这取决于你的风格和偏好,你的项目、任务和团队。你更习惯以类为思维方式吗?那么选择一个与构造函数一起工作的方法。你只需要一个或几个类的实例吗?那么选择一个基于对象的模式。

这些是实现继承的唯一方式吗?不是。你可以从上表中选择一个模式,你可以混合它们,或者你可以想出自己的。重要的是要理解并熟悉对象、原型和构造函数;其余只是纯粹的快乐。

第八章:类和模块

在本章中,我们将探讨 ES6 中引入的一些最有趣的特性。JavaScript 是一种基于原型的语言,支持原型继承。在上一章中,我们讨论了对象的原型属性以及原型继承在 JavaScript 中的工作原理。ES6 引入了类。如果你来自 Java 等传统的面向对象语言,你会立即理解类的这些熟悉概念。然而,在 JavaScript 中它们并不相同。JavaScript 中的类是对我们在上一章中讨论的原型继承的一种语法糖。

在本章中,我们将详细了解 ES6 类和模块-这些是 JavaScript 这一版本中的受欢迎的变化,使面向对象编程OOP)和继承变得更加容易。

如果你来自传统的面向对象语言,原型继承可能会让你感到有些不适。ES6 类为你提供了更传统的语法,让你熟悉 JavaScript 中的原型继承。

在我们尝试深入了解类之前,让我向你展示为什么你应该使用 ES6 类语法而不是 ES5 的原型继承语法。

在这个片段中,我正在创建一个PersonEmployeeEngineer的类层次结构,非常简单。首先,我们将看到 ES5 原型继承,写法如下:

    var Person = function(firstname) { 
        if (!(this instanceof Person)) { 
            throw new Error("Person is a constructor"); 
        } 
        this.firstname = firstname; 
    }; 

    Person.prototype.giveBirth = function() { 
        // ...we give birth to the person 
    }; 

    var Employee = function(firstname, lastname, job) { 
        if (!(this instanceof Employee)) { 
            throw new Error("Employee is a constructor"); 
        } 
        Person.call(this, firstname); 
        this.job = job; 
    };  
    Employee.prototype = Object.create(Person.prototype); 
    Employee.prototype.constructor = Employee; 
    Employee.prototype.startJob = function() { 
        // ...Employee starts job 
    }; 

    var Engineer = function(firstname, lastname, job, department) { 
        if (!(this instanceof Engineer)) { 
            throw new Error("Engineer is a constructor"); 
        } 
        Employee.call(this, firstname, lastname, job); 
        this.department = department; 
    }; 
    Engineer.prototype = Object.create(Employee.prototype); 
    Engineer.prototype.constructor = Engineer; 
    Engineer.prototype.startWorking = function() { 
        // ...Engineer starts working 
    }; 

现在让我们看一下使用 ES6 类语法的等效代码:

    class Person { 
        constructor(firstname) { 
            this.firsnamet = firstname; 
        } 
        giveBirth() { 
            // ... a person is born 
        } 
    } 

    class Employee extends Person { 
        constructor(firstname, lastname, job) { 
            super(firstname); 
            this.lastname = lastname; 
            this.position = position; 
        } 

         startJob() { 
            // ...Employee starts job 
        } 
    } 

    class Engineer extends Employee { 
        constructor(firstname, lastname, job, department) { 
            super(firstname, lastname, job); 
            this.department = department; 
        } 

        startWorking() { 
            // ...Engineer starts working 
        } 
    } 

如果你观察前面的两个代码片段,你会发现第二个例子非常整洁。如果你已经了解 Java 或 C#,你会感到非常熟悉。然而,要记住的一点是,类并没有引入任何新的面向对象的继承模型到语言中,而是引入了一种更好的创建对象和处理继承的方式。

定义类

在底层,类是特殊的函数。就像你可以使用函数表达式和声明来定义函数一样,你也可以定义类。定义类的一种方式是使用类声明。

你可以使用class关键字和类的名称。这个语法非常类似于 Java 或 C#:

    class Car { 
      constructor(model, year){ 
        this.model = model; 
        this.year = year; 
      } 
    } 
    console.log(typeof Car); //"function" 

为了证明类是一个特殊的函数,如果我们使用typeof操作符来获取Car类,我们会得到一个函数。

类和普通函数之间有一个重要的区别。普通函数会被提升,而类不会。普通函数在进入声明它的作用域时立即可用;这被称为提升,这意味着普通函数可以在作用域中的任何地方声明,并且它将可用。然而,类不会被提升;它们只有在声明后才可用。对于普通函数,你可以说:

    normalFunction();   //use first 
    function normalFunction() {}  //declare later 

然而,你不能在声明之前使用类,例如:

    var ford = new Car(); //Reference Error 
    class Car {} 

定义类的另一种方式是使用类表达式。类表达式,就像函数表达式一样,可能有也可能没有名称。

以下示例显示了一个匿名类表达式:

    const Car = class { 
      constructor(model, year){ 
        this.model = model; 
        this.year = year; 
      } 
    } 

如果你给类表达式命名,这个名称只在类的主体内部可用,而在外部是不可用的。

    const NamedCar = class Car{ 
      constructor(model, year){ 
        this.model = model; 
        this.year = year; 
      } 
      getName() { 
          return Car.name; 
      } 
    } 
    const ford = new NamedCar(); 
    console.log(ford.getName()); // Car 
    console.log(ford.name); // ReferenceError: name is not defined 

如你所见,在这里,我们给Car类命名。这个名称在类的主体内是可用的,但当我们尝试在类外部访问它时,会得到一个引用错误。

在类的成员之间不能使用逗号。但分号是有效的。这很有趣,因为 ES6 忽略了分号,关于在 ES6 中使用分号有激烈的争论。考虑以下代码片段作为例子:

    class NoCommas { 
      method1(){} 
      member1;  //This is ignored and can be used to 
        separate class members 
      member2,  //This is an error 
      method2(){} 
    } 

一旦定义,我们可以通过new关键字而不是函数调用来使用类;以下是例子:

    class Car { 
      constructor(model, year){ 
        this.model = model; 
        this.year = year; 
      } 
    } 
    const fiesta = new Car('Fiesta','2010'); 

构造函数

到目前为止,我们在示例中使用了constructor函数。构造函数是用于创建和初始化使用类创建的对象的特殊方法。一个类中只能有一个构造函数。构造函数与普通构造函数有些不同。与普通构造函数不同,类构造函数可以通过super()调用其父类构造函数。当我们讨论继承时,我们将详细讨论这一点。

原型方法

原型方法是类的原型属性,并且被类的实例继承。

原型方法也可以有gettersetter方法。获取器和设置器的语法与 ES5 相同:

    class Car { 
      constructor(model, year){ 
        this.model = model; 
        this.year = year; 
      } 
      get model(){ 
        return this.model 
      } 

      calculateCurrentValue(){ 
        return "7000" 
      } 
    } 
    const fiesta = new Car('Fiesta','2010') 
    console.log(fiesta.model) 

同样,计算属性也是支持的。您可以使用表达式定义方法的名称。表达式需要放在方括号内。我们在之前的章节中讨论了这种简写语法。以下都是等价的:

    class CarOne { 
        driveCar() {} 
    } 
    class CarTwo { 
        ['drive'+'Car']() {} 
    } 
    const methodName = 'driveCar'; 
    class CarThree { 
        [methodName]() {} 
    } 

静态方法

静态方法与类相关联,而不是与该类的实例(对象)相关联。换句话说,您只能使用类的名称来调用静态方法。静态方法在不实例化类的情况下被调用,它们不能在类的实例上调用。静态方法在创建实用程序或辅助方法时很受欢迎。考虑以下代码片段:

    class Logger { 
      static log(level, message) { 
        console.log(`${level} : ${message}`) 
      } 
    } 
    //Invoke static methods on the Class 
    Logger.log("ERROR","The end is near") //"ERROR : The end is near" 

    //Not on instance 
    const logger = new Logger("ERROR") 
    logger.log("The end is near")     //logger.log is not a function 

静态属性

您可能会问,我们有静态方法,那静态属性呢?在忙于准备 ES6 的过程中,他们没有添加静态属性。它们将在语言的未来迭代中添加。

生成器方法

我们在前几章中讨论了非常有用的生成器函数。您可以将生成器函数作为类的一部分添加,并将它们称为生成器方法。生成器方法很有用,因为您可以将它们的键定义为Symbol.iterator。以下示例显示了如何在类内部定义生成器方法:

    class iterableArg { 
        constructor(...args) { 
            this.args = args; 
        } 
        * [Symbol.iterator]() { 
            for (const arg of this.args) { 
                yield arg; 
            } 
        } 
    } 

    for (const x of new iterableArg('ES6', 'wins')) { 
        console.log(x); 
    } 

    //ES6 
    //wins 

子类化

到目前为止,我们讨论了如何声明类以及类可以支持的成员类型。类的一个主要用途是作为创建其他子类的模板。当您从一个类创建子类时,您会继承父类的属性,并通过添加自己的特性来扩展父类。

让我们看看继承的以下事实例子:

    class Animal {  
      constructor(name) { 
        this.name = name; 
      } 
        speak() { 
        console.log(this.name + ' generic noise'); 
      } 
    } 
    class Cat extends Animal { 
      speak() { 
        console.log(this.name + ' says Meow.'); 
      } 
    } 
    var c = new Cat('Grace');  
    c.speak();//"Grace says Meow." 

在这里,Animal是基类,Cat类是从类Animal派生出来的。扩展子句允许您创建现有类的子类。此示例演示了子类化的语法。让我们通过编写以下代码来进一步增强此示例:

    class Animal {  
      constructor(name) { 
        this.name = name; 
      } 
      speak() { 
        console.log(this.name + ' generic noise'); 
      } 
    } 
    class Cat extends Animal { 
      speak() { 
        console.log(this.name + ' says Meow.'); 
      } 
   } 
    class Lion extends Cat { 
      speak() { 
        super.speak(); 
        console.log(this.name + ' Roars....'); 
      } 
    } 
    var l = new Lion('Lenny');  
    l.speak(); 
    //"Lenny says Meow." 
    //"Lenny Roar...." 

在这里,我们使用super关键字来调用父类的函数。以下是super关键字的三种用法:

  • 您可以使用super (<params>)作为函数调用来调用父类的构造函数

  • 您可以使用super.<parentClassMethod>来访问父类方法

  • 您可以使用super.<parentClassProp>来访问父类属性

在派生类构造函数中,您必须在使用this关键字之前调用super()方法;例如,以下代码将失败:

    class Base {} 
    class Derive extends Base { 
      constructor(name){ 
        this.name = name; //'this' is not allowed before super() 
      } 
    } 

您不能将派生构造函数隐式地使用super()方法作为错误:

    class Base {} 
    class Derive extends Base { 
      constructor(){  //missing super() call in constructor 
      } 
    } 

如果您没有为基类提供构造函数,则将使用以下构造函数:

    constructor() {} 

对于派生类,默认构造函数如下:

    constructor(...args){ 
      super(...args); 
    } 

混入

JavaScript 仅支持单一继承。最多,一个类可以有一个超类。当您想要创建类层次结构,但又想要从不同来源继承工具方法时,这是有限的。

假设我们有一个场景,我们有一个Person类,并且我们创建一个子类Employee

    class Person {} 
    class Employee extends Person{} 

我们还希望从两个实用类BackgroundCheck(此类执行员工背景检查)和Onboard(此类处理员工入职流程,例如打印工牌等)中继承函数:

    class BackgroundCheck { 
      check() {} 
    } 
    class Onboard { 
      printBadge() { } 
    } 

BackgroundCheckOnboard类都是模板,它们的功能将被多次使用。这种模板(抽象子类)称为 mixin。

由于 JavaScript 不支持多重继承,我们将采用不同的技术来实现这一点。在 ES6 中实现 mixin 的一种流行方式是编写一个以超类为输入的函数,并将子类扩展为输出,例如:

    class Person {} 
    const BackgroundCheck = Tools => class extends Tools { 
      check() {} 
    }; 
    const Onboard = Tools => class extends Tools { 
      printBadge() {} 
    }; 
    class Employee extends BackgroundCheck(Onboard(Person)){  
    } 

这基本上意味着EmployeeBackgroundCheck的子类,而BackgroundCheck又是Onboard的子类,而Onboard又是Person的子类。

模块

JavaScript 模块并不新鲜。事实上,有一段时间支持模块的库。然而,ES6 提供了内置模块。传统上,JavaScript 主要用于浏览器,大部分 JavaScript 代码要么是嵌入式的,要么足够小,可以轻松管理。事情已经改变。JavaScript 项目现在规模庞大。如果没有一个有效的系统将代码分散到文件和目录中,管理代码将变成一场噩梦。

ES6 模块是文件。一个文件对应一个模块。没有模块关键字。您在模块文件中编写的任何代码都是局部的,除非您导出它。您可能在一个模块中有一堆函数,但您只想导出其中的一些。您可以以几种方式导出模块功能。

第一种方法是使用export关键字。您可以导出任何顶级functionclassvarletconst

以下示例显示了server.js中的一个模块,我们在其中导出了一个function,一个class和一个const。我们不导出processConfig()函数,任何导入此模块的文件都无法访问未导出的函数:

    //----------------server.js--------------------- 
    export const port = 8080; 
    export function startServer() { 
      //...start server 
    } 
    export class Config { 
      //... 
    } 
    function processConfig() { 
      //... 
    } 

任何可以访问server.js的代码都可以导入导出的功能:

    //--------------app.js---------------------------- 
    import {Config, startServer} from 'server' 
    startServer(port); 

在这种情况下,另一个 JavaScript 文件正在从server模块(对应的 JavaScript 文件server.js)中导入ConfigstartServer(我们省略了文件扩展名)。

您还可以导入模块中导出的所有内容:

    import * from 'server' 

如果只有一件事要导出,可以使用默认导出语法。考虑以下代码片段作为示例:

    //----------------server.js--------------------- 
    export default class { 
      //... 
    } 
    //--------------app.js---------------------------- 
    import Server from 'server'; 
    const s = new Server(); 

在这个例子中,我们将保持类匿名,因为我们可以在外部使用模块名称本身作为引用。

在 ES6 模块之前,外部库支持了几种模块方法。它们为 ES6 制定了相当不错的指南/风格。ES6 遵循以下风格:

  • 模块是单例的。模块只被导入一次,即使您在代码中尝试多次导入它。

  • 变量、函数和其他类型的声明都是局部的。只有用export标记的声明才能在模块外部进行import

  • 模块可以从其他模块导入。以下是引用其他模块的三种选项:

  • 您可以使用相对路径("../lib/server");这些路径是相对于导入模块的文件解析的。例如,如果您从<project_path>/src/app.js导入模块,并且模块文件位于<project_path>/lib/server.js,您需要提供相对于app.js的路径 - 在这种情况下是../lib/server

  • 绝对路径也可以直接指向模块文件。

  • 在导入模块时,可以省略文件的.js扩展名。

在我们深入了解 ES6 模块系统的更多细节之前,我们需要了解 ES5 是如何通过外部库支持它们的。ES5 有两种不兼容的模块系统,分别是:

  • CommonJS:这是主导标准,因为 Node.js 采用了它

  • AMD异步模块定义):这比 CommonJS 稍微复杂一些,设计用于异步模块加载,并针对浏览器

ES6 模块的目标是让来自任何这些系统的工程师都能轻松使用。

导出列表

与使用export关键字为模块中的每个导出函数或类打标签不同,您可以编写一个包含要从模块导出的所有内容的单个列表,如下所示:

    export {port, startServer, Config}; 
    const port = 8080; 
    function startServer() { 
      //...start server 
    } 
    class Config { 
      //... 
    } 
    function processConfig() { 
      //... 
    } 

模块的第一行是导出的列表。您可以在模块文件中有多个export列表,并且列表可以出现在文件的任何位置。您还可以在同一个模块文件中同时拥有export列表和export声明的混合,但是您只能export一个名称一次。

在一个大型项目中,有时会遇到名称冲突的情况。假设您导入了两个模块,并且它们都导出了相同名称的函数。在这种情况下,您可以按如下方式重命名导入:

    import {trunc as StringLib} from "../lib/string.js" 
    import {trunc as MathLib} from "../lib/math.js" 

在这里,导入的两个模块都导出了一个名为trunc的名称,因此创建了名称冲突。我们可以为它们取别名以解决这个冲突。

您也可以在导出时进行重命名,如下所示:

    function v() {} 
    function v2() {} 
    export { 
      v as functionV(), 
      v2 as functionV2(), 
      v2 as functionLatest() 
    } 

如果您已经在使用 ES5 模块系统,ES6 模块可能看起来多余。然而,对于语言来说,支持这样一个重要的功能非常重要。ES6 模块语法也是标准化的,比其他替代方案更紧凑。

总结

在本章中,我们专注于理解 ES6 类。ES6 类正式支持了使用函数和原型模拟类似继承层次结构的常见 JavaScript 模式。它们是基于原型的 OO 的语法糖,为鼓励互操作性的类模式提供了方便的声明形式。ES6 类提供了一个更好、更清晰的语法,用于创建这些对象并处理继承。ES6 类支持构造函数、实例和静态方法、(基于原型的)继承和 super 调用。

到目前为止,JavaScript 缺少最基本的功能之一 - 模块。在 ES6 之前,我们使用 CommonJS 或 AMD 编写模块。ES6 正式将模块引入 JavaScript。在本章中,我们详细了解了 ES6 中模块的使用。

下一章将重点介绍 ES6 的另一个有趣的新增功能 - 代理和承诺。

第九章:承诺和代理

本章介绍了异步编程的重要概念,以及 JavaScript 如何成为利用它的理想语言。本章我们还将介绍代理的元编程这两个概念是在 ES6 中引入的。

在本章中,我们的主要重点是理解异步编程,在我们深入语言特定的构造之前,让我们花时间来理解这个概念。

第一个模型-同步模型-是一切的开始。这是最简单的编程模型。每个任务都是依次执行的,只有在第一个任务完成执行后,下一个任务才能开始。当你在这个模型中编程时,我们期望当前任务之前的所有任务都已经完成,没有错误。看一下以下图:

Promises and Proxies

单线程异步模型是我们都熟悉的模型。然而,这个模型可能是低效的和需要优化的。对于由几个不同任务组成的任何非平凡程序,这个模型可能会很慢。以以下假设情景为例:

    var result = database.query("SELECT * FROM table");      
    console.log("After reading from the database"); 

考虑到同步模型,两个任务是依次执行的。这意味着第二个语句只有在第一个语句完成执行后才会执行。假设第一个语句是一个昂贵的任务,需要 10 秒(从远程数据库读取甚至需要更长的时间是正常的),第二个语句将被阻塞。

当你需要编写高性能和可扩展的系统时,这是一个严重的问题。还有另一个问题,当你编写需要为人类交互编写接口的程序时,比如我们在浏览器上运行的网站。当你执行可能需要一些时间的任务时,你不能阻塞用户。他们可能正在输入输入字段中的内容,而昂贵的任务正在运行;如果我们在忙于执行昂贵的操作时阻止用户输入,那将是一种糟糕的体验。在这种情况下,昂贵的任务需要在后台运行,而我们可以愉快地接受用户的输入。

为了解决这个问题,一个解决方案是将每个任务拆分成自己的控制线程。这被称为多线程线程模型。考虑以下图:

Promises and Proxies

不同之处在于任务的拆分方式。在线程模型中,每个任务都在自己的控制线程中执行。通常,线程由操作系统管理,并且可以在不同的 CPU 核心上并行运行,或者在单个核心上通过 CPU 进行适当的线程调度。对于现代 CPU 来说,线程模型在性能上可以非常优化。许多语言支持这种流行的模型。尽管是一种流行的模型,线程模型在实践中可能会很复杂。线程需要相互通信和协调。线程间通信可能会很快变得棘手。还有线程模型的变体,其中状态是不可变的。在这种情况下,模型变得更简单,因为每个线程负责不可变状态,而无需在线程之间管理状态。

异步编程模型

第三个模型是我们最感兴趣的。在这个模型中,任务在单个控制线程中交错进行。考虑以下图:

异步编程模型

异步模型更简单,因为你只有一个线程。当你执行一个任务时,你可以确定只有这个任务在执行。这个模型不需要复杂的线程协调机制,因此更可预测。线程模型和异步模型之间还有一个区别;在线程模型中,你没有办法控制线程的执行,因为线程调度大部分是由操作系统完成的。然而,在异步模型中,没有这样的挑战。

在哪些情况下,异步模型可以胜过同步模型?如果我们只是将任务分成更小的块,直觉上,即使这些更小的块在最后加起来也会花费相当多的时间。

还有一个重要的因素我们还没有考虑。当你执行一个任务时,你最终会等待某些东西-磁盘读取、数据库查询或网络调用;这些都是阻塞操作。当你进入阻塞模式时,你的任务在同步模型中只是等待。看一下下面的图:

异步编程模型

在上图中,黑色块是任务等待某些东西的地方。什么是可能导致这种阻塞的典型操作?任务在 CPU 和 RAM 中执行。典型的 CPU 和 RAM 可以处理数据传输命令,比典型的磁盘读取或网络调用快几个数量级。

提示

请参考 CPU、内存和磁盘之间延迟的比较(gist.github.com/jboner/2841832)。

当你的任务等待来自这些来源的I/O输入/输出)时,延迟是不可预测的。对于一个做大量 I/O 的同步程序来说,这是性能不佳的原因。

同步和异步模型之间最重要的区别是它们处理阻塞操作的方式。在异步模型中,当程序面临一个遇到阻塞的任务时,会在不等待阻塞操作完成的情况下执行另一个任务。在一个可能存在阻塞的程序中,异步程序的性能优于等效的同步程序,因为等待的时间更少。这种模型的一个略微不准确的可视化如下图所示:

异步编程模型

有了这个异步模型的概念背景,我们可以看看语言特定的构造来支持这个模型。

JavaScript 调用堆栈

在 JavaScript 中,函数调用形成一个帧的堆栈。考虑以下例子:

    function c(z2) { 
        console.log(new Error().stack); 
    } 
    function b(z1) { 
        c(z1+ 1); 
    } 
    function a(z) { 
        b(z + 1); 
    } 
    a(1);  

    //at c (eval at <anonymous>) 
    //at b (eval at <anonymous>) 
    //at a (eval at <anonymous>) 

当我们调用函数a()时,堆栈中创建了第一个帧,其中包含函数a()中的参数和所有局部变量。当函数a()调用函数b()时,第二个帧被创建并推到堆栈顶部。所有函数调用都是这样进行的。当c()函数返回时,堆栈顶部的帧被弹出,留下函数b()a();这一直持续到整个堆栈为空。这是必要的,因为一旦函数执行完毕,JavaScript 需要知道返回的位置。

消息队列

JavaScript 运行时包含一个消息队列。这个队列包含要处理的消息列表。这些消息是响应事件(如click或收到的 HTTP 响应)而排队的。每个消息都与一个回调函数相关联。

事件循环

浏览器标签在一个单线程-事件循环中运行。这个循环不断地从消息队列中挑选消息并执行与之相关联的回调。事件循环只是不断地从消息队列中挑选任务,而其他进程则向消息队列添加任务。其他进程,如定时器和事件处理程序,会并行运行并不断向队列中添加任务。

定时器

setTimeout()方法创建一个计时器,并等待直到触发。当计时器执行时,一个任务被添加到消息队列中。setTimeOut()方法接受两个参数:一个回调和持续时间(以毫秒为单位)。在持续时间之后,回调被添加到消息队列中。一旦回调被添加到消息队列中,事件循环最终会捡起它并执行它。然而,并没有保证事件循环何时会捡起回调。

完全运行

当事件循环从队列中取出消息时,相关的回调会完全运行。这意味着在处理下一个消息之前,消息会完全被处理。这个特性给了异步模型一种可预测性。由于在执行之间没有任何干预来中断任何消息,这个模型比其他模型简单得多。然而,一旦消息被取出,即使执行时间太长,浏览器上的任何其他交互也会被阻塞。

事件

您可以为对象注册事件处理程序,并异步接收方法的结果。以下示例显示了如何为XMLHttpRequest API 设置事件处理程序:

    var xhr = new XMLHttpRequest(); 
    xhr.open('GET', 'http://babeljs.io', true); 
    xhr.onload = function(e) { 
      if (this.status == 200) { 
        console.log("Works"); 
      } 
    }; 
    xhr.send(); 

在上面的片段中,我们创建了XMLHttpRequest类的对象。一旦请求对象被创建,我们将为其注册事件处理程序。例如onload()等事件处理程序在从open()方法接收到响应时会异步触发。

send()方法实际上并不会启动请求,它会将请求添加到消息队列中,以便事件循环捡起并执行与之关联的必要回调。

回调

Node.js 应用程序推广了这种接收异步数据的风格。回调是作为异步函数调用的最后一个参数传递的函数。

为了说明用法,让我们使用 Node.js 中读取文件的以下示例:

    fs.readFile('/etc/passwd', (err, data) => { 
      if (err) throw err; 
     console.log(data); 
    }); 

不要担心这里的一些细节。我们使用文件系统模块作为fs的别名。这个模块有一个readFile方法来异步读取文件。我们将文件路径和文件名作为第一个参数传递,将回调函数作为函数的最后一个参数传递。在示例中,我们使用匿名函数作为回调。

回调函数有两个参数-错误和数据。当readFile()方法成功时,回调函数接收data,如果失败,error参数将包含错误详情。

我们还可以使用稍微函数式的风格来编写相同的回调。考虑以下示例:

    fs.readFile('/etc/passwd',  
      //success 
      function(data) { 
        console.log(data) 
      }, 
      //error 
      function(error) { 
        console.log(error) 
      } 
    );   

这种传递回调的风格也被称为连续传递风格CPS);执行的下一步或继续作为参数传递。下面的例子进一步说明了回调的 CPS 风格:

    console.log("1"); 
    cps("2", function cps_step2(val2){ 
      console.log(val2); 
      cps("3", function cos_step3(val3){ 
        console.log(val3); 
      }) 
      console.log("4"); 
    }); 
    console.log("5"); 
    //1 5 2 4 3 

    function cps(val, callback) { 
      setTimeout(function () { 
            callback(val); 
      }, 0); 
    } 

我们将为每个步骤提供继续(下一个回调)。这种嵌套的回调风格有时也会引起一个问题,被称为回调地狱。

回调和 CPS 引入了一种完全不同的编程风格。虽然与其他构造相比,理解回调更容易,但回调可能会创建稍微难以理解的代码。

承诺

ES6 引入了承诺作为回调的替代。与回调一样,承诺用于检索异步函数调用的结果。使用承诺比回调更容易,并产生更可读的代码。然而,为了为您的异步函数实现承诺需要更多的工作。

承诺对象代表一个可能现在或将来可用的值,或者可能永远不可用。顾名思义,承诺可能会被实现或被拒绝。承诺充当了最终结果的占位符。

承诺有三种互斥的状态,如下所示:

  1. 在结果准备好之前,承诺是待定的;这是初始状态。

  2. 当结果准备好时,承诺是实现的。

  3. 发生错误时,promise 被拒绝

当一个待定的 promise 被完成或拒绝时,与 promise 的then()方法排队的相关回调/处理程序将被执行。

promise 的目的是为 CPS 回调提供更好的语法。典型的 CPS 风格的异步函数如下:

    asyncFunction(arg, result => { 
      //... 
    }) 

前面的代码可以用 promise 写得有些不同,如下面的代码行所示:

    asyncFunction(arg). 
    then(result=>{ 
      //... 
    }); 

异步函数现在返回一个 promise,这是一个最终结果的占位符。使用then()方法注册的回调在结果准备好时会被通知。

你可以链接then()方法。当then()方法看到回调触发了另一个返回 promise 的异步操作时,它会返回该 promise。看一下以下例子:

    asyncFunction(arg) 
    .then(resultA=>{ 
      //... 
      return asyncFunctionB(argB); 
    }) 
    .then(resultB=>{ 
      //... 
    }) 

让我们看一个实际的例子,说明我们如何使用 promise。我们在 Node.js 中看到了一个典型的异步文件读取的例子;现在让我们看看当使用 promise 时,这个例子会是什么样子。为了唤起我们的记忆,我们写了类似以下的东西:

    fs.readFile('text.json', 
      function (error, text) { 
          if (error) { 
              console.error('Error while reading text file'); 
          } else { 
              try { 
                  //... 
              } catch (e) { 
                  console.error('Invalid content'); 
              } 
          } 
      }); 

我们在这里将回调视为延续;现在让我们看看如何使用 promise 来编写相同的函数:

    readFileWithPromises('text.json') 
    .then(text=>{ 
      //...process text 
    }) 
    .catch(error=>{ 
      console.error('Error while reading text file'); 
    }) 

现在回调通过结果和方法then()catch()被调用。错误处理更加清晰,因为我们不再写if...elsetry...catch构造了。

创建 promise

我们看到了如何消费 promise。现在,让我们看看如何生成它们。

作为生产者,你可以创建一个Promise对象,并通过Promise发送结果。这个构造看起来像以下的代码片段:

    const p = new Promise( 
      function (resolve, reject) { // (1) 

          if (   ) { 
              resolve(value); // success 
          } else { 
              reject(reason); // failure 
          } 
      }); 

Promise的参数是一个执行函数。执行函数处理 promise 的两种状态,如下:

  • 解决:如果结果成功生成,执行器通过resolve()方法将结果发送回来。这个方法通常会完成Promise对象。

  • 拒绝:如果发生错误,执行器通过reject()方法通知消费者。如果发生异常,也会通过reject()方法通知。

作为消费者,你可以通过then()catch()方法被通知 promise 的完成或拒绝。考虑以下代码片段作为例子:

    promise 
    .then(result => { /* promise fulfilled */ }) 
    .catch(error => { /* promise rejected */ }); 

现在我们对如何生成 promise 有了一些背景知识,让我们重新编写我们之前的异步文件read方法的例子,以生成 promise。我们将使用 Node.js 的文件系统模块和readFile()方法,就像上次一样。如果你不理解以下代码片段中的任何 Node.js 特定构造,请不要担心。考虑以下代码:

    import {readFile} from 'fs'; 
    function readFileWithPromises(filename) { 
        return new Promise( 
            function (resolve, reject) { 
                readFile(filename,  
                    (error, data) => { 
                        if (error) { 
                            reject(error); 
                        } else { 
                            resolve(data); 
                        } 
                    }); 
            }); 
    } 

在前面的代码片段中,我们创建了一个新的Promise对象并将其返回给消费者。正如我们之前看到的,Promise对象的参数是执行函数,执行函数负责Promise的两种状态-完成和拒绝。执行函数接受两个参数,resolvereject。这些是通知Promise对象状态给消费者的函数。

在执行函数内部,我们将调用实际的函数-readFile()方法;如果这个函数成功,我们将使用resolve()方法返回结果,如果有错误,我们将使用reject()方法通知消费者。

如果在then()中发生错误,它们会在后续的catch()块中被捕获。看一下以下代码:

    readFileWithPromises('file.txt') 
    .then(result=> { 'something causes an exception'}) 
    .catch(error=> {'Something went wrong'}); 

在这种情况下,then()的反应引起了异常或错误,后续的catch()块可以处理这个问题。

同样,在then()catch()处理程序中抛出的异常会传递给下一个错误处理程序。考虑以下代码片段:

    readFileWithPromises('file.txt') 
    .then(throw new Error()) 
    .catch(error=> {'Something went wrong'}); 

Promise.all()

一个有趣的用例是创建一个可迭代的 promise。假设您有一个要访问和解析结果的 URL 列表。您可以为每个 fetch URL 调用创建 promise,并单独使用它们,或者您可以创建一个包含所有 URL 的迭代器,并一次性使用 promise。Promise.all()方法将可迭代的 promise 作为参数。当所有 promise 都被实现时,数组将填充其结果。考虑以下代码作为示例:

    Promise.all([ 
        f1(), 
        f2() 
    ]) 
    .then(([r1,r2]) => { 
        //    
    }) 
    .catch(err => { 
        //.. 
    }); 

元编程和代理

元编程是指一种编程方法,其中程序了解其结构并可以操纵自身。许多语言支持元编程,以宏的形式。宏是函数式语言(如LISPLocator/ID Separation Protocol))中的重要构造。在 Java 和 C#等语言中,反射是一种元编程形式,因为程序可以使用反射来检查有关自身的信息。

在 JavaScript 中,您可以说对象的方法允许您检查结构,因此它们提供了元编程。有三种元编程范式(元对象协议的艺术,Kiczales 等人,mitpress.mit.edu/books/art-metaobject-protocol):

  • 内省: 这给予程序内部的只读访问

  • 自我修改: 这使得对程序进行结构性更改成为可能

  • 干预: 这改变了语言语义

Object.keys()方法是内省的一个例子。在下面的例子中,程序正在检查自己的结构:

    const introspection = { 
      intro() { 
        console.log("I think therefore I am"); 
      } 
    } 
    for (const key of Object.keys(introspection)){ 
      console.log(key);  //intro 
    } 

在 JavaScript 中,通过改变对象的属性,也可以进行自我修改。

然而,干预或更改语言语义的能力在 ES6 之前在 JavaScript 中是不可用的。代理被引入以开放这种可能性。

代理

您可以使用代理来确定对象的行为,每当访问其属性时,称为目标。代理用于定义对象的基本操作的自定义行为,例如查找属性、函数调用和赋值。

代理需要两个参数,如下所示:

  • 处理程序: 对于要自定义的每个操作,您需要一个handler方法。此方法拦截操作,有时称为陷阱。

  • 目标: 当handler未拦截操作时,target作为回退使用。

让我们看下面的例子,以更好地理解这个概念:

    var handler = { 
      get: function(target, name){ 
        return name in target ? target[name] :42; 
      } 
    } 
    var p = new Proxy({}, handler); 
    p.a = 100; 
    p.b = undefined; 
    console.log(p.a, p.b); // 100, undefined 
    console.log('c' in p, p.c); // false, 42 

在这个例子中,我们正在捕获从对象获取属性的操作。如果属性不存在,我们将返回42作为默认属性值。我们使用get处理程序来捕获此操作。

您可以使用代理在将其设置在对象上之前验证值。为此,我们可以捕获set处理程序如下:

    let ageValidator = { 
      set: function(obj, prop, value) { 
        if (prop === 'age') { 
          if (!Number.isInteger(value)) { 
            throw new TypeError('The age is not an number'); 
          } 
          if (value > 100) { 
            throw new RangeError('You cant be older than 100'); 
          } 
        } 
        // If no error - just store the value in the property 
        obj[prop] = value; 
      } 
    }; 
    let p = new Proxy({}, ageValidator); 
    p.age = 100; 
    console.log(p.age); // 100 
    p.age = 'Two'; // Exception 
    p.age = 300; // Exception 

在上面的例子中,我们正在捕获set处理程序。当我们设置对象的属性时,我们正在捕获该操作并引入值的验证。如果值有效,我们将设置属性。

函数陷阱

如果目标是函数,可以捕获两种操作:applyconstruct

要拦截函数调用,您需要捕获getapply操作。首先获取函数,然后应用调用该函数。因此,您get函数并返回函数。

让我们考虑以下示例,以了解方法拦截是如何工作的:

    var car = { 
      name: "Ford", 
      method_1: function(text){ 
        console.log("Method_1 called with "+ text); 
      } 
    } 
    var methodInterceptorProxy = new Proxy(car, { 
     //target is the object being proxied, receiver is the proxy 
     get: function(target, propKey, receiver){ 
      //I only want to intercept method calls, not property access 
      var propValue = target[propKey]; 
      if (typeof propValue != "function"){ 
       return propValue; 
  } 
      else{ 
       return function(){ 
        console.log("intercepting call to " + propKey
          + " in car " + target.name); 
        //target is the object being proxied 
        return propValue.apply(target, arguments); 
       } 
      } 
     } 
    }); 
    methodInterceptorProxy.method_1("Mercedes"); 
    //"intercepting call to method_1 in car Ford" 
    //"Method_1 called with Mercedes" 

在上面的例子中,我们正在捕获get操作。如果要get的属性的类型是函数,我们将使用apply来调用该函数。如果您看到输出,我们会得到两个console.logs;第一个是来自代理的,我们捕获了get操作,第二个是来自实际方法调用。

元编程是一个有趣的构造。然而,任何形式的内省或反射都会影响性能。在使用代理时应该小心,因为它们可能会很慢。

总结

在本章中,我们看了两个重要的概念。ES6 代理是用于定义基本操作的自定义行为的有用的元编程构造(例如,属性查找、赋值、枚举、函数调用等)。我们看了如何使用处理程序、陷阱和代理目标来拦截和修改操作的默认行为。这为我们提供了在 JavaScript 中以前缺乏的非常强大的元编程能力。

本章讨论的另一个重要构造是 ES6 的 Promise。Promise 很重要,因为它们使得异步编程构造更容易使用。Promise 充当了一个值的代理,当 Promise 被创建时值不一定已知。这使得异步方法可以返回类似于同步方法的值 - 异步方法返回的不是最终值,而是将来某个时间点的值的 Promise。

这些都是 ES6 中非常强大的构造,极大地增强了语言的核心能力。

在下一章中,我们将探讨使用 JavaScript 在浏览器和 DOM 操作方面的迷人可能性。

第十章:浏览器环境

您知道 JavaScript 程序需要一个宿主环境。到目前为止,您在本书中学到的大部分内容都与核心 ECMAScript/JavaScript 有关,并且可以在许多不同的宿主环境中使用。现在,让我们把重点转移到浏览器上,因为这是 JavaScript 程序最受欢迎和自然的宿主环境。在本章中,您将学习以下主题:

  • 浏览器对象模型BOM

  • 文档对象模型DOM

  • 浏览器事件

  • XMLHttpRequest对象

在 HTML 页面中包含 JavaScript

要在 HTML 页面中包含 JavaScript,您需要使用以下<script>标签:

    <!DOCTYPE> 
    <html> 
      <head> 
        <title>JS test</title> 
        <script src="somefile.js"></script> 
      </head> 
      <body> 
        <script> 
          var a = 1; 
          a++; 
        </script> 
      </body> 
    </html> 

在此示例中,第一个<script>标签包含一个外部文件somefile.js,其中包含 JavaScript 代码。第二个<script>标签直接在页面的 HTML 代码中包含 JavaScript 代码。浏览器按照在页面上找到的顺序执行 JavaScript 代码,并且所有标签中的代码共享相同的全局命名空间。这意味着当您在somefile.js中定义一个变量时,它也存在于第二个<script>块中。

BOM 和 DOM - 概述

页面中的 JavaScript 代码可以访问多个对象。这些对象可以分为以下类型:

  • 核心 ECMAScript 对象:这包括前几章中提到的所有对象

  • DOM:这包括与当前加载的页面有关的对象,也称为文档

  • BOM:这包括处理页面外的一切内容-浏览器窗口和桌面屏幕

DOM 代表文档对象模型,BOM 代表浏览器对象模型。

DOM 是由万维网联盟W3C)制定的标准,有不同的版本,称为 DOM Level 1、DOM Level 2 等。今天使用的浏览器对标准的遵从程度不同,但总的来说,它们几乎都完全实现了 DOM Level 1。在浏览器供应商各自实现了访问文档的方式之后,DOM 被标准化后事实上成为了标准。W3C 接管之前的遗留部分仍然存在,并被称为 DOM 0,尽管实际上并不存在真正的 DOM Level 0 标准。DOM 0 的一些部分已成为事实上的标准,因为所有主要浏览器都支持它们;其中一些已添加到 DOM Level 1 标准中。未能在 DOM 1 中找到其位置的 DOM 0 的其余部分太过特定于浏览器,这里不会讨论。

从历史上看,BOM 不是任何标准的一部分。与 DOM 0 类似,它具有所有主要浏览器支持的对象的子集,以及特定于浏览器的另一个子集。HTML5 标准对浏览器之间的共同行为进行了编码,并包括了常见的 BOM 对象。此外,移动设备配备了它们特定的对象(HTML5 也旨在标准化这些对象),这在传统上对于台式电脑来说并不是必需的,但在移动世界中是有意义的,例如地理位置、相机访问、振动、触摸事件、电话和短信。

本章仅讨论 BOM 和 DOM Level 1 的跨浏览器子集,除非在文本中另有说明。即使这些安全子集构成了一个很大的主题,但完整的参考资料超出了本书的范围。您还可以参考以下参考资料:

BOM

BOM 是一组对象,它们让你可以访问浏览器和计算机屏幕。这些对象可以通过全局对象window访问。

window 对象重访

正如您已经知道的,在 JavaScript 中,宿主环境提供了一个全局对象。在浏览器环境中,可以使用window访问此全局对象。所有全局变量也可以作为window对象的属性访问。例如,看一下以下代码:

    > window.somevar = 1; 
           1 
    > somevar; 
           1 

此外,所有核心 JavaScript 函数,如第二章中讨论的基本数据类型、数组、循环和条件,都是全局对象的方法。考虑以下代码片段:

    > parseInt('123a456'); 
           123 
    > window.parseInt('123a456'); 
           123 

除了作为全局对象的引用外,window对象还有第二个目的-提供有关浏览器环境的信息。每个框架、iframe、弹出窗口或浏览器选项卡都有一个window对象。

让我们看看window对象的一些与浏览器相关的属性。同样,这些属性在不同浏览器中可能会有所不同,因此让我们只考虑在所有主要浏览器中一致可靠实现的属性。

使用 window.navigator 属性

navigator是一个对象,其中包含有关浏览器及其功能的一些信息。一个属性是navigator.userAgent,它是浏览器标识的长字符串。在 Firefox 中,您将获得以下输出:

    > window.navigator.userAgent; 
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) 
          AppleWebKit/536.28.10
          (KHTML, like Gecko) Version/6.0.3 Safari/536.28.10" 

在 Microsoft Internet Explorer 中,userAgent字符串如下所示:

       "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)" 

由于浏览器具有不同的功能,开发人员使用userAgent字符串来识别浏览器并提供代码的不同版本。例如,以下代码片段搜索MSIE字符串的存在以识别 Internet Explorer:

    if (navigator.userAgent.indexOf('MSIE') !== -1) { 
      // this is IE 
    } else { 
      // not IE 
    } 

最好不要依赖userAgent字符串,而是使用特性嗅探(也称为能力检测)。原因是很难跟踪所有浏览器及其不同的版本。简单检查您打算在用户的浏览器中使用的功能是否确实可用。例如,看一下以下代码片段:

    if (typeof window.addEventListener === 'function') { 
      // feature is supported, let's use it 
    } else { 
      // hmm, this feature is not supported, will have to 
      // think of another way 
    } 

避免使用userAgent嗅探的另一个原因是,一些浏览器允许用户修改字符串并假装他们正在使用不同的浏览器。

您的控制台是一个速查表

控制台允许您检查对象中的内容,包括所有 BOM 和 DOM 属性。只需键入以下代码:

    > navigator; 

然后单击结果。结果是属性及其值的列表,如下截图所示:

您的控制台是一个速查表

使用 window.location 属性

location属性指向一个包含有关当前加载页面的 URL 信息的对象。例如,location.href是完整的 URL,location.hostname只是域名。通过简单的循环,您可以查看location对象的完整属性列表。

假设您在具有以下 URL 的页面上:

search.phpied.com:8080/search?q=java&what=script#results

考虑以下代码:

    for (var i in location) { 
      if (typeof location[i] === "string") { 
        console.log(i + ' = "' + location[i] + '"'); 
      } 
    } 
           href = "http://search.phpied.com:8080/search?
             q=java&what=script#results" 
           hash = "#results" 
           host = "search.phpied.com:8080" 
           hostname = "search.phpied.com" 
           pathname = "/search" 
           port = <<8080>> 
           protocol = <<http:>> 
           search = "?q=java&what=script" 

location属性还提供了三种方法,即reload()assign()replace()

有很多不同的方法可以让您导航到另一个页面。以下是一些方法:

    > window.location.href = 'http://www.packtpub.com'; 
    > location.href = 'http://www.packtpub.com'; 
    > location = 'http://www.packtpub.com'; 
    > location.assign('http://www.packtpub.com'); 

replace()方法与assign()几乎相同。不同之处在于它不会在浏览器的历史记录中创建条目,如下所示:

    > location.replace('http://www.yahoo.com'); 

要重新加载页面,您可以使用以下代码:

    > location.reload(); 

或者,您可以使用location.href将其指向自身,如下所示:

    > window.location.href = window.location.href; 

或者,只需使用以下代码:

    > location = location; 

使用 window.history 属性

window.history 属性允许有限访问同一浏览器会话中先前访问的页面。例如,您可以查看用户在访问您的页面之前访问了多少页面,如下所示:

    > window.history.length; 
           5 

但是你无法看到实际的 URL。出于隐私原因,这是行不通的。看下面的代码:

    > window.history[0]; 

然而,您可以像用户点击后退/前进浏览器按钮一样在用户会话中前后导航,如下所示:

    > history.forward(); 
    > history.back(); 

您还可以使用 history.go() 来前后跳转页面。这与调用 history.back() 相同。history.go() 的代码如下:

    > history.go(-1); 

要后退两页,请使用以下代码:

    > history.go(-2); 

使用以下代码重新加载当前页面:

    > history.go(0); 

更近期的浏览器还支持 HTML5 历史 API,它允许您在不重新加载页面的情况下更改 URL。这对于动态页面非常理想,因为它们可以允许用户收藏特定的 URL,该 URL 代表应用程序的状态,当他们回来时,或与朋友分享时,页面可以根据 URL 恢复应用程序状态。要了解历史 API 的情况,请转到任何页面并在控制台中编写以下代码行:

    > history.pushState({a: 1}, "", "hello"); 
    > history.pushState({b: 2}, "", "hello-you-too"); 
    > history.state; 

注意 URL 的变化,但页面是相同的。现在,在浏览器中尝试使用后退和前进按钮,并再次检查 history.state 对象。

使用 window.frames 属性

window.frames 属性是当前页面中所有框架的集合。它不区分框架和 iframe(内联框架)。无论页面上是否有框架,window.frames 都始终存在并指向 window,如下所示:

    > window.frames === window; 
           true 

让我们考虑一个页面有一个 iframe 的例子,如下所示:

    <iframe name="myframe" src="hello.html" /> 

为了判断页面上是否有任何框架,您可以检查 length 属性。在一个 iframe 的情况下,您将看到以下输出:

    > frames.length 
           1 

每个框架包含另一个页面,该页面有自己的全局 window 对象。

要访问 iframe 的 window,可以执行以下任何一个:

    > window.frames[0]; 
    > window.frames[0].window; 
    > window.frames[0].window.frames; 
    > frames[0].window; 
    > frames[0]; 

从父页面,您也可以访问子框架的属性。例如,您可以按如下方式重新加载框架:

    > frames[0].window.location.reload(); 

从子页面内部,您可以按如下方式访问父页面:

    > frames[0].parent === window; 
           true 

使用名为 top 的属性,您可以从任何框架内访问顶部页面-包含所有其他框架的页面,如下所示:

    > window.frames[0].window.top === window; 
           true 
    > window.frames[0].window.top === window.top; 
           true 
    > window.frames[0].window.top === top; 
           true 

此外,selfwindow 是相同的,如下面的代码片段所示:

    > self === window; 
           true 
    > frames[0].self == frames[0].window; 
           true 

如果一个框架有一个 name 属性,你不仅可以通过名称访问框架,还可以通过索引访问,如下面的代码片段所示:

    > window.frames['myframe'] === window.frames[0]; 
           true 

或者,您可以使用以下代码:

    > frames.myframe === window.frames[0]; 
           true 

使用 window.screen 属性

screen 属性提供有关浏览器外部环境的信息。例如,screen.colorDepth 属性包含显示器的颜色位深度(颜色质量)。这主要用于统计目的。看一下以下代码行:

    > window.screen.colorDepth; 
           32 

您还可以检查可用的屏幕房地产(分辨率),如下所示:

    > screen.width; 
           1440 
    > screen.availWidth; 
           1440 
    > screen.height; 
           900 
    > screen.availHeight; 
           847 

heightavailHeight 之间的区别在于 height 是整个屏幕,而 availHeight 减去了任何操作系统菜单,例如 Windows 任务栏。widthavailWidth 也是如此。

与下面的代码相关的属性有:

    > window.devicePixelRatio; 
           1 

它告诉您在移动设备的视网膜显示器中物理像素和设备像素之间的差异(比率),例如,iPhone 中的值为 2。

window.open()/close() 方法

在探索了window对象的一些最常见的跨浏览器属性之后,让我们转向一些方法。其中一种方法是open(),它允许你打开新的浏览器窗口(弹出窗口)。各种浏览器策略和用户设置可能会阻止你打开弹出窗口(因为滥用该技术进行营销目的),但通常情况下,如果是用户发起的,你应该能够打开一个新窗口。否则,如果你尝试在页面加载时打开弹出窗口,很可能会被阻止,因为用户没有明确发起它。

window.open()方法接受以下参数:

  • 要在新窗口中加载的 URL

  • 新窗口的名称,可以作为表单target属性的值

  • 以逗号分隔的功能列表,如下所示:

  • resizable:用户是否可以调整新窗口的大小

  • widthheight:弹出窗口的宽度和高度

  • status:状态栏是否可见

window.open()方法返回对新创建的浏览器实例的window对象的引用。以下是一个例子:

    var win = window.open('http://www.packtpub.com', 'packt', 
      'width=300,height=300,resizable=yes'); 

win变量指向弹出窗口的window对象。你可以检查win是否为假值,这意味着弹出窗口被阻止了。

win.close()方法关闭新窗口。

出于可访问性和可用性原因,最好不要打开新窗口。如果你不喜欢网站给你弹出窗口,为什么要对你的用户这样做呢?有合法的目的,比如在填写表单时提供帮助信息,但通常可以用替代方案实现,比如在页面内使用浮动的<div>

window.moveTo()和 window.resizeTo()方法

继续过去的不良做法,以下是更多方法来激怒你的用户,前提是他们的浏览器和个人设置允许你这样做:

  • window.moveTo(100, 100):这将浏览器窗口移动到屏幕位置x = 100y = 100,从左上角计算

  • window.moveBy(10, -10):这将窗口从当前位置向右移动 10 像素,向上移动 10 像素

  • window.resizeTo(x, y)window.resizeBy(x, y):这些方法接受与移动方法相同的参数,但它们调整窗口的大小而不是移动它

再次尝试解决你面临的问题,而不要求助于这些方法。

window.alert()、window.prompt()和 window.confirm()方法

在第二章中,基本数据类型、数组、循环和条件,我们讨论了alert()函数。现在你知道全局函数可以作为全局对象的方法访问,所以alert('小心')window.alert('小心')是完全相同的。

alert()函数不是 ECMAScript 函数,而是 BOM 方法。除此之外,还有两个 BOM 方法允许你通过系统消息与用户交互。以下是这些方法:

  • confirm():这给用户两个选项,确定取消

  • prompt():这收集文本输入

它的工作原理如下:

    > var answer = confirm('Are you cool?');  
    > answer; 

它会显示一个类似于以下截图的窗口(确切的外观取决于浏览器和操作系统):

window.alert(), window.prompt(), and window.confirm() methods

你会注意到以下几点:

  • 在关闭此消息之前,不会有任何内容写入控制台,这意味着任何 JavaScript 代码执行都会被冻结,等待用户的答复

  • 点击确定返回true,点击取消或使用X图标关闭消息,或按下ESC键,返回false

这对于确认用户操作非常方便,如下面的代码所示:

    if (confirm('Sure you want to delete this?')) { 
      // delete 
    } else { 
      // abort 
    } 

确保为禁用 JavaScript 的用户或搜索引擎蜘蛛提供确认用户操作的替代方法。

window.prompt()方法向用户显示一个对话框,让其输入文本,如下所示:

    > var answer = prompt('And your name was?');  
    > answer; 

这导致了以下对话框(Chrome,MacOS):

window.alert(), window.prompt(), and window.confirm() methods

answer的值是以下之一:

  • null:如果你点击CancelX图标,或按ESC键,就会发生这种情况。

  • ""(空字符串):如果你点击OK或按下没有输入任何内容的Enter

  • 文本字符串:如果你输入一些内容然后点击OK或按Enter

该函数还接受一个字符串作为第二个参数,并将其显示为预填充到输入字段中的默认值。

使用 window.setTimeout()和 window.setInterval()方法

setTimeout()setInterval()方法允许安排执行一段代码。setTimeout()方法尝试在指定的毫秒数后执行给定的代码一次。setInterval()方法尝试在指定的毫秒数后重复执行它。

这将在大约 2 秒后(2000 毫秒)显示一个警报。考虑以下代码:

    > function boo() { alert('Boo!'); } 
    > setTimeout(boo, 2000); 
           4 

正如你所看到的,该函数返回了一个整数(在这种情况下是4),表示超时的 ID。你可以使用这个 ID 来使用clearTimeout()取消超时。在以下示例中,如果你足够快,并在 2 秒钟之前清除超时,警报将永远不会显示,就像你在以下代码中看到的那样:

    > var id = setTimeout(boo, 2000); 
    > clearTimeout(id); 

让我们将boo()改为更不具侵入性的东西,如下所示:

    > function boo() { console.log('boo'); } 

现在,使用setInterval(),你可以安排boo()每 2 秒执行一次,直到你用clearInterval()取消安排的执行。考虑以下代码:

    > var id = setInterval(boo, 2000); 
           boo 
           boo 
           boo 
           boo 
           boo 
           boo 
    > clearInterval(id); 

请注意,这两个函数都接受一个回调函数的指针作为第一个参数。它们也可以接受一个字符串,该字符串将使用eval()进行评估;然而,正如你所知,eval()是邪恶的,所以应该避免使用。此外,如果你想向函数传递参数怎么办?在这种情况下,你可以将函数调用包装在另一个函数中。

以下代码是有效的,但不建议使用:

    // bad idea 
    var id = setInterval("alert('boo, boo')", 2000); 

这个替代方案更受欢迎:

    var id = setInterval( 
      function () { 
        alert('boo, boo'); 
      }, 
      2000 
    ); 

请注意,安排一个函数在一定的毫秒数后执行并不保证它会准确地在那个时间执行。一个原因是大多数浏览器没有毫秒分辨率的时间。如果你安排在 3 毫秒后执行某事,在较旧的 IE 中至少要等待 15 毫秒,而在更现代的浏览器中会更快,但很可能不会在 1 毫秒内执行。另一个原因是浏览器维护一个你要求它们执行的队列。100 毫秒的超时意味着在 100 毫秒后添加到队列中。然而,如果队列被某些慢的事情延迟了,你的函数将不得不等待并在之后执行,比如说在 120 毫秒后。

更近期的浏览器实现了requestAnimationFrame()函数。它比超时函数更可取,因为你要求浏览器在有可用资源时调用你的函数,而不是在预定义的毫秒时间之后。尝试在控制台中执行以下代码片段:

    function animateMe() { 
      webkitRequestAnimationFrame(function(){ 
        console.log(new Date()); 
        animateMe(); 
      }); 
    } 

    animateMe(); 

window.document 属性

window.document属性是一个 BOM 对象,指的是当前加载的文档(页面)。它的方法和属性属于 DOM 对象的范畴。深呼吸一下(也许先看一下本章末尾的 BOM 练习),然后让我们深入了解 DOM。

DOM

DOM 将 XML 或 HTML 文档表示为节点树。使用 DOM 方法和属性,你可以访问页面上的任何元素,修改或删除元素,或添加新元素。DOM 是一种与语言无关的 API,不仅可以在 JavaScript 中实现,还可以在任何其他语言中实现。例如,你可以使用 PHP 的 DOM 实现(php.net/dom)在服务器端生成页面。

看一下这个示例 HTML 页面:

    <!DOCTYPE html> 
    <html> 
      <head> 
        <title>My page</title> 
      </head> 
      <body> 
        <p class="opener">first paragraph</p> 
        <p><em>second</em> paragraph</p> 
        <p id="closer">final</p> 
        <!-- and that's about it --> 
      </body> 
    </html> 

考虑第二段(

第二

)。您会看到它是一个

标签,并且包含在标签中。如果您从家庭关系的角度思考,您可以说是

的父级,

是子级。第一和第三段也将是标签的子级,并且与第二段同时是兄弟姐妹。标签是第二个

的子级,因此

是其父级。父子关系可以在一个称为 DOM 树的祖先树中以图形方式表示:

DOM

前面的屏幕截图显示了在 WebKit 控制台的元素选项卡中展开每个节点后会看到的内容。

您可以看到所有标签都显示为树上可展开的节点。尽管没有显示,但存在所谓的文本节点,例如<em>标签内的文本(单词 second)是一个文本节点。空格也被视为文本节点。HTML 代码中的注释也是树中的节点,HTML 源代码中的<!- and that's about it ->注释是树中的注释节点。

DOM 树中的每个节点都是一个对象,右侧的属性部分列出了您可以使用的所有属性和方法,遵循了创建此对象的继承链:

DOM

您还可以看到在幕后使用的构造函数来创建这些对象。尽管这对日常任务来说并不太实用,但知道例如<p>是由HTMLParagraphElement()构造函数创建的,表示head标签的对象是由HTMLHeadElement()创建的等等可能会很有趣。但是,您不能直接使用这些构造函数创建对象。

核心 DOM 和 HTML DOM

在继续更实际的示例之前,最后再偏离一下。正如您现在所知,DOM 代表 XML 文档和 HTML 文档。实际上,HTML 文档是 XML 文档,但更具体。因此,作为 DOM Level 1 的一部分,有一个适用于所有 XML 文档的核心 DOM 规范,还有一个 HTML DOM 规范,它扩展并构建在核心 DOM 之上。当然,HTML DOM 并不适用于所有 XML 文档,而只适用于 HTML 文档。让我们看一些核心 DOM 和 HTML DOM 构造函数的示例:

构造函数 继承自 核心或 HTML 注释
Node 核心 树上的任何节点
Document Node 核心 文档对象,任何 XML 文档的主要入口点
HTMLDocument Document HTML 这是 window.document 或简单地 document,前一个对象的 HTML 特定版本,您将广泛使用它
Element Node 核心 源代码中的每个标签都由一个元素表示。这就是为什么您说-P元素表示<p></p>标签
HTMLElement Element HTML 通用构造函数,所有 HTML 元素的构造函数都继承自它
HTMLBodyElement HTMLElement HTML 表示<body>标签的元素
HTMLLinkElement HTMLElement HTML A 元素:<a href="..."></a>标签
和其他构造函数 HTMLElement HTML 所有其他 HTML 元素
CharacterData Node 核心 用于处理文本的通用构造函数
Text CharacterData 核心 标签内的文本节点;在<em>second</em>中,您有元素节点EM和值为 second 的文本节点
Comment CharacterData 核心 <!-- 任何注释 -->
Attr Node 核心 表示标签的属性;在<p id="closer">中,id属性是由Attr()构造函数创建的 DOM 对象
NodeList 核心 节点列表,具有length属性的类似数组的对象
NamedNodeMap 核心 NodeList 相同,但节点可以通过名称访问,而不仅仅是通过数字索引。
HTMLCollection HTML 类似于 NamedNodeMap,但专门用于 HTML。

这些绝不是所有核心 DOM 和 HTML DOM 对象。 要获取完整列表,请参阅 www.w3.org/TR/DOM-Level-1/

既然 DOM 理论的这一部分已经过去了,让我们专注于处理 DOM 的实际方面。 在接下来的几节中,您将学习以下主题:

  • 访问 DOM 节点

  • 修改节点

  • 创建新节点

  • 删除节点

访问 DOM 节点

在您可以验证页面上表单中的用户输入或交换图像之前,您需要访问要检查或修改的元素。 幸运的是,有许多方法可以访问任何元素,无论是通过导航遍历 DOM 树还是使用快捷方式。

最好是您开始尝试所有新对象和方法。 您将看到的示例使用了与 DOM 部分开头看到的相同简单文档,您可以在 www.phpied.com/files/jsoop/ch7.html 上访问。 打开控制台,让我们开始吧。

文档节点

document 节点使您可以访问当前文档。 要探索此对象,您可以使用控制台作为备忘单。 输入 console.dir(document) 并单击结果:

文档节点

或者,您可以在元素面板中浏览 document 对象 DOM 属性的所有属性和方法:

文档节点

所有节点,包括文档节点、文本节点、元素节点和属性节点都有 nodeTypenodeNamenodeValue 属性:

    > document.nodeType; 
           9 

有 12 种节点类型,用整数表示。 如您所见,文档节点类型是 9。 最常用的是 1(元素)、2(属性)和 3(文本)。

节点也有名称。 对于 HTML 标签,节点名称是标签名称(tagName 属性)。 对于文本节点,它是 #text,对于文档节点,名称如下:

    > document.nodeName; 
           "#document" 

节点也可以有节点值。 例如,对于文本节点,值是实际文本。 文档节点没有值,可以如下所示:

    > document.nodeValue; 
           null 

documentElement

现在,让我们在树中移动。 XML 文档始终有一个根节点,用于包装文档的其余部分。 对于 HTML 文档,根是 <html> 标签。 要访问根,您将使用 document 对象的 documentElement 属性。

    > document.documentElement; 
           <html>...</html> 

nodeType1(元素节点),可以如下所示:

    > document.documentElement.nodeType; 
           1 

对于元素节点,nodeNametagName 属性都包含标签的名称,如下所示:

    > document.documentElement.nodeName; 
           "HTML" 
    > document.documentElement.tagName; 
           "HTML" 

子节点

要确定节点是否有任何子节点,您将使用 hasChildNodes(),如下所示:

    > document.documentElement.hasChildNodes(); 
           true 

HTML 元素有三个子节点,headbody 元素以及它们之间的空格(在大多数浏览器中计算空格)。 您可以使用 childNodes 类似数组的集合来访问它们,如下所示:

    > document.documentElement.childNodes.length; 
           3 
    > document.documentElement.childNodes[0]; 
           <head>...</head> 
    > document.documentElement.childNodes[1]; 
           #text 
    > document.documentElement.childNodes[2]; 
           <body>...</body> 

任何子节点都可以通过 parentNode 属性访问其父节点,如下所示:

    > document.documentElement.childNodes[1].parentNode; 
           <html>...</html> 

让我们将 body 的引用分配给一个变量,如下所示:

    > var bd = document.documentElement.childNodes[2]; 

body 元素有多少子节点? 考虑以下代码片段

    > bd.childNodes.length; 
           9 

作为复习,这里是文档的主体:

      <body> 
        <p class="opener">first paragraph</p> 
        <p><em>second</em> paragraph</p> 
        <p id="closer">final</p> 
        <!-- and that's about it --> 
      </body> 

为什么 body9 个子节点? 好吧,三个段落加上一个注释共四个节点。 这四个节点之间的空格产生了另外三个文本节点。 到目前为止总共有七个。 <body> 和第一个 <p> 之间的空格是第八个节点。 注释和闭合 </body> 之间的空格是另一个文本节点。 这样总共有九个子节点。 只需在控制台中键入 bd.childNodes 来检查它们。

属性

由于 body 的第一个子节点是空格,因此第二个子节点(索引 1)是第一个段落。请参考以下代码片段:

    > bd.childNodes[1]; 
           <p class="opener">first paragraph</p> 

您可以使用hasAttributes()来检查元素是否具有属性,如下所示:

    > bd.childNodes[1].hasAttributes(); 
            true 

有多少属性?在这个例子中,一个是class属性,可以如下所示:

    > bd.childNodes[1].attributes.length; 
            1 

您可以按索引和名称访问属性。您还可以使用getAttribute()方法获取值,如下所示:

    > bd.childNodes[1].attributes[0].nodeName; 
           "class" 
    > bd.childNodes[1].attributes[0].nodeValue; 
           "opener" 
    > bd.childNodes[1].attributes['class'].nodeValue; 
           "opener" 
    > bd.childNodes[1].getAttribute('class'); 
           "opener" 

访问标签内的内容

让我们来看看第一段:

    > bd.childNodes[1].nodeName; 
           "P" 

您可以使用textContent属性获取段落中包含的文本。它在旧版 IE 中不存在,但另一个名为innerText的属性返回相同的值,如下输出所示:

    > bd.childNodes[1].textContent; 
           "first paragraph" 

还有innerHTML属性。尽管它以前存在于所有主要浏览器中,但它是 DOM 标准的一个相对较新的添加。它返回(或设置)节点中包含的 HTML 代码。您可以看到这有点不一致,因为 DOM 将文档视为节点树,而不是标签字符串。然而,innerHTML非常方便使用,您会在各处看到它。请参考以下代码:

    > bd.childNodes[1].innerHTML; 
           "first paragraph" 

第一段只包含文本,所以innerHTMLtextContent(或 IE 中的innerText)相同。然而,第二段包含一个em节点,因此您可以如下所示看到差异:

    > bd.childNodes[3].innerHTML; 
           "<em>second</em> paragraph" 
    > bd.childNodes[3].textContent; 
           "second paragraph" 

另一种获取第一段中包含的文本的方法是使用p节点内包含的文本节点的nodeValue方法,如下所示:

    > bd.childNodes[1].childNodes.length; 
            1 
    > bd.childNodes[1].childNodes[0].nodeName; 
           "#text" 
    > bd.childNodes[1].childNodes[0].nodeValue; 
           "first paragraph" 

DOM 访问快捷方式

使用childNodesparentNodenodeNamenodeValueattributes,您可以在树中上下导航并对文档进行任何操作。然而,空格是文本节点的事实使得这种处理 DOM 的方式很脆弱。如果页面发生变化,您的脚本可能不再正确工作。此外,如果要深入树中的节点,可能需要一些代码才能到达那里。这就是为什么有快捷方式方法,即getElementsByTagName()getElementsByName()getElementById()

getElementsByTagName()方法接受一个标签名称(元素节点的名称)并返回一个匹配标签名称的节点的 HTML 集合(类似数组的对象)。例如,以下示例要求给出所有段落的计数,如下所示:

    > document.getElementsByTagName('p').length; 
            3 

您可以使用括号表示法或item()方法访问列表中的项目,并传递索引(第一个元素为 0)。使用item()是不鼓励的,因为数组括号更一致,而且输入更短。请参考以下代码片段:

    > document.getElementsByTagName('p')[0]; 
            <p class="opener">first paragraph</p> 
    > document.getElementsByTagName('p').item(0); 
            <p class="opener">first paragraph</p> 

获取第一个p的内容可以如下所示:

    > document.getElementsByTagName('p')[0].innerHTML; 
           "first paragraph" 

访问最后一个p可以如下所示:

    > document.getElementsByTagName('p')[2]; 
            <p id="closer">final</p> 

要访问元素的属性,您可以使用attributes集合或getAttribute(),如前所示。但是,更简洁的方法是将属性名称作为要处理的元素的属性。因此,要获取id属性的值,您只需将id作为属性使用,如下所示:

    > document.getElementsByTagName('p')[2].id; 
           "closer" 

但是,无法获取第一段的class属性。这是一个例外,因为在 ECMAScript 中 class 是一个保留字。您可以使用className代替,如下所示:

    > document.getElementsByTagName('p')[0].className; 
           "opener" 

使用getElementsByTagName(),您可以获取页面上的所有元素,如下所示:

    > document.getElementsByTagName('*').length; 
            8 

在 IE7 之前的早期版本中,*作为标签名称是不可接受的。要获取所有元素,可以使用 IE 的专有document.all集合,尽管很少需要选择每个元素。

另一个提到的快捷方式是getElementById()。这可能是访问元素的最常见方法。您只需为要处理的元素分配 ID,以后访问它们会很容易,如下代码所示:

    > document.getElementById('closer'); 
    <p id="closer">final</p> 

更近期浏览器中的其他快捷方式方法包括以下内容:

  • getElementByClassName():此方法使用其 class 属性查找元素

  • querySelector():该方法使用 CSS 选择器字符串查找元素

  • querySelectorAll():该方法与前一个方法相同,但返回所有匹配的元素,而不仅仅是第一个

兄弟节点、body、第一个和最后一个子节点

一旦您有了对一个元素的引用,nextSiblingpreviousSibling是另外两个方便的属性,用于导航 DOM 树。考虑以下代码:

    > var para = document.getElementById('closer'); 
    > para.nextSibling; 
           #text 
    > para.previousSibling; 
           #text 
    > para.previousSibling.previousSibling; 
           <p>...</p> 
    > para.previousSibling.previousSibling.previousSibling; 
           #text 
    > para.previousSibling.previousSibling.nextSibling.nextSibling; 
           <p id="closer">final</p> 

body元素经常使用,因此它有自己的快捷方式,如下所示:

    > document.body; 
            <body>...</body> 
    > document.body.nextSibling; 
            null 
    > document.body.previousSibling.previousSibling; 
            <head>...</head> 

firstChildlastChild属性也很方便。firstChild属性与childNodes[0]相同,lastChildchildNodes[childNodes.length - 1]属性相同:

    > document.body.firstChild; 
           #text 
    > document.body.lastChild; 
           #text 
    > document.body.lastChild.previousSibling; 
            <!-- and that's about it --> 
    > document.body.lastChild.previousSibling.nodeValue; 
           " and that's about it " 

以下屏幕截图显示了 body 和其中三个段落之间的家族关系。为简单起见,屏幕截图中删除了所有空白文本节点:

兄弟节点、body、第一个和最后一个子节点

遍历 DOM

总之,以下是一个函数,它接受任何节点并从给定节点开始递归地遍历 DOM 树,如下所示:

   function walkDOM(n) { 
      do { 
        console.log(n); 
        if (n.hasChildNodes()) { 
          walkDOM(n.firstChild); 
        } 
      } while (n = n.nextSibling); 
    } 

您可以按照以下方式测试该函数:

    > walkDOM(document.documentElement); 
    > walkDOM(document.body); 

修改 DOM 节点

现在您已经了解了访问 DOM 树及其属性的许多方法,让我们看看如何修改这些节点:

让我们将指针分配给最后一个段落,变量名为my,如下所示:

    > var my = document.getElementById('closer'); 

现在,更改段落的文本可以像更改innerHTML值一样简单,如下所示:

    > my.innerHTML = 'final!!!'; 
           "final!!!" 

由于innerHTML接受 HTML 源代码的字符串,因此您也可以按照以下方式在 DOM 树中创建新的em节点:

    > my.innerHTML = '<em>my</em> final'; 
           "<em>my</em> final" 

新的em节点成为树的一部分。让我们看一下以下代码:

    > my.firstChild; 
           <em>my</em> 
    > my.firstChild.firstChild; 
           "my" 

改变文本的另一种方法是获取实际的文本节点并更改其nodeValue,如下所示:

    > my.firstChild.firstChild.nodeValue = 'your'; 
           "your" 

修改样式

通常您不会更改节点的内容,而是更改其呈现方式。元素具有style属性,该属性又具有映射到每个 CSS 属性的属性。例如,更改段落的样式以添加红色边框,如下所示:

    > my.style.border = "1px solid red"; 
           "1px solid red" 

CSS 属性通常带有破折号,但是在 JavaScript 标识符中破折号是不可接受的。在这种情况下,您跳过破折号并将下一个字母大写。因此,padding-top变成了paddingTopmargin-left变成了marginLeft,等等。看一下以下代码:

    > my.style.fontWeight = 'bold'; 
           "bold" 

您还可以访问stylecssText属性,它允许您将样式作为字符串进行处理,如下所示:

    > my.style.cssText; 
           "border: 1px solid red; font-weight: bold;" 

此外,修改样式是一种字符串操作:

    > my.style.cssText += " border-style: dashed;" 
    "border: 1px dashed red; font-weight: bold; border-style: dashed;" 

表单乐趣

如前所述,JavaScript 非常适合客户端输入验证,并且可以节省几次往返服务器。让我们练习表单操作,并在一个流行页面上玩一下,www.google.com上的表单:

玩转表单

使用querySelector()方法和 CSS 选择器字符串查找第一个文本输入的方法如下:

    > var input = document.querySelector('input[type=text]'); 

访问搜索框。考虑以下代码:

    > input.name; 
           "q" 

通过设置value属性中包含的文本来更改搜索查询的结果如下所示:

    > input.value = 'my query'; 
 **"my query"**

现在,让我们玩得开心,将按钮中的Lucky更改为Tricky

    > var feeling = document.querySelectorAll("button")[2]; 
    > feeling.textContent = feelingtextContent.replace(/Lu/, 'Tri'); 
 **"I'm Feeling Tricky"**

玩转表单

现在,让我们实现一些技巧,并使该按钮在一秒钟内显示和隐藏。您可以使用一个简单的函数来实现这一点。让我们称之为toggle()。每次调用该函数时,它都会检查 CSS 属性visibility的值,并使用以下代码片段将其设置为可见(如果它是隐藏的)或相反:

    function toggle() { 
      var st = document.querySelectorAll('button')[2].style; 
      st.visibility = (st.visibility === 'hidden') 
        ? 'visible' 
        : 'hidden'; 
    } 

不要手动调用该函数,让我们设置一个间隔,每秒调用一次:

    > var myint = setInterval(toggle, 1000); 

结果?按钮开始闪烁,使得点击变得更加困难。当您厌倦追逐它时,只需编写以下代码来删除超时间隔:

    > clearInterval(myint); 

创建新节点

要创建新节点,可以使用createElement()createTextNode()方法。一旦有了新节点,可以使用appendChild()insertBefore()replaceChild()将它们添加到 DOM 树中。

重新加载www.phpied.com/files/jsoop/ch7.html,让我们来玩一下。

创建一个新的p元素并设置其innerHTML,如下所示:

    > var myp = document.createElement('p'); 
    > myp.innerHTML = 'yet another'; 
           "yet another" 

新元素自动获得所有默认属性,比如style,你可以按照以下方式修改它:

    > myp.style; 
           CSSStyleDeclaration 
    > myp.style.border = '2px dotted blue'; 
           "2px dotted blue" 

使用appendChild(),你可以将新节点添加到 DOM 树中。在document.body节点上调用此方法意味着在最后一个子节点之后创建一个更多的子节点,如下所示:

    > document.body.appendChild(myp); 
           <p style="border: 2px dotted blue;">yet another</p> 

以下是在新节点附加后页面的示例:

创建新节点

仅使用 DOM 方法

innerHTML属性比纯 DOM 更快地完成了一些工作。在纯 DOM 中,你需要执行以下步骤:

  1. 创建一个包含另一个文本的新文本节点。

  2. 创建一个新的段落节点。

  3. 将文本节点作为段落的子节点添加。

  4. 将段落作为子节点添加到 body。

这样,你可以创建任意数量的文本节点和元素,并将它们嵌套,就像你喜欢的那样。假设你想要将以下 HTML 添加到 body 的末尾:

    <p>one more paragraph<strong>bold</strong></p> 

将前面的代码呈现为层次结构,会像以下代码片段一样:

    P element 
        text node with value "one more paragraph" 
        STRONG element 
            text node with value "bold" 

实现这一点的代码如下:

    // create P 
    var myp = document.createElement('p'); 
    // create text node and append to P 
    var myt = document.createTextNode('one more paragraph'); 
    myp.appendChild(myt); 
    // create STRONG and append another text node to it 
    var str = document.createElement('strong'); 
    str.appendChild(document.createTextNode('bold')); 
    // append STRONG to P 
    myp.appendChild(str); 
    // append P to BODY 
    document.body.appendChild(myp); 

使用 cloneNode()方法

另一种创建节点的方法是通过复制或克隆现有节点。cloneNode()方法可以做到这一点,并接受一个布尔参数(true = 深拷贝,包括所有子节点,false = 浅拷贝,只有这个节点)。让我们测试一下这个方法。

获取要克隆的元素的引用可以通过以下方式完成:

    > var el = document.getElementsByTagName('p')[1]; 

现在,el指的是页面上第二个段落,如下所示的代码:

    <p><em>second</em> paragraph</p> 

让我们创建el的浅克隆,并将其附加到body中,如下所示:

    > document.body.appendChild(el.cloneNode(false)); 

你在页面上看不到任何区别,因为浅拷贝只复制了P节点而没有任何子节点。这意味着段落内的文本节点子节点没有被克隆。前面的行等同于以下代码行:

    > document.body.appendChild(document.createElement('p')); 

然而,如果你创建一个深拷贝,从P开始的整个 DOM 子树都会被复制,包括文本节点和EM元素。这一行将第二段复制(在视觉上也是)到文档的末尾。考虑以下代码行:

    > document.body.appendChild(el.cloneNode(true)); 

如果需要,你也可以只复制EM,就像以下代码行所示:

    > document.body.appendChild(el.firstChild.cloneNode(true)); 
           <em>second</em> 

或者,你可以只复制值为second的文本节点,如下所示:

    > document.body.appendChild( 
        el.firstChild.firstChild.cloneNode(false)); 
           "second" 

使用 insertBefore()方法

使用appendChild(),你只能在所选元素的末尾添加新的子节点。要更精确地控制位置,可以使用insertBefore()。这与appendChild()相同,但接受一个额外的参数,指定在哪里(在哪个元素之前)插入新节点。例如,以下代码在body元素的末尾插入一个文本节点:

    > document.body.appendChild(document.createTextNode('boo!')); 

此外,这将创建另一个文本节点,并将其添加为body元素的第一个子节点:

    document.body.insertBefore( 
      document.createTextNode('first boo!'), 
      document.body.firstChild 
    ); 

移除节点

要从 DOM 树中移除节点,可以使用removeChild()方法。同样,让我们从具有相同 body 的同一页开始:

      <body> 
        <p class="opener">first paragraph</p> 
        <p><em>second</em> paragraph</p> 
        <p id="closer">final</p> 
        <!-- and that's about it --> 
      </body> 

以下是如何移除第二段的方法:

    > var myp = document.getElementsByTagName('p')[1]; 
    > var removed = document.body.removeChild(myp); 

如果你想以后使用,该方法会返回已移除的节点。即使元素不再在树中,你仍然可以使用所有 DOM 方法。让我们看一下以下代码:

    > removed; 
           <p>...</p> 
    > removed.firstChild; 
           <em>second</em> 

还有一个replaceChild()方法,它删除一个节点并将另一个节点放在它的位置。

移除节点后,树的结构如下:

      <body> 
        <p class="opener">first paragraph</p> 
        <p id="closer">final</p> 
        <!-- and that's about it --> 
      </body> 

现在,第二段是 ID 为"closer"的段落,如下所示:

    > var p = document.getElementsByTagName('p')[1]; 
    > p; 
           <p id="closer">final</p> 

让我们用removed变量中的段落替换这个段落。考虑以下代码:

    > var replaced = document.body.replaceChild(removed, p); 

就像removeChild()一样,replaceChild()返回一个对现在已经不在树中的节点的引用:

    > replaced; 
           <p id="closer">final</p> 

现在,body 看起来像下面的代码:

      <body> 
        <p class="opener">first paragraph</p> 
        <p><em>second</em> paragraph</p> 
        <!-- and that's about it --> 
      </body> 

清除子树的所有内容的快速方法是将innerHTML设置为空字符串。这将删除body元素的所有子元素:

    > document.body.innerHTML = ''; 
           "" 

测试如下进行:

    > document.body.firstChild; 
           null 

使用innerHTML进行删除快速而简单。仅使用 DOM 的方法将是遍历所有子节点并逐个删除每个节点。以下是一个从给定起始节点删除所有节点的小函数:

    function removeAll(n) { 
      while (n.firstChild) { 
        n.removeChild(n.firstChild); 
      } 
    } 

如果要删除body元素的所有子元素,并使页面保留一个空的<body></body>,请使用以下代码:

    > removeAll(document.body); 

仅适用于 HTML 的 DOM 对象

如您所知,DOM 适用于 XML 和 HTML 文档。您之前学到的关于遍历树,然后添加、删除或修改节点的内容,适用于任何 XML 文档。然而,有一些仅适用于 HTML 的对象和属性。

document.body是这样一个仅适用于 HTML 的对象。在 HTML 文档中有一个<body>标签是如此常见,而且经常被访问,因此有一个比等效的document.getElementsByTagName('body')[0]更短更友好的对象是有意义的。

document.body元素是从史前 DOM Level 0 继承并移至 DOM 规范的 HTML 扩展的legacy对象的一个示例。还有其他类似document.body元素的对象。对于其中一些对象,没有核心 DOM 等效物,对于其他对象,有一个等效物;然而,DOM 0 原始版本无论如何都被简化和保留了遗留目的。让我们看看其中一些对象。

访问文档的原始方法

对 HTML 文档的元素进行访问。这主要是通过一些集合来完成的,这些集合如下:与 DOM 不同,DOM 可以访问任何元素,甚至是注释和空格,最初,JavaScript 只能有限地访问 HTML 文档的元素。这主要是通过一些集合来完成的,这些集合如下:

  • document.images:这是页面上所有图像的集合。核心 DOM 等效物是document.getElementsByTagName('img')

  • document.applets:这与document.getElementsByTagName('applet')相同。

  • document.linksdocument.links集合包含页面上所有<a href="..."></a>标签的列表,即具有href属性的<a>标签。

  • document.anchorsdocument.anchors集合包含所有具有name属性的链接(<a name="..."></a>)。

  • document.forms:最常用的集合之一是document.forms,其中包含<form>元素的列表。

让我们玩一个包含表单和输入的页面(www.phpied.com/files/jsoop/ch7-form.html)。以下代码行让您访问页面上的第一个表单:

    > document.forms[0]; 

它与以下代码行相同:

    > document.getElementsByTagName('forms')[0]; 

document.forms集合包含通过elements属性访问的输入字段和按钮的集合。以下是如何访问页面上第一个表单的第一个输入:

    > document.forms[0].elements[0]; 

一旦您访问了一个元素,就可以将其属性作为对象属性进行访问。测试页面中第一个表单的第一个字段如下:

    <input name="search" id="search" type="text" size="50" 
        maxlength="255" value="Enter email..." /> 

您可以使用以下代码更改字段中的文本(value属性的值):

    > document.forms[0].elements[0].value = 'me@example.org'; 
        "me@example.org" 

如果要动态禁用字段,请使用以下代码:

    > document.forms[0].elements[0].disabled = true; 

当表单或form元素具有name属性时,您也可以按名称访问它们,如下所示:

    > document.forms[0].elements['search']; // array notation 
    > document.forms[0].elements.search;    // object property 

使用document.write()方法

document.write()方法允许您在页面加载时将 HTML 插入页面。您可以有以下代码:

    <p>It is now  
      <script> 
        document.write("<em>" + new Date() + "</em>"); 
      </script> 
    </p> 

这与您直接在 HTML 文档的源代码中使用日期是一样的,如下所示:

    <p>It is now
      <em>Fri Apr 26 2013 16:55:16 GMT-0700 (PDT)</em> 
    </p> 

请注意,只能在页面加载时使用document.write()方法。如果尝试在页面加载后使用它,它将替换整个页面的内容。

很少情况下你会需要document.write()方法,如果你认为需要,尝试另一种方法。首选使用 DOM Level 1 提供的修改页面内容的方式,更加灵活。

Cookies、标题、引荐者和域

你将在本节中看到的document的四个附加属性也是从 DOM Level 0 移植到 DOM Level 1 的 HTML 扩展中。与之前的不同,对于这些属性,没有核心 DOM 等价物。

document.cookie是一个包含字符串的属性。这个字符串是服务器和客户端之间交换的 cookie 的内容。当服务器向浏览器发送页面时,它可能包含Set-Cookie HTTP 头。当客户端向服务器发送请求时,它会在Cookie头中将 cookie 信息发送回去。使用document.cookie,你可以改变浏览器发送到服务器的 cookie。例如,访问cnn.com并在控制台中输入document.cookie会得到以下输出:

    > document.cookie;
      "mbox=check#true#1356053765|session#1356053704195-121286#1356055565;... 

document.title属性允许你更改浏览器窗口中显示的页面标题。例如,看下面的代码:

    > document.title = 'My title'; 
       "My title" 

请注意,这不会改变<title>元素的值,而只是在浏览器窗口中显示,因此它不等同于document.querySelector('title')

document.referrer属性告诉你先前访问的页面的 URL。这是浏览器在请求页面时发送的Referer HTTP 头的相同值。(注意,在 HTTP 头中Referer拼写错误,但在 JavaScript 的document.referrer中是正确的)。如果你先在 Yahoo 上搜索后访问 CNN 页面,你可以看到类似以下的内容:

    > document.referrer; 
       "http://search.yahoo.com/search?p=cnn&ei=UTF-8&fr=moz2" 

document.domain属性允许你访问当前加载页面的域名。当你需要执行所谓的域放宽时,这通常会被使用。想象一下你的页面是www.yahoo.com,在其中,你有一个托管在music.yahoo.com子域上的 iframe。这是两个独立的域,所以浏览器的安全限制不允许页面和 iframe 进行通信。为了解决这个问题,你可以在两个页面上都设置document.domain属性为yahoo.com,它们就可以互相通信了。

请注意,你只能将域设置为更少特定的域,例如,你可以将www.yahoo.com更改为yahoo.com,但你不能将yahoo.com更改为www.yahoo.com,或者任何其他非雅虎域。考虑以下代码:

    > document.domain; 
       "www.yahoo.com" 
    > document.domain = 'yahoo.com'; 
       "yahoo.com" 
    > document.domain = 'www.yahoo.com'; 
       Error: SecurityError: DOM Exception 18 
    > document.domain = 'www.example.org'; 
       Error: SecurityError: DOM Exception 18 

在本章中,你看到了window.location对象。同样的功能也可以通过document.location对象实现:

    > window.location === document.location; 
       true 

事件

想象一下你正在听收音机节目,他们宣布说:“大事件!巨大的!外星人已经降落在地球上!”你可能会想:“是啊,随便”;其他一些听众可能会想“他们是和平的”;还有一些人可能会想:“我们都会死!”同样,浏览器会广播事件,如果你决定调整并监听事件发生,你的代码可以被通知。一些示例事件如下:

  • 用户点击按钮

  • 用户在表单字段中输入字符

  • 页面加载完成

你可以将一个名为事件监听器或事件处理程序的 JavaScript 函数附加到特定事件上,浏览器将在事件发生时立即调用你的函数。让我们看看如何做到这一点。

内联 HTML 属性

向标签添加特定属性是最懒惰但最不可维护的方式;以以下代码为例:

    <div onclick="alert('Ouch!')">click</div> 

在这种情况下,当用户点击<div>时,点击事件触发,onclick属性中包含的 JavaScript 代码字符串将被执行。虽然没有明确的函数监听点击事件;然而,在幕后,仍然创建了一个函数,它包含你在onclick属性的值中指定的代码。

元素属性

当点击事件触发时,另一种执行一些代码的方法是将函数分配给 DOM 节点元素的onclick属性。例如,看一下以下代码片段:

    <div id="my-div">click</div> 
    <script> 
      var myelement = document.getElementById('my-div'); 
      myelement.onclick = function () { 
        alert('Ouch!'); 
        alert('And double ouch!'); 
      }; 
    </script> 

这种方式更好,因为它有助于保持<div>标签不含任何 JavaScript 代码。请始终记住,HTML 用于内容,JavaScript 用于行为,CSS 用于格式化,您应尽可能将这三者分开。

这种方法的缺点是您只能将一个函数附加到事件,就像广播节目只有一个听众一样。确实,您可以在同一个函数中做很多事情,但这并不总是方便的,就像所有广播听众都在同一个房间里一样。

DOM 事件监听器

与浏览器事件一起工作的最佳方法是使用 DOM Level 2 中概述的事件监听器方法,其中可以有多个函数监听事件。当事件触发时,所有函数都会被执行。所有监听器不需要相互了解,并且可以独立工作。它们可以随时调整,而不会影响其他监听器。

让我们使用上一节中相同的简单标记,你可以在www.phpied.com/files/jsoop/ch7.html上玩耍。它有以下标记:

    <p id="closer">final</p> 

您的 JavaScript 代码可以使用addEventListener()方法为点击事件分配监听器。让我们按如下方式附加两个监听器:

    var mypara = document.getElementById('closer'); 
    mypara.addEventListener('click', function () { 
      alert('Boo!'); 
    }, false); 
    mypara.addEventListener( 
      'click', console.log.bind(console), false); 

正如你所看到的,addEventListeners

捕获和冒泡

在调用addEventListener()时,有一个第三个参数-false。让我们看看它是什么。

假设您在无序列表中有一个链接,如下所示:

    <body> 
      <ul> 
        <li><a href="http://phpied.com">my blog</a></li> 
      </ul> 
    </body> 

当您点击链接时,实际上也点击了列表项<li><ul>列表,<body>标签,最终是整个文档。这称为事件传播。单击链接也可以看作是单击文档。传播事件的过程可以通过以下两种方式实现:

  • 事件捕获:此点击首先发生在文档中,然后向下传播到 body,列表,列表项,最后到链接

  • 事件冒泡:此点击发生在链接上,然后冒泡到文档

DOM Level 2 事件规范建议事件在三个阶段传播,即捕获,目标,冒泡。这意味着事件从文档传播到链接(目标),然后再冒泡回文档。事件对象具有一个eventPhase属性,反映当前阶段:

捕获和冒泡

从历史上看,IE 和 Netscape(各自独立工作,没有遵循的标准)实现了完全相反的方法。IE 只实现了冒泡,而 Netscape 只实现了捕获。今天,在 DOM 规范很久之后,现代浏览器实现了所有三个阶段。

与事件传播相关的实际影响如下:

  • addEventListener()的第三个参数指定是否使用捕获。为了使您的代码在各种浏览器中更具可移植性,最好始终将此参数设置为false并仅使用冒泡。

  • 您可以在监听器中停止事件传播,使其停止冒泡并永远不会到达文档。为此,您可以调用事件对象的stopPropagation()方法;下一节中有一个示例。

  • 您还可以使用事件委托。如果在<div>内有十个按钮,您可以始终附加十个事件监听器,每个按钮一个。但是,更明智的做法是仅将一个监听器附加到包装的<div>上,一旦事件发生,检查哪个按钮是点击的目标。

顺便说一句,在旧版 IE 中也有一种方法可以使用事件捕获(使用setCapture()releaseCapture()方法),但仅适用于鼠标事件。不支持捕获任何其他事件(例如按键事件)。

停止传播

让我们看一个示例,说明如何阻止事件冒泡。回到测试文档,有这段代码:

    <p id="closer">final</p> 

让我们定义一个处理段落点击的函数,如下所示:

    function paraHandler() { 
      alert('clicked paragraph'); 
    } 

现在,让我们将此函数作为点击事件的侦听器附加:

    var para = document.getElementById('closer'); 
    para.addEventListener('click', paraHandler, false); 

让我们还将侦听器附加到 body、document 和浏览器窗口的点击事件:

    document.body.addEventListener('click', function () { 
      alert('clicked body'); 
    }, false); 
    document.addEventListener('click', function () { 
      alert('clicked doc'); 
    }, false); 
    window.addEventListener('click', function () { 
      alert('clicked window'); 
    }, false); 

请注意,DOM 规范对窗口上的事件没有任何说明。为什么会这样呢?因为 DOM 处理的是文档,而不是浏览器。因此,浏览器对窗口事件的实现是不一致的。

现在,如果您点击段落,您将看到四个警报,内容如下:

  • 点击段落

  • 点击 body

  • 点击文档

  • 点击窗口

这说明了同一个单击事件是如何从目标一直冒泡到窗口的。

addEventLister()的相反操作是removeEventListener(),它接受完全相同的参数。让我们通过编写以下代码来删除附加到段落的侦听器:

    > para.removeEventListener('click', paraHandler, false); 

如果您现在尝试,您将只会在 body、document 和 window 的点击事件上看到警报,而不会在段落上看到。

现在,让我们停止事件的传播。您作为侦听器添加的函数将接收事件对象作为参数,并且您可以调用该事件对象的stopPropagation()方法,如下所示:

    function paraHandler(e) { 
      alert('clicked paragraph'); 
      e.stopPropagation(); 
    } 

添加修改后的侦听器如下所示:

    para.addEventListener('click', paraHandler, false); 

现在,当您点击段落时,您将只会看到一个警报,因为事件不会冒泡到 body、document 或 window。

请注意,当您删除侦听器时,您必须传递指向先前附加的相同函数的指针。否则,以下操作不起作用,因为第二个参数是一个新函数,而不是您在添加事件侦听器时传递的相同函数,即使 body 完全相同也是如此。请考虑以下代码:

    document.body.removeEventListener('click',  
      function () { 
        alert('clicked body'); 
      },  
    false); //  does NOT remove the handler 

阻止默认行为

一些浏览器事件具有预定义的行为。例如,单击链接会导致浏览器导航到另一个页面。您可以附加侦听器以监听链接的点击,并且还可以通过在事件对象上调用preventDefault()方法来禁用默认行为。

让我们看看如何通过每次点击链接时询问“您确定要跟随此链接吗?”来打扰您的访问者?如果用户点击“取消”(导致confirm()返回false),则调用preventDefault()方法,如下所示:

    // all links 
    var all_links = document.getElementsByTagName('a');  
    for (var i = 0; i < all_links.length; i++) { // loop all links 
      all_links[i].addEventListener( 
        'click',       // event type 
        function (e) { // handler 
          if (!confirm('Sure you want to follow this link?')) { 
            e.preventDefault(); 
          } 
        }, 
        false // don't use capturing 
      );  
    } 

请注意,并非所有事件都允许您阻止默认行为。大多数事件都可以,但如果您想确保,可以检查事件对象的cancellable属性。

跨浏览器事件侦听器

正如您已经知道的那样,大多数现代浏览器几乎完全实现了 DOM Level 1 规范。但是,直到 DOM 2 标准化之前,事件才得到标准化。因此,IE 在版本 9 之前实现此功能与现代浏览器相比存在相当多的差异。

查看一个示例,导致被点击元素(目标元素)的nodeName被写入控制台:

    document.addEventListener('click', function (e) { 
      console.log(e.target.nodeName); 
    }, false); 

现在,让我们看看 IE 有何不同:

  • 在 IE 中,没有addEventListener()方法;尽管自 IE 5 版本以来,有一个等效的attachEvent()方法。对于早期版本,您唯一的选择是直接访问属性,例如onclick

  • 使用attachEvent()时,click事件变为onclick

  • 如果您以老式方式监听事件(例如,通过将函数值设置为onclick属性),当调用回调函数时,它不会作为参数传递事件对象。但是,无论您如何在 IE 中附加侦听器,始终存在一个指向最新事件的全局对象window.event

  • 在 IE 中,事件对象没有目标属性,告诉您事件触发的元素,但它确实有一个称为srcElement的等效属性。

  • 如前所述,事件捕获不适用于所有事件,因此只应使用冒泡。

  • 没有stopPropagation()方法,但是你可以将 IE 专用的cancelBubble属性设置为true

  • 没有preventDefault()方法,但是你可以将 IE 专用的returnValue属性设置为false

  • 要停止监听事件,而不是在 IE 中使用removeEventListener(),您将需要detachEvent()

因此,这是先前代码的修订版本,可在各种浏览器中使用:

    function callback(evt) { 
      // prep work 
      evt = evt || window.event; 
      var target = evt.target || evt.srcElement; 

     // actual callback work 
      console.log(target.nodeName); 
    } 

    //  start listening for click events 
    if (document.addEventListener) { // Modern browsers 
      document.addEventListener('click', callback, false); 
    } else if (document.attachEvent) { // old IE 
      document.attachEvent('onclick', callback); 
    } else { 
      document.onclick = callback; // ancient 
    } 

事件类型

现在您知道如何处理跨浏览器事件了。但是,所有先前的示例只使用了点击事件。其他事件正在发生吗?您可能已经猜到,不同的浏览器提供不同的事件。有一组跨浏览器事件和一些特定于浏览器的事件。要获取事件的完整列表,您应该查阅浏览器的文档,但是这里是一些跨浏览器事件的选择:

  • 鼠标事件

  • mouseupmousedownclick(顺序为 mousedown-up-click),dblclick

  • mouseover(鼠标悬停在元素上),mouseout(鼠标悬停在元素上但离开了它),mousemove

  • 键盘事件

  • keydownkeypresskeyup(按此顺序发生)

  • 加载/窗口事件

  • load(加载图像或页面及其所有组件完成加载),unload(用户离开页面),beforeunload(脚本可以为用户提供停止卸载的选项)

  • abort(用户停止在 IE 中加载页面或图像),error(JavaScript 错误,也是在 IE 中无法加载图像时)

  • resize(浏览器窗口被调整大小),scroll(页面被滚动),contextmenu(右键菜单出现)

  • 表单事件

  • focus(进入表单字段),blur(离开表单字段)

  • change(在值更改后离开字段),select(在文本字段中选择文本)

  • reset(清除所有用户输入),submit(发送表单)

此外,现代浏览器提供拖动事件(dragstartdragenddrop等),触摸设备提供touchstarttouchmovetouchend

这结束了事件的讨论。请参考本章末尾的练习部分,挑战自己创建自己的事件实用程序来处理跨浏览器事件。

XMLHttpRequest

XMLHttpRequest()是一个允许您从 JavaScript 发送 HTTP 请求的对象(构造函数)。从历史上看,XHR(XMLHttpRequest)是在 IE 中引入的,并且被实现为 ActiveX 对象。从 IE7 开始,它是一个本机浏览器对象,就像其他浏览器中一样。跨浏览器对此对象的常见实现产生了所谓的 Ajax 应用程序,不再需要每次需要新内容时刷新整个页面。使用 JavaScript,您可以向服务器发出 HTTP 请求,获取响应,并仅更新页面的一部分。这样,您可以构建更具响应性和类似桌面的网页。

Ajax代表异步 JavaScript 和 XML

  • 异步是因为在发送 HTTP 请求后,您的代码不需要等待响应;但是,它可以做其他事情,并在响应到达时通过事件通知。

  • JavaScript,因为很明显 XHR 对象是用 JavaScript 创建的。

  • XML,因为最初开发人员正在为 XML 文档发出 HTTP 请求,并且正在使用其中包含的数据来更新页面。尽管这不再是常见做法,因为您可以请求以纯文本、更方便的 JSON 格式或简单地作为准备插入页面的 HTML 的数据。

使用XMLHttpRequest对象有两个步骤,如下所示:

  • 发送请求:这包括创建一个XMLHttpRequest对象并附加事件监听器

  • 处理响应:当您的事件监听器得到通知响应已经到达,并且您的代码忙于处理响应时

发送请求

为了创建一个对象,你只需使用以下代码(让我们稍后再处理浏览器的不一致性):

    var xhr = new XMLHttpRequest(); 

接下来要做的是将事件监听器附加到对象触发的readystatechange事件上:

    xhr.onreadystatechange = myCallback; 

然后,您需要调用open()方法,如下所示:

    xhr.open('GET', 'somefile.txt', true); 

第一个参数指定 HTTP 请求的类型,例如GETPOSTHEAD等。GETPOST是最常见的。当您不需要发送太多数据的请求并且您的请求不会修改(写入)服务器上的数据时,请使用GET,否则请使用POST。第二个参数是您正在请求的 URL。在这个例子中,它是位于与页面相同目录中的文本文件somefile.txt。最后一个参数是一个布尔值,指定请求是异步的(true,始终首选)还是同步的(false,阻止所有 JavaScript 执行并等待直到响应到达)。

最后一步是发出请求,步骤如下:

    xhr.send(''); 

send()方法接受您想要发送的请求数据。对于GET请求,这是一个空字符串,因为数据在 URL 中。对于POST请求,它是一个查询字符串,形式为key=value&key2=value2

此时,请求已发送,您的代码和用户可以继续其他任务。当响应从服务器返回时,回调函数myCallback将被调用。

处理响应

监听器附加到readystatechange事件。那么,准确来说 ready 状态是什么,它是如何改变的呢?

XHR 对象有一个叫做readyState的属性。每次它改变时,readystatechange事件就会触发。readyState属性的可能值如下:

  • 0-未初始化

  • 1-加载

  • 2-已加载

  • 3-交互

  • 4-完成

readyState得到值4时,意味着响应已经返回并准备好被处理。在myCallback中,确保readyState4后,要检查的另一件事是 HTTP 请求的状态码。例如,您可能已经请求了一个不存在的 URL,并得到了404(文件未找到)的状态码。有趣的代码是200OK)代码,所以myCallback应该检查这个值。状态码在 XHR 对象的status属性中可用。

一旦xhr.readyState4xhr.status200,您就可以使用xhr.responseText属性访问所请求的 URL 的内容。让我们看看如何实现myCallback来简单地alert()所请求的 URL 的内容:

    function myCallback() { 

      if (xhr.readyState < 4) { 
        return; // not ready yet 
      } 

      if (xhr.status !== 200) { 
        alert('Error!'); // the HTTP status code is not OK 
        return; 
      } 

      //  all is fine, do the work 
      alert(xhr.responseText); 
    } 

一旦您收到了您请求的新内容,您可以将其添加到页面上,用于一些计算,或者用于您认为合适的任何其他目的。

总的来说,这个两步过程(发送请求和处理响应)是整个 XHR/Ajax 功能的核心。现在您已经了解了基础知识,可以继续构建下一个 Gmail。哦是的,让我们看看一些次要的浏览器不一致性。

在 IE 7 之前创建 XMLHttpRequest 对象

在 Internet Explorer 7 之前的版本中,XMLHttpRequest对象是一个 ActiveX 对象,因此创建 XHR 实例有点不同。步骤如下:

    var xhr = new ActiveXObject('MSXML2.XMLHTTP.3.0'); 

MSXML2.XMLHTTP.3.0是您想要创建的对象的标识符。XMLHttpRequest对象有几个版本,如果您的页面访问者没有安装最新版本,您可以在放弃之前尝试两个旧版本。

为了一个完全跨浏览器的解决方案,您应该首先测试用户的浏览器是否支持XMLHttpRequest作为一个原生对象,如果不支持,尝试 IE 的方式。因此,创建 XHR 实例的整个过程可能如下所示:

    var ids = ['MSXML2.XMLHTTP.3.0', 
           'MSXML2.XMLHTTP', 
           'Microsoft.XMLHTTP']; 

    var xhr; 
    if (XMLHttpRequest) { 
      xhr = new XMLHttpRequest(); 
    } else { 
      // IE: try to find an ActiveX object to use 
      for (var i = 0; i < ids.length; i++) { 
        try { 
          xhr = new ActiveXObject(ids[i]); 
          break; 
        } catch (e) {} 
      } 
    } 

这是在做什么?ids数组包含要尝试的 ActiveX 程序 ID 列表。xhr变量指向新的 XHR 对象。代码首先检查XMLHttpRequest是否存在。如果存在,这意味着浏览器原生支持XMLHttpRequest(),因此浏览器相对较新。如果不存在,代码会循环尝试创建对象。catch(e)块会静默忽略失败,循环继续。一旦创建了xhr对象,就会跳出循环。

如您所见,这是相当多的代码,最好将其抽象成一个函数。实际上,在本章末尾的练习中,有一个练习要求您创建自己的 Ajax 实用程序。

A 代表异步

现在您知道如何创建 XHR 对象,给它一个 URL 并处理请求的响应。当您异步发送两个请求时会发生什么?如果第二个请求的响应在第一个请求之前到达会怎么样?

在上面的示例中,XHR 对象是全局的,myCallback依赖于全局对象的存在来访问其readyStatestatusresponseText属性。另一种方法,可以避免依赖全局变量,就是将回调函数封装在闭包中。让我们看看:

    var xhr = new XMLHttpRequest(); 

    xhr.onreadystatechange = (function (myxhr) { 
      return function () {  
        myCallback(myxhr);  
      }; 
    }(xhr)); 

    xhr.open('GET', 'somefile.txt', true); 
    xhr.send(''); 

在这种情况下,myCallback()将 XHR 对象作为参数接收,并不会在全局空间中寻找它。这也意味着在接收响应时,原始的xhr可能会被重用来进行第二次请求。闭包保持指向原始对象。

X 代表 XML

尽管如今 JSON(在下一章中讨论)作为数据传输格式优先于 XML,但 XML 仍然是一个选项。除了responseText属性之外,XHR 对象还有另一个属性叫做responseXML。当您发送一个 XML 文档的 HTTP 请求时,responseXML指向一个 XML DOM 文档对象。要处理此文档,您可以使用本章前面讨论过的所有核心 DOM 方法,比如getElementsByTagName()getElementById()等。

一个例子

让我们用一个例子总结不同的 XHR 主题。您可以访问位于www.phpied.com/files/jsoop/xhr.html的页面来自己操作示例。

主页xhr.html是一个简单的静态页面,里面只包含三个<div>标签,如下所示:

    <div id="text">Text will be here</div> 
    <div id="html">HTML will be here</div> 
    <div id="xml">XML will be here</div> 

使用控制台,您可以编写代码请求三个文件,并将它们各自的内容加载到每个<div>中。

要加载的三个文件如下:

  • content.txt:这是一个包含文本I am a text file的简单文本文件

  • content.html:这是一个包含 HTML 代码的文件I am <strong>formatted</strong> <em>HTML</em>

  • content.xml:这是一个包含以下代码的 XML 文件:

    <?xml version="1.0" ?> 
    <root> 
        I'm XML data. 
    </root> 

所有文件都存储在与xhr.html相同的目录中。

出于安全原因,您只能使用原始的XMLHttpRequest来请求与同一域上的文件。然而,现代浏览器支持 XHR2,允许您进行跨域请求,前提是适当的 Access-Control-Allow-Origin HTTP 头已经就位。

首先,让我们创建一个函数来抽象请求/响应部分:

    function request(url, callback) { 
      var xhr = new XMLHttpRequest();  
      xhr.onreadystatechange = (function (myxhr) { 
        return function () { 
          if (myxhr.readyState === 4 && myxhr.status === 200) { 
            callback(myxhr); 
          } 
        }; 
      }(xhr)); 
      xhr.open('GET', url, true); 
      xhr.send(''); 
    } 

此函数接受一个要请求的 URL 和一个一旦响应到达就要调用的回调函数。让我们调用该函数三次,每次请求一个文件,如下所示:

    request( 
      'http://www.phpied.com/files/jsoop/content.txt', 
      function (o) { 
        document.getElementById('text').innerHTML = 
          o.responseText; 
      } 
    ); 
    request( 
      'http://www.phpied.com/files/jsoop/content.html', 
      function (o) { 
        document.getElementById('html').innerHTML = 
          o.responseText; 
      } 
    ); 
    request( 
      'http://www.phpied.com/files/jsoop/content.xml', 
      function (o) { 
        document.getElementById('xml').innerHTML = 
          o.responseXML 
           .getElementsByTagName('root')[0] 
           .firstChild 
           .nodeValue; 
      }   
    ); 

回调函数是内联定义的。前两个是相同的。它们只是用请求文件的内容替换相应<div>的 HTML。第三个有点不同,因为它涉及 XML 文档。首先,您将访问 XML DOM 对象作为o.responseXML。然后,使用getElementsByTagName(),您将得到所有<root>标签的列表(只有一个)。<root>firstChild是一个文本节点,nodeValue是其中包含的文本(I'm XML data)。然后,只需用新内容替换<div id="xml">的 HTML。结果如下截图所示:

一个例子

在处理 XML 文档时,您还可以使用o.responseXML.documentElement来获取<root>元素,而不是o.responseXML.getElementsByTagName('root')[0]。请记住,documentElement给您提供了 XML 文档的根节点。HTML 文档中的根节点始终是<html>标签。

练习

在以前的章节中,练习的解决方案可以在章节的文本中找到。这一次,一些练习需要您进行更多阅读或实验,超出本书的范围。

  1. BOM:作为 BOM 练习,尝试编写一些错误的、侵入性的、用户不友好的,总的来说,非常 Web 1.0 的代码,使浏览器窗口摇晃。尝试实现打开一个 200 x 200 的弹出窗口,然后慢慢地将其调整大小到 400 x 400。接下来,移动窗口,就像发生地震一样。您只需要一个move*()函数,一个或多个setInterval()调用,也许还有一个setTimeout()/clearInterval()来停止整个过程。或者,这里有一个更简单的方法-在document.title中打印当前日期/时间,并每秒更新一次,就像时钟一样。

  2. DOM:

  • 以不同的方式实现walkDOM()。还使其接受回调函数,而不是硬编码console.log()

  • 使用innerHTML删除内容很容易(document.body.innerHTML = ''),但并非总是最佳选择。问题将出现在已附加到已删除元素的事件侦听器上;它们在 IE 中不会被移除,导致浏览器泄漏内存,因为它存储对不存在的东西的引用。实现一个通用函数,删除 DOM 节点,但首先删除任何事件侦听器。您可以循环遍历节点的属性,并检查值是否为函数。如果是,它很可能是像onclick这样的属性。在删除元素之前,您需要将其设置为null

  • 创建一个名为include()的函数,根据需要包含外部脚本。这意味着您需要动态创建一个新的<script>标签,设置其src属性,并将其附加到文档的<head>中。通过使用以下代码进行测试:

        > include('somescript.js'); 

  1. 事件:
  • 创建一个名为myevent的事件实用程序(对象),它具有以下跨浏览器工作的方法:

  • addListener``(element, event_name, callback),其中element也可以是元素的数组

  • removeListener``(element, event_name, callback)

  • getEvent(event)只是为了检查旧版本 IE 的window.event

  • getTarget(event)

  • stopPropagation(event)

  • preventDefault(event)

  • 使用示例如下:

        function myCallback(e) { 
          e = myevent.getEvent(e); 
          alert(myevent.getTarget(e).href); 
          myevent.stopPropagation(e); 
          myevent.preventDefault(e); 
        } 
        myevent.addListener(document.links, 'click', myCallback); 

  • 示例代码的结果应该是文档中的所有链接都不起作用,只会弹出href属性。

  • 创建一个绝对定位的<div>,比如在x = 100pxy = 100px的位置。编写代码,使得能够使用箭头键或J(左)、K(右)、M(下)和I(上)键在页面上移动 div。重用您自己的事件实用程序从 3.1。

  1. XMLHttpRequest:
  • 创建您自己的 XHR 实用程序(对象)称为ajax。例如,看一下以下代码:
        function myCallback(xhr) { 
          alert(xhr.responseText); 
        } 
        ajax.request('somefile.txt', 'get', myCallback); 
        ajax.request('script.php', 'post', myCallback, 
        'first=John&last=Smith'); 

摘要

在本章中,您学到了很多东西。您学到了以下跨浏览器 BOM 对象:

  • 全局window对象的属性,如navigatorlocationhistoryframesscreen

  • 方法,如setInterval()setTimeout()alert()confirm()prompt()moveTo/By()resizeTo/By()

然后,您学习了 DOM,这是一个用于表示 HTML 或 XML 文档的 API,它将其表示为树结构,其中每个标签或文本都是树上的一个节点。您还学习了如何执行以下操作:

  • 访问节点:

  • 使用父/子关系属性,如parentNodechildNodesfirstChildlastChildnextSiblingpreviousSibling

  • 使用getElementsById()getElementsByTagName()getElementsByName()querySelectorAll()

  • 修改节点:

  • 使用innerHTMLinnerText/textContent

  • 使用nodeValuesetAttribute(),或者只是将属性作为对象属性使用

  • 使用removeChild()replaceChild()删除节点

  • 使用appendChild()cloneNode()insertBefore()添加新节点

您还学习了以下 DOM 0(预标准化)属性,迁移到 DOM Level 1:

  • 集合,如document.formsimageslinksanchorsapplets。不建议使用这些,因为 DOM1 具有更灵活的getElementsByTagName()方法。

  • document.body元素,它方便地让您访问<body>

  • document.titlecookiereferrerdomain

接下来,您学习了浏览器如何广播事件,您可以监听这些事件。以跨浏览器的方式执行此操作并不直接,但是是可能的。事件会冒泡,因此您可以使用事件委托来更全局地监听事件。您还可以阻止事件的传播并干预默认的浏览器行为。

最后,您学习了XMLHttpRequest对象,它允许您构建响应式网页,执行以下任务:

  • 向服务器发出 HTTP 请求以获取数据片段

  • 处理响应以更新页面的部分

第十一章:编码和设计模式

既然您已经了解了 JavaScript 中的所有对象,掌握了原型和继承,并看到了使用特定于浏览器的对象的一些实际示例,让我们继续前进,或者说,向上移动一级。让我们来看看一些常见的 JavaScript 模式。

但首先,什么是模式?简而言之,模式是对常见问题的良好解决方案。将解决方案编码为模式使其可重复使用。

有时,当您面对一个新的编程问题时,您可能立即意识到您以前解决过另一个非常相似的问题。在这种情况下,值得将这类问题隔离出来,并寻找一个共同的解决方案。模式是一种经过验证和可重复使用的解决方案(或解决方案的方法)。

有时,模式只是一个想法或一个名称。有时,仅仅使用一个名称可以帮助您更清晰地思考问题。此外,在团队中与其他开发人员合作时,当每个人使用相同的术语讨论问题或解决方案时,沟通会更容易。

有时,您可能会遇到一个独特的问题,看起来与您以前见过的任何东西都不一样,并且不容易适应已知的模式。盲目地应用模式只是为了使用模式,这不是一个好主意。最好不要使用任何已知的模式,而是尝试调整问题,使其适应现有的解决方案。

本章讨论了以下两种模式:

  • 编码模式:这些主要是 JavaScript 特定的最佳实践

  • 设计模式:这些是与语言无关的模式,由著名的四人帮书籍推广

编码模式

让我们从一些反映 JavaScript 独特特性的模式开始。一些模式旨在帮助您组织代码,例如命名空间;其他与改进性能有关,例如延迟定义和初始化时分支;还有一些弥补了缺失的功能,例如私有属性。本节讨论的模式包括以下主题:

  • 分离行为

  • 命名空间

  • 初始化时分支

  • 延迟定义

  • 配置对象

  • 私有变量和方法

  • 特权方法

  • 将私有函数作为公共方法

  • 立即函数

  • 链接

  • JSON

分离行为

如前所述,网页的三个构建块如下:

  • 内容(HTML)

  • 演示(CSS)

  • 行为(JavaScript)

内容

HTML 是网页的内容,实际文本。理想情况下,内容应该使用尽可能少的 HTML 标记进行标记,以充分描述该内容的语义含义。例如,如果您正在处理导航菜单,最好使用<ul><li>标记,因为导航菜单本质上只是一个链接列表。

您的内容(HTML)应该不包含任何格式化元素。视觉格式应属于演示层,并且应通过CSS(层叠样式表)来实现。这意味着以下内容:

  • 如果可能的话,不应该使用 HTML 标记的样式属性。

  • 根本不应该使用<font>等呈现 HTML 标签。

  • 标记应该根据其语义含义使用,而不是因为浏览器默认呈现它们。例如,开发人员有时会在更适合使用<p>的地方使用<div>标记。使用<strong><em>而不是<b><i>也是有利的,因为后者描述的是视觉呈现而不是含义。

演示

将演示内容与内容分开的一个好方法是重置或清空所有浏览器默认设置,例如使用来自 Yahoo! UI 库的reset.css。这样,浏览器的默认呈现不会让您分心,而是会让您有意识地考虑使用适当的语义标记。

行为

网页的第三个组件是行为。行为应该与内容和表现分开。通常使用隔离在<script>标签中的 JavaScript 来添加,最好包含在外部文件中。这意味着不使用任何内联属性,如onclickonmouseover等。相反,您可以使用上一章中的addEventListener/attachEvent方法。

将行为与内容分离的最佳策略如下:

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

  • 避免内联事件处理程序

  • 不要使用 CSS 表达式

  • 在内容的末尾,当您准备关闭<body>标签时,插入一个external.js文件

行为分离示例

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

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

behaviors.js中,您可以将事件侦听器附加到提交事件。在您的侦听器中,您可以检查文本输入字段是否为空,如果是,则阻止表单提交。这样,您将节省服务器和客户端之间的往返,并使应用程序立即响应。

behaviors.js的内容如下所示。它假定您已经根据上一章的练习创建了您的myevent实用程序:

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

异步 JavaScript 加载

您注意到脚本是在 HTML 结束前加载的,就在关闭 body 之前。原因是 JavaScript 会阻止页面的 DOM 构建,并且在某些浏览器中,甚至会阻止后续组件的下载。通过将脚本移动到页面底部,您可以确保脚本不会妨碍,并且当它到达时,它只是增强了已经可用的页面。

防止外部 JavaScript 文件阻止页面的另一种方法是异步加载它们。这样您可以更早地开始加载它们。HTML5 具有此目的的defer属性。请考虑以下代码行:

    <script defer src="behaviors.js"></script> 

不幸的是,defer属性不受旧版浏览器支持,但幸运的是,有一个可以跨浏览器(新旧)工作的解决方案。解决方案是动态创建一个script节点并将其附加到 DOM。换句话说,您可以使用一点内联 JavaScript 来加载外部 JavaScript 文件。您可以在文档顶部放置此脚本加载程序片段,以便下载可以尽早开始。请看以下代码示例:

    ... 
    <head> 
    <script> 
    (function () { 
      var s = document.createElement('script'); 
      s.src = 'behaviors.js'; 
      document.getElementsByTagName('head')[0].appendChild(s); 
    }()); 
    </script> 
    </head> 
    ... 

命名空间

应避免全局变量以减少变量命名冲突的可能性。通过为变量和函数命名空间化,您可以最小化全局变量的数量。这个想法很简单,您只会创建一个全局对象,而您的所有其他变量和函数都成为该对象的属性。

对象作为命名空间

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

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

现在,不再需要全局的myevent实用程序(来自上一章),您可以将其作为MYAPP对象的event属性,如下所示:

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

event实用程序添加方法仍然是相同的。请考虑以下示例:

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

命名空间构造函数

使用命名空间不妨碍您创建构造函数。以下是如何创建具有Element构造函数的 DOM 实用程序,它允许您轻松创建 DOM 元素:

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

类似地,您可以有一个Text构造函数来创建文本节点。请考虑以下代码示例:

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

使用构造函数在页面底部创建链接可以按以下方式完成:

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

一个命名空间()方法

您可以创建一个命名空间实用程序,使您的生活更轻松,以便您可以使用更方便的语法,如下所示:

    MYAPP.namespace('dom.style'); 

而不是更冗长的语法如下:

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

以下是如何创建namespace()方法的方法。首先,您将使用句点(.)作为分隔符拆分输入字符串,创建一个数组。然后,对于新数组中的每个元素,如果全局对象中不存在该属性,则添加一个属性,如下所示:

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

通过以下方式进行新方法的测试:

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

前面代码的结果与以下操作相同:

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

初始化时分支

在前一章中,您注意到有时不同的浏览器对相同或类似的功能有不同的实现。在这种情况下,您需要根据当前执行脚本的浏览器支持的内容对代码进行分支。根据您的程序,这种分支可能会发生得太频繁,结果可能会减慢脚本的执行速度。

您可以通过在初始化时对代码的某些部分进行分支来缓解这个问题,当脚本加载时,而不是在运行时。借助动态定义函数的能力,您可以根据浏览器的不同分支和定义相同的函数,具体取决于浏览器。让我们看看如何。

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

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

此时,添加或删除侦听器的方法尚未实现。根据特性嗅探的结果,可以以不同的方式定义这些方法,如下所示:

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

脚本执行后,您将以与浏览器相关的方式定义addListener()removeListener()方法。现在,每次调用这些方法时,都不再需要特性嗅探,这将减少工作量并加快执行速度。

在嗅探特性时要注意的一点是,在检查一个特性后不要假设太多。在前面的示例中,这条规则被打破了,因为代码只检查了addEventListener的支持,但随后定义了addListener()removeListener()。在这种情况下,可以假设如果浏览器实现了addEventListener(),那么它也实现了removeEventListener()。然而,想象一下,如果浏览器实现了stopPropagation()但没有实现preventDefault(),而您没有单独检查这些情况会发生什么。您假设因为addEventListener()未定义,浏览器必须是一个旧的 IE,并使用您对 IE 工作方式的知识和假设来编写代码。请记住,您所有的知识都是基于某个浏览器今天的工作方式,但不一定是明天的工作方式。因此,为了避免在新的浏览器版本发布时多次重写代码,最好单独检查您打算使用的特性,并不要对某个浏览器支持的特性进行概括。

懒惰定义

懒惰定义模式类似于先前的初始化时分支模式。不同之处在于分支只会在第一次调用函数时发生。当调用函数时,它会使用最佳实现重新定义自身。与初始化时分支不同,初始化时分支只发生一次,在加载时,而在这里,当函数从未被调用时,可能根本不会发生。懒惰定义还使初始化过程更轻松,因为不需要进行初始化时分支工作。

让我们通过定义一个addListener()函数的示例来说明这一点。首先,该函数使用通用的主体进行定义。当首次调用函数时,它会检查浏览器支持的功能,然后使用最合适的实现重新定义自身。在第一次调用结束时,函数会调用自身,以便执行实际的事件附加。下次调用相同的函数时,它将使用新的主体进行定义,并准备好使用,因此不需要进一步的分支。以下是代码片段:

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

配置对象

当您有一个接受许多可选参数的函数或方法时,这种模式很方便。由您决定多少个构成了很多。但一般来说,一个具有三个以上参数的函数不方便调用,因为您必须记住参数的顺序,当一些参数是可选的时,这更加不方便。

而不是有许多参数,您可以使用一个参数并将其设置为对象。对象的属性是实际参数。这适用于传递配置选项,因为这些 tend to be numerous and optional (with smart defaults). 使用单个对象而不是多个参数的美妙之处如下所述:

  • 顺序无关紧要

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

  • 很容易添加更多的可选配置属性

  • 它使代码更易读,因为配置对象的属性与它们的名称一起出现在调用代码中

想象一下,您有一些 UI 小部件构造函数,用于创建漂亮的按钮。它接受要放在按钮内部的文本(<input>标签的value属性)以及type按钮的可选参数。为简单起见,让我们假设漂亮的按钮采用与常规按钮相同的配置。看一下以下代码:

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

使用构造函数很简单;您只需给它一个字符串。然后,您可以将新按钮添加到文档的主体中,如下所示:

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

这一切都很好,运行良好,但是然后您决定还想能够设置按钮的一些样式属性,比如颜色和字体。您最终可能会得到以下定义:

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

现在,使用构造函数可能会变得有点不方便,特别是当您想设置第三个和第五个参数,但不想设置第二个或第四个时。考虑以下示例:

    new MYAPP.dom.FancyButton( 
      'puuush', null, 'white', null, 'Arial'); 

更好的方法是使用一个config对象参数来设置所有的设置。函数定义可以变成以下代码片段:

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

使用构造函数如下所示:

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

另一个用法示例如下:

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

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

这种模式的缺点与其优点相同。很容易不断添加更多的参数,这意味着滥用这种技术很容易。一旦您有理由向这个自由的属性包中添加更多内容,您会发现很容易不断添加一些并非完全可选的属性,或者一些依赖于其他属性的属性。

作为一个经验法则,所有这些属性都应该是独立的和可选的。如果您必须在函数内部检查所有可能的组合(“哦,A 已设置,但只有在 B 也设置了 A 才会被使用”),这将导致一个庞大的函数体,很快就会变得令人困惑和难以理解,甚至是不可能测试,因为所有的组合。

私有属性和方法

JavaScript 没有访问修饰符的概念,它设置对象中属性的特权。其他语言通常有访问修饰符,如下所示:

  • Public: 对象的所有用户都可以访问这些属性或方法

  • Private: 只有对象本身才能访问这些属性

  • Protected: 只有继承所讨论的对象的对象才能访问这些属性

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

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

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

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

特权方法

特权方法(这个术语是由 Douglas Crockford 创造的)是可以访问私有方法或属性的普通公共方法。它们可以充当桥梁,以受控的方式包装特定的私有功能,使其可访问。

私有函数作为公共方法

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

让我们将_setStyle()_getStyle()定义为私有函数,然后将它们分配给公共的setStyle()getStyle(),考虑以下示例:

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

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

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

现在,结果如下:

  • MYAPP.dom.setStyle指向新函数

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

  • _setStyle()在任何其他内部代码依赖它按预期工作时始终可用,而不受外部代码的影响

当您公开私有内容时,请记住对象(函数和数组也是对象)是通过引用传递的,因此可以从外部修改。

立即函数

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

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

此模式特别适用于一次性初始化任务,在脚本加载时执行。

立即自执行函数模式可以扩展到创建和返回对象。如果创建这些对象更复杂并涉及一些初始化工作,那么您可以在自执行函数的第一部分中执行此操作,并返回一个可以访问和受益于顶部私有属性的单个对象,如下所示:

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

模块

结合前面几种模式可以得到一个新模式,通常称为模块模式。编程中的模块概念很方便,因为它允许您编写单独的代码片段或库,并根据需要组合它们,就像拼图一样。

模块模式包括以下内容:

  • 命名空间以减少模块之间的命名冲突

  • 立即函数提供私有作用域和初始化

  • 私有属性和方法

注意

ES5 没有内置的模块概念。有来自www.commonjs.org的模块规范,它定义了一个require()函数和一个 exports 对象。然而,ES6 支持模块。第八章类和模块已经详细介绍了模块。

  • 返回具有模块公共 API 的对象,如下所示:
        namespace('MYAPP.module.amazing'); 

        MYAPP.module.amazing = (function () { 

          // short names for dependencies 
          var another = MYAPP.module.another; 

          // local/private variables 
          var i, j; 

          // private functions 
          function hidden() {} 

          // public API 
          return { 
            hi: function () { 
              return "hello"; 
            } 
          }; 
        }()); 

而且,您可以以以下方式使用模块:

    MYAPP.module.amazing.hi(); // "hello" 

链接

链接是一种模式,允许你在一行上调用多个方法,就好像这些方法是链条中的链接一样。当调用几个相关的方法时,这是很方便的。你在前一个方法的结果上调用下一个方法,而不使用中间变量。

假设你已经创建了一个构造函数,可以帮助你处理 DOM 元素。创建一个新的添加到<body>标签的<span>标签的代码可能如下所示:

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

如你所知,构造函数返回所谓的this关键字所创建的对象。你可以让你的方法,比如setText()setStyle(),也返回this关键字,这样你就可以在前一个方法返回的实例上调用下一个方法。这样,你可以链式调用方法,如下所示:

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

如果你在新元素添加到树之后不打算使用obj变量,那么代码看起来像下面这样:

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

这种模式的一个缺点是,当长链中的某个地方发生错误时,它会使得调试变得有点困难,因为你不知道哪个链接有问题,因为它们都在同一行上。

JSON

让我们用几句话来总结本章的编码模式部分关于 JSON 的内容。JSON 在技术上并不是一个编码模式,但你可以说使用它是一个很好的模式。

JSON 是一种流行的轻量级数据交换格式。在使用XMLHttpRequest()从服务器检索数据时,它通常优先于 XML。JSON除了它极其方便之外,没有什么特别有趣的地方。JSON 格式由使用对象和数组文字定义的数据组成。以下是一个 JSON 字符串的示例,你的服务器可以在XHR请求之后用它来响应:

    { 
      'name':   'Stoyan', 
      'family': 'Stefanov', 
      'books':  ['OOJS', 'JSPatterns', 'JS4PHP'] 
    } 

这个的 XML 等价物将是以下代码片段:

    <?xml version="1.1" encoding="iso-8859-1"?> 
    <response> 
      <name>Stoyan</name> 
      <family>Stefanov</family> 
      <books> 
        <book>OOJS</book> 
        <book>JSPatterns</book> 
        <book>JS4PHP</book> 
      </books> 
    </response> 

首先,你可以看到 JSON 在字节数量上更轻。然而,主要好处不是较小的字节大小,而是在 JavaScript 中使用 JSON 非常简单。比如,你已经发出了一个XHR请求,并在XHR对象的responseText属性中收到了一个 JSON 字符串。你可以通过简单地使用eval()将这个数据字符串转换为一个可用的 JavaScript 对象。考虑以下示例:

    // warning: counter-example 
    var response = eval('(' + xhr.responseText + ')'); 

现在,你可以像下面这样访问obj中的数据作为对象属性:

    console.log(response.name); // "Stoyan" 
    console.log(response.books[2]); // "JS4PHP" 

问题在于eval()是不安全的,所以最好使用 JSON 对象来解析 JSON 数据(旧版浏览器的备用方案可在json.org/找到)。从 JSON 字符串创建对象仍然很简单,如下所示:

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

要做相反的事情,也就是将对象转换为 JSON 字符串,你可以使用stringify()方法,如下所示:

    var str = JSON.stringify({hello: "you"}); 

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

高阶函数

到目前为止,函数式编程一直局限于有限的一组语言。随着越来越多的语言添加支持函数式编程的特性,人们对这一领域的兴趣正在增长。JavaScript 正在发展以支持函数式编程的常见特性。你将逐渐看到很多以这种风格编写的代码。重要的是要理解函数式编程风格,即使你现在还不想在你的代码中使用它。

高阶函数是函数式编程的重要支柱之一。高阶函数是至少做以下一种事情的函数:

  • 以一个或多个函数作为参数

  • 返回一个函数作为结果

由于 JavaScript 中函数是一等对象,因此将函数传递给函数并从函数返回函数是一件相当常见的事情。回调函数是高阶函数。让我们看看如何将这两个原则结合起来编写一个高阶函数。

让我们编写一个filter函数;这个函数根据由函数确定的条件从数组中过滤出值。这个函数接受两个参数-一个返回布尔值true以保留此元素的函数。

例如,使用这个函数,我们正在从数组中过滤出所有奇数值。考虑以下代码行:

    console.log([1, 2, 3, 4, 5].filter(function(ele){
      return ele % 2 == 0; })); 
    //[2,4] 

我们将一个匿名函数作为第一个参数传递给filter函数。这个函数根据一个条件返回一个布尔值,检查元素是奇数还是偶数。

这是 ECMAScript 5 中添加的几个高阶函数之一的示例。我们试图表达的观点是,您将越来越多地看到 JavaScript 中类似的使用模式。您必须首先了解高阶函数的工作原理,然后,一旦您对概念感到舒适,尝试在您的代码中也加入它们。

随着 ES6 函数语法的变化,编写高阶函数变得更加优雅。让我们以 ES5 中的一个小例子来看看它如何转换为 ES6:

    function add(x){ 
      return function(y){ 
        return y + x; 
      }; 
    } 
     var add3 = add(3); 
    console.log(add3(3));          // => 6 
    console.log(add(9)(10));       // => 19 

add函数接受x并返回一个接受y作为参数的函数,然后返回表达式y+x的值。

当我们讨论箭头函数时,我们讨论了箭头函数隐式返回单个表达式的结果。因此,前面的函数可以通过将箭头函数的主体变为另一个箭头函数来转换为箭头函数。看看下面的例子:

    const add = x => y => y + x; 

在这里,我们有一个外部函数,x => [带有x作为参数的内部函数],以及一个内部函数,y => y+x

这个介绍将帮助您熟悉高阶函数的增加使用,以及它们在 JavaScript 中的增加重要性。

设计模式

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

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

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

  • 描述对象之间通信方式的行为模式

《四人帮》中有 23 种模式,自该书出版以来已经发现了更多模式。讨论所有这些模式远远超出了本书的范围,因此本章的其余部分仅演示了四种模式,以及它们在 JavaScript 中的实现示例。请记住,这些模式更多关于接口和关系而不是实现。一旦您了解了设计模式,通常很容易实现它,特别是在 JavaScript 这样的动态语言中。

本章剩余部分讨论的模式如下:

  • 单例

  • 工厂

  • 装饰器

  • 观察者

单例模式

单例是一种创建型设计模式,意味着它的重点是创建对象。当您想要确保只有一个给定种类或类的对象时,它会帮助您。在经典语言中,这意味着只创建一个类的实例,并且任何后续尝试创建相同类的新对象都将返回原始实例。

在 JavaScript 中,由于没有类,单例是默认和最自然的模式。每个对象都是单例对象。

JavaScript 中单例的最基本实现是对象字面量。看一下下面的代码行:

    var single = {}; 

那很容易,对吧?

单例 2 模式

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

    var my_log = new Logger(); 
    my_log.log('some event'); 

    // ... 1000 lines of code later in a different scope ... 

    var other_log = new Logger(); 
    other_log.log('some new event'); 
    console.log(other_log === my_log); // true 

思想是,尽管使用了new,但只需要创建一个实例,然后在连续调用中返回该实例。

全局变量

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

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

使用此构造函数会产生预期的结果,如下所示:

    var a = new Logger(); 
    var b = new Logger(); 
    console.log(a === b); // true 

缺点显而易见,就是使用全局变量。它可以在任何时候被意外覆盖,您可能会丢失实例。相反,覆盖别人的全局变量也是可能的。

构造函数的属性

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

    function Logger() { 
      if (!Logger.single_instance) { 
        Logger.single_instance = this; 
      } 
      return Logger.single_instance; 
    } 

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

这种方法确实解决了全局命名空间问题,因为不会创建全局变量。唯一的缺点是Logger构造函数的属性是公开可见的,因此可以随时被覆盖。在这种情况下,单个实例可能会丢失或修改。当然,您只能提供有限的保护,以防止其他程序员自食其力。毕竟,如果有人可以干扰单实例属性,他们也可以直接干扰Logger构造函数。

在私有属性中

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

工厂模式

工厂是另一种创建型设计模式,因为它涉及创建对象。当您有类似类型的对象并且事先不知道要使用哪个时,工厂可以帮助您。根据用户输入或其他条件,您的代码可以动态确定所需的对象类型。

假设您有三种不同的构造函数,实现类似的功能。它们创建的所有对象都需要一个 URL,但对其执行不同的操作。一个创建文本 DOM 节点;第二个创建一个链接;第三个创建一个图像,如下所示:

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

使用三种不同的构造函数完全相同-传递url变量并调用insert()方法,如下所示:

    var url = 'http://www.phpied.com/images/covers/oojs.jpg'; 

    var o = new MYAPP.dom.Image(url); 
    o.insert(document.body); 

    var o = new MYAPP.dom.Text(url); 
    o.insert(document.body); 

    var o = new MYAPP.dom.Link(url); 
    o.insert(document.body); 

想象一下,您的程序事先不知道需要哪种类型的对象。用户在运行时通过单击按钮等方式决定。如果type包含所需的对象类型,则需要使用ifswitch语句,并编写以下代码片段:

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

这样做效果很好;但是,如果您有很多构造函数,代码会变得太长且难以维护。此外,如果您正在创建允许扩展或插件的库或框架,您甚至不知道所有构造函数的确切名称。在这种情况下,有一个工厂函数来负责创建动态确定类型的对象是很方便的。

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

    MYAPP.dom.factory = function (type, url) { 
      return new MYAPP.domtype; 
    }; 

现在,您可以用更简单的代码替换三个if函数,如下所示:

    var image = MYAPP.dom.factory("Image", url); 
    image.insert(document.body); 

先前代码中的示例factory()方法很简单;但是,在实际情况下,您可能希望针对类型值进行一些验证(例如,检查MYAPP.dom[type]是否存在),并且可能对所有对象类型进行一些通用的设置工作(例如,设置所有构造函数使用的 URL)。

装饰器模式

装饰者设计模式是一种结构模式;它与对象如何创建没有太多关系,而是与它们的功能如何扩展有关。你可以有一个基础对象和一组不同的装饰者对象,它们提供额外的功能,而不是使用继承,继承是线性的(父-子-孙),你的程序可以选择想要的装饰者,以及顺序。对于不同的程序或代码路径,你可能有不同的需求集,并从同一个池中选择不同的装饰者。看一下以下代码片段,看看装饰者模式的使用部分如何实现:

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

你可以看到如何从一个具有doSomething()方法的简单对象开始。然后,你可以选择你手头上的一个装饰者对象,并通过名称进行识别。所有装饰者都提供一个doSomething()方法,首先调用前一个装饰者的相同方法,然后继续执行自己的代码。每次添加一个装饰者,都会用改进版本的obj覆盖基础对象。最后,当你添加完装饰者后,调用doSomething()。结果,所有装饰者的doSomething()方法都按顺序执行。让我们看一个例子。

装饰一棵圣诞树

让我们用一个装饰一棵圣诞树的例子来说明装饰者模式。你可以按照以下方式开始decorate()方法:

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

现在,让我们实现一个getDecorator()方法,添加额外的装饰者。装饰者将作为构造函数实现,并且它们都将从基础tree对象继承,如下所示:

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

现在,让我们创建第一个装饰者RedBalls(),作为tree的属性,以保持全局命名空间更清洁。红色球对象也提供一个decorate()方法,但它们确保首先调用它们父级的decorate()。例如,看一下以下代码:

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

同样,按照以下方式实现BlueBalls()Angel()装饰者:

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

现在,让我们将所有装饰者添加到基础对象中,如下所示的代码片段:

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

最后,按照以下方式运行decorate()方法:

    tree.decorate(); 

这个单一的调用会导致以下警报,具体顺序如下:

  1. 确保树不会倒下。

  2. 添加蓝色的球。

  3. 在顶部添加一个天使。

  4. 添加一些红色的球。

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

观察者模式

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

  • 一个或多个发布者对象,它们在做重要事情时会宣布。

  • 一个或多个订阅者调整到一个或多个发布者。他们听取发布者的宣布然后采取适当的行动。

观察者模式可能对你来说很熟悉。它听起来与前一章讨论的浏览器事件类似,这是正确的,因为浏览器事件是这种模式的一个应用实例。浏览器是发布者;它宣布了事件(如click)发生的事实。订阅了这种类型事件的事件监听函数在事件发生时会收到通知。浏览器-发布者向所有订阅者发送一个事件对象。在自定义实现中,你可以发送任何你认为合适的数据。

观察者模式有两种子类型:推(push)和拉(pull)。推是指发布者负责通知每个订阅者,而拉是指订阅者监视发布者状态的变化。

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

  • 一个 subscribers 数组,它们只是回调函数

  • addSubscriber()removeSubscriber() 方法,用于向 subscribers 集合添加和移除订阅者

  • 一个 publish() 方法,它接受数据并调用所有订阅者,将数据传递给它们

  • 一个 make() 方法,它接受任何对象,并通过向其添加之前提到的所有方法将其转换为发布者

这是一个包含所有订阅相关方法的观察者混合对象,可以用来将任何对象转换为发布者:

    var observer = { 
      addSubscriber: function (callback) { 
        if (typeof callback === "function") { 
          this.subscribers[this.subscribers.length] = callback; 
        } 
      }, 
      removeSubscriber: function (callback) { 
        for (var i = 0; i < this.subscribers.length; i++) { 
          if (this.subscribers[i] === callback) { 
            delete this.subscribers[i]; 
          } 
        } 
      }, 
      publish: function (what) { 
        for (var i = 0; i < this.subscribers.length; i++) { 
          if (typeof this.subscribers[i] === 'function') { 
            this.subscribersi; 
          } 
        } 
      }, 
      make: function (o) { // turns an object into a publisher 
        for (var i in this) { 
          if (this.hasOwnProperty(i)) { 
            o[i] = this[i]; 
            o.subscribers = []; 
          } 
        } 
      } 
   }; 

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

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

另一个对象可以是 LA Times 报纸,当有新的报纸发布时调用 publish()。考虑以下代码行:

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

您可以将这些对象转换为发布者,如下所示:

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

现在,让我们来看一下以下两个简单的对象,jackjill

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

jackjill 对象可以通过提供他们想要在发布时调用的回调方法来订阅 blogger 对象,如下所示:

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

现在,当 blogger 对象写了一个新的帖子时会发生什么?结果是 jackjill 会收到通知:

    > blogger.writeBlogPost(); 
       I just read that Today is Fri Jan 04 2013 19:02:12 GMT-0800 (PST) 
       You didn't hear it from me, but Today is Fri Jan 04 2013 19:02:12 GMT-0800    
         (PST) 

在任何时候,jill 可能决定取消她的订阅。然后,在写另一篇博客文章时,已取消订阅的对象将不再收到通知。考虑以下代码片段:

    > blogger.removeSubscriber(jill.gossip); 
    > blogger.writeBlogPost();
    I just read that Today is Fri Jan 04 2013 19:03:29 GMT-0800 (PST) 

jill 对象可以决定订阅 LA Times,因为一个对象可以订阅多个发布者,如下所示:

    > la_times.addSubscriber(jill.gossip); 

然后,当 LA Times 发布新问题时,jill 被通知并执行 jill.gossip(),如下所示:

    > la_times.newIssue();
    You didn't hear it from me, but Martians have landed on Earth! 

总结

在本章中,您了解了常见的 JavaScript 编码模式,并学会了如何使您的程序更清洁、更快速,并更好地与其他程序和库一起工作。然后,您看到了《四人组设计模式》中一些设计模式的讨论和示例实现。您可以看到 JavaScript 是一种功能齐全的动态编程语言,而在动态弱类型语言中实现经典模式是相当容易的。总的来说,模式是一个大主题,您可以加入本书的作者在 JSPatterns.com 进一步讨论 JavaScript 模式,或者查看 JavaScript Patterns 书籍。下一章将重点介绍测试和调试方法论。

第十二章:测试和调试

当你编写 JavaScript 应用程序时,你很快会意识到拥有一个完善的测试策略是不可或缺的。事实上,不写足够的测试几乎总是一个坏主意。覆盖代码的所有非平凡功能是至关重要的,以确保以下几点:

  • 现有代码按规范行为

  • 任何新代码都不会破坏规范定义的行为

这两点都非常重要。许多工程师只考虑第一点作为足够测试代码的唯一原因。测试覆盖的最明显优势是确保推送到生产系统的代码大部分是无错误的。聪明地编写测试用例以覆盖代码的最大功能区域,通常可以很好地指示代码的整体质量。在这一点上不应该有争论或妥协。尽管很不幸,许多生产系统仍然缺乏足够的代码覆盖。建立一个工程文化,让开发人员像编写代码一样考虑编写测试是非常重要的。

第二点更加重要。传统系统通常很难管理。当你在处理代码时,无论是别人写的还是由一个大型分布式团队编写的,很容易引入错误并破坏事物。即使是最优秀的工程师也会犯错。当你在处理一个你不熟悉的大型代码库时,如果没有足够的测试覆盖来帮助你,你会引入错误。因为没有测试用例来确认你的更改,你对自己的更改没有信心,你的代码发布将是不稳定的,缓慢的,显然充满了隐藏的错误。

你将不会重构或优化你的代码,因为你不会真正确定对代码库的更改可能会破坏什么(同样,因为没有测试用例来确认你的更改);所有这些都是一个恶性循环。这就像土木工程师说-尽管我建造了这座桥,但我对建筑质量没有信心。它可能会立即倒塌,也可能永远不会。尽管这听起来可能有些夸张,但我见过很多高影响的生产代码被推送而没有测试覆盖。这是有风险的,应该避免。当你编写足够的测试用例来覆盖大部分功能代码时,当你对这些部分进行更改时,你会立即意识到是否存在问题。如果你的更改导致测试用例失败,你会意识到问题。如果你的重构破坏了测试场景,你会意识到问题;所有这些都发生在代码被推送到生产之前。

近年来,像测试驱动开发和自测试代码这样的想法在敏捷方法中变得越来越重要。这些都是基本合理的想法,将帮助你编写健壮的代码-你有信心的代码。我们将在本章讨论所有这些想法。我们将了解如何在现代 JavaScript 中编写良好的测试用例。我们还将看一些工具和方法来调试你的代码。传统上,由于缺乏工具,JavaScript 测试和调试都有些困难,但现代工具使这两者都变得容易和自然。

单元测试

当我们谈论测试用例时,我们大多指的是单元测试。假设我们要测试的单元总是一个函数是不正确的。这个单元,或者说工作单元,是一个构成单一行为的逻辑单元。这个单元应该能够通过公共接口被调用,并且应该能够独立进行测试。

因此,单元测试可以执行以下功能:

  • 它测试一个单一的逻辑函数

  • 它可以在没有特定执行顺序的情况下运行

  • 它负责处理自己的依赖和模拟数据

  • 对于相同的输入,总是返回相同的结果

  • 它应该是自解释的,可维护的和可读的

Martin Fowler 提倡测试金字塔martinfowler.com/bliki/TestPyramid.html)策略,以确保我们有大量的单元测试来确保最大的代码覆盖率。在本章中,我们将讨论两种重要的测试策略。

测试驱动开发

测试驱动开发TDD)在过去几年中变得非常重要。这个概念最初是作为极限编程方法论的一部分提出的。其核心思想是有短小的重复开发周期,重点是先编写测试用例。这个周期看起来像下面这样:

  1. 根据代码单元的具体规格添加一个测试用例。

  2. 运行现有的测试套件,看看你编写的新测试用例是否失败;它应该失败,因为该单元尚未有代码。这一步确保当前的测试工具能够正常工作。

  3. 编写的代码主要用于确认测试用例。这段代码并没有经过优化、重构,甚至可能并不完全正确。但是,目前来说这是可以接受的。

  4. 重新运行测试,看看所有的测试用例是否通过。经过这一步,你可以确信新代码没有破坏任何东西。

  5. 重构代码以确保你正在优化单元并处理所有边缘情况

这些步骤对于你添加的任何新代码都是重复的。这是一种对敏捷方法论非常有效的优雅策略。只有当可测试的代码单元很小并且仅符合测试用例时,TDD 才会成功。

行为驱动开发

在尝试遵循 TDD 时一个非常常见的问题是词汇和正确性的定义。BDD 试图在遵循 TDD 时引入一种通用语言。这种语言确保业务和工程都在讨论同一件事情。

我们将使用 Jasmine 作为主要的 BDD 框架,并探索各种测试策略。

注意

你可以通过从github.com/jasmine/jasmine/releases/download/v2.3.4/jasmine-standalone-2.3.4.zip下载独立包来安装 Jasmine。

当你解压这个包时,你会看到以下的目录结构:

行为驱动开发

lib目录包含了你在项目中需要的 JavaScript 文件,用于开始编写 Jasmine 测试用例。如果你打开SpecRunner.html,你会发现其中包含了以下 JavaScript 文件:

    <script src="lib/jasmine-2.3.4/jasmine.js"></script> 
    <script src="lib/jasmine-2.3.4/jasmine-html.js"></script> 
    <script src="lib/jasmine-2.3.4/boot.js"></script>     

    <!-- include source files here... -->    
    <script src="src/Player.js"></script>    
    <script src="src/Song.js"></script>     
    <!-- include spec files here... -->    
    <script src="spec/SpecHelper.js"></script>    
    <script src="spec/PlayerSpec.js"></script> 

前三个是 Jasmine 自己的框架文件。接下来的部分包括我们想要测试的源文件和实际的测试规格。

让我们通过一个非常普通的例子来尝试 Jasmine。创建一个bigfatjavascriptcode.js文件,并将其放在src/目录中。我们将要测试的函数如下:

    function capitalizeName(name){ 
      return name.toUpperCase(); 
    } 

这是一个简单的函数,只做了一件事情。它接收一个字符串并返回一个大写的字符串。我们将围绕这个函数测试各种情况。这就是我们之前讨论过的代码单元。

接下来,创建测试规格。创建一个 JavaScript 文件test.spec.js,并将其放在spec/目录中。你需要将以下两行添加到你的SpecRunner.html中:文件应包含以下内容:

    <script src="src/bigfatjavascriptcode.js"></script>     
    <script src="spec/test.spec.js"></script>    

包含的顺序并不重要。当我们运行SpecRunner.html时,你会看到类似以下图片的内容:

行为驱动开发

这是 Jasmine 报告,显示了执行的测试数量以及失败和成功的数量。现在,让我们让测试用例失败。我们想要测试一个情况,即将一个undefined变量传递给函数。让我们添加一个测试用例,如下所示:

    it("can handle undefined", function() { 
        var str= undefined; 
        expect(capitalizeName(str)).toEqual(undefined); 
    }); 

现在,当你运行SpecRunner时,你会看到以下结果:

行为驱动开发

正如你所看到的,这个测试用例显示了一个详细的错误堆栈的失败。现在,我们将着手解决这个问题。在你的原始 JS 代码中,处理 undefined 如下:

    function capitalizeName(name){ 
      if(name){ 
        return name.toUpperCase(); 
      }   
    } 

通过这个改变,你的测试用例将通过,并且你将在 Jasmine 报告中看到以下结果:

行为驱动开发

这非常类似于测试驱动开发的样子。你编写测试用例,然后填写必要的代码以符合规范,并重新运行测试套件。让我们了解一下 Jasmine 测试的结构。

我们的测试规范看起来像以下代码片段:

    describe("TestStringUtilities", function() { 
          it("converts to capital", function() { 
              var str = "albert"; 
              expect(capitalizeName(str)).toEqual("ALBERT"); 
          }); 
          it("can handle undefined", function() { 
              var str= undefined; 
              expect(capitalizeName(str)).toEqual(undefined); 
          }); 
    }); 

describe("TestStringUtilities"是一个测试套件。测试套件的名称应该描述我们正在测试的代码单元;这可以是一个函数或一组相关功能。在规范内部,你将调用全局的 Jasmine 函数it,并传递规范的标题和验证测试用例条件的函数。这个函数就是实际的测试用例。你可以使用expect函数捕获一个或多个断言或一般的期望。当所有期望都为true时,你的规范通过了。你可以在describeit函数内部编写任何有效的 JavaScript 代码。作为期望的一部分验证的值使用匹配器进行匹配。在我们的例子中,toEqual是匹配器,用于匹配两个值是否相等。Jasmine 包含丰富的匹配器,适合大多数常见用例。Jasmine 支持的一些常见匹配器如下:

  • toBe:这个匹配器检查被比较的两个对象是否相等。这与===比较相同。例如,看下面的代码片段:
        var a = { value: 1}; 
        var b = { value: 1 }; 

        expect(a).toEqual(b);  // success, same as == comparison 
        expect(b).toBe(b);     // failure, same as === comparison 
        expect(a).toBe(a);     // success, same as === comparison 

  • not:你可以用 not 前缀否定一个匹配项。例如,expect(1).not.toEqual(2);将否定toEqual()所做的匹配。

  • toContain:这个检查一个元素是否是数组的一部分。它不是一个精确的对象匹配,如 toBe。例如,看一下下面的代码行:

        expect([1, 2, 3]).toContain(3); 
        expect("astronomy is a science").toContain("science"); 

  • toBeDefined 和 toBeUndefined:这两个匹配项很方便,可以检查变量是否为 undefined。

  • toBeNull:这个检查变量的值是否为 null。

  • toBeGreaterThan 和 toBeLessThan:这两个匹配器执行数字比较(也适用于字符串)。例如,考虑以下代码片段:

        expect(2).toBeGreaterThan(1); 
        expect(1).toBeLessThan(2); 
        expect("a").toBeLessThan("b"); 

Jasmine 的一个有趣特性是间谍。当你编写一个大型系统时,不可能确保所有系统始终可用和正确。同时,你不希望你的单元测试因为一个可能被破坏或不可用的依赖而失败。为了模拟所有依赖都可用于我们要测试的代码单元的情况,我们将模拟这个依赖,始终给出我们期望的响应。模拟是测试的一个重要方面,大多数测试框架都提供了模拟支持。Jasmine 允许使用一个名为Spy的功能进行模拟。Jasmine 间谍本质上是我们可能在编写测试用例时没有准备好的函数的存根,但作为功能的一部分,我们需要跟踪我们正在执行这些依赖项而不是忽略它们。考虑以下例子:

    describe("mocking configurator", function() { 
      var cofigurator = null; 
      var responseJSON = {}; 

      beforeEach(function() { 
        configurator = { 
          submitPOSTRequest: function(payload) { 
            //This is a mock service that will eventually be replaced  
            //by a real service 
            console.log(payload); 
            return {"status": "200"}; 
          } 
        }; 
        spyOn(configurator, 'submitPOSTRequest').and.returnValue
         ({"status": "200"}); 
       configurator.submitPOSTRequest({ 
          "port":"8000", 
          "client-encoding":"UTF-8" 
        }); 
      }); 

      it("the spy was called", function() { 
        expect(configurator.submitPOSTRequest).toHaveBeenCalled(); 
      }); 

      it("the arguments of the spy's call are tracked", function() { 
        expect(configurator.submitPOSTRequest).toHaveBeenCalledWith(
          {"port":"8000", "client-encoding":"UTF-8"}); 
      }); 
    }); 

在这个例子中,当我们编写这个测试用例时,我们要么没有依赖项configurator.submitPOSTRequest()的真正实现,要么有人正在修复这个特定的依赖项;无论哪种情况,我们都没有它可用。为了使我们的测试工作,我们需要模拟它。Jasmine 间谍允许我们用它的模拟替换一个函数,并允许我们跟踪它的执行。

在这种情况下,我们需要确保调用了依赖项。当实际的依赖项准备好时,我们将重新访问这个测试用例,以确保它符合规范;然而,此时,我们只需要确保调用了依赖项。Jasmine 函数tohaveBeenCalled()让我们跟踪可能是模拟的函数的执行。我们可以使用toHaveBeenCalledWith(),它允许我们确定存根函数是否使用正确的参数进行了调用。使用 Jasmine 间谍可以创建几种其他有趣的场景。本章的范围不允许我们覆盖它们所有,但我鼓励你自己发现这些领域。

摩卡,柴和西农

尽管 Jasmine 是最突出的 JavaScript 测试框架,但 mocha 和 chai 在Node.js环境中也越来越受到重视。

  • Mocha 是用于描述和运行测试用例的测试框架

  • 柴是 Mocha 支持的断言库

  • 西农在创建测试时非常方便地创建模拟和存根

我们不会在本书中讨论这些框架;然而,如果你想尝试这些框架,对 Jasmine 的经验将会很有帮助。

JavaScript 调试

如果你不是一个完全新的程序员,我相信你一定花了一些时间来调试你自己或别人的代码。调试几乎就像一种艺术形式。每种语言在调试方面都有不同的方法和挑战。JavaScript 传统上是一种难以调试的语言。我曾经在痛苦中度过了几天几夜,试图使用alert()函数调试糟糕的 JavaScript 代码。幸运的是,现代浏览器,如 Mozilla,Firefox 和 Google Chrome,都有出色的开发者工具,可以帮助在浏览器中调试 JavaScript。还有像 IntelliJ IDEA 和 WebStorm 这样的 IDE,对 JavaScript 和 Node.js 有很好的调试支持。在本章中,我们将主要关注 Google Chrome 内置的开发者工具。Firefox 也支持 Firebug 扩展,并且有出色的内置开发者工具,但由于它们的行为与 Google Chrome 的开发者工具几乎相同,我们将讨论在这两种工具中都适用的常见调试方法。

在我们讨论具体的调试技术之前,让我们了解一下在尝试调试我们的代码时我们感兴趣的错误类型。

语法错误

当你的代码有一些不符合 JavaScript 语法的东西时,解释器会拒绝那部分代码。如果你的 IDE 帮助你进行语法检查,这些错误很容易被捕捉到。大多数现代 IDE 都可以帮助解决这些错误。早些时候,我们讨论了工具的有用性,比如 JSLint 和 JSHint,可以帮助捕捉代码中的语法问题。它们分析代码并标记语法错误。JSHint 的输出可能非常有启发性。例如,以下输出显示了我们可以在代码中进行的许多更改。以下代码片段来自我的一个现有项目:

    temp git:(dev_branch) X jshint test.js 
    test.js: line 1, col 1, Use the function form of "use strict". 
    test.js: line 4, col 1, 'destructuring expression' 
      is available in ES6 (use esnext option) or 
      Mozilla JS extensions (use moz). 
    test.js: line 44, col 70, 'arrow function syntax (=>)' 
      is only available in ES6 (use esnext option). 
    test.js: line 61, col 33, 'arrow function syntax (=>)'
      is only available in ES6 (use esnext option). 
    test.js: line 200, col 29, Expected ')' to match '(' from
      line 200 and instead saw ':'. 
    test.js: line 200, col 29, 'function closure expressions' 
      is only available in Mozilla JavaScript extensions (use moz option). 
    test.js: line 200, col 37, Expected '}' to match '{' from 
      line 36 and instead saw ')'. 
    test.js: line 200, col 39, Expected ')' and instead saw '{'. 
    test.js: line 200, col 40, Missing semicolon. 

使用严格模式

我们在前几章中简要讨论了严格模式。当你启用严格模式时,JavaScript 不再接受代码中的语法错误。严格模式不会悄悄失败,而是会将这些失败抛出错误。它还可以帮助你将错误转换为实际的错误。有两种强制使用严格模式的方法。如果你想要整个脚本都使用严格模式,你可以在 JavaScript 程序的第一行添加use strict语句(带引号)。如果你想要特定函数符合严格模式,你可以将指令添加为函数的第一行。例如,看一下以下代码片段:

    function strictFn(){    
      // This line makes EVERYTHING under this scrict mode 
      'use strict';    
      ... 
      function nestedStrictFn() {  
        //Everything in this function is also nested 
        ... 
      }    
    } 

运行时异常

当您执行代码时,出现这些错误,尝试引用一个未定义的变量,或者尝试处理null。当运行时异常发生时,导致异常的特定行之后的任何代码都不会被执行。在代码中正确处理这种异常情况是至关重要的。虽然异常处理可以帮助防止崩溃,但它们也有助于调试。您可以将可能遇到运行时异常的代码包装在try{ }块中。当此块内的任何代码生成运行时异常时,相应的处理程序会捕获它。处理程序由catch(exception){}块定义。让我们通过以下示例来澄清这一点:

    try { 
      var a = doesnotexist; // throws a runtime exception 
    } catch(e) {  
      console.log(e.message);  //handle the exception 
      //prints - "doesnotexist is not defined" 
    } 

在这个例子中,var a = doesnotexist行试图将一个未定义的变量doesnotexist赋值给另一个变量a。这会导致运行时异常。当我们将这个有问题的代码包装在try{}catch(){}块中,或者当异常发生(或被抛出)时,执行会在try{}块中停止,并直接转到catch() {}处理程序。捕获处理程序负责处理异常情况。在这种情况下,我们为了调试目的在控制台上显示错误消息。您可以明确地抛出异常来触发代码中未处理的情况。考虑以下的例子:

    function engageGear(gear){ 
      if(gear==="R"){ console.log ("Reversing");} 
      if(gear==="D"){ console.log ("Driving");} 
      if(gear==="N"){ console.log ("Neutral/Parking");} 
      throw new Error("Invalid Gear State"); 
    } 
    try 
    { 
      engageGear("R");  //Reversing 
      engageGear("P");  //Invalid Gear State 
    } 
    catch(e){ 
      console.log(e.message); 
    } 

在这个例子中,我们正在处理变速器的有效状态:RND;然而,当我们收到无效状态时,我们明确地抛出异常,清楚地说明原因。当我们调用可能引发异常的函数时,我们将在try{}块中包装代码,并附加一个带有catch(){}的处理程序。当异常被catch()块捕获时,我们将适当地处理异常情况。

Console.log 和断言

在控制台上显示执行状态在调试时可能非常有用。尽管现代开发工具允许您设置断点并在运行时停止执行以检查特定值,但通过在控制台上记录一些变量状态,您可以快速检测到一些小问题。

有了这些概念,让我们看看如何使用 Chrome 开发者工具来调试 JavaScript 代码。

Chrome 开发者工具

您可以通过单击菜单 | 更多工具 | 开发者工具来启动 Chrome 开发者工具。看一下以下的屏幕截图:

Chrome Developer Tools

Chrome 开发者工具在浏览器的下方窗格中打开,并且有一堆非常有用的部分。考虑以下的屏幕截图:

Chrome Developer Tools

元素面板帮助您检查和监视 DOM 树和每个组件的相关样式表。

网络面板对于理解网络活动非常有用。例如,您可以实时监视通过网络下载的资源。

对我们来说最重要的窗格是Sources窗格。这个窗格显示了 JavaScript 和调试器。让我们创建一个包含以下内容的示例 HTML:

    <!DOCTYPE html> 
    <html> 
    <head> 
      <meta charset="utf-8"> 
      <title>This test</title> 
      <script type="text/javascript"> 
      function engageGear(gear){ 
        if(gear==="R"){ console.log ("Reversing");} 
        if(gear==="D"){ console.log ("Driving");} 
        if(gear==="N"){ console.log ("Neutral/Parking");} 
        throw new Error("Invalid Gear State"); 
      } 
      try 
      { 
        engageGear("R");  //Reversing 
        engageGear("P");  //Invalid Gear State 
      } 
      catch(e){ 
        console.log(e.message); 
      } 
      </script> 
    </head> 
    <body> 
    </body> 
    </html> 

保存这个 HTML 文件并在 Google Chrome 中打开它。在浏览器中打开开发者工具,您将看到以下屏幕:

Chrome Developer Tools

这是Sources面板的视图。您可以在此面板中看到 HTML 和嵌入的 JavaScript 源代码。您还可以看到Console窗口,并且可以看到文件被执行并且输出显示在控制台上。

在右侧,您将看到调试器窗口,如下面的屏幕截图所示:

Chrome Developer Tools

Sources面板中,单击行号815以添加断点。断点允许您在指定的位置停止脚本的执行。考虑以下的屏幕截图:

Chrome Developer Tools

在调试窗格中,您可以看到所有现有的断点。看一下以下的屏幕截图:

Chrome 开发者工具

现在,当您重新运行同一个页面时,您会看到执行停在调试点。请看下面的截图:

Chrome 开发者工具

这个窗口现在有了所有的操作。您可以看到执行已经暂停在第 15 行。在调试窗口中,您可以看到触发了哪个断点。您还可以看到调用堆栈并以多种方式恢复执行。调试命令窗口有很多操作。看一下下面的截图:

Chrome 开发者工具

您可以通过点击以下按钮恢复执行,直到下一个断点:

Chrome 开发者工具

当您这样做时,执行会继续,直到遇到下一个断点。在我们的情况下,我们将在第 8 行停下来。请看下面的截图:

Chrome 开发者工具

您可以观察到调用堆栈窗口显示了我们是如何到达第 8 行的。作用域面板显示了局部作用域,在断点到达时您可以看到作用域中的变量。您还可以步进或跳过下一个函数。

使用 Chrome 开发者工具还有其他非常有用的机制来调试和分析您的代码。我建议您尝试使用这个工具,并将其作为您常规开发流程的一部分。

总结

测试和调试阶段对于开发健壮的 JavaScript 代码至关重要。TDD 和 BDD 是与敏捷方法学密切相关的方法,被 JavaScript 开发者社区广泛接受。在本章中,我们回顾了围绕 TDD 的最佳实践以及 Jasmine 作为测试框架的使用。此外,我们还看到了使用 Chrome 开发者工具调试 JavaScript 的各种方法。

在下一章中,我们将探索 ES6、DOM 操作和跨浏览器策略的新颖世界。

第十三章:响应式编程和 React

随着 ES6,一些新的想法正在涌现。这些是强大的想法,可以帮助你用更简洁的代码和设计构建强大的系统。在本章中,我们将向你介绍两种这样的想法-响应式编程和 react。尽管它们听起来相似,但它们是非常不同的。本章不会详细讨论这些想法的实际细节,但会给你必要的信息,让你了解这些想法的潜力。有了这些信息,你可以开始将这些想法和框架融入到你的项目中。我们将讨论响应式编程的基本思想,并更详细地看一下 react。

响应式编程

响应式编程最近受到了很多关注。这个想法相对较新,像许多新想法一样,有很多令人困惑的,有时是矛盾的信息在流传。我们在本书的前面讨论了异步编程。JavaScript 通过提供支持异步编程的一流语言构造,将异步编程推向了新的高度。

响应式编程本质上是使用异步事件流进行编程。事件流是随时间发生的事件序列。考虑以下图表:

Reactive programming

在上图中,时间从左到右流逝,不同的事件随时间发生。随着事件随时间发生,我们可以向整个序列添加事件监听器。每当事件发生时,我们都可以通过做一些事情来对其做出反应。

JavaScript 中的另一种序列是数组。例如,考虑以下代码行:

    var arr = [1,1,13,'Rx',0,0]; 
    console.log(arr); 
    >>> [1, 1, 13, "Rx", 0, 0] 

在这种情况下,整个序列同时存在于内存中。然而,在事件流的情况下,事件随时间发生,此时没有状态。考虑以下代码行:

    var arr = Rx.Observable.interval(500).take(9).map(
      a=>[1,1,13,'Rx',0,0][a]); 
    var result = arr; 
    result.subscribe(x=>console.log(x)); 

暂时不要太担心这个例子中发生了什么。在这里,事件是随时间发生的。这里不是有一个固定的数组元素,而是随时间发生的,500 毫秒后。

我们将向arr事件流添加一个事件监听器,当事件发生时,我们将在控制台上打印出元素。你可以看到数组和事件流中的方法之间的相似之处。现在,为了扩展这种相似性,假设你想要从这个列表中过滤掉所有的非数字。你可以使用map函数来处理这个事件流,就像你在数组上使用它一样,然后你会想要过滤结果,只显示整数。考虑以下代码行:

    var arr = [1,1,13,'Rx',0,0]; 
    var result = arr.map(x => parseInt(x)).filter(x => !isNan(x)); 
    console.log(result); 

有趣的是,相同的方法也适用于事件流。看一下以下代码示例:

    var arr = Rx.Observable.interval(500).take(9).map(
      a=>[1,1,13,'Rx',0,0][a]); 
    var result = arr.map(x => parseInt(x)).filter(x => !isNaN(x)); 
    result.subscribe(x=>console.log(x)); 

这些只是更简单的例子,只是为了确保你开始看到事件流随时间流动。请暂时不要担心语法和结构。在我们能够看它们之前,我们需要确保我们理解如何在响应式编程中思考。事件流对于响应式编程至关重要;它们允许你在声明时定义值的动态行为(定义来自 Andre Staltz 的博客)。

假设你有一个a变量,最初的值是3。然后,你有一个b变量,它是10 * a。如果我们在控制台上输出b,我们会看到30。考虑以下代码行:

    let a = 3; 
    let b = a * 10; 
    console.log(b); //30 
    a = 4; 
    console.log(b); // Still 30 

我们知道结果是非常直接的。当我们将a的值更改为4时,b的值不会改变。这就是静态声明的工作原理。当我们谈论响应式编程和事件流时,这是人们在理解事件流如何流动时遇到困难的地方。理想情况下,我们希望创建一个公式,b=a10*,随着时间的推移,无论a的值如何变化,变化的值都会反映在公式中。

这就是我们可以通过事件流实现的。假设a是一个只有值3的事件流。然后,我们有streamB,它是streamA映射的结果。每个a值都将映射为10 * a

如果我们给streamB添加一个事件监听器,并且我们控制台记录,我们会看到b30。看一下以下的例子:

    var streamA = Rx.Observable.of(3, 4); 
    var streamB = streamA.map(a => 10 * a); 
    streamB.subscribe(b => console.log(b)); 

如果我们这样做,我们将得到一个事件流,它只有两个事件。它有事件3,然后有事件4,当a改变时,b也会相应地改变。如果我们运行这个,我们会看到b3040

现在我们已经花了一些时间来了解响应式编程的基础知识,你可能会问以下问题。

为什么你应该考虑响应式编程?

在我们编写现代网络和移动应用程序时,需要高度响应和交互式的 UI 应用程序,有必要找到一种处理实时事件而不会停止用户在 UI 上交互的方法。当你处理多个 UI 和服务器事件时,你将花费大部分时间编写处理这些事件的代码。这很繁琐。响应式编程为你提供了一个结构化的框架,以最少的代码处理异步事件,同时你可以专注于应用程序的业务逻辑。

响应式编程不仅限于 JavaScript。响应式扩展在许多平台和语言中都有,比如 Java、Scala、Clojure、Ruby、Python 和 Object C/Cocoa。Rx.jsBacon.js是流行的提供响应式编程支持的 JavaScript 库。

深入研究Rx.js不是本章的目的。我们的目的是向你介绍响应式编程的概念。如果你有兴趣为你的项目采用响应式编程,你应该看看 Andre Staltz 的优秀介绍(gist.github.com/staltz/868e7e9bc2a7b8c1f754)。

React

React 正在以 JavaScript 世界为风暴。Facebook 创建了 React 框架来解决一个古老的问题-如何有效地处理传统的模型-视图-控制器应用程序的视图部分。

React 提供了一种声明式和灵活的构建用户界面的方式。关于 React 最重要的一点是,它只处理一个东西-视图或 UI。React 不处理数据、数据绑定或其他任何东西。有完整的框架,比如 Angular,处理数据、绑定和 UI;React 不是那样的。

React 提供了一个模板语言和一小组函数来渲染 HTML。React 组件可以在内存中存储它们自己的状态。要构建一个完整的应用程序,你还需要其他部分;React 只是处理该应用程序的视图部分。

在编写复杂 UI 时的一个大挑战是在模型改变时管理 UI 元素的状态。React 提供了一个声明式 API,这样你就不必担心每次更新时确切发生了什么变化。这使得编写应用程序变得更加容易。React 使用虚拟 DOM 和差异算法,因此组件更新是可预测的,同时也足够快以用于高性能应用程序。

虚拟 DOM

让我们花一点时间来了解什么是虚拟 DOM。我们讨论了DOM(文档对象模型),一个网页上 HTML 元素的树结构。DOM 是事实上的,也是网页的主要渲染机制。DOM 的 API,比如getElementById(),允许遍历和修改 DOM 树中的元素。DOM 是一棵树,这种结构非常适合遍历和更新元素。然而,DOM 的遍历和更新都不是很快。对于一个大页面,DOM 树可能会相当大。当你想要一个有大量用户交互的复杂 UI 时,更新 DOM 元素可能会很繁琐和缓慢。我们已经尝试过 jQuery 和其他库来减少频繁 DOM 修改的繁琐语法,但 DOM 本身作为一种结构是相当有限的。

如果我们不必一遍又一遍地遍历 DOM 来修改元素呢?如果您只是声明组件应该是什么样子,然后让其他人处理如何渲染该组件的逻辑呢?react 就是这样做的。React 允许您声明您希望 UI 元素看起来像什么,并将低级别的 DOM 操作 API 抽象出来。除了这个非常有用的抽象之外,react 还做了一些相当聪明的事情来解决性能问题。

React 使用一种称为虚拟 DOM 的东西。虚拟 DOM 是 HTML DOM 的轻量级抽象。您可以将其视为 HTML DOM 的本地内存副本。React 使用它来执行呈现 UI 组件状态所需的所有计算。

您可以在facebook.github.io/react/docs/reconciliation.html找到有关此优化的更多详细信息。

然而,React 的主要优势不仅仅是虚拟 DOM。React 是一个很棒的抽象,使得在开发大型应用程序时更容易进行组合、单向数据流和静态建模。

安装和运行 react

首先,让我们安装 react。早些时候,在您的计算机上安装和设置 react 需要处理一堆依赖项。但是,我们将使用一个相对更快的方法来让 react 运行起来。我们将使用create-react-app,通过它可以安装 react 而无需任何构建配置。安装是通过npm完成的,如下所示:

    npm install -g create-react-app 

在这里,我们正在全局安装create-react-app节点模块。安装了create-react-app之后,您可以为应用程序设置目录。考虑以下命令:

    create-react-app react-app 
    cd react-app/ 
    npm start 

然后,打开http://localhost:3000/来查看您的应用程序。您应该会看到类似以下屏幕截图的内容:

Installing and running react

如果您在编辑器中打开目录,您将看到为您创建了几个文件,如下面的屏幕截图所示:

Installing and running react

在这个项目中,node_modules是运行此项目所需的依赖项,也是 react 本身的依赖项。重要的目录是src,其中保存了源代码。对于这个示例,让我们只保留两个文件-App.jsindex.js/public/index.html文件应该只包含根div,它将用作我们的 react 组件的目标。考虑以下代码片段:

    <!doctype html> 
    <html lang="en"> 
      <head> 
        <title>React App</title> 
      </head> 
      <body> 
 **<div id="root"></div>** 
      </body> 
    </html> 

进行这种更改的时候,您将看到以下错误:

Installing and running react

使用 react 进行开发的美妙之处在于代码更改是实时重新加载的,您可以立即得到反馈。

接下来,清空App.js的所有内容,并用以下代码替换它:

    import React from 'react'; 
    const App = () => <h1>Hello React</h1> 
    export default App 

现在,转到index.js并删除import ./index.css;行。您无需做任何操作,比如重新启动服务器和刷新浏览器,就可以在浏览器上看到修改后的页面。考虑以下屏幕截图:

Installing and running react

在我们创建HelloWorld react 组件之前,有一些重要的事情需要注意。

App.jsindex.js中,我们导入了创建 react 组件所需的两个库。考虑以下代码行:

    import React from 'react'; 
    import ReactDOM from 'react-dom'; 

在这里,我们导入了React,这是一个允许我们构建 react 组件的库。我们还导入了ReactDOM,这是一个允许我们放置我们的组件并在 DOM 的上下文中使用它们的库。然后,我们导入了我们刚刚工作过的组件-App 组件。

我们还在App.js中创建了我们的第一个组件。考虑以下代码行:

    const App = () => <h1>Hello React</h1> 

这是一个无状态函数组件。创建组件的另一种方法是创建一个类组件。我们可以用以下类组件替换前面的组件:

    class App extends React.Component { 
      render(){ 
        return <h1>Hello World</h1> 
      } 
    } 

这里有很多有趣的事情正在发生。首先,我们使用class关键字创建一个类组件,它继承自超类React.Component

我们的组件App是一个 react 组件类或 react 组件类型。组件接受参数,也称为props,并通过render函数返回要显示的视图层次结构。

render方法返回要渲染的描述,然后 react 接受该描述并将其渲染到屏幕上。特别是,render返回一个 react 元素,它是要渲染的轻量级描述。大多数 react 开发人员使用一种称为 JSX 的特殊语法,这使得编写这些结构更容易。<div />语法在构建时转换为React.createElement('div')。JSX 表达式<h1>Hello World</h1>在构建时转换为以下内容:

    return React.createElement('h1', null, 'Hello World'); 

类组件和无状态函数组件之间的区别在于,类组件可以包含状态,而无状态(因此名称为)函数组件不能。

react 组件的render方法只允许返回单个节点。如果你做了以下操作:

    return <h1>Hello World</h1><p>React Rocks</p> 

你会得到以下错误:

    Error in ./src/App.js 
    Syntax error: Adjacent JSX elements must be wrapped in 
      an enclosing tag (4:31) 

这是因为你实质上返回了两个React.createElement函数,这不是有效的 JavaScript。虽然这可能看起来像是一个破坏者,但这很容易解决。我们可以将我们的节点包装成一个父节点,并从render函数返回该父节点。我们可以创建一个父div,并将其他节点包装在其中。考虑以下示例:

    render(){ 
        return ( 
          <div> 
            <h1>Hello World</h1> 
            <p>React Rocks</p> 
          </div> 
          ) 
    } 

组件和 props

组件在概念上可以被视为 JavaScript 函数。它们像普通函数一样接受任意数量的输入。这些输入被称为 props。为了说明这一点,让我们考虑以下函数:

    function Greet(props) { 
      return <h1>Hello, {props.name}</h1>; 
    } 

这是一个普通函数,也是一个有效的 react 组件。它接受一个名为props的输入,并返回一个有效的 JSX。我们可以在 JSX 中使用props,使用大括号和name等属性使用标准对象表示法。现在Greet是一个一流的 react 组件,让我们在render()函数中使用它,如下所示:

    render(){ 
      return ( 
       return <Greet name="Joe"/> 
      ) 
    } 

我们将Greet()作为一个普通组件调用,并将this.props传递给它。自定义组件必须大写。React 认为以小写字母开头的组件名称是标准 HTML 标签,并期望自定义组件名称以大写字母开头。正如我们之前看到的,我们可以使用 ES6 类创建一个类组件。这个组件是React.component的子类。与我们的Greet函数等效的组件如下:

    class Greet extends React.Component { 
      render(){ 
          return <h1>Hello, {this.props.name}</h1> 
      } 
    } 

就实际目的而言,我们将使用这种创建组件的方法。我们很快就会知道为什么。

一个重要的要点是组件不能修改自己的 props。这可能看起来像是一个限制,因为在几乎所有非平凡的应用程序中,你都希望用户交互在 react 中改变 UI 组件状态,例如,在表单中更新出生日期,props是只读的,但有一个更健壮的机制来处理 UI 更新。

状态

状态类似于 props,但它是私有的,并且完全由组件控制。正如我们之前看到的,React 中的函数组件和类组件是等效的,一个重要的区别是状态仅在类组件中可用。因此,就实际目的而言,我们将使用类组件。

我们可以改变我们现有的问候示例来使用状态,每当状态改变时,我们将更新我们的Greet组件以反映更改的值。

首先,我们将在我们的App.js中设置状态,如下所示:

    class Greet extends React.Component { 
 **constructor(props) {**
 **super(props);** 
 **this.state = {** 
**greeting: "this is default greeting text"** 
**}** 
 **}** 
      render(){ 
          return <h1>{this.state.greeting}, {this.props.name} </h1> 
      } 
    } 

在这个例子中有一些重要的事情需要注意。首先,我们调用类constructor来初始化this.state。我们还调用基类构造函数super(),并将props传递给它。调用super()后,我们通过将this.state设置为一个对象来初始化我们的默认状态。例如,我们在这里给一个greeting属性赋值。在render方法中,我们将使用{this.state.greeting}来使用这个属性。设置了初始状态后,我们可以添加 UI 元素来更新这个状态。让我们添加一个输入框,当输入框改变时,我们将更新我们的状态和greeting元素。考虑以下代码行:

    class Greet extends React.Component { 
      constructor(props) { 
        super(props); 
        this.state = { 
          greeting: "this is default greeting text" 
        } 
      } 
 **updateGreeting(event){** 
**this.setState({** 
**greeting:
      event.target.value,**
 **})**
 **}** 
      render(){ 
          return ( 
          <div>   
 **<input type="text" onChange={this.updateGreeting.bind(this)}/>** 
            <h1>{this.state.greeting}, {this.props.name} </h1> 
           </div>  
          ) 
        } 
    } 

在这里,我们添加一个输入框,并在输入框的onChange方法被调用时更新组件的状态。我们使用自定义的updateGreeting()方法通过调用this.setState和更新属性来更新状态。当您运行此示例时,您会注意到当您在文本框上输入内容时,只有greeting元素被更新,而name没有。看一下下面的截图:

State

React 的一个重要特性是,一个 React 组件可以输出或渲染其他 React 组件。我们这里有一个非常简单的组件。它有一个值为文本的状态。它有一个update方法,它将从事件中更新文本的值。我们将创建一个新的组件。这将是一个无状态函数组件。我们将它称为 widget。它将接受props。我们将在这里返回这个 JSX 输入。考虑以下代码片段:

    render(){ 
        return ( 
          <div>   
 **<Widget update={this.updateGreeting.bind(this)} />** 
 **<Widget update={this.updateGreeting.bind(this)} />** 
 **<Widget update={this.updateGreeting.bind(this)} />** 
          <h1>{this.state.greeting}, {this.props.name} </h1> 
          </div>  
        ) 
      } 
    } 
    const Widget = (props) => <input type="text" 
      onChange={props.update}/> 

首先,我们将输入元素提取到一个无状态函数组件中,并将其称为Widget。我们将props传递给此组件。然后,我们将onChange更改为使用props.update。现在,在我们的render方法中,我们使用Widget组件并传递一个绑定updateGreeting()方法的 prop update。现在Widget是一个组件,我们可以在Greet组件的任何地方重用它。我们创建了Widget的三个实例,当任何一个Widget被更新时,问候文本将被更新,如下面的截图所示:

State

生命周期事件

当您有一堆组件和几个状态更改和事件时,清理工作变得很重要。React 为您提供了几个组件生命周期钩子来处理组件的生命周期事件。了解组件的生命周期将使您能够在创建或销毁组件时执行某些操作。此外,它还为您提供了决定是否应该首先更新组件的机会,并根据props或状态更改做出反应。

组件经历三个阶段-挂载、更新和卸载。对于每个阶段,我们都有钩子。看一下下面的图表:

Life cycle events

当组件初始渲染时,会调用两个方法getDefaultPropsgetInitialState,正如它们的名称所暗示的,我们可以在这些方法中设置组件的默认props和初始状态。

componentWillMount在执行render方法之前被调用。我们已经知道render是我们返回要渲染的组件的地方。一旦render方法完成,componentDidMount方法就会被调用。您可以在此方法中访问 DOM,并建议在此方法中执行任何 DOM 交互。

状态更改会调用一些方法。shouldComponentUpdate方法在render方法之前被调用,它让我们决定是否应该允许重新渲染或跳过。这个方法在初始渲染时从未被调用。componentWillUpdate方法在shouldComponentUpdate方法返回true后立即被调用。componentDidUpdate方法在render完成后被渲染。

props对象的任何更改都会触发类似状态更改的方法。另一个被调用的方法是componentWillReceiveProps;它仅在props发生变化时被调用,而且不是初始渲染。您可以在此方法中基于新旧 props 更新状态。

当组件从 DOM 中移除时,将调用componentWillUnmount。这是一个执行清理的有用方法。

React 的一个很棒的地方是,当您开始使用它时,这个框架对您来说会感觉非常自然。您只需要学习很少的移动部分,抽象程度恰到好处。

摘要

本章旨在介绍一些最近备受关注的重要新观念。响应式编程和 React 都可以显著提高程序员的生产力。React 绝对是由 Facebook 和 Netflix 等公司支持的最重要的新兴技术之一。

本章旨在为您介绍这两种技术,并帮助您开始更详细地探索它们。

附录 A. 保留字

这个附录提供了两个保留关键字列表,这些关键字在 ECMAScript 5(ES5)中定义。第一个是当前的单词列表,第二个是为将来的实现保留的单词列表。

还有一些单词不再保留,尽管它们曾经在 ES3 中保留。

您不能将保留字用作变量名:

    var break = 1; // syntax error 

如果您将这些单词用作对象属性,您必须对它们进行引用:

    var o = {break: 1};   // OK in many browsers, error in IE 
    var o = {"break": 1}; // Always OK 
    alert(o.break);       // error in IE 
    alert(o["break"]);    // OK 

关键词

ES5 当前保留的单词列表如下:

  • 休息

  • 案例

  • 捕捉

  • 继续

  • 调试器

  • 默认

  • 删除

  • 其他

  • 最后

  • 功能

  • 如果

  • 的例子

  • 返回

  • 转换

  • 这个

  • 尝试

  • 类型

  • 变量

ES6 保留的单词

以下关键字在 ES6 中保留:

  • 常数

  • 枚举

  • 出口

  • 扩展

  • 实现

  • 进口

  • 接口

  • 私人的

  • 受保护的

  • 公共

  • 静态

  • 超级

  • 产量

未来保留的单词

这些关键字目前没有使用,但它们被保留供将来版本使用:

  • 枚举

  • 等待

以前保留的单词

以下单词从 ES5 开始不再保留,但最好远离它们,以便老版本的浏览器:

  • 抽象

  • 布尔

  • 字节

  • 字符

  • 最终

  • 浮动

  • 转到

  • 整数

  • 本地

  • 同步

  • 瞬态

  • 挥发

附录 B. 内置函数

本附录包含了内置函数(全局对象的方法)的列表,讨论在第三章中,函数

函数 描述

| parseInt() | 接受两个参数:一个输入对象和基数;然后尝试返回输入的整数表示。不处理输入中的指数。默认基数为10(十进制数)。失败时返回NaN。省略基数可能会导致意外结果(例如对于08这样的输入),因此最好总是指定它:

    > parseInt('10e+3');   
    10   
    > parseInt('FF');   
    NaN   
    > parseInt('FF', 16);   
    255   

|

parseFloat() | 接受一个参数并尝试返回其浮点数表示。理解输入中的指数:

    > parseFloat('10e+3');   
    10000   
    > parseFloat('123.456test');   
    123.456   

|

| isNaN() | 缩写自“不是一个数字”。接受一个参数,如果参数不是有效的数字则返回true,否则返回false。首先尝试将输入转换为数字:

    > isNaN(NaN);   
    true   
    > isNaN(123);   
    false   
    > isNaN(parseInt('FF'));   
    true   
    > isNaN(parseInt('FF', 16));   
    false   

|

| isFinite() | 如果输入是一个数字(或可以转换为数字),则返回true,但如果不是数字,则返回Infinity- Infinity。对于无穷大或非数值的情况返回false

    > isFinite(1e+1000);   
    false   
    > isFinite(-Infinity);   
    false   
    > isFinite("123");   
    true   

|

| encodeURIComponent() | 将输入转换为 URL 编码的字符串。有关 URL 编码工作原理的更多详细信息,请参阅维基百科文章en.wikipedia.org/wiki/Url_encode

    > encodeURIComponent   
    ('http://phpied.com/');   
    "http%3A%2F%2Fphpied.com%2F"   
    > encodeURIComponent   
    ('some script?key=v@lue');   
    "some%20script%3Fkey%3Dv%40lue"   

|

| decodeURIComponent() | 接受一个 URL 编码的字符串并解码它:

    > decodeURIComponent('%20%40%20');   
    " @ "   

|

| encodeURI() | 对输入进行 URL 编码,但假定给定了完整的 URL,因此通过不对协议(例如http://)和主机名(例如www.phpied.com)进行编码来返回有效的 URL:

    > encodeURI('http://phpied.com/');   
    "http://phpied.com/"   
    > encodeURI('some   script?key=v@lue');   
    "some%20script?key=v@lue"   

|

| decodeURI() | encodeURI() 的相反操作:

    > decodeURI("some%20script?key=v@lue");   
    "some script?key=v@lue"   

|

| eval() | 接受一个 JavaScript 代码的字符串并执行它。返回输入字符串中最后一个表达式的结果。尽量避免使用:

    > eval('1 + 2');   
    3   
    > eval('parseInt("123")');   
    123   
    > eval('new Array(1, 2, 3)');   
    [1, 2, 3]   
    > eval('new Array(1, 2, 3); 1 +   2;');   
    3   

|

附录 C.内置对象

本附录列出了ECMAScriptES)标准中概述的内置构造函数,以及由这些构造函数创建的对象的属性和方法。ES5 特定的 API 被单独列出。

Object

Object()是一个创建对象的构造函数,例如:

    > var o = new Object(); 

这与使用对象文字相同:

    > var o = {}; // recommended 

你可以将任何东西传递给构造函数,它将尝试猜测它是什么,并使用更合适的构造函数。例如,将字符串传递给new Object()将与使用new String()构造函数相同。这不是一种推荐的做法(最好是明确而不是让猜测渗入),但仍然是可能的:

    > var o = new Object('something'); 
    > o.constructor; 
    function String() { [native code] } 
    > var o = new Object(123); 
    > o.constructor; 
    function Number() { [native code] } 

所有其他对象,无论是内置的还是自定义的,都继承自Object。因此,以下部分列出的属性和方法适用于所有类型的对象。

Object 构造函数的成员

以下是Object构造函数的成员:

属性/方法 描述

| Object.prototype | 所有对象的原型(也是一个对象本身)。你添加到这个原型的任何东西都将被所有其他对象继承,所以要小心:

    > var s = new String('noodles');   
    > Object.prototype.custom = 1;   
    1   
    > s.custom;   
    1   

|

Object.prototype 成员

不要说“由 Object 构造函数创建的对象的成员”,让我们称之为“Object.prototype成员”。对于Array.prototype以及在附录中的其他对象也是一样的:

属性/方法 描述

| constructor | 指向用于创建对象的构造函数,这里是Object

    > Object.prototype.constructor === Object;   
    true   
    > var o = new Object();   
    > o.constructor === Object;   
    true   

|

| toString(radix) | 返回对象的字符串表示。如果对象恰好是一个Number对象,则基数参数定义了返回数字的基数。默认基数是10

    > var o = {prop: 1};   
    > o.toString();   
    "[object Object]"   
    > var n = new Number(255);   
    > n.toString();   
    "255"   
    > n.toString(16);   
    "ff"   

|

| toLocaleString() | 与toString()相同,但匹配当前区域设置。旨在由对象自定义,例如Date()Number()Array(),并提供特定于区域设置的值,例如不同的日期格式。对于Object()实例以及大多数其他情况,它只是调用toString()。在浏览器中,您可以使用导航器 BOM 对象的language属性(或 IE 中的userLanguage)来确定语言:

    > navigator.language;   
    "en-US"   

|

| valueOf() | 如果适用,返回this的原始表示。例如,Number对象返回一个原始数字,Date对象返回一个时间戳。如果没有合适的原始值,它简单地返回this

    > var o = {};   
    > typeof o.valueOf();   
    "object"   
    > o.valueOf() === o;   
    true   
    > var n = new Number(101);   
    > typeof n.valueOf();   
    "number"   
    > n.valueOf() === n;   
    false   
    > var d = new Date();   
    > typeof d.valueOf();   
    "number"   
    > d.valueOf();   
    1357840170137   

|

| hasOwnProperty(prop) | 如果属性是对象的自有属性,则返回true,如果是从原型链继承的,则返回false。如果属性不存在,则也返回false

    > var o = {prop: 1};   
    > o.hasOwnProperty('prop');   
    true   
    > o.hasOwnProperty('toString');   
    false   
    > o.hasOwnProperty('fromString');   
    false   

|

| isPrototypeOf(obj) | 如果一个对象被用作另一个对象的原型,则返回true。可以测试原型链中的任何对象,不仅仅是直接创建者:

    > var s = new String('');   
    > Object.prototype.isPrototypeOf(s);   
    true   
    > String.prototype.isPrototypeOf(s);   
    true   
    > Array.prototype.isPrototypeOf(s);   
    false   

|

| propertyIsEnumerable(prop) | 如果属性在for...in循环中显示,则返回true

    > var a = [1, 2, 3];   
    > a.propertyIsEnumerable('length');   
    false   
    > a.propertyIsEnumerable(0);   
    true   

|

ECMAScript 5 对对象的增加

在 ECMAScript 3 中,所有对象属性都可以随时更改、添加或删除,除了一些内置属性(例如Math.PI)。在 ES5 中,您可以定义不能更改或删除的属性,这是以前保留给内置属性的特权。ES5 引入了属性描述符的概念,让您更严格地控制您定义的属性。

将属性描述符视为指定属性特征的对象。描述这些特征的语法是一个常规对象文字,因此属性描述符有自己的属性和方法,但让我们称它们为属性以避免混淆。这些属性是:

  • value - 当访问属性时得到的值

  • writable - 可以更改此属性吗

  • enumerable - 它是否应该出现在for...in循环中

  • configurable - 可以删除它

  • set() - 每次更新值时调用的函数

  • get() - 当访问属性的值时调用

此外,数据描述符(您定义属性enumerableconfigurablevaluewritable)和访问器描述符(您定义enumerableconfigurableset()get())之间存在区别。如果定义了set()get(),则描述符被视为访问器,并且尝试定义值或可写性将引发错误。

定义一个常规的老式 ES3 风格的属性:

    var person = {}; 
    person.legs = 2; 

使用 ES5 数据描述符相同:

    var person = {}; 
    Object.defineProperty(person, "legs", { 
      value: 2, 
      writable: true, 
      configurable: true, 
      enumerable: true 
    }); 

如果将value的值默认设置为undefined,则所有其他值都为false。因此,如果要能够稍后更改此属性,则需要显式将它们设置为true

或者使用 ES5 访问器描述符的相同属性:

    var person = {}; 
    Object.defineProperty(person, "legs", { 
      set: function (v) {this.value = v;}, 
      get: function (v) {return this.value;}, 
      configurable: true, 
      enumerable: true 
    }); 
    person.legs = 2; 

如您所见,属性描述符是更多的代码,因此只有在您真正想要防止其他人篡改您的属性时才使用它们,并且您忘记了与 ES3 浏览器的向后兼容性,因为与例如Array.prototype的添加不同,您不能在旧浏览器中“shim”此功能。

并且描述符的功能(定义不可塑性属性):

    > var person = {}; 
    > Object.defineProperty(person, 'heads', {value: 1}); 
    > person.heads = 0; 
    0 
    > person.heads; 
    1 
    > delete person.heads; 
    false 
    > person.heads; 
    1 

以下是Object的所有 ES5 新增内容的列表:

属性/方法 描述

| Object.getPrototypeOf(obj) | 在 ES3 中,您必须猜测给定对象的原型是什么,使用Object.prototype.isPrototypeOf()方法,而在 ES5 中,您可以直接询问“你的原型是谁?”

    > Object.getPrototypeOf([]) ===    
       Array.prototype;   
    true   

|

| Object.create(obj, descr) | 在第七章中讨论,继承。创建一个新对象,设置其原型,并使用属性描述符定义该对象的属性(前面讨论过):

    > var parent = {hi: 'Hello'};   
    > var o = Object.create(parent,
    { prop: {value: 1 }});   
    > o.hi;   
    "Hello"   

它甚至让您创建一个完全空白的对象,这是您在 ES3 中无法做到的:

    > var o = Object.create(null);   
    > typeof o.toString;   
    "undefined"   

|

| Object.getOwnPropertyDescriptor(obj, property) | 允许您检查属性的定义方式。您甚至可以窥视内置对象,并查看所有这些以前隐藏的属性:

    > Object.getOwnProperty
      Descriptor (Object.prototype,
      'toString');   
    Object   
    configurable: true   
    enumerable: false   
    value: function toString() {
     [native code] }   
    writable: true   

|

| Object.getOwnPropertyNames(obj) | 返回所有自有属性名称(作为字符串的数组),无论是否可枚举。使用Object.keys()仅获取可枚举的属性:

    > Object.getOwnPropertyNames(   
      Object.prototype);   
    ["constructor", "toString",
    "toLocaleString", "valueOf",
     ...   

|

Object.defineProperty(obj, descriptor) 使用属性描述符定义对象的属性。请参阅本表之前的讨论。

| Object.defineProperties(obj, descriptors) | 与defineProperty()相同,但允许您一次定义多个属性:

    > var glass =    
     Object.defineProperties({}, {   
        "color": {   
          value: "transparent",   
          writable: true   
        },   
        "fullness": {   
          value: "half",   
          writable: false   
        }   
      });   

    > glass.fullness;   
    "half"   

|

| Object.preventExtensions(obj)``Object.isExtensible(obj) | preventExtensions()禁止向对象添加更多属性,isExtensible()检查是否可以添加属性:

    > var deadline = {};   
    > Object.isExtensible(deadline);   
    true   
    > deadline.date = "yesterday";   
    "yesterday"   
    > Object.preventExtensions(
     deadline);   
    > Object.isExtensible(deadline);   
    false   
    > deadline.date = "today";   
    "today"   
    > deadline.date;   
    "today"   

尝试向不可扩展的对象添加属性不会报错,但只是不起作用:

    > deadline.report = true;   
    > deadline.report;   
    undefined   

|

Object.seal(obj)``Object.isSealed(obj) seal()preventExtensions()相同,并且还使所有现有属性不可配置。这意味着您可以更改现有属性的值,但不能删除它或重新配置它(使用defineProperty()不起作用)。因此,您不能将可枚举属性变为不可枚举,例如。

| Object.freeze(obj)``Object.isFrozen(obj) | seal()所做的一切,还阻止更改属性的值:

    > var deadline = Object.freeze(   
        {date: "yesterday"});   
    > deadline.date = "tomorrow";   
    > deadline.excuse = "lame";   
 **> deadline.date;**
**"yesterday"**
**>
    deadline.excuse;**
**undefined**
**> 
    Object.isSealed(deadline);**
**true**

|

| Object.keys(obj) | 用于for...in循环的替代方法。仅返回自有属性(不像for...in)。属性需要可枚举才会显示出来(不像Object.getOwnPropertyNames())。返回值是一个字符串数组。

 **>Object.prototype.customProto =
     101;**
 **> Object.getOwnPropertyNames(** 
 **Object.prototype);** 
 **["constructor", "toString", ...,
      "customProto"]** 
 **> Object.keys(Object.prototype);** 
 **["customProto"]**
**> var o = {own: 202};**
**> o.customProto;** 
**101**
**> Object.keys(o);**
**"own"]**

|

ES6 对象的新增内容

ES6 具有一些有趣的对象定义和属性语法。这种新语法是为了使对象的操作更加简单和简洁。

属性简写

ES6 提供了一种更短的语法来定义常见对象。

ES5:obj = { x: x, y: y };

ES6:obj = {x,y};

计算属性名称

ES6 对象定义语法中可以计算属性名称:

    let obj = { 
      foo: "bar", 
      [ "baz" + q() ]: 42 
    } 

在这里,属性名称是计算的,其中"baz"与函数调用的结果连接在一起。

Object.assign

Object.assign() 方法用于将一个或多个源对象的所有可枚举自有属性的值复制到目标对象中:

    var dest  = { quux: 0 } 
    var src1 = { foo: 1, bar: 2 } 
    var src2 = { foo: 3, baz: 4 } 
    Object.assign(dst, src1, src2) 

数组

Array 构造函数创建数组对象:

    > var a = new Array(1, 2, 3); 

这与数组文字相同:

    > var a = [1, 2, 3]; //recommended 

当您将一个数值传递给 Array 构造函数时,假定它是数组的长度:

    > var un = new Array(3); 
    > un.length; 
    3 

您会得到所需长度的数组,如果您要求每个数组元素的值,您会得到 undefined

    > un; 
    [undefined, undefined, undefined] 

数组中充满元素和没有元素但只有长度之间有微妙的差异:

    > '0' in a; 
    true 
    > '0' in un; 
    false 

Array() 构造函数在指定一个参数与指定多个参数时的行为差异可能会导致意想不到的结果。例如,以下对数组文字的使用是有效的:

    > var a = [3.14]; 
    > a; 
    [3.14] 

然而,将浮点数传递给 Array 构造函数是一个错误:

    > var a = new Array(3.14); 
    Range Error: invalid array length 

Array.prototype 成员

以下是 Array 的所有元素列表:

属性/方法 描述

| length | 数组中的元素数量:

    > [1, 2, 3, 4].length;   
    4   

|

| concat(i1, i2, i3,...) | 合并数组:

    > [1, 2].concat([3, 5], [7, 11]);   
    [1, 2, 3, 5, 7, 11]   

|

| join(separator) | 将数组转换为字符串。分隔符参数是一个字符串,默认值为逗号:

    > [1, 2, 3].join();   
    "1,2,3"   
    > [1, 2, 3].join('&#124;');   
    "1&#124;2&#124;3"   
    > [1, 2, 3].join(' is less than   ');   
    "1 is less than 2 is less   than 3"   

|

| pop() | 移除数组的最后一个元素并返回它:

    > var a = ['une', 'deux', 'trois'];   
    > a.pop();   
    "trois"   
    > a;   
    ["une", "deux"]   

|

| push(i1, i2, i3,...) | 将元素附加到数组的末尾并返回修改后数组的长度:

    > var a = [];   
    > a.push('zig', 'zag', 'zebra','zoo');   
    4   

|

| reverse() | 反转数组的元素并返回修改后的数组:

    > var a = [1, 2, 3];   
    > a.reverse();   
    [3, 2, 1]   
    > a;   
    [3, 2, 1]   

|

| shift() | 类似于 pop(),但是移除的是第一个元素,而不是最后一个:

    > var a = [1, 2, 3];   
    > a.shift();   
    1   
    > a;   
    [2, 3]   

|

| slice(start_index, end_index) | 提取数组的一部分并将其作为新数组返回,而不修改源数组:

    > var a = ['apple', 'banana', 'js',
      'css', 'orange'];   
    > a.slice(2,4);   
    ["js", "css"]   
    > a;   
    ["apple", "banana", "js", "css", "orange"]   

|

| sort(callback) | 对数组进行排序。还可以接受一个用于自定义排序的回调函数。回调函数接收两个数组元素作为参数,如果它们相等则返回 0,如果第一个大于第二个则返回正数,如果第二个大于第一个则返回负数。下面是一个自定义排序函数的示例,它执行正确的数字排序(因为默认是字符排序):

    function customSort(a, b) {   
             if (a > b) return 1;    
             if (a < b) return -1;    
             return 0;   
    }   
    Example use of sort():   
    > var a = [101, 99, 1, 5];   
    > a.sort();   
     [1, 101, 5, 99]   
    > a.sort(customSort);   
    [1, 5, 99, 101]   
    > [7, 6, 5, 9].sort(customSort);   
    [5, 6, 7, 9]   

|

| splice(start, delete_count, i1, i2, i3,...) | 同时移除和添加元素。第一个参数是开始移除的位置,第二个是要移除的项目数,其余参数是要插入到已移除项目位置的新元素:

    > var a = ['apple', 'banana',
      'js', 'css', 'orange'];   
    > a.splice(2, 2, 'pear', 'pineapple');   
    ["js", "css"]   
    > a;   
    ["apple", "banana",   "pear", 
      "pineapple", "orange"]   

|

| unshift(i1, i2, i3,...) | 类似于 push(),但是将元素添加到数组的开头而不是末尾。返回修改后数组的长度:

    > var a = [1, 2, 3];    
    > a.unshift('one', 'two');    
    5   
    > a;   
    ["one", "two",   1, 2, 3]   

|

Array 的 ECMAScript 5 添加内容

以下是 Array 的 ECMAScript 5 添加内容:

属性/方法 描述

| Array.isArray(obj) | 告诉对象是否是数组,因为 typeof 不够好:

    > var arraylike = {0: 101, 
    length: 1};   
    > typeof arraylike;   
    "object"   
    > typeof [];   
    "object"   

两者都不是鸭子类型(如果它走起来像鸭子,叫起来像鸭子,那它一定是鸭子):

    typeof arraylike.length;   
    "number"   

在 ES3 中,您需要冗长的:

    > Object.prototype.toString
    .call
    ([]) === "[object Array]"; 
    true
    > Object.prototype.toString.call
    (arraylike) === 
    "[object Array]";
    false   

在 ES5 中,您会得到更简短的:

    Array.isArray([]);   
    true   
    Array.isArray(arraylike);   
    false   

|

| Array.prototype.indexOf(needle, idx) | 搜索数组并返回第一个匹配项的索引。如果没有匹配项,则返回 -1。还可以选择从指定索引开始搜索:

    > var ar = ['one', 'two', 
      'one',   'two'];   
    > ar.indexOf('two');   
    1   
    > ar.indexOf('two', 2);   
    3   
    > ar.indexOf('toot');   
    -1   

|

| Array.prototype.lastIndexOf(needle, idx) | 类似于 indexOf(),只是从末尾开始搜索:

    > var ar = ['one', 'two', 
    'one', 'two']; 
    > ar.lastIndexOf('two');   
    3   
    > ar.lastIndexOf('two', 2);   
    1   
    > ar.indexOf('toot');   
    -1   

|

| Array.prototype.forEach(callback, this_obj) | 替代 for 循环的方法。您可以指定一个回调函数,该函数将对数组的每个元素进行调用。回调函数接收参数:元素、其索引和整个数组:

    > var log =
    console.log.bind(console);   
    > var ar = ['itsy', 'bitsy',
      'spider'];   
    > ar.forEach(log);   
     itsy      0   ["itsy", 
     "bitsy", "spider"]   
     bitsy    1   ["itsy", 
     "bitsy", "spider"] 
     spider  2   ["itsy", 
    "bitsy", "spider"]   

还可以指定第二个参数:绑定到回调函数内部的对象。因此,这也是有效的:

    > ar.forEach(console.log, 
      console);   

|

| Array.prototype.every(callback, this_obj) | 您提供一个测试数组每个元素的回调函数。您的回调函数与 forEach() 接收相同的参数,并且必须根据给定元素是否满足您的测试返回 truefalse。如果所有元素都满足您的测试,every() 返回 true。如果至少有一个不满足,every() 返回 false

    > function hasEye(el, idx,
    ar) { 
     return el.indexOf('i') !== 
      -1; 
     }  
    > ['itsy', 'bitsy',
      'spider'].
    every(hasEye); 
    true
    > ['eency', 'weency', 
    'spider'].every(hasEye);   
    false   

如果在循环过程中某个时刻变得清楚结果将是false,循环将停止并返回false

    > [1,2,3].every(function (e)
      { 
        console.log(e);   
        return false;   
      });   
     1   
     false   

|

| Array.prototype.some(callback, this_obj) | 类似于every(),只有至少一个元素满足您的测试时返回true

    > ['itsy', 'bitsy', 
      'spider'].some(hasEye);
      true   
    > ['eency', 'weency',
     'spider'].some(hasEye);   
      true   

|

| Array.prototype.filter(callback, this_obj) | 类似于some()every(),但它返回满足您测试的所有元素的新数组:

    > ['itsy', 'bitsy', 
      'spider'].filter(hasEye); 
     ["itsy", "bitsy",
     "spider"]
    > ['eency', 'weency',
      'spider'].filter(hasEye); 
      ["spider"]   

|

| Array.prototype.map(callback, this_obj) | 类似于forEach(),因为它对每个元素执行回调,但另外它构造一个新数组,并返回回调的返回值。让我们将数组中的所有字符串大写:

    > function uc(element, index,
      array) {
      return element.toUpperCase(); 
    }   
    > ['eency', 'weency',
      'spider'].map(uc);
    ["EENCY", "WEENCY", "SPIDER"]   

|

| Array.prototype.reduce(callback, start) | 对数组的每个元素执行回调。您的回调返回一个值。这个值随后传回到您的回调中进行下一次迭代。整个数组最终被减少为一个单一的值:

    > function sum(res, element, 
      idx, arr) {
       return res + element;
     } 
    > [1, 2, 3].reduce(sum);
     6   

可以选择传递一个起始值,该值将在第一个回调调用时使用:

    > [1, 2, 3].reduce(sum, 100);   
    106   

|

| Array.prototype.reduceRight(callback, start) | 类似于reduce(),但是从数组的末尾循环:

    > function concat(
    result_so_far, el) { 
    return "" +  result_so_far 
    + el;
    } 
    > [1, 2, 3].reduce(concat);   
    "123" 
    > [1, 2, 3].reduceRight
    (concat);
    "321"   

|

ES6 对数组的添加

以下是对数组的添加:

| Array.from(arrayLike, mapFunc?, thisArg?) | Array.from()方法的基本功能是将两种类型的值转换为数组-arrayLike值和Iterable值:

    const arrayLike = { length:
     2, 0: 'a', 1: 'b' };
    const arr =
    Array.from(arrayLike);
    for (const x of arr) {
     // OK, iterable
    console.log(x);
    }
    // Output:
    // a
    // b

|

| Array.of(...items) | 从传递给该方法的项目创建一个数组

    let a = Array.of(
      1,2,3,'foo');
    console.log(a); //[1, 2,
     3, "foo"]

|

| Array.prototype.entries()``Array.prototype.keys()``Array.prototype.values() | 这些方法的结果是一系列值。这些方法分别返回键、值和条目的迭代器。

    let a = Array.of(1,2,
    3,'foo');
    let k,v,e;
    for (k of a.keys()) {
    console.log(k); //0 1 2 3
    }
    for (v of a.values()) {
    console.log(v); //1 2 
    3 foo
    }
    for (e of a.entries()){
    console.log(e);
    }
    //[[0,1],[1,2],[2,3]
    [3,'foo']]

|

| Array.prototype.find(predicate, thisArg?) | 返回回调函数返回true的第一个数组元素。如果没有这样的元素,则返回undefined

    [1, -2, 3].find(x => x < 0)
     //-2

|

| Array.prototype.findIndex(predicate, thisArg?) | 返回回调函数返回 true 的第一个元素的索引。如果没有这样的元素,则返回-1

    [1, -2, 3].find(x => x < 0)
     //1

|

| Array.prototype.fill(value : any, start=0, end=this.length) : This | 用给定值填充数组:

const arr = ['a', 'b', 'c'];
arr.fill(7)
[ 7, 7, 7 ]
You can specify start and end ranges.
['a', 'b', 'c'].fill(7, 1, 2)
[ 'a', 7, 'c' ]

|

函数

JavaScript 函数是对象。它们可以使用Function构造函数进行定义,如下所示:

    var sum = new Function('a', 'b', 'return a + b;'); 

这是函数文字(也称为函数表达式)的(通常不推荐的)替代方法:

    var sum = function (a, b) { 
      return a + b; 
    }; 

或者,更常见的函数定义:

    function sum(a, b) { 
      return a + b; 
    } 

Function.prototype 成员

以下是Function构造函数的成员列表:

属性/方法 描述

| apply(this_obj, params_array) | 允许您调用另一个函数,同时覆盖另一个函数的this值。apply()接受的第一个参数是要绑定到函数内部的对象,第二个参数是要发送到被调用函数的参数数组:

    function whatIsIt(){   
      return this.toString();   
    }   
    > var myObj = {};   
    > whatIsIt.apply(myObj);   
    "[object Object]"   
    > whatIsIt.apply(window);   
    "[object Window]"   

|

call(this_obj, p1, p2, p3, ...) apply()相同,但是逐个接受参数,而不是作为一个数组。

| length | 函数期望的参数数量:

    > parseInt.length;   
    2   

如果您忘记了call()apply()之间的区别:

    > Function.prototype.call.length;   
    1   
    > Function.prototype.apply.length;   
    2   

call()属性的长度为1,因为除第一个参数外,所有参数都是可选的。|

ECMAScript 5 对函数的添加

以下是 ECMAScript 5 对Function构造函数的添加:

属性/方法 描述

| Function.prototype.bind() | 当您想要调用一个内部使用 this 的函数,并且您想要定义 this 是什么时。call()apply()方法调用函数,而bind()返回一个新函数。当您将一个方法提供为另一个对象的方法的回调时,并且您希望 this 是您选择的对象时,这是有用的:

    > whatIsIt.apply(window);   
    "[object Window]"   

|

ECMAScript 6 对函数的添加

以下是 ECMAScript 6 对Function构造函数的添加:

箭头函数箭头函数表达式与函数表达式相比具有更短的语法,并且不绑定自己的 this、arguments、super 或new.target。箭头函数始终是匿名的。
    () => { ... }
    // no parameter 
    x => { ... }
    // one 
    parameter, an
    identifier 
    (x, y) => 
    {   ... }
    // several
    parameters
    const squares =
    [1, 2, 3].map(
    x => x * x);   

|

语句主体更具表现力和简洁的闭包语法
    arr.forEach(v =>
    { if (v % 5 
     ===0)
filtered:ist.push(v)
    })   

|

布尔

Boolean构造函数创建布尔对象(不要与布尔原语混淆)。布尔对象并不那么有用,这里列出来是为了完整起见。

    > var b = new Boolean(); 
    > b.valueOf(); 
    false 
    > b.toString(); 
    "false" 

布尔对象与布尔原始值不同。如您所知,所有对象都是真值:

    > b === false; 
    false 
    > typeof b; 
    "object" 

布尔对象除了从Object继承的属性之外,没有任何属性。

数字

这将创建数字对象:

    > var n = new Number(101); 
    > typeof n; 
    "object" 
    > n.valueOf(); 
    101 

Number对象不是原始对象,但如果您在原始数字上使用任何Number.prototype方法,原始对象将在后台转换为Number对象,并且代码将正常工作。

    > var n = 123; 
    > typeof n; 
    "number" 
    > n.toString(); 
    "123" 

在不使用new的情况下,Number构造函数返回原始数字。

    > Number("101"); 
    101 
    > typeof Number("101"); 
    "number" 
    > typeof new Number("101"); 
    "object" 

Number 构造函数的成员

考虑Number构造函数的以下成员:

属性/方法 描述

| Number.MAX_VALUE | 一个常量属性(无法更改),包含允许的最大数字:

    > Number.MAX_VALUE;   
    1.7976931348623157e+308   

|

| Number.MIN_VALUE | 您可以在 JavaScript 中使用的最小数字:

    > Number.MIN_VALUE;   
    5e-324   

|

| Number.NaN | 包含非数字的数字。与全局 NaN 相同:

    > Number.NaN;    
    NaN   

NaN 不等于任何东西,包括它自己:

    > Number.NaN === Number.NaN;   
    false   

|

Number.POSITIVE_INFINITY 与全局Infinity数字相同。
Number.NEGATIVE_INFINITY -Infinity相同。

Number.prototype 成员

以下是Number构造函数的成员:

属性/方法 描述

| toFixed(fractionDigits) | 返回具有数字的定点表示的字符串。四舍五入返回值:

    > var n = new Number(Math.PI);   
    > n.valueOf();   
    3.141592653589793   
    > n.toFixed(3);   
    "3.142"   

|

| toExponential(fractionDigits) | 返回具有数字对象的指数表示的字符串。四舍五入返回值:

    > var n = new Number(56789);   
    > n.toExponential(2);   
    "5.68e+4"   

|

| toPrecision(precision) | 数字对象的字符串表示,指数或定点表示,取决于数字对象:

    > var n = new Number(56789);   
    > n.toPrecision(2);   
    "5.7e+4"   
    > n.toPrecision(5);   
    "56789"   
    > n.toPrecision(4);   
    "5.679e+4"   
    > var n = new Number(Math.PI);   
    > n.toPrecision(4);   
    "3.142"   

|

字符串

String()构造函数创建字符串对象。如果您像操作对象一样调用方法,原始字符串将在后台转换为对象。省略new会给您原始字符串。

创建字符串对象和字符串原始值:

    > var s_obj = new String('potatoes');  
    > var s_prim = 'potatoes'; 
    > typeof s_obj; 
    "object" 
    > typeof s_prim; 
    "string" 

当使用===按类型比较时,对象和原始值不相等,但当使用==进行比较时,它们会进行类型强制转换:

    > s_obj === s_prim; 
    false 
    > s_obj == s_prim; 
    true 

length是字符串对象的属性:

    > s_obj.length; 
    8 

如果在原始字符串上访问length,则原始字符串将在后台转换为对象,并且操作将成功:

    > s_prim.length; 
    8 

字符串文字也可以正常工作:

    > "giraffe".length; 
 **7**

字符串构造函数的成员

以下是String构造函数的成员:

属性/方法 描述

| String.fromCharCode (code1, code2, code3, ...) | 返回使用输入的 Unicode 值创建的字符串:

    > String.fromCharCode(115, 99,
    114,
    105, 112, 116);   
    "script"   

|

String.prototype 成员

考虑以下String.prototype成员:

属性/方法 描述

| length | 字符串中的字符数:

    > new String('four').length;   
    4   

|

| charAt(position) | 返回指定位置的字符。位置从0开始:

    > "script".charAt(0);   
    "s"   

自 ES5 以来,也可以使用数组表示法来实现相同的目的。(在 ES5 之前,许多浏览器长期支持此功能,但 IE 不支持):

    > "script"[0];   
    "s"   

|

| charCodeAt(position) | 返回指定位置的字符的数字代码(Unicode):

    > "script".charCodeAt(0);   
    115   

|

| concat(str1, str2, ....) | 返回从输入片段粘合的新字符串:

    > "".concat('zig', '-',   'zag');   
    "zig-zag"   

|

| indexOf(needle, start) | 如果 needle 与字符串的一部分匹配,则返回匹配的位置。可选的第二个参数定义搜索应从何处开始。如果找不到匹配项,则返回-1

    > "javascript".indexOf('scr');   
    4   
    > "javascript".indexOf('scr',   5);   
    -1   

|

| lastIndexOf(needle, start) | 与indexOf()相同,但从字符串的末尾开始搜索。最后一次出现的a

    > "javascript".lastIndexOf('a');   
    3   

|

| localeCompare(needle) | 比较当前区域设置中的两个字符串。如果两个字符串相等,则返回0,如果 needle 在字符串对象之前排序,则返回1,否则返回-1

    > "script".localeCompare('crypt');   
    1   
    > "script".localeCompare('sscript');   
    -1   
    > "script".localeCompare('script');   
    0   

|

| match(regexp) | 接受一个正则表达式对象并返回一个匹配的数组:

    > "R2-D2 and C-3PO".match(/[0-9]/g);   
    ["2", "2", "3"]   

|

| replace(needle, replacement) | 允许您替换正则表达式模式的匹配结果。替换也可以是一个回调函数。捕获组可以作为$1, $2,...$9使用:

    > "R2-D2".replace(/2/g, '-two');   
    "R-two-D-two"   
    > "R2-D2".replace(/(2)/g,'$1$1');   
    "R22-D22"   

|

| search(regexp) | 返回第一个正则表达式匹配的位置:

    > "C-3PO".search(/[0-9]/);   
    2   

|

| slice(start, end) | 返回由开始和结束位置标识的字符串的部分。如果start为负数,则开始位置为length + start,类似地,如果end参数为负数,则结束位置为 length + end:

    > "R2-D2 and C-3PO".slice(4,   13);   
    "2 and C-3"   
    > "R2-D2 and C-3PO".slice(4,   -1);   
    "2 and C-3P"   

|

| split(separator, limit) | 将字符串转换为数组。第二个参数 limit 是可选的。与replace()search()match()一样,分隔符是一个正则表达式,但也可以是一个字符串。

    > "1,2,3,4".split(/,/);   
    ["1", "2", "3",   "4"]   
    > "1,2,3,4".split(',',   2);   
    ["1", "2"]   

|

| substring(start, end) | 类似于slice()。当 start 或 end 为负数或无效时,它们被视为 0。如果它们大于字符串长度,则被视为长度。如果end大于start,它们的值将被交换。

    > "R2-D2 and C-3PO".substring(4, 13);   
    "2 and C-3"   
    > "R2-D2 and C-3PO".substring(13, 4);   
    "2 and C-3"   

|

| toLowerCase()``toLocaleLowerCase() | 将字符串转换为小写:

    > "Java".toLowerCase();   
    "java"   

|

| toUpperCase()``toLocaleUpperCase() | 将字符串转换为大写:

    > "Script".toUpperCase();   
    "SCRIPT"   

|

ECMAScript 5 对 String 的新增内容

以下是 ECMAScript 5 对 String 的新增内容:

属性/方法 描述

String.prototype.trim() | 在 ES3 中使用正则表达式来去除字符串前后的空格,而在 ES5 中有一个trim()方法。

    > " \t beard \n".trim();   
    "beard"   
    Or in ES3:   
    > " \t beard \n".replace(/\s/g, "");   
    "beard"   

|

ECMAScript 6 对 String 的新增内容

以下是所有 ECMAScript 6 对 String 的新增内容:

模板文字用于插入单行或多行字符串。模板文字用反引号( )(重音符号)字符括起来,而不是双引号或单引号。模板文字可以包含占位符。这些由美元符号和大括号(${expression})表示。占位符中的表达式和它们之间的文本被传递给一个函数。默认函数只是将部分连接成一个字符串。
    var a = 5; 
    var b = 10;   
    console.log(`Fifteen
    is ${a + b}`);   

|

String.prototype.repeat - 此方法允许您重复一个字符串 n 次
    " ".repeat(4 * 
    depth)
    "foo".repeat(3)   

|

String.prototype.startsWith``String.prototype.endsWith``String.prototype.includes这些是新的字符串搜索方法
    "hello".startsWith(
    "ello", 1) // true
    "hello".endsWith(
    "hell",4) // true
"hello".includes(
  "ell")
  // true 
"hello".includes(
  "ell", 1) // true 
"hello".includes(
 "ell", 2) // false   

|

日期

Date构造函数可以与多种类型的输入一起使用:

  • 可以传递年、月、日期、小时、分钟、秒和毫秒的值,如下所示:
        > new Date(2015, 0, 1, 13, 30, 35, 505); 
        Thu Jan 01 2015 13:30:35 GMT-0800 (PST) 

  • 您可以跳过任何输入参数,此时它们被假定为 0。请注意,月份值从 0(一月)到 11(十二月),小时从 0 到 23,分钟和秒从 0 到 59,毫秒从 0 到 999。

  • 您可以传递一个时间戳:

        > new Date(1420147835505); 
        Thu Jan 01 2015 13:30:35 GMT-0800 (PST) 

  • 如果您什么都不传,将假定为当前日期/时间:
        > new Date(); 
        Fri Jan 11 2013 12:20:45 GMT-0800 (PST) 

  • 如果传递一个字符串,它将尝试解析以提取可能的日期值:
        > new Date('May 4, 2015'); 
        Mon May 04 2015 00:00:00 GMT-0700 (PDT) 

省略new会给您当前日期的字符串版本:

    > Date() === new Date().toString(); 
    true 

Date 构造函数的成员

以下是 Date 构造函数的成员:

属性/方法 描述

| Date.parse(string) | 类似于将字符串传递给新的Date()构造函数,此方法解析输入字符串以尝试提取有效的日期值。成功时返回时间戳,失败时返回NaN

    > Date.parse('May 5, 2015');   
    1430809200000   
    > Date.parse('4th');   
    NaN   

|

| Date.UTC(year, month, date, hours, minutes, seconds, ms) | 返回一个时间戳,但是在协调世界时(UTC)中,而不是在本地时间中。

    > Date.UTC 
    (2015, 0, 1, 13, 30, 35, 505);
    1420119035505   

|

The Date.prototype members

以下是Date.prototype成员的列表:

属性/方法 描述/示例

| toUTCString() | 与toString()相同,但是使用世界协调时间。太平洋标准时间(PST)本地时间与 UTC 的区别如下:

    > var d = new Date(2015, 0, 1);   
    > d.toString();   
    "Thu Jan 01 2015 00:00:00 GMT-0800 (PST)"   
    > d.toUTCString();   
    "Thu, 01 Jan 2015 08:00:00   GMT"   

|

| toDateString() | 仅返回toString()的日期部分:

    > new Date(2015, 0,   1).toDateString();   
    "Thu Jan 01 2010"   

|

| toTimeString() | 仅返回toString()的时间部分:

    > new Date(2015, 0,   1).toTimeString();   
    "00:00:00 GMT-0800 (PST)"   

|

| toLocaleString()``toLocaleDateString()``toLocaleTimeString() | 等同于toString()toDateString()toTimeString(),但是以更友好的格式显示,根据当前用户的区域设置:

    > new Date(2015, 0,   1).toString();   
    "Thu Jan 01 2015 00:00:00 GMT-0800   (PST)"   
    > new Date(2015, 0,   1).toLocaleString();   
    "1/1/2015 12:00:00 AM"   

|

| getTime()``setTime(time) | 获取或设置日期对象的时间(使用时间戳)。以下示例创建一个日期并将其向前移动一天:

    > var d = new Date(2015, 0, 1);   
    > d.getTime();   
    1420099200000   
    > d.setTime(d.getTime() +
     1000 * 60 * 60 *   24);   
    1420185600000   
    > d.toLocaleString();   
    "Fri Jan 02 2015 00:00:00  
      GMT-0800 (PST)"   

|

| getFullYear()``getUTCFullYear()``setFullYear(year, month, date)``setUTCFullYear(year, month, date) | 使用本地或 UTC 时间获取或设置完整年份。还有getYear(),但它不符合 Y2K 标准,因此请改用getFullYear()

    > var d = new Date(2015, 0, 1);   
    > d.getYear();   
    115   
    > d.getFullYear();   
    2015   
    > d.setFullYear(2020);   
    1577865600000   
    > d;   
    Wed Jan 01 2020 00:00:00 GMT-0800 
      (PST)   

|

| getMonth()``getUTCMonth()``setMonth(month, date)``setUTCMonth(month, date) | 获取或设置月份,从 0 开始(一月):

    > var d = new Date(2015, 0, 1);   
    > d.getMonth();   
    0   
    > d.setMonth(11);   
    1448956800000   
    > d.toLocaleDateString();   
    "12/1/2015"   

|

| getDate()``getUTCDate()``setDate(date)``setUTCDate(date) | 获取或设置月份的日期。

    > var d = new Date(2015, 0, 1);   
    > d.toLocaleDateString();   
    "1/1/2015"   
    > d.getDate();   
    1   
    > d.setDate(31);   
    1422691200000   
    > d.toLocaleDateString();   
    "1/31/2015"   

|

| getHours()``getUTCHours()``setHours(hour, min, sec, ms)``setUTCHours(hour, min, sec, ms)``getMinutes()``getUTCMinutes()``setMinutes(min, sec, ms)``setUTCMinutes(min, sec, ms)``getSeconds()``getUTCSeconds()``setSeconds(sec, ms)``setUTCSeconds(sec, ms)``getMilliseconds()``getUTCMilliseconds()``setMilliseconds(ms)``setUTCMilliseconds(ms) | 获取/设置小时,分钟,秒,毫秒,全部从0开始:

    > var d = new Date(2015, 0, 1);   
    > d.getHours() + ':' + d.getMinutes();   
    "0:0"   
    > d.setMinutes(59);   
    1420102740000   
    > d.getHours() + ':' + d.getMinutes();   
    "0:59"   

|

| getTimezoneOffset() | 返回本地时间和世界标准时间(UTC)之间的差异,以分钟为单位。例如,太平洋标准时间(PST)和 UTC 之间的差异:

    > new   Date().getTimezoneOffset();   
    480   
    > 420 / 60; // hours   
    8   

|

| getDay()``getUTCDay() | 返回一周的第几天,从 0 开始(星期日):

    > var d = new Date(2015, 0, 1);      
    > d.toDateString();   
    "Thu Jan 01 2015"   
    > d.getDay();   
    4   
    > var d = new Date(2015, 0, 4);      
    > d.toDateString();   
    "Sat Jan 04 2015"   
    > d.getDay();   
    0   

|

ECMAScript 5 对 Date 的补充

以下是对Date构造函数的补充:

属性/方法 描述

| Date.now() | 获取当前时间戳的便捷方式:

    > Date.now() === new   Date().getTime();   
    true   

|

| Date.prototype.toISOString() | 另一个toString()

    > var d = new Date(2015, 0, 1);   
    > d.toString();   
    "Thu Jan 01 2015 00:00:00 GMT-0800
     (PST)" 
    > d.toUTCString();   
    "Thu, 01 Jan 2015 08:00:00   GMT"   
    > d.toISOString();   
    "2015-01-01T00:00:00.000Z"   

|

| Date.prototype.toJSON() | 被JSON.stringify()使用(参见本附录的末尾),并返回与toISOString()相同的值:

    > var d = new Date();   
    > d.toJSON() === d.toISOString();   
    true   

|

Math

Math与其他内置对象不同,因为它不能用作构造函数来创建对象。它只是一组静态函数和常量。以下是一些示例,以说明区别:

    > typeof Date.prototype; 
    "object" 
    > typeof Math.prototype; 
    "undefined" 
    > typeof String; 
    "function" 
    > typeof Math; 
    "object" 

Math 对象的成员

以下是Math对象的成员:

属性/方法 描述

| Math.E``Math.LN10``Math.LN2``Math.LOG2E``Math.LOG10E``Math.PI``Math.SQRT1_2``Math.SQRT2 | 这些是一些有用的数学常数,都是只读的。以下是它们的值:

    > Math.E;   
    2.718281828459045   
    > Math.LN10;   
    2.302585092994046   
    > Math.LN2;   
    0.6931471805599453   
    > Math.LOG2E;   
    1.4426950408889634   
    > Math.LOG10E;   
    0.4342944819032518   
    > Math.PI;   
    3.141592653589793   
    > Math.SQRT1_2;   
    0.7071067811865476   
    > Math.SQRT2;   
    1.4142135623730951   

|

Math.acos(x)``Math.asin(x)``Math.atan(x)``Math.atan2(y, x)``Math.cos(x)``Math.sin(x)``Math.tan(x) 三角函数

| Math.round(x)``Math.floor(x)``Math.ceil(x) | round()返回最接近的整数,ceil()向上舍入,floor()向下舍入:

    > Math.round(5.5);   
    6   
    > Math.floor(5.5);   
    5   
    > Math.ceil(5.1);   
    6   

|

| Math.max(num1, num2, num3, ...)``Math.min(num1, num2, num3, ...) | max()返回传递给它们作为参数的数字中的最大值,min()返回传递给它们作为参数的数字中的最小值。如果至少有一个输入参数是NaN,结果也是NaN

    > Math.max(4.5, 101, Math.PI);   
    101   
    > Math.min(4.5, 101, Math.PI);   
    3.141592653589793   

|

| Math.abs(x) | 绝对值:

    > Math.abs(-101);   
    101   
    > Math.abs(101);   
    101   

|

| Math.exp(x) | 指数函数:Math.Ex次方:

    > Math.exp(1) === Math.E;   
    true   

|

| Math.log(x) | x的自然对数:

    > Math.log(10) === Math.LN10;   
 **true**

|

| Math.sqrt(x) | x的平方根:

    > Math.sqrt(9);   
    3   
    > Math.sqrt(2) === Math.SQRT2;   
    true   

|

Math.pow(x, y) | xy次方:

    > Math.pow(3, 2);   
    9   

|

| Math.random() | 0 到 1 之间的随机数(包括 0)。

    > Math.random();   
    0.8279076443185321   
    For an random integer in a range,
     say between 10 and 100:   
    > Math.round(Math.random() * 90   + 10);   
    79   

|

RegExp

您可以使用RegExp()构造函数创建正则表达式对象。将表达式模式作为第一个参数传递,将模式修饰符作为第二个参数传递:

    > var re = new RegExp('[dn]o+dle', 'gmi'); 

这匹配"noodle","doodle","doooodle"等。这相当于使用正则表达式字面量:

    > var re = ('/[dn]o+dle/gmi'); // recommended 

第四章,对象和附录 D,正则表达式包含有关正则表达式和模式的更多信息。

RegExp.prototype 的成员

以下是RegExp.prototype的成员:

属性/方法 描述
global 如果在创建regexp对象时设置了g修饰符,则为只读的true
ignoreCase 只读。如果在创建regexp对象时设置了i修饰符,则为true
multiline 只读。如果在创建regexp对象时设置了m修饰符,则为true

| lastIndex | 包含下一个匹配应该开始的字符串中的位置。test()exec()在成功匹配后设置这个位置。只有在使用了g(全局)修饰符时才相关:

    > var re = /[dn]o+dle/g;   
    > re.lastIndex;   
    0   
    > re.exec("noodle doodle");   
    ["noodle"]   
    > re.lastIndex;   
    6   
    > re.exec("noodle doodle");   
    ["doodle"]   
    > re.lastIndex;   
    13   
    > re.exec("noodle doodle");   
    null   
    > re.lastIndex;   
    0   

|

| source | 只读。返回正则表达式模式(不包括修饰符):

    > var re = /[nd]o+dle/gmi;   
    > re.source;   
    "[nd]o+dle"   

|

| exec(string) | 用正则表达式匹配输入字符串。成功匹配时返回一个包含匹配和任何捕获组的数组。使用g修饰符时,它匹配第一个出现并设置lastIndex属性。没有匹配时返回null

    > var re = /([dn])(o+)dle/g;   
    > re.exec("noodle doodle");   
    ["noodle", "n",   "oo"]   
    > re.exec("noodle doodle");   
    ["doodle", "d",   "oo"]   

exec()返回的数组有两个额外的属性:index(匹配的位置)和 input(被搜索的输入字符串)。

| test(string) | 与exec()相同,但只返回truefalse

    > /noo/.test('Noodle');   
    false   
    > /noo/i.test('Noodle');   
    true   

|

错误对象

错误对象是由环境(浏览器)或您的代码创建的:

    > var e = new Error('jaavcsritp is _not_ how you spell it'); 
    > typeof e; 
    "object" 

除了Error构造函数,还存在六个额外的构造函数,它们都继承自Error

  • EvalError

  • RangeError

  • ReferenceError

  • SyntaxError

  • TypeError

  • URIError

错误原型成员

以下是Error.prototype的成员:

属性 描述

name | 用于创建对象的错误构造函数的名称:

    > var e = new EvalError('Oops');   
    > e.name;   
    "EvalError"   

|

| message | 附加的错误信息:

    > var e = new Error('Oops...   again');   
    > e.message;   
    "Oops... again"   

|

JSON

JSON 对象是 ES5 中的新内容。它不是一个构造函数(类似于Math),只有两个方法:parse()stringify()。对于不原生支持 JSON 的 ES3 浏览器,可以使用来自json.org的“shim”。

JSON代表JavaScript 对象表示法。它是一种轻量级的数据交换格式。它是 JavaScript 的一个子集,只支持原始类型、对象字面量和数组字面量。

JSON 对象的成员

以下是JSON对象的成员:

方法 描述

parse(text, callback) | 接受一个 JSON 编码的字符串并返回一个对象:

    > var data = '{"hello":   1, "hi": [1, 2, 3]}';   
    > var o = JSON.parse(data);   
    > o.hello;   
    1   
    > o.hi;   
    [1, 2, 3]   

可选的回调让你提供自己的函数,可以检查和修改结果。回调接受keyvalue参数,可以修改value或删除它(通过返回undefined)。

    > function callback(key, value)   {   
        console.log(key, value);   
        if (key === 'hello') {   
          return 'bonjour';   
        }   
        if (key === 'hi') {   
          return undefined;   
        }   
        return value;   
      }   

    > var o = JSON.parse(data, callback);   
    hello 1   
    0 1   
    1 2   
    2 3   
    hi [1, 2, 3]   
    Object {hello: "bonjour"}   
    > o.hello;   
    "bonjour"   
    > 'hi' in o;   
    false   

|

| stringify(value, callback, white) | 接受任何值(通常是对象或数组)并将其编码为 JSON 字符串。

    > var o = {   
    hello: 1,    
    hi: 2,    
    when: new Date(2015, 0, 1)   
   };   

    > JSON.stringify(o);   
   "{"hello":1,"hi":2,"when":
    "2015-01-01T08:00:00.000Z"}"   

第二个参数让你提供一个回调(或一个白名单数组)来自定义返回值。白名单包含你感兴趣的键:

    JSON.stringify(o, ['hello', 'hi']);   
    "{"hello":1,"hi":2}"   

最后一个参数帮助你获得可读的版本。你可以将空格的数量指定为字符串或数字:

    > JSON.stringify(o, null, 4);   
    "{   
    "hello": 1,   
    "hi": 2,   
    "when": "2015-01-01T08:00:00.000Z"   
    }"   

|

附录 D. 正则表达式

当您使用正则表达式(在第四章中讨论,对象)时,您可以匹配文字字符串,例如:

    > "some text".match(/me/); 
    ["me"] 

然而,正则表达式的真正威力来自于匹配模式,而不是文字字符串。以下表格描述了您可以在模式中使用的不同语法,并提供了一些使用示例:

模式 描述

| [abc] | 匹配一类字符:

    > "some text".match(/[otx]/g);   
    ["o", "t", "x",   "t"]   

|

| [a-z] | 作为范围定义的一类字符。例如,[a-d]与[abcd]相同,[a-z]匹配所有小写字符,[a-zA-Z0-9_]匹配所有字符,数字和下划线字符:

    > "Some Text".match(/[a-z]/g);   
    ["o", "m", "e",   "e", "x", "t"]   
    > "Some Text".match(/[a-zA-Z]/g);   
    ["S", "o", "m",   "e", "T", "e", "x", "t"]   

|

| [^abc] | 匹配不属于字符类的所有内容:

    > "Some Text".match(/[^a-z]/g);   
    ["S", " ", "T"]   

|

| a&#124;b | 匹配 a 或 b。管道字符表示或,可以使用多次:

    > "Some Text".match(/t&#124;T/g);
    ["T", "t"]
    > "Some Text".match(/t&#124;T&#124;Some/g);
    ["Some", "T",   "t"]

|

| a(?=b) | 只有在后面跟着 b 时才匹配 a:

    > "Some Text".match(/Some(?=Tex)/g);   
    null   
    > "Some Text".match(/Some(?=Tex)/g);   
    ["Some"]   

|

| a(?!b) | 只有在后面不跟着 b 时才匹配 a:

    > "Some Text".match(/Some(?!Tex)/g);   
    null   
    > "Some Text".match(/Some(?!Tex)/g);   
    ["Some"]   

|

| \ | 转义字符,用于帮助您匹配模式中用作文字的特殊字符:

    > "R2-D2".match(/[2-3]/g);   
    ["2", "2"]   
    > "R2-D2".match(/[2\-3]/g);   
    ["2", "-", "2"]   

|

\n``\r``\f``\t``\v 换行符回车换行符制表符垂直制表符

| \s | 空格,或前面五个转义序列中的任何一个:

    > "R2\n D2".match(/\s/g);   
    ["\n", " "]   

|

| \S | 与上面相反;匹配除空格之外的所有内容。与[^\s]相同:

    > "R2\n D2".match(/\S/g);   
    ["R", "2", "D",   "2"]   

|

| \w | 任何字母,数字或下划线。与[A-Za-z0-9_]相同:

    > "S0m3 text!".match(/\w/g);   
    ["S", "0", "m",   "3", "t", "e", "x", "t"]   

|

| \W | \w的相反:

    > "S0m3 text!".match(/\W/g);   
    [" ", "!"]   

|

| \d | 匹配数字,与[0-9]相同:

    > "R2-D2 and C-3PO".match(/\d/g);   
    ["2", "2", "3"]   

|

| \D | \d的相反;匹配非数字,与[⁰-9]或[^\d]相同:

    > "R2-D2 and C-3PO".match(/\D/g);   
    ["R", "-", "D",   " ", "a", "n", "d",
      " ", "C",   "-", "P", "O"]   

|

| \b | 匹配词边界,如空格或标点符号。匹配 R 或 D 后面跟着 2:

    > "R2D2 and C-3PO".match(/[RD]2/g);   
    ["R2", "D2"]   

与上面相同,但只在单词的末尾:

    > "R2D2 and C-3PO".match(/[RD]2\b/g);   
    ["D2"]   

相同的模式,但输入中有一个破折号,这也是一个单词的结尾:

    > "R2-D2 and C-3PO".match(/[RD]2\b/g);   
    ["R2", "D2"]   

|

| \B | \b的相反:

    > "R2-D2 and C-3PO".match(/[RD]2\B/g);   
    null   
    > "R2D2 and C-3PO".match(/[RD]2\B/g);   
    ["R2"]   

|

[\b] 匹配退格字符。
\0 空字符。

| \u0000 | 匹配 Unicode 字符,由四位十六进制数字表示:

    > "стоян".match(/\u0441\u0442\u043E/);   
    ["сто"]   

|

| \x00 | 匹配由两位十六进制数字表示的字符代码:

    > "\x64";   
     "d"   
    > "dude".match(/\x64/g);   
    ["d", "d"]   

|

| ^ | 要匹配的字符串的开头。如果设置了m修饰符(多行),则匹配每行的开头:

   > "regular\nregular\nexpression".match(/r/g);   
    ["r", "r", "r",   "r", "r"]   
    > "regular\nregular\nexpression".match(/^r/g);   
    ["r"]   
   > "regular\nregular\nexpression".match(/^r/mg);   
    ["r", "r"]   

|

| $ | 匹配输入的结尾,或者在使用多行修饰符时,匹配每行的结尾:

    > "regular\nregular\nexpression".match(/r$/g);   
    null   
    > "regular\nregular\nexpression".match(/r$/mg);   
    ["r", "r"]   

|

| . | 匹配除换行符和换行符之外的任何单个字符:

    > "regular".match(/r./g);   
    ["re"]   
    > "regular".match(/r.../g);   
    ["regu"]   

|

| * | 如果出现零次或多次,则匹配前面的模式。例如,/.*/将匹配任何内容,包括空(空输入):

    > "".match(/.*/);   
    [""]   
    > "anything".match(/.*/);   
    ["anything"]   
    > "anything".match(/n.*h/);   
    ["nyth"]   

请记住,模式是“贪婪”的,这意味着它会尽可能多地匹配:

    > "anything within".match(/n.*h/g);   
    ["nything with"]   

|

| ? | 如果出现零次或一次,则匹配前面的模式:

    > "anything".match(/ny?/g);   
    ["ny", "n"]   

|

| + | 如果至少出现一次(或更多次),则匹配前面的模式:

    > "anything".match(/ny+/g);   
    ["ny"]   
    > "R2-D2 and C-3PO".match(/[a-z]/gi);   
    ["R", "D", "a",   "n", "d", "C", "P", "O"]   
    > "R2-D2 and C-3PO".match(/[a-z]+/gi);   
    ["R", "D", "and",   "C", "PO"]   

|

| {n} | 如果出现 n 次,则匹配前面的模式:

    > "regular expression".match(/s/g);   
    ["s", "s"]   
    > "regular expression".match(/s{2}/g);   
    ["ss"]   
    > "regular expression".match(/\b\w{3}/g);   
    ["reg", "exp"]   

|

| {min,max} | 如果出现在 min 和 max 次之间,则匹配前面的模式。您可以省略 max,这将意味着没有最大值,但只有最小值。您不能省略 min。输入为“doodle”,其中“o”重复了 10 次的示例:

    > "doooooooooodle".match(/o/g);   
    ["o", "o", "o",   "o", "o", 
    "o", "o", "o", "o",   "o"]   
    > "doooooooooodle".match(/o/g).length;   
    10   
    > "doooooooooodle".match(/o{2}/g);   
    ["oo", "oo", "oo",   "oo", "oo"]   
    > "doooooooooodle".match(/o{2,}/g);   
    ["oooooooooo"]   
    > "doooooooooodle".match(/o{2,6}/g);   
    ["oooooo", "oooo"]   

|

| (pattern) | 当模式在括号中时,它会被记住,以便可以用于替换。这些也被称为捕获模式。捕获的匹配可用作$1,$2,... $9 匹配所有“r”出现并重复它们:

    > "regular expression".replace(/(r)/g, '$1$1');   
    "rregularr exprression"   

匹配“re”并将其变为“er”:

    > "regular expression".replace(/(r)(e)/g, '$2$1');   
    "ergular experssion"   

|

| (?:pattern) | 非捕获模式,不会被记住,也不会在$1,$2...中可用。以下是一个示例,说明如何匹配“re”,但不会记住“r”,第二个模式变为$1:

   > "regular expression".replace(/(?:r)(e)/g, '$1$1');   
   "eegular expeession"   

|

当特殊字符有两种含义时,请确保您注意,就像^?\b一样。

附录 E. 练习问题的答案

本附录列出了章节末尾练习的可能答案。可能的答案意味着它们不是唯一的答案,所以如果您的解决方案不同,不要担心。

与本书的其余部分一样,您应该在控制台中尝试并玩一下。

第一章和最后一章没有练习部分,所以让我们从第二章开始,原始数据类型、数组、循环和条件

第二章,原始数据类型、数组、循环和条件

让我们尝试解决以下练习:

练习

  1. 结果将如下:
        > var a; typeof a; 
        "undefined" 

当您声明一个变量但不用值初始化它时,它会自动获得未定义的值。您还可以检查:

        > a === undefined; 
        true 

v的值将是:

        > var s = '1s'; s++; 
        NaN 

将数字1添加到字符串'1s'中,返回字符串'1s1',这是不是一个数字,但++运算符应该返回一个数字;所以它返回特殊的NaN数字。

程序如下:

        > !!"false"; 
        true 

问题的棘手部分在于"false"是一个字符串,所有字符串在转换为布尔值时都是true(除了空字符串"")。如果问题不是关于字符串"false"而是布尔值false,则双重否定!!将返回相同的布尔值:

        > !!false; 
        false 

正如您所期望的,单个否定返回相反的值:

        > !false; 
        true 
        > !true; 
        false 

您可以测试任何字符串,它都会转换为布尔值true,除了空字符串:

        > !!"hello"; 
        true 
        > !!"0"; 
        true 
        > !!""; 
        false 

执行undefined后的输出如下:

        > !!undefined; 
        false 

这里undefined是假值之一,它转换为false。您可以尝试任何其他假值,例如前面示例中的空字符串""NaN0

        > typeof -Infinity; 
        "number" 

数字类型包括所有数字、NaN、正数和负数Infinity

执行以下操作后的输出是:

        > 10 % "0"; 
        NaN 

字符串"0"被转换为数字0。除以0得到Infinity,没有余数。

执行以下操作后的输出是:

        > undefined == null; 
        true 

==运算符的比较不检查类型,但转换操作数;在这种情况下,两者都是假值。严格比较也检查类型:

        > undefined === null; 
        false 

以下是代码行及其输出:

        > false === ""; 
        false 

不同类型之间的严格比较(在本例中是布尔值和字符串)注定会失败,无论值是什么。

以下是代码行及其输出:

        > typeof "2E+2"; 
        "string" 

引号中的任何内容都是字符串,尽管:

        > 2E+2; 
        200 
        > typeof 2E+2; 
        "number" 

以下是代码行及其输出:

        > a = 3e+3; a++; 
        3000 

3e+33加上三个零,意思是3000。然后++是后增量,意思是它返回旧值,然后增加它并将其分配给a。这就是为什么您在控制台中得到返回值3000,尽管a现在是3001

        > a; 
        3001 

  1. 执行以下操作后的v的值是:
        > var v = v || 10; 
        > v; 
        10 

如果v从未被声明过,则为undefined,因此这与以下内容相同:

        > var v = undefined || 10; 
        > v; 
        10 

但是,如果v已经被定义并初始化为一个非假值,您将获得先前的值。

        > var v = 100; 
        > var v = v || 10; 
        > v; 
        100 

第二次使用var不会“重置”变量。

如果v已经是一个假值(不是100),则检查v || 10将返回10

        > var v = 0; 
        > var v = v || 10; 
        > v; 
        10 

  1. 要打印乘法表,请执行以下操作:
        for (var i = 1; i <= 12; i++) { 
          for (var j = 1; j <= 12; j++) { 
            console.log(i + ' * ' + j + ' = ' + i * j); 
          } 
        } 

或:

        var i = 1, j = 1; 
        while (i <= 12) { 
          while (j <= 12) { 
            console.log(i + ' * ' + j + ' = ' + i * j); 
            j++; 
          } 
          i++; 
          j = 1; 
        } 

第三章,函数

让我们做以下练习:

练习

  1. 要将十六进制颜色转换为 RGB,请执行以下操作:
        function getRGB(hex) { 
          return "rgb(" + 
            parseInt(hex[1] + hex[2], 16) + ", " + 
            parseInt(hex[3] + hex[4], 16) + ", " + 
            parseInt(hex[5] + hex[6], 16) + ")"; 
        } 
        Testing: 
        > getRGB("#00ff00"); 
               "rgb(0, 255, 0)" 
        > getRGB("#badfad"); 
               "rgb(186, 223, 173)" 

这种解决方案的一个问题是,像hex[0]这样的字符串数组访问不在 ECMAScript 3 中,尽管许多浏览器长期支持它,现在在 ES5 中也有描述。

但是,在本书的这一部分,尚未讨论对象和方法。否则,符合 ES3 的解决方案将是使用字符串方法之一,例如charAt()substring()slice()。您还可以使用数组来避免太多的字符串连接:

    function getRGB2(hex) { 
      var result = []; 
      result.push(parseInt(hex.slice(1, 3), 16)); 
      result.push(parseInt(hex.slice(3, 5), 16)); 
      result.push(parseInt(hex.slice(5), 16)); 
      return "rgb(" + result.join(", ") + ")"; 
    } 

奖励练习:重写前面的函数,使用循环,这样您就不必三次输入parseInt(),而只需一次。

  1. 结果如下:
        > parseInt(1e1); 
        10 
        Here, you're parsing something that is already an integer: 
        > parseInt(10); 
        10 
        > 1e1; 
        10 

在这里,字符串的解析放弃了第一个非整数值。parseInt()不理解指数文字,它期望整数表示法:

        > parseInt('1e1'); 
        1 

这是解析字符串'1e1',同时期望它是十进制表示法,包括指数:

        > parseFloat('1e1'); 
        10 

以下是代码行及其输出:

        > isFinite(0 / 10); 
        true 

因为0/100,而0是有限的。

以下是代码行及其输出:

        > isFinite(20 / 0); 
        false 

因为除以0Infinity

        > 20 / 0; 
        Infinity 

以下是代码行及其输出:

        > isNaN(parseInt(NaN)); 
        true 

解析特殊的NaN值是NaN

  1. 以下是结果:
        var a = 1; 
        function f() { 
          function n() { 
            alert(a); 
          } 
          var a = 2; 
          n(); 
        } 
        f(); 

这段代码警报2,即使n()在赋值a = 2之前被定义。在函数n()内部,你看到的是在相同作用域中的变量a,并且在调用f()(因此n())时访问它的最新值。由于变量提升,f()的行为就像是:

        function f() { 
          var a; 
          function n() { 
            alert(a); 
          } 
          a = 2; 
          n(); 
        } 

更有趣的是,考虑这段代码:

        var a = 1; 
        function f() { 
          function n() { 
            alert(a); 
          } 
          n(); 
          var a = 2; 
          n(); 
        } 
        f(); 

它警报undefined,然后是2。你可能期望第一个警报显示1,但由于变量提升,a的声明(而不是初始化)被移动到函数的顶部。就像f()是:

        var a = 1; 
        function f() { 
          var a; // a is now undefined 
          function n() { 
            alert(a); 
          } 
          n(); // alert undefined 
          a = 2; 
          n(); // alert 2 
        } 
        f(); 

本地的a“遮蔽”了全局的a,即使它在底部。

  1. 为什么所有这些警报都是“Boo!”

以下是示例 1 的结果:

        var f = alert; 
        eval('f("Boo!")'); 

以下是示例 2 的结果。你可以将一个函数分配给另一个变量。所以f()指向alert()。评估这个字符串就像这样:

        > f("Boo"); 

在我们执行eval()之后,以下是输出:

        var e; 
        var f = alert; 
        eval('e=f')('Boo!'); 

以下是示例 3 的输出。eval()返回评估的结果。在这种情况下,它是一个赋值e = f,也返回e的新值。就像下面这样:

        > var a = 1; 
        > var b; 
        > var c = (b = a); 
        > c; 
        1 

所以eval('e=f')给你一个指向alert()的指针,它立即执行带有"Boo!"alert()

立即(自调用)匿名函数返回对函数alert()的指针,然后立即用参数"Boo!"调用它:

        (function(){ 
          return alert; 
        })()('Boo!'); 

第四章,对象

让我们解决以下练习:

练习

  1. 这里发生了什么?this是什么,o是什么?
        function F() { 
          function C() { 
            return this; 
          } 
          return C(); 
        } 
        var o = new F(); 

在这里,this === window,因为C()是在没有new的情况下调用的。

还有o === window,因为new F()返回C()返回的对象,即this,而thiswindow

你可以将对C()的调用变成构造函数调用:

        function F() { 
          function C() { 
            return this; 
          } 
          return new C(); 
        } 
        var o = new F(); 

在这里,this是由C()构造函数创建的对象。o也是:

        > o.constructor.name; 
        "C" 

在 ES5 的严格模式下会更有趣。在严格模式下,非构造函数调用会导致thisundefined,而不是全局对象。在F()C()构造函数的主体内部使用"use strict"thisC()中将是undefined。因此,return C()不能返回非对象的undefined(因为所有构造函数调用都返回某种对象),并返回F实例的this(在闭包范围内)。试试看:

        function F() { 
          "use strict"; 
          this.name = "I am F()"; 
          function C() { 
            console.log(this); // undefined 
            return this; 
          } 
          return C(); 
        } 

测试:

        > var o = new F(); 
        > o.name; 
        "I am F()" 

  1. 使用new调用这个构造函数会发生什么?
        function C() { 
          this.a = 1; 
          return false; 
        } 
        And testing: 
        > typeof new C(); 
        "object" 
        > new C().a; 
         1 

new C()是一个对象,不是布尔值,因为构造函数调用总是产生一个对象。这是你得到的this对象,除非你在构造函数中返回其他对象。返回非对象是行不通的,你仍然得到this

  1. 这是做什么?
        > var c = [1, 2, [1, 2]]; 
        > c.sort(); 
        > c; 
         [1, Array[2], 2] 

这是因为sort()比较字符串。[1, 2].toString()"1,2",所以它在"1"之后和"2"之前。

使用join()也是一样的:

        > c.join('--'); 
        > c; 
        "1--1,2--2" 

  1. 假设String()不存在,并创建模仿String()MyString()。将输入的原始字符串视为数组(ES5 中正式支持数组访问)。

这是一个只有练习要求的方法的示例实现。随意继续使用其他方法。参考附录 C,内置对象,获取完整列表。

        function MyString(input) { 
          var index = 0; 

          // cast to string 
          this._value = '' + input; 

          // set all numeric properties for array access 
          while (input[index] !== undefined) { 
            this[index] = input[index]; 
            index++; 
          } 

          // remember the length 
          this.length = index; 
        } 

        MyString.prototype = { 
          constructor: MyString, 
          valueOf: function valueOf() { 
            return this._value; 
          }, 
          toString: function toString() { 
            return this.valueOf(); 
          }, 
          charAt: function charAt(index) { 
            return this[parseInt(index, 10) || 0]; 
          }, 
          concat: function concat() { 
            var prim = this.valueOf(); 
            for (var i = 0, len = arguments.length; i < len; i++) { 
              prim += arguments[i]; 
            } 
            return prim; 
          }, 
          slice: function slice(from, to) { 
            var result = '', 
                original = this.valueOf(); 
            if (from === undefined) { 
              return original; 
            } 
            if (from > this.length) { 
              return result; 
            } 
            if (from < 0) { 
              from = this.length - from; 
            } 
            if (to === undefined || to > this.length) { 
              to = this.length; 
            } 
            if (to < 0) { 
              to = this.length + to; 
            } 
            // end of validation, actual slicing loop now 
            for (var i = from; i < to; i++) { 
              result += original[i]; 
            } 
            return result; 
          }, 
          split: function split(re) { 
            var index = 0, 
               result = [], 
                original = this.valueOf(), 
                match, 
                pattern = '', 
                modifiers = 'g'; 

            if (re instanceof RegExp) { 
              // split with regexp but always set "g" 
              pattern = re.source; 
              modifiers += re.multiline  ? 'm' : ''; 
              modifiers += re.ignoreCase ? 'i' : ''; 
            } else { 
              // not a regexp, probably a string, we'll convert it 
              pattern = re; 
            } 
            re = RegExp(pattern, modifiers); 

            while (match = re.exec(original)) { 
              result.push(this.slice(index, match.index)); 
              index = match.index + new MyString(match[0]).length; 
            } 
            result.push(this.slice(index)); 
            return result; 
           } 
        }; 

测试:

         > var s = new MyString('hello'); 
        > s.length; 
         5 
        > s[0]; 
        "h" 
         > s.toString(); 
         "hello" 
        > s.valueOf(); 
         "hello" 
        > s.charAt(1); 
         "e" 
        > s.charAt('2'); 
        "l" 
        > s.charAt('e'); 
        "h" 
        > s.concat(' world!'); 
        "hello world!" 
        > s.slice(1, 3); 
        "el" 
        > s.slice(0, -1); 
        "hell" 
        > s.split('e'); 
         ["h", "llo"] 
        > s.split('l'); 
         ["he", "", "o"] 

随意使用正则表达式进行拆分。

  1. reverse()方法更新MyString()
        > MyString.prototype.reverse = function reverse() { 
            return this.valueOf().split("").reverse().join(""); 
          }; 
        > new MyString("pudding").reverse(); 
         "gniddup" 

  1. 想象Array()消失了,世界需要你实现MyArray()。以下是一些方法,让你开始:
        function MyArray(length) { 
          // single numeric argument means length 
          if (typeof length === 'number' && 
              arguments[1] === undefined) { 
            this.length = length; 
            return this; 
          } 

          // usual case 
           this.length = arguments.length; 
          for (var i = 0, len = arguments.length; i < len; i++) { 
            this[i] = arguments[i]; 
          } 
          return this; 

          // later in the book you'll learn how to support 
          // a non-constructor invocation too 
        } 

        MyArray.prototype = { 
          constructor: MyArray, 
          join: function join(glue) { 
            var result = ''; 
            if (glue === undefined) { 
              glue = ','; 
            } 
            for (var i = 0; i < this.length - 1; i++) { 
              result += this[i] === undefined ? '' : this[i]; 
              result += glue; 
            } 
            result += this[i] === undefined ? '' : this[i]; 
            return result; 
          }, 
          toString: function toString() { 
            return this.join(); 
          }, 
          push: function push() { 
            for (var i = 0, len = arguments.length; i < len; i++) { 
              this[this.length + i] = arguments[i]; 
            } 
            this.length += arguments.length; 
            return this.length; 
          }, 
          pop: function pop() { 
            var poppd = this[this.length - 1]; 
            delete this[this.length - 1]; 
            this.length--; 
            return poppd; 
          } 
        }; 

测试:

        > var a = new MyArray(1, 2, 3, "test"); 
        > a.toString(); 
        "1,2,3,test" 
        > a.length; 
         4 
        > a[a.length - 1]; 
        "test" 
        > a.push('boo'); 
         5 
        > a.toString(); 
        "1,2,3,test,boo" 
        > a.pop(); 
        "boo" 
        > a.toString(); 
        "1,2,3,test" 
        > a.join(','); 
        "1,2,3,test" 
        > a.join(' isn't '); 
        "1 isn't 2 isn't 3 isn't test" 

如果你觉得这个练习有趣,不要停在join()上;尽可能多地使用方法。

  1. 创建一个MyMath对象,它还具有rand()min([])max([])

这里的重点是Math不是一个构造函数,而是一个具有一些“静态”属性和方法的对象。以下是一些方法,供您开始使用。

我们还可以使用立即函数来保留一些私有实用函数。您也可以采用上面的MyString方法,在那里this._value可以真正是私有的。

        var MyMath = (function () { 

         function isArray(ar) { 
            return 
              Object.prototype.toString.call(ar) === 
                '[object Array]'; 
         } 

          function sort(numbers) { 
            // not using numbers.sort() directly because 
            // `arguments` is not an array and doesn't have sort() 
            return Array.prototype.sort.call(numbers, function (a, b) { 
              if (a === b) { 
                return 0; 
              } 
              return  1 * (a > b) - 0.5; // returns 0.5 or -0.5 
           }); 
          } 

          return { 
            PI:   3.141592653589793, 
            E:    2.718281828459045, 
            LN10: 2.302585092994046, 
            LN2:  0.6931471805599453, 
            // ... more constants 
            max: function max() { 
              // allow unlimited number of arguments 
              // or an array of numbers as first argument 
              var numbers = arguments; 
              if (isArray(numbers[0])) { 
                numbers = numbers[0]; 
              } 
              // we can be lazy:  
              // let Array sort the numbers and pick the last 
              return sort(numbers)[numbers.length - 1]; 
            }, 
            min: function min() { 
              // different approach to handling arguments: 
              // call the same function again 
              if (isArray(numbers)) { 
                return this.min.apply(this, numbers[0]); 
              } 

              // Different approach to picking the min: 
              // sorting the array is an overkill, it's too much  
              // work since we don't worry about sorting but only  
              // about the smallest number. 
              // So let's loop: 
              var min = numbers[0]; 
              for (var i = 1; i < numbers.length; i++) { 
                if (min > numbers[i]) { 
                  min = numbers[i]; 
                } 
             } 
              return min; 
            }, 
            rand: function rand(min, max, inclusive) { 
              if (inclusive) { 
                return Math.round(Math.random() * (max - min) + min); 
                // test boundaries for random number 
                // between 10 and 100 *inclusive*: 
                // Math.round(0.000000 * 90 + 10); // 10 
                // Math.round(0.000001 * 90 + 10); // 10 
                // Math.round(0.999999 * 90 + 10); // 100 

              } 
              return Math.floor(Math.random() * (max - min - 1) + min + 1); 
              // test boundaries for random number 
              // between 10 and 100 *non-inclusive*: 
              // Math.floor(0.000000 * (89) + (11)); // 11 
              // Math.floor(0.000001 * (89) + (11)); // 11 
              // Math.floor(0.999999 * (89) + (11)); // 99 
            } 
          }; 
        })(); 

在您完成本书并了解 ES5 之后,您可以尝试使用defineProperty()来更严格地控制和更接近内置对象的复制。

第五章,原型

让我们尝试解决以下练习:

练习

  1. 创建一个名为shape的对象,该对象具有type属性和getType()方法:
        var shape = { 
          type: 'shape', 
          getType: function () { 
            return this.type; 
          } 
        }; 

  1. 以下是Triangle()构造函数的程序:
        function Triangle(a, b, c) { 
          this.a = a; 
          this.b = b; 
          this.c = c; 
        } 

        Triangle.prototype = shape; 
        Triangle.prototype.constructor = Triangle; 
        Triangle.prototype.type = 'triangle'; 

  1. 要添加getPerimeter()方法,请使用以下代码:
        Triangle.prototype.getPerimeter = function () { 
          return this.a + this.b + this.c; 
        }; 

  1. 测试以下代码:
        > var t = new Triangle(1, 2, 3); 
        > t.constructor === Triangle; 
        true 
        > shape.isPrototypeOf(t); 
        true 
        > t.getPerimeter(); 
        6 
        > t.getType(); 
        "triangle" 

  1. 循环遍历t,只显示自有属性和方法:
        for (var i in t) { 
          if (t.hasOwnProperty(i)) { 
            console.log(i, '=', t[i]); 
         } 
        } 

  1. 使用以下代码片段随机化数组元素:
        Array.prototype.shuffle = function () { 
          return this.sort(function () { 
            return Math.random() - 0.5; 
          }); 
        }; 

测试:

        > [1, 2, 3, 4, 5, 6, 7, 8, 9].shuffle(); 
         [4, 2, 3, 1, 5, 6, 8, 9, 7] 
        > [1, 2, 3, 4, 5, 6, 7, 8, 9].shuffle(); 
         [2, 7, 1, 3, 4, 5, 8, 9, 6] 
        > [1, 2, 3, 4, 5, 6, 7, 8, 9].shuffle(); 
         [4, 2, 1, 3, 5, 6, 8, 9, 7] 

第六章,继承

让我们解决以下练习:

练习

  1. 通过混合到原型中进行多重继承,例如:
        var my = objectMulti(obj, another_obj, a_third, { 
          additional: "properties" 
        }); 
        A possible solution: 
        function objectMulti() { 
          var Constr, i, prop, mixme; 

        // constructor that sets own properties 
        var Constr = function (props) { 
          for (var prop in props) { 
            this[prop] = props[prop]; 
          } 
        }; 

       // mix into the prototype 
       for (var i = 0; i < arguments.length - 1; i++) { 
         var mixme = arguments[i]; 
         for (var prop in mixme) { 
           Constr.prototype[prop] = mixme[prop]; 
         } 
       } 

      return new Constr(arguments[arguments.length - 1]);
   } 

测试:

        > var obj_a = {a: 1}; 
        > var obj_b = {a: 2, b: 2}; 
        > var obj_c = {c: 3}; 
        > var my = objectMulti(obj_a, obj_b, obj_c, {hello: "world"}); 
        > my.a; 
         2 

属性a2,因为obj_b覆盖了与obj_a相同名称的属性(最后一个获胜):

        > my.b; 
        2 
        > my.c; 
        3 
        > my.hello; 
        "world" 
        > my.hasOwnProperty('a'); 
        false 
        > my.hasOwnProperty('hello'); 
        true 

  1. www.phpied.com/files/canvas/上使用画布示例进行练习。

使用以下代码片段绘制几个三角形:

        new Triangle( 
          new Point(100, 155), 
          new Point(30, 50), 
          new Point(220, 00)).draw(); 

        new Triangle( 
          new Point(10, 15),   
          new Point(300, 50), 
          new Point(20, 400)).draw(); 

使用以下代码片段绘制几个正方形:

        new Square(new Point(150, 150), 300).draw(); 
        new Square(new Point(222, 222), 222).draw(); 

使用以下代码片段绘制几个矩形:

        new Rectangle(new Point(100, 10), 200, 400).draw(); 
        new Rectangle(new Point(400, 200), 200, 100).draw(); 

  1. 要添加菱形、风筝、五边形、梯形和圆(重新实现draw()),请使用以下代码:
        function Kite(center, diag_a, diag_b, height) { 
          this.points = [ 
            new Point(center.x - diag_a / 2, center.y), 
            new Point(center.x, center.y + (diag_b - height)), 
            new Point(center.x + diag_a / 2, center.y), 
            new Point(center.x, center.y - height) 
          ]; 
          this.getArea = function () { 
            return diag_a * diag_b / 2; 
          }; 
        } 

        function Rhombus(center, diag_a, diag_b) { 
          Kite.call(this, center, diag_a, diag_b, diag_b / 2); 
        } 

        function Trapezoid(p1, side_a, p2, side_b) { 
          this.points = [p1, p2, new Point(p2.x + side_b, p2.y),
          new Point(p1.x + side_a, p1.y) 
          ]; 

          this.getArea = function () { 
            var height = p2.y - p1.y; 
            return height * (side_a + side_b) / 2; 
          }; 
        } 

        // regular pentagon, all edges have the same length 
        function Pentagon(center, edge) { 
          var r = edge / (2 * Math.sin(Math.PI / 5)), 
              x = center.x, 
              y = center.y; 

          this.points = [new Point(x + r, y),
        new Point(x + r * Math.cos(2 * Math.PI / 5), y - r * 
         Math.sin(2 * Math.PI / 5)), 
        new Point(x - r * Math.cos(    Math.PI / 5), y - r * 
         Math.sin(    Math.PI / 5)), 
        new Point(x - r * Math.cos(    Math.PI / 5), y + r * 
         Math.sin(    Math.PI / 5)), 
        new Point(x + r * Math.cos(2 * Math.PI / 5), y + r * 
         Math.sin(2 * Math.PI / 5)) 
          ]; 

          this.getArea = function () { 
            return 1.72 * edge * edge; 
          }; 
        } 

        function Circle(center, radius) { 
          this.getArea = function () { 
            return Math.pow(radius, 2) * Math.PI; 
          }; 

          this.getPerimeter = function () { 
            return 2 * radius * Math.PI; 
          };   

          this.draw = function () { 
            var ctx = this.context; 
            ctx.beginPath(); 
            ctx.arc(center.x, center.y, radius, 0, 2 * Math.PI); 
            ctx.stroke(); 
          }; 
        } 

        (function () { 
          var s = new Shape(); 
          Kite.prototype = s; 
          Rhombus.prototype = s; 
          Trapezoid.prototype = s; 
          Pentagon.prototype = s; 
          Circle.prototype = s; 
        }()); 

测试:

        new Kite(new Point(300, 300), 200, 300, 100).draw(); 
        new Rhombus(new Point(200, 200), 350, 200).draw(); 
        new Trapezoid( 
          new Point(100, 100), 100,  
          new Point(50, 250), 400).draw(); 
        new Pentagon(new Point(400, 400), 100).draw(); 
        new Circle(new Point(500, 300), 270).draw(); 

练习

测试新形状的结果

  1. 想出另一种继承的方法。使用uber让子类可以访问其父类。还要让父类意识到它们的子类。

请记住,并非所有子类都继承Shape;例如,Rhombus继承KiteSquare继承Rectangle。您最终会得到类似这样的东西:

        // inherit(Child, Parent) 
        inherit(Rectangle, Shape); 
        inherit(Square, Rectangle); 

在本章和上一个练习中的继承模式中,所有子类都共享相同的原型,例如:

        var s = new Shape(); 
        Kite.prototype = s; 
        Rhombus.prototype = s; 

虽然这很方便,但这也意味着没有人可以触及原型,因为这会影响其他人的原型。缺点是所有自定义方法都需要自有属性,例如this.getArea

最好将方法共享在实例之间,并在原型中定义,而不是为每个对象重新创建它们。以下示例将自定义的getArea()方法移动到原型中。

在继承函数中,您会看到子类只继承父类的原型。因此,诸如this.lines之类的自有属性将不会被设置。因此,您需要让每个子类构造函数调用其uber以获取自有属性,例如:

        Child.prototype.uber.call(this, args...) 

另一个很好的功能是将已添加到子类的原型属性传递给子类。这允许子类首先继承,然后添加更多自定义或者反过来,这也更方便一些。

        function inherit(Child, Parent) { 
          // remember prototype 
          var extensions = Child.prototype; 

          // inheritance with an intermediate F() 
          var F = function () {}; 
           F.prototype = Parent.prototype; 
          Child.prototype = new F(); 
          // reset constructor 
          Child.prototype.constructor = Child; 
          // remember parent 
          Child.prototype.uber = Parent; 

          // keep track of who inherits the Parent 
          if (!Parent.children) { 
            Parent.children = []; 
          } 
          Parent.children.push(Child); 

          // carry over stuff previsouly added to the prototype 
          // because the prototype is now overwritten completely 
          for (var i in extensions) { 
            if (extensions.hasOwnProperty(i)) { 
              Child.prototype[i] = extensions[i]; 
            } 
          } 
        } 

Shape()Line()Point()的一切都保持不变。变化只发生在子类中:

        function Triangle(a, b, c) { 
          Triangle.prototype.uber.call(this); 
          this.points = [a, b, c]; 
        } 

        Triangle.prototype.getArea = function () { 
          var p = this.getPerimeter(), s = p / 2; 
          return Math.sqrt(s * (s - this.lines[0].length) * 
        (s - this.lines[1].length) * (s - this.lines[2].length)); 
        }; 

        function Rectangle(p, side_a, side_b) { 
          // calling parent Shape() 
          Rectangle.prototype.uber.call(this); 

          this.points = [ p, 
            new Point(p.x + side_a, p.y), 
            new Point(p.x + side_a, p.y + side_b), 
            new Point(p.x, p.y + side_b) 
          ]; 
        } 

       Rectangle.prototype.getArea = function () { 
           // Previsouly we had access to side_a and side_b  
           // inside the constructor closure. No more. 
          // option 1: add own properties this.side_a and this.side_b 
          // option 2: use what we already have: 
          var lines = this.getLines(); 
          return lines[0].length * lines[1].length; 
        }; 

        function Square(p, side) { 
          this.uber.call(this, p, side, side); 
          // this call is shorter than Square.prototype.uber.call() 
          // but may backfire in case you inherit  
          // from Square and call uber 
          // try it :-) 
        } 

继承:

        inherit(Triangle, Shape); 
        inherit(Rectangle, Shape); 
        inherit(Square, Rectangle); 

测试:

        > var sq = new Square(new Point(0, 0), 100); 
        > sq.draw(); 
        > sq.getArea(); 
        10000 

测试instanceof是否正确:

        > sq.constructor === Square; 
        true 
        > sq instanceof Square; 
        true 
        > sq instanceof Rectangle; 
        true 
        > sq instanceof Shape; 
        true 

children数组:

        > Shape.children[1] === Rectangle; 
        true 
        > Rectangle.children[0] === Triangle; 
        false 
        > Rectangle.children[0] === Square; 
        true 
        > Square.children; 
        undefined 

并且uber看起来也不错:

        > sq.uber === Rectangle; 
        true 

调用isPrototypeOf()也会返回预期的结果:

        Shape.prototype.isPrototypeOf(sq); 
        true 
        Rectangle.prototype.isPrototypeOf(sq); 
        true 
        Triangle.prototype.isPrototypeOf(sq); 
        false 

完整的代码可在www.phpied.com/files/canvas/index2.html上找到,还有来自上一个练习的额外的Kite()Circle()等。

第七章,浏览器环境

让我们练习以下练习:

练习

  1. 标题时钟程序如下:
        setInterval(function () { 
          document.title = new Date().toTimeString(); 
        }, 1000); 

  1. 要动画调整弹出窗口的大小从 200 x 200 到 400 x 400,请使用以下代码:
        var w = window.open( 
            'http://phpied.com', 'my', 
             'width = 200, height = 200'); 

        var i = setInterval((function () { 
          var size = 200; 
          return function () { 
            size += 5; 
            w.resizeTo(size, size); 
            if (size === 400) { 
              clearInterval(i); 
            } 
          }; 
        }()), 100); 

每 100 毫秒(1/10 秒),弹出窗口的大小增加五个像素。您保留对间隔i的引用,以便在完成后清除它。变量size跟踪弹出窗口的大小(为什么不在闭包内保持它私有)。

  1. 地震程序如下:
       var i = setInterval((function () { 
          var start = +new Date(); // Date.now() in ES5 
          return function () { 
            w.moveTo( 
              Math.round(Math.random() * 100), 
              Math.round(Math.random() * 100)); 
            if (new Date() - start > 5000) { 
              clearInterval(i); 
            } 
          }; 
         }()), 20); 

尝试所有这些,但使用requestAnimationFrame()而不是setInterval()

  1. 带有回调的不同的walkDOM()如下:
        function walkDOM(n, cb) { 
          cb(n); 
          var i, 
              children = n.childNodes, 
              len = children.length, 
              child; 
          for (i = 0; i < len; i++) { 
          child = n.childNodes[i]; 
            if (child.hasChildNodes()) { 
              walkDOM(child, cb); 
            } 
          } 
        } 

测试:

        > walkDOM(document.documentElement,
        console.dir.bind(console)); 
       html 
       head 
       title 
       body 
       h1 
       ... 

  1. 要删除内容并清理函数,请使用以下代码:
        // helper 
        function isFunction(f) { 
          return Object.prototype.toString.call(f) === 
            "[object Function]"; 
        } 

        function removeDom(node) { 
          var i, len, attr; 

          // first drill down inspecting the children 
          // and only after that remove the current node 
          while (node.firstChild) { 
            removeDom(node.firstChild); 
          } 

          // not all nodes have attributes, e.g. text nodes don't 
          len = node.attributes ? node.attributes.length : 0; 

          // cleanup loop 
          // e.g. node === <body>,  
          // node.attributes[0].name === "onload" 
          // node.onload === function()... 
          // node.onload is not enumerable so we can't use  
          // a for-in loop and have to go the attributes route 
          for (i = 0; i < len; i++) { 
            attr = node[node.attributes[i].name]; 
            if (isFunction(attr)) { 
              // console.log(node, attr); 
              attr = null; 
            } 
          } 

          node.parentNode.removeChild(node); 
        } 

测试:

        > removeDom(document.body); 

  1. 要动态包含脚本,请使用以下代码:
        function include(url) { 
          var s = document.createElement('script'); 
          s.src = url; 
          document.getElementsByTagName('head')[0].
          appendChild(s); 
        } 

测试:

        > include("http://www.phpied.com/files/jinc/1.js"); 
        > include("http://www.phpied.com/files/jinc/2.js"); 

  1. 事件:事件实用程序如下:
        var myevent = (function () { 

          // wrap some private stuff in a closure 
          var add, remove, toStr = Object.prototype.toString; 

          // helper 
          function toArray(a) { 
            // already an array 
            if (toStr.call(a) === '[object Array]') { 
              return a; 
           } 

            // duck-typing HTML collections, arguments etc 
            var result, i, len; 
            if ('length' in a) { 
              for (result = [], i = 0, len = a.length; i < len; i++)
              { 
                result[i] = a[i]; 
              } 
              return result; 
           } 

            // primitives and non-array-like objects 
            // become the first and single array element 
            return [a]; 
          } 

          // define add() and remove() depending 
          // on the browser's capabilities 
          if (document.addEventListener) { 
            add = function (node, ev, cb) { 
              node.addEventListener(ev, cb, false); 
            }; 
            remove = function (node, ev, cb) { 
              node.removeEventListener(ev, cb, false); 
            }; 
          } else if (document.attachEvent) { 
            add = function (node, ev, cb) { 
              node.attachEvent('on' + ev, cb); 
            }; 
            remove = function (node, ev, cb) { 
              node.detachEvent('on' + ev, cb); 
            }; 
          } else { 
            add = function (node, ev, cb) { 
              node['on' + ev] = cb; 
            }; 
            remove = function (node, ev) { 
              node['on' + ev] = null; 
            }; 
          } 

          // public API 
          return { 

            addListener: function (element, event_name, callback) { 
              // element could also be an array of elements 
              element = toArray(element); 
              for (var i = 0; i < element.length; i++) { 
                add(element[i], event_name, callback); 
              } 
            }, 

           removeListener: function (element, event_name, callback) { 
              // same as add(), only practicing a different loop 
              var i = 0, els = toArray(element), len = els.length; 
             for (; i < len; i++) { 
                remove(els[i], event_name, callback); 
              } 
           }, 

            getEvent: function (event) { 
              return event || window.event; 
            }, 

            getTarget: function (event) { 
              var e = this.getEvent(event); 
              return e.target || e.srcElement; 
            }, 

            stopPropagation: function (event) { 
              var e = this.getEvent(event); 
              if (e.stopPropagation) { 
                e.stopPropagation(); 
              } else { 
                e.cancelBubble = true; 
              } 
            }, 

            preventDefault: function (event) { 
              var e = this.getEvent(event); 
              if (e.preventDefault) { 
                e.preventDefault(); 
              } else { 
                e.returnValue = false; 
              } 
            } 

          }; 
        }()); 

测试:转到任何带有链接的页面,执行以下操作,然后单击任何链接:

        function myCallback(e) { 
          e = myevent.getEvent(e); 
          alert(myevent.getTarget(e).href); 
          myevent.stopPropagation(e); 
          myevent.preventDefault(e); 
        } 
        myevent.addListener(document.links, 'click', myCallback); 

  1. 使用以下代码使用键盘移动div
        // add a div to the bottom of the page 
        var div = document.createElement('div'); 
        div.style.cssText = 'width: 100px; height:
         100px; background: red; position: absolute;'; 
        document.body.appendChild(div); 

        // remember coordinates 
        var x = div.offsetLeft; 
        var y = div.offsetTop; 

        myevent.addListener(document.body, 
         'keydown', function (e) { 
         // prevent scrolling 
          myevent.preventDefault(e); 

          switch (e.keyCode) { 
            case 37: // left 
              x--; 
              break; 
            case 38: // up 
              y--; 
              break; 
            case 39: // right 
              x++; 
              break; 
            case 40: // down 
              y++; 
              break; 
            default: 
              // not interested 
          } 

          // move 
          div.style.left = x + 'px'; 
          div.style.top  = y + 'px'; 

        }); 

  1. 你自己的 Ajax 实用程序:
        var ajax = { 
          getXHR: function () { 
            var ids = ['MSXML2.XMLHTTP.3.0', 
             'MSXML2.XMLHTTP', 'Microsoft.XMLHTTP']; 
            var xhr; 
            if (typeof XMLHttpRequest === 'function') { 
              xhr = new XMLHttpRequest(); 
            } else { 
              // IE: try to find an ActiveX object to use 
              for (var i = 0; i < ids.length; i++) { 
                try { 
                  xhr = new ActiveXObject(ids[i]); 
                  break; 
                } catch (e) {} 
              } 
            } 
            return xhr; 

          }, 
          request: function (url, method, cb, post_body) { 
            var xhr = this.getXHR(); 
            xhr.onreadystatechange = (function (myxhr) { 
              return function () { 
                if (myxhr.readyState === 4 && myxhr.status === 200) { 
                  cb(myxhr); 
                } 
              }; 
            }(xhr)); 
            xhr.open(method.toUpperCase(), url, true); 
            xhr.send(post_body || ''); 
          } 
        }; 

在测试时,请记住相同的来源限制适用,因此您必须在相同的域上。您可以转到www.phpied.com/files/jinc/,这是一个目录列表,然后在控制台中进行测试:

        function myCallback(xhr) { 
          alert(xhr.responseText); 
        } 
        ajax.request('1.css', 'get', myCallback); 
        ajax.request('1.css', 'post', myCallback,
         'first=John&last=Smith'); 

两者的结果是相同的,但是如果您查看 Web 检查器的网络选项卡,您会发现第二个确实是带有主体的POST请求。

posted @ 2024-05-22 12:07  绝不原创的飞龙  阅读(4)  评论(0编辑  收藏  举报