原文:
zh.annas-archive.org/md5/9BD01417886F7CF4434F47DFCFFE13F5
译者:飞龙
前言
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 Explorer(IE)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 引擎)上运行的代码。 -
制作移动应用程序;您可以使用PhoneGap或Titanium完全使用 JavaScript 为 iPhone、Android 和其他手机和平板电脑创建应用程序。此外,为移动电话的 Firefox OS 应用程序完全由 JavaScript、HTML 和 CSS 创建。来自 Facebook 的 React Native 是一种令人兴奋的新方法,可以使用 JavaScript 开发本机 iOS、Android 和 Windows(实验性)应用程序。
-
使用基于 ECMAScript 的 ActionScript 创建丰富的媒体应用程序,如 Flash 或 Flex。
-
使用Windows Scripting Host(WSH)或 WebKit 的JavaScriptCore在桌面上编写命令行工具和脚本,以自动化管理任务。这些工具在所有 Mac 上都可用。
-
为大量桌面应用程序编写扩展和插件,如 Dreamweaver、Photoshop 和大多数其他浏览器。
-
使用 Mozilla 的XULRunner和Electron创建跨操作系统的桌面应用程序。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 5(ES5),于 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 中,因为它是一种解释性语言,您可以看到源代码,但概念仍然是一样的-您使用对象的接口而不用担心其实现。
信息隐藏的另一个方面是方法和属性的可见性。在某些语言中,对象可以具有public
、private
和protected
方法和属性。这种分类定义了对象用户的访问级别。例如,只有相同对象的方法才能访问private
方法,而任何人都可以访问public
方法。在 JavaScript 中,所有方法和属性都是public
,但我们将看到有方法来保护对象内部的数据并实现隐私。
聚合
将几个对象组合成一个新对象称为聚合或组合。这是将问题分解为更小且更易管理的部分的强大方式(分而治之)。当问题范围如此复杂以至于不可能在整体上以详细级别考虑它时,您可以将问题分解为几个较小的领域,可能然后将每个领域分解为更小的部分。这使您可以在几个抽象级别上考虑问题。
例如,个人计算机。这是一个复杂的对象。您无法考虑启动计算机时需要发生的所有事情。但是,您可以抽象地说,您需要初始化Computer
对象包含的所有单独对象,Monitor
对象,Mouse
对象,Keyboard
对象等。然后,您可以深入研究每个子对象。通过组装可重用部分,您可以组合复杂对象。
再举一个类比,一个Book
对象可以包含(聚合)一个或多个Author
对象,一个Publisher
对象,几个Chapter
对象,一个TOC
(目录)等。
继承
继承是重用现有代码的一种优雅方式。例如,您可以拥有一个通用对象Person
,它具有诸如name
和date_of_birth
之类的属性,并且还实现了walk
、talk
、sleep
和eat
功能。然后,您发现自己需要另一个名为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 对象。 | 聚合和组合 |
Designer ,ProjectManager 和Programmer 都基于并扩展了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 代码:
为了在 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时,您将看到表达式的结果。看一下以下的屏幕截图:
更多控制台
所有现代浏览器都内置了控制台。您之前已经看到了 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 中的原始数据类型,如字符串和数字
-
数组
-
常见运算符,如
+
、-
、delete
和typeof
-
流程控制语句,如循环和
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
在上面的代码中:
-
+
符号是运算符 -
操作是加法
-
输入值是
1
和2
(它们也被称为操作数) -
结果值是
3
-
整个东西被称为表达式
不要直接在表达式中使用值1
和2
,你可以使用变量。你也可以使用一个变量来存储操作的结果,如下面的例子所示:
> 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
,100
,3
.14
。 -
字符串:这些由任意数量的字符组成,例如,
a
,one
和one 2 three
。 -
布尔:这可以是
true
或false
。 -
未定义:当你尝试访问一个不存在的变量时,你会得到特殊值未定义。当你声明一个变量但尚未给它赋值时,也会发生同样的情况。JavaScript 在幕后用值
undefined
初始化变量。未定义数据类型只能有一个值-特殊值undefined
。 -
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(蓝色)的数量,范围从
0
到255
。例如,*rgb(0, 0, 0)*是黑色,*rgb(255, 0, 0)*是红色(红色的最大量,没有绿色或蓝色)。 -
使用十六进制并为每个 R、G 和 B 值指定两个字符。例如,#000000是黑色,#ff0000是红色。这是因为ff是
255
的十六进制值。
在 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+1
或1E1
或1E+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 Number(NaN)的东西,例如:
> 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
布尔值
只有两个值属于布尔数据类型-不带引号的true
和false
值:
> var b = true;
> typeof b;
"boolean"
> var b = false;
> typeof b;
"boolean"
如果你引用true
或false
,它们会变成字符串,如下例所示:
> 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"
尽管null
和undefined
之间的差异很小,但有时也可能非常关键。例如,如果尝试进行算术运算,将会得到不同的结果:
> var i = 1 + undefined;
> i;
NaN
> var i = 1 + null;
> i;
1
这是因为null
和undefined
转换为其他原始类型的方式不同。以下示例显示了可能的转换:
- 转换为数字:
> 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 中有五种原始数据类型:
-
数字
-
字符串
-
布尔
-
未定义
-
空
-
-
不是原始数据类型的所有东西都是对象。
原始数字数据类型可以存储正整数和负整数或浮点数、十六进制数、八进制数、指数以及特殊的数-
NaN
、Infinity
和-Infinity
。 -
字符串数据类型包含带引号的字符。模板文字允许在字符串中嵌入表达式。
-
布尔数据类型的唯一值是
true
和false
。 -
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
语句 -
while
,do...while
,for
,和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';
}
在if
和else
语句之间,还可以有无限数量的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
已经定义和初始化,但包含一个类似false
或0
的假值。
检查变量是否定义的更好方法是使用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";
你应该只在简单条件下使用这种语法。要小心不要滥用它,因为它很容易使你的代码难以阅读。以下是一个示例。
假设你想确保一个数字在一个特定范围内,比如在50
和100
之间:
> 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
语句的逐步过程如下:
-
评估括号中的
switch
表达式;记住它。 -
移动到第一个
case
并将其值与步骤 1 中的值进行比较。 -
如果步骤 2 中的比较返回
true
,则执行case
块中的代码。 -
在执行
case
块之后,如果在其末尾有一个break
语句,则退出switch
。 -
如果没有
break
或者步骤 2 返回false
,则继续下一个case
块。 -
重复步骤 2 到 5。
-
如果你还在这里(第 4 步没有退出),执行
default
语句后面的代码。
小贴士
缩进跟在case
行之后的代码。你也可以缩进switch
的case
,但这对可读性没有太大帮助。
别忘了加上break
有时,你可能有意地省略 break
,但这很少见。这被称为穿透,一定要记录下来,因为它可能看起来像是一个意外的遗漏。另一方面,有时你可能希望省略跟在一个 case
后面的整个代码块,并且让两个 case
共享相同的代码。这是可以的,但这并不改变以下规则:如果有跟在一个 case
语句后面的代码,这段代码应该以 break
结尾。至于缩进,将 break
与 case
或 case
内部的代码对齐是个人偏好;再次强调,保持一致才是最重要的。
使用默认情况。这可以确保在 switch
语句之后始终有一个有意义的结果,即使没有任何一个 case
与被切换的值匹配。
循环
if...else
和 switch
语句允许你的代码走不同的路径,就好像你站在十字路口一样,在依据条件决定往哪个方向走。另一方面,循环允许你的代码在回到主干路之前绕几个圈。重复多少次?这取决于在每次迭代之前(或之后)评估条件的结果。
假设你(你的程序执行)从 A 到 B。在某一时刻,你将到达一个需要评估条件 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
循环是最常用的循环类型,你应该确保自己熟悉这一点。它在语法方面需要稍微多一点:
除了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
循环用于遍历数组或对象的元素,稍后你会看到。这是它的唯一用途;它不能用作替换for
或while
的通用重复机制。让我们看一个使用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,可以解析你的代码,并根据你的注释提取有意义的文档。
练习
- 在控制台执行这些行的结果是什么?为什么?
> 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`)来控制程序流程的不同方法。
这是相当多的信息;在深入下一章之前,给自己一个当之无愧的鼓励。更有趣的内容即将到来!