书名:JavaScript面向对象编程指南(第3版)
ISBN:978-7-115-54437-7
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 [印] 韦德•安塔尼(Ved Antani)
[意] 斯托扬•斯特凡诺夫(Stoyan Stefanov)
译 余博伦
责任编辑 武晓燕
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
Copyright ©2021 Packt Publishing. First published in the English language under the title Object-Oriented JavaScript (Third Edition).
All rights reserved.
本书由英国Packt Publishing公司授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。
版权所有,侵权必究。
JavaScript语言是一种具有高度表达能力的、基于原型特性的、非常灵活的面向对象(Object-Oriented,OO)编程语言。本书着重于介绍JavaScript在面向对象方面的特性,以为你展示如何构建强健、可维护、功能强大的应用程序及程序库。
本书是《JavaScript面向对象编程指南》的第3版,全书包括13章和4个附录。依次介绍了JavaScript的发展历史、基础知识(变量、数据类型、数组、循环以及条件表达式等)、函数、对象、迭代器和生成器、原型、继承的实现、类与模块、Promise与Proxy、浏览器环境、编程模式与设计模式、测试与调试和响应式编程等。附录部分包括学习JavaScript编程常用的参考资源。
本书全面地覆盖了JavaScript语言的OO特性,同时兼顾基础知识,对初学者来说,是难得的JavaScript佳作,读者不需要具备任何JavaScript基础知识及项目经验。本书适用于任何希望学习JavaScript的编程初学者,也可以作为有JavaScript使用经验的读者的参考书。
韦德•安塔尼(Ved Antani)拥有多年的使用JavaScript、Go以及Java开发大型服务器端及移动端平台的经验。现任Myntra副总裁,此前也有美国电艺公司(EA, Electronic Arts)以及甲骨文(Oracle)公司的工作经历。他本人热衷于阅读和写作,现居住于印度的班加罗尔。韦德也热衷于古典音乐,并喜欢与他的儿子共度时光。
我倾注了大量的时间和精力来写作本书,在此我要由衷地感谢我的父母以及我的家庭对我的支持和鼓励,尤其是在我废寝忘食的那段日子里。
斯托扬•斯特凡诺夫(Stoyan Stefanov)是Facebook公司工程师、作家、演说家。他经常会在博客和相关会议上就Web开发话题发表独到见解。他还运营着一些网站,其中包括JSPatterns——一个专门探讨JavaScript模式的网站。斯托扬曾在雅虎公司任职,担任YSlow 2.0架构师,并且是图像优化工具Smush的作者。 作为一个“世界公民”,斯托扬在保加利亚出生,拥有加拿大国籍,并在美国洛杉矶工作。业余时间里他喜欢弹吉他,学习飞机驾驶以及在圣莫妮卡海滩与他的家人共度时光。
谨以此书献给我的妻子伊娃及我的女儿兹拉蒂娜和娜塔丽。感谢你们的耐心、支持与鼓励。
穆罕默德•萨纳乌拉(Mohamed Sanaulla)是一名拥有7年经验的开发者,他主要从事企业级应用的Java后端解决方案以及电子商务应用相关的开发工作。
他专注于企业级应用开发、应用重构、REST风格的服务架构、Java应用性能优化以及测试驱动开发等领域。
他在Java应用开发、ADF(基于JSF的JavaEE Web框架)、SQL、PL/SQL、JUnit、REST风格的服务设计、Spring、Struts、Elasticsearch以及MongoDB等方面拥有丰富的经验。他也是Sun认证的Java开发者。此外他还是JavaRanch的主创者。他也会经常在自己的博客上进行分享。
JavaScript已经成为最强大和多功能的编程语言之一。如今的JavaScript包含大量经过时间考验的优良特性。其中一些功能正在慢慢塑造下一代的Web和服务端。ES6引入了非常重要的语言结构,例如Promise、类、箭头函数以及一些备受期待的特性。本书详细介绍了语言结构及其实际用途。本书不需要读者有任何JavaScript的先验知识。本书的讲解深入浅出,可使读者对该语言有一个透彻的了解。此前了解过该语言的读者也可以用作参考书。对已经了解JavaScript且熟悉ES5语法的人来说,本书将是你了解ES6功能的非常有用的入门读物。本书所涵盖的内容如下。
在阅读本书之前,你需要安装一个现代浏览器——推荐Google Chrome或者Firefox,并可自由选择是否安装Node.js。本书大部分代码示例可以通过BABEL或JS Bin进行测 试。当然,你可以自行选择用于编写JavaScript代码的文本编辑器。
本书适用于任何希望学习JavaScript的编程初学者,包括那些懂一点JavaScript却对其面向对象特性不甚了解的读者。
在本书中,读者会发现几种不同样式的文本,它们各自代表了不同类型的信息。下面,我们将通过一些文本示例来解释一下这些样式所代表的含义。
对于正文中出现代码、数据库表名、文件夹名、文件名、文件扩展名、路径名、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中启动控制台,请右键单击页面上的任意位置,然后选择Inspect Element。显示的附加窗口是Web Inspector功能。选择Console选项卡,你就可以开始使用了”。
这种形式表达的是一些需要读者警惕或需要重点关注的内容。
这种形式所提供的是一些提示或小技巧。
本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。
本书提供免费的源代码和部分课后习题答案下载,要获得相关配套资源,请在异步社区本书页面中单击,跳转到下载界面,按提示进行操作即可。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线投稿(直接访问www.epubit.com/ selfpublish/submission即可)。
如果您来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。
异步社区
微信服务号
自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有基本的了解,不要求读者具备任何的编程知识。除了有一章专门介绍浏览器环境,本书讨论的JavaScript相关知识适用于所有开发环境。
让我们先从下面两个话题开始:
起初,所谓的Web只不过是一些科学出版物的静态HTML文档,它们直接通过超链接简单地联系在一起。这听起来可能难以置信,早期的网页竟然连图像都不支持。但很快,随着Web的发展和用户的增长,这些创建HTML页面的管理者们的需求也日益增长。他们希望网页能够进行更复杂的用户交互,例如表单验证,以此来减少一些浏览器与服务器端的通信。当时出现了两种解决方案——Java applets和LiveScript。其中,LiveScript是1995年由Netscape公司的Brendan Eich开发的编程语言,Netscape 2.0浏览器发布之后,它被更名为JavaScript并包含于其中。
applets很快退出了历史舞台,而JavaScript延续了下来。这种通过在HTML页面中嵌入代码片段来操作页面静态元素的功能在网站管理者社区中广受欢迎。很快,竞品就出现了,微软发布了带有JScript的浏览器Internet Explorer (IE),它简直就是添加了一些IE专有特性的JavaScript的翻版。最终,致力于统一不同版本浏览器脚本语言的标准ECMAScript诞生了。欧洲计算机制造商协会(ECMA)创建了名为ECMA-262的标准,该标准脱离了浏览器和网页的专有特性,规范了JavaScript作为独立编程语言的核心部分。
你可以把JavaScript理解为以下3个部分的统称。
虽然本书专门有一章来阐述浏览器、DOM及BOM,但大部分内容都在讲述JavaScript 语言的核心部分,你在这里所学到的JavaScript知识基本上可应用于任何JavaScript运行环境。
塞翁失马,焉知非福。JavaScript在第一次浏览器大战(大约在1996—2001年)期间推广迅速。那时正值Netscape与Microsoft两大浏览器厂商抢占市场份额引发的互联网发展的第一波浪潮中。两家都不断地为各自浏览器中的JavaScript、DOM和BOM添加五花八门的新特性,这也自然导致了兼容性问题的产生。与此同时,浏览器无法提供相适应的开发工具,文档也严重滞后。这也使开发者工作起来异常痛苦,很多时候你在某一个浏览器当中编写完脚本,测试运行没有问题,本以为大功告成了,在另一个浏览器里却莫名其妙地出错。最后也只能得到类似“操作中止”等不明所以的错误提示。
不一致的实现、不完整的文档、不合适的开发工具,这样光景下的JavaScript,很多开发者连看都不愿意看一眼。
另一方面,那些愿意尝试JavaScript的开发者却又做得过犹不及。他们为页面添加了过多的特效却不考虑最终的实用性。开发者总是迫不及待地尝试浏览器提供的所有特性,使用诸如状态栏动画、变幻的颜色、闪烁的文本、酷炫的光标等事实上有损用户体验的功能。虽然今天已经很少看到对JavaScript的如此滥用,但是我们也无法否认这是损害它名声的原因之一。许多“专业的”程序员蔑称 JavaScript是设计师的玩具,根本不适合拿来开发专业应用。如此也导致很多项目直接禁止使用JavaScript编写客户端程序,全部交由更加可控的服务器端处理。说实在的,你何苦浪费几倍的时间去测试不同浏览器之间的兼容性问题呢?
第一次浏览器大战结束之后情况有所改观。以下事件对Web开发领域产生了积极的 影响。
在这种更加和谐的环境中,开发者们也利用已有的工具找到了更加优化的开发模式。在一些诸如Gmail和Google Maps等重客户端编程的应用发布之后,JavaScript已经可以称得上是一种成熟的、某些方面独特的、拥有强大原型体系的面向对象语言了。最好的例子莫过于对XMLHttpRequest
对象的重新发现和推广,该对象起初只是一个IE独有的特性,但如今它已经得到绝大多数浏览器的支持。XMLHttpRequest
对象允许JavaScript 通过 HTTP请求的方式从服务器上获取新的内容,从而实现对页面的局部更新。这样一来,我们就不必每次都刷新整个页面。随着XMLHttpRequest
对象的广泛应用,一种类桌面式的Web应用模式诞生了,我们称其为Ajax应用。
有意思的是,JavaScript是依赖于某种宿主环境而运行的。浏览器只是其中的一种。JavaScript 同样可以运行在服务器端、桌面程序以及移动设备上。如今,我们已经能够使用JavaScript 来实现下面这些功能。
Node.js
编写服务器端的程序,也可以使用Rhino(一种用Java编写的JavaScript引擎)来运行你的JavaScript代码。asm.js
格式、使其能够在浏览器中运行的技术。这当然不是一个穷尽JavaScript所有应用场景的列表。从最早的网页开始,JavaScript现在几乎可以成熟地被应用在各个地方。除此之外,各大浏览器厂商也在争先恐后地开发更加高效的JavaScript引擎,这让用户和开发者都能从中受益,也为JavaScript在图像、音视频处理、游戏开发等方面提供了新的可能。
未来如何难以想象,但其中一定少不了JavaScript的角色。在过去的很长一段时间里,JavaScript都曾被低估、小觑(或者通过错误的方式被滥用)。但逐渐地,各种使用JavaScript开发的有趣且具有创造性的应用与日俱增。这一切都开始于在HTML行内嵌入的一些诸如 onclick
的简单脚本。如今开发者们已经能够编写出成熟的、设计完整、架构合理且具有可扩展性、使用一套代码可兼容多个平台的应用。JavaScript这一编程语言发展得越来越严谨,开发者们也开始重新审视并更加能享受它的语言特性。
现在,JavaScript被摆在了招聘启事中最重要的位置,你对其知识的了解、掌握程度将对你能否应聘成功产生决定性的影响。例如面试中经常会被问到这些问题:JavaScript是面向对象的编程语言吗?很好,那么你在使用JavaScript的时候是如何实现继承的?通过阅读本书,你将能够轻松应对你的JavaScript面试,甚至会掌握一些连面试官都不知道的知识点,从而受到他们的青睐。
最近一次具有里程碑意义的ECMAScript的修订是ECMAScript 5(ES5),于2009年12月通过ECMAScript 5标准已在所有主流浏览器及服务器端技术中实现。
ES5标准除修订了一些重要的语法上的改变和添加了一些标准库之外,还引入了一些新的构造。
例如,ES5除了引入一些新的对象与属性,还提供了“严格模式”(strict mode)。所谓严格模式其实就是在ES5发布之前,市面上各版本互不兼容的语言的子集。严格模式是可选的,也就是说,选择以严格模式运行的代码段(以函数为单位,或者整个程序)都必须要在其头部作如下声明:
"use strict";
这其实是一个JavaScript字符串。虽然我们并没有将其赋值给某个变量,运行后也不会有什么效果,但它符合JavaScript语法。因此不支持ES5严格模式的老式浏览器会直接忽略它,然后以普通的JavaScript特性对待其后的代码。也就是说,这种严格模式是向后兼容的,使用严格模式不会导致老式浏览器无法运行代码。
出于向后兼容的考虑,本书所有的示例都将遵守ES3规则,但同时本书中所有的代码也都能在ES5中的严格模式下正常运行,不会有任何警告。另外,本书中专门为ES5所写的部分会被清楚地标记出来。而关于ES5的新特性,我们在附录C中会有详细收录。
ES5中的严格模式是一项可选设置,而在ES6中,所有的模块和类都是默认遵循严格模式的。你很快就会了解到,大部分ES6的代码使用了模块化的方式编写,因此,严格模式也就相当于默认启用了。然而,在此标准出现之前的其他构造都是没有默认启用严格模式的。在标准制定的过程中,ES也曾考虑过为一些新的诸如箭头函数(arrow function)和生成器函数(generator function)添加默认的严格模式,但为了保证语言的一致性后续并没有通过。
ECMAScript 6版本经过了相当长一段时间才最终在2015年6月17日正式通过。ES6的新特性也迈着缓慢的步伐,逐步在主流浏览器和服务器端中实现。在实际生产过程中,你可以使用转义器(transpiler)把ES6的代码转义成ES5代码来解决兼容性问题(后续我们会专门讨论语法转义器)。
ES6极大地完善了JavaScript这一编程语言,带来了令人兴奋的新的语法特性和语言构造。大概来讲,这一版本对ECMAScript有两大方面的改动,具体如下。
ES6让你从全新的角度来思考自己的代码。新的语法特性能够让你编写出更加清晰、可维护、不依赖特殊技巧(trick)的代码。语言本身就支持一些之前只能通过第三方库实现的功能。当然这也可能要求你改掉一些旧有的JavaScript编程的习惯和思维定式。
注意:ECMAScript 6、ES6、ECMAScript 2015指同一标准。
大多数的浏览器和服务器端框架都在逐步支持ES6的新特性,你可以在GitHub查看具体的支持情况。
虽然目前ES6还没有在所有的运行环境中得到完整的支持,但通过使用转义器我们就能够得到ES6的许多新特性了。转义器是一种可以在不同源代码之间相互转换的编译器。ES6的转义器让你能够将ES6的语法编译/转换成对应的ES5的语法。这样你的代码就能在对ES6支持不完善的浏览器中运行了。
目前应用最广泛的ES6转义器是Babel。在本书中,我们将会使用它来编写和测试代码示例。
Babel辅以各类插件可以支持几乎所有ES6的特性。你可以在各类系统中、框架上、模板引擎里使用Babel,它提供了完备的命令行工具以及内置的交互环境(REPL)支持。
你可以在Babel官网提供的在线交互环境Babel REPL里体验它是如何将ES6语法转义ES5语法的。
你可以在 Babel REPL 里测试一些简单的ES6代码片段。在浏览器中打开上述链接,在左侧面板中输入如下代码:
var name = "John", mood = "happy";
console.log(`Hey ${name}, are you feeling ${mood} today?`)
当你输入完毕后,就能在右侧面板中看到Babel转义ES6语法后的结果:
"use strict";
var name = "John",
mood = "happy";
console.log("Hey " + name + ",
are you feeling " + mood + " today?");
以上就是与我们编写的内容相对应的ES5语法的代码。Babel REPL为你提供了一个非常好的体验ES6新语法的环境。当然,在实际使用中,我们更希望Babel能够自动完成语法转义的工作。为此,你需要在你现有的项目或框架中安装Babel。
我们首先来安装Babel的命令行工具。在这里,我们假设你已经熟悉了node
和npm
的使用了。通过npm
来安装Babel非常简单。首先,创建好一个用来测试的工作目录babel_test
,然后通过npm init
命令来初始化项目,之后使用npm
来进行安装操作:
mkdir babel_test
cd babel_test && npm init
npm install --save-dev babel-cli
如果你对npm
比较熟悉的话,也可以选择全局安装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": "^6.10.1"
}
}
在开发依赖中看到Babel就证明安装成功了。你可以看到开发依赖中已经新增了版本高于6.10.1的Babel支持。现在你可以通过命令行使用Babel或者将它添加到你的项目打包命令中。对于所有的正式项目,你都需要使用后一种方法,在项目构建过程中调用Babel,你需要在package.json
文件中添加一步build
命令:
"scripts": {
"build": "babel src -d lib"
},
在你运行npm build
的时候,Babel会将你src
目录下的代码转义并保存在lib
目录中。当然,你也可以通过命令行来运行:
$ ./node_modules/.bin/babel src -d lib
在本书的后续内容当中,我们会详细介绍Babel的配置和使用。本节简单介绍了ES6。
在深入学习JavaScript之前,我们首先要了解一下“面向对象”的具体含义,以及这种编程风格的主要特征。我们列出了一系列在面向对象编程(Object-Oriented Programming,OOP)中最常用到的概念,具体如下:
现在,我们就来详细了解每个概念。当然,如果你在面向对象编程方面是一个新手,或者不能确定自己是否真的理解了这些概念,也不必过于担心。以后我们还会通过一些代码来为你具体分析它们。尽管这些概念说起来好像很复杂、很高级,但一旦我们进入真正的实践,往往就会简单得多。
既然这种编程风格称为面向对象,那么其重点就应该在对象上。而所谓对象,实质上就是指“事物”(包括人和物)在编程语言中的表现形式。这里的“事物”可以是任何东西(如某个客观存在的对象,或者某些较为抽象的概念)。例如,对于猫这种常见对象来说,我们可以看到它们具有某些明确的特征(如颜色、名字、体重等),能执行某些动作(如喵喵叫、睡觉、躲起来、逃跑等)。在OOP语义中,这些对象特征称为属性,而这些动作则称为方法。
此外,我们还有一个口语方面的类比[1]。
我们可以试一下。例如,在“The black cat sleeps on the mat”这个句子中,“The cat”(名词)是一个对象,“black”(形容词)则是一个颜色属性值,而“sleep”(动词)则代表一个动作,也就是OOP语义中的方法。甚至,为了进一步证明这种类比的合理性,我们也可以将句子中的“on the mat”看作动作“sleep”的一个限定条件,因此,它也可以被当作传递给sleep
方法的一个参数。
在现实生活中,相似对象之间往往都有一些共同的组成特征。例如,蜂鸟和老鹰都具有鸟类的特征,因此它们可以被统称为鸟类。在OOP中,类实际上就是对象的设计蓝图或制作配方。“对象”这个词,我们有时候也叫作“实例”,所以我们可以说老鹰是鸟类的一个实例[2]。我们可以基于同一个类创建出许多不同的对象。因为类更多地是一种模板,而对象则是在这些模板的基础上被创建出来的实体。
但我们要明白,JavaScript与C++或Java这种传统的面向对象语言不同,它实际上没有类。该语言的一切都是基于对象的,其依靠的是一套原型(prototype)系统。而原型本身实际上也是一种对象,我们后面会再来详细讨论这个问题。在传统的面向对象语言中,我们一般会这样描述自己的做法:“我基于Person
类创建了一个叫作Bob
的新对象。”而在这种基于原型的面向对象语言中,我们则要这样描述:“我将现有的Person
对象扩展成了一个叫作Bob
的新对象。”
封装是另一个与OOP相关的概念,其主要用于阐述对象中所包含的内容。对象通常包含(封装)两部分。
除此之外,封装这个术语中还有另一层信息隐藏的概念,这完全是另一方面的问题。因此,我们在理解这个概念时,必须要留意它在OOP中的具体语境。
以一个MP3播放器为例。如果我们假设它是一个对象,那么作为该对象的用户,我们无疑需要一些类似于像按钮、显示屏这样的工作接口。这些接口会帮助我们使用该对象(如播放歌曲之类)。至于它们内部是如何工作的,我们并不清楚,而且大多数情况下也不会关注。换句话说,这些接口的实现对我们来说是隐藏的。同样,在OOP中也是如此。当我们在代码中调用一个对象的方法时,无论该对象是来自我们自己的实现还是某个第三方库,我们都不需要知道该方法是如何工作的。在编译型语言中,我们甚至无法查看这些对象的工作代码。由于JavaScript是一种解释型语言,因此源代码是可以查看的,但至少在封装概念上它们是一致的,即我们只需要知道所操作对象的接口,而不必去关心它的具体实现。
关于信息隐藏,还有另一方面内容,即方法与属性的可见性。在某些语言中,我们能通过public
、private
、protected
这些关键字来限定方法和属性的可见性。这种限定分类定义了对象用户所能访问的层次。例如,private
方法只有其所在对象内部的代码才有权访问,而public
方法则是任何人都能访问的。在JavaScript中,尽管所有的方法和属性都是public
的,但是我们将会看到,该语言还是提供了一些隐藏数据的方法,以保护程序的隐蔽性。
聚合,有时候也叫作组合,实际上是指我们将几个现有对象合并成一个新对象的过程。通过聚合这种强有力的方法,我们可以将一个问题分解成多个较小的问题。当一个问题域的复杂程度令我们难以接受时,我们就可以考虑将它分解成若干子问题区,并且必要的话,这些子问题区还可以再继续分解成更小的分区。这样做有利于我们从几个不同的抽象层次来考虑这个问题。
例如,个人计算机是一个非常复杂的对象,我们不可能知道它启动时所发生的全部事情。但如果我们将这个问题的抽象级别降低到一定的程度,只关注它的几个组件对象的初始化工作,例如Monitor
对象、Mouse
对象、Keyboard
对象等,我们就很容易深入了解这些子对象情况。然后再将这些部分的结果合并起来,之前那个复杂问题就迎刃而解了。
我们还可以找到其他类似情况,例如Book对象
是由一个或多个Author
对象、一个Publisher
对象、若干Chapter
对象以及一个TOC
对象等组合(聚合)而成的对象。
通过继承这种方式,我们可以非常优雅地实现对现有代码的复用。例如,我们有一个叫作Person
的一般性对象,其中包含一些name
、date_of_birth
之类的属性,以及一些功能性函数,如walk
、talk
、sleep
、eat
等。然后,当我们发现自己需要一个Programmer
对象时,当然,这时候你可以再将Person
对象中所有的方法与属性重新实现一遍,但除此之外还有一种更聪明的做法,即我们可以让Programmer
继承自Person
,这样就省去了不少工作。因为Programmer
对象只需要实现属于它自己的那部分特殊功能(例如“编写代码”),而其余部分只需复用Person
的实现。
在传统的OOP环境中,继承通常指的是类与类之间的关系,但由于JavaScript中不存在类,因此它的继承只能发生在对象之间。
当一个对象继承自另一个对象时,通常会往其中加入新的方法,以扩展被继承的老对象。我们通常将这一过程称为“B继承自A”或者“B扩展自A”。另外对新对象来说,它也可以根据自己的需要,从继承的那组方法中选择几个来重新定义。这样做并不会改变对象的接口,因为其方法名是相同的,只不过当我们调用新对象时,该方法的行为与之前的不同了。我们将这种重新定义继承方法的过程叫作覆写。
在之前的例子中,我们的Programmer
对象继承了上一级对象Person
的所有方法。这意味着这两个对象都实现了talk
等方法。现在,我们的代码中有一个叫作Bob
的变量,即便是在我们不知道它是一个Person
对象还是一个Programmer
对象情况下,我们也依然可以直接调用该对象的talk
方法,而不必担心这会影响代码的正常工作。类似这种不同对象通过相同的方法调用来实现各自行为的能力,我们称为多态。
下面,让我们再来回顾一下这些概念(见表1-1)。
表1-1
特征描述 |
相应概念 |
---|---|
Bob是一个男人(后者是一个对象) |
对象 |
Bob出生于1980年6月1日、男性、黑头发 |
属性 |
Bob能吃饭、睡觉、喝水、做梦以及记录自己的年龄 |
方法 |
Bob是 |
传统OOP中的类 |
Bob是一个由 |
基于原型OOP中的原型对象 |
Bob对象中包含了数据(如 |
封装 |
我们不需要知道其记录年龄的方法是如何实现的。对象通常都可以拥有一些私有数据,例如闰年二月的天数,我们就不知道,而且也不会想知道 |
信息隐藏 |
Bob只是整个 |
聚合、组合 |
|
继承 |
我们可以随时调用Bob、Jill和Jack这3个对象各自的 |
多态、方法覆写 |
在这本书中,凡涉及代码的部分我们都强调“自己动手”,因为在我们的理念中,学好一门编程语言最好的途径就是不停地编写代码。因此,这里将不提供任何可供你直接复制/粘贴的代码下载。恰恰相反,我们必须让你亲自来输入代码,并观察它们是如何工作的,思考需要做哪些调整,这样周而复始地利用它们。因而,当你想尝试这些代码示例时,我们建议你使用JavaScript控制台这一类的工具。下面就让我们来看看这些工具是如何使用的。
对于开发人员来说,机器上应该大多已安装了一些Web浏览器了,例如Firefox、Safari、Chrome或Internet Explorer。而所有现代浏览器中都应该自带了JavaScript控制台组件,该组件是我们在阅读本书过程中始终会用到的东西,是帮助你进行语言学习和实验的环境。更具体地说,尽管本书用的是WebKit的控制台(Safari和Chrome都支持该控制台),但书中的这些示例在任何控制台上都是能正常工作的。
图1-1展示了如何在控制台中通过输入代码的方式将Google主页上的图标换成我们自己指定的图片。如你所见,我们可以在任何页面上测试这段JavaScript代码。
图1-1
在Chrome和Safari中,你可以通过右键单击相关页面,并选择“审查元素”来打开控制台。然后Web审查工具就会出现在下面的弹出窗口中。选择其标签栏上的“控制台”标签,就来到了真正的控制台界面中。
然后,我们直接在控制台中输入代码,按Enter键,代码就会被执行。其返回值也会在控制台中被打印出来。代码会在当前页面的环境中运行,所以,如果你在其中输入location.href
,控制台就会返回当前页面的URL。除此之外,该控制台还有一个自动完成功能,其工作方式与我们平时所用的操作系统命令行类似。例如,如果我们在其中输入docu
,然后按Tab键,docu
就会被自动补全为document
。这时如果再继续输入一个“.”(点操作符),我们就可以通过重复按Tab键的方式来遍历document
对象中所有可调用的方法和属性。
另外通过上下箭头键,我们还可以随时从相关列表中找回已经执行过的命令,并在控制台中重新执行它们。
通常情况下,控制台只提供单行输入,但我们可以用分号作为分隔符来执行多个JavaScript语句。如果你需要输入更多行代码的话,也可以通过按组合键Shift+Enter来实现换行,在这种情况下代码不会被立即执行。
在Mac上,我们不用浏览器也可以通过终端来执行JavaScript。
如果你之前没有使用过终端,可以通过Spotlight找到它。打开终端之后,在其中输入:
alias jsc='/System/Library/Frameworks/JavaScriptCore.framework/Versions/ Current/Resources/jsc'
该命令为JSC(即JavaScriptCore)设置了一个别名。JSC其实是WebKit引擎的一部分。Mac系统自带该引擎。
我们也可以直接将这个alias
命令放入~/.profile
文件,这样每次打开终端时,都可以通过jsc
这个别名来启动JavaScriptCore了。
现在,终端在任何目录下都可以通过直接输入jsc
来打开其交互环境了。然后你可以在其中输入相关的JavaScript表达式。当你按Enter键之后,表达式的结果就会被显示出来,如图1-2所示。
图1-2
如今,几乎所有现代浏览器都有自带的控制台。除了之前提到的Chrome及Safari的控制台,Firefox浏览器的所有版本也都能安装Firebug组件,该组件中也有一个控制台。另外,新版的Firefox中也有一个自带的控制台,你可以通过菜单栏的工具→Web开发者→Web控制台来打开它,如图1-3所示。
图1-3
Internet Explorer从第8版开始,只要按F12键就可以打开开发者工具组件。打开组件,单击Script标签栏就可进入控制台。
另外,通过Node.js
的交互环境来学习JavaScript也是一个不错的选择。你可以从Node官网 中获取并安装Node.js
,然后在终端中尝试其控制台,如图1-4所示。
图1-4
如你所见,我们既可以用Node.js
的控制台测试一些小型示例,也可以写一些较长的shell脚本(如图1-4中的test.js
),然后以scriptname.js
的形式在Node.js
的终端中执行。
Node REPL是非常强大的开发者工具,你可以在命令行里输入node
来开启它并测试JavaScript代码:
node
> console.log("Hellow World");
Hellow World
undefined
> a=10, b=10;
10
> console.log(a*b);
100
undefined
在这一章中,我们首先介绍了JavaScript语言的发展历程和现状。然后,对面向对象程序设计的概念进行了一些基本论述。接着,我们详细阐述了为什么JavaScript不是传统的基于类的面向对象语言,而是一套独特的原型系统。现在,你已经为下一步深入学习JavaScript语言、掌握其面向对象特性打下了一定的基础,但让我们一步步来。
在第2章中,我们将会介绍JavaScript的数据类型(JavaScript的数据类型非常少),以及条件、循环语句和数组。如果你确信自己已经掌握了这些知识,并且对该章末尾的那几个小练习完全没有疑问的话,那么就请自行跳过第2章吧。
[1] 这里应该特指英文环境,在中文的语言环境中,这种类比或许并不是太合适。——译者注
[2] 至少在中文环境中,老鹰更像是鸟类的一个子类。希望读者在理解对象与类的关系时,不要过分依赖这种类比。——译者注
在深入学习JavaScript的面向对象特性之前,我们首先要了解一些基础知识。在这一章中,我们将会从以下几个方面入手。
+
、-
、delete
、typeof
等。if...else
条件表达式等。通常,变量都是用来存储数据的,即它是存放具体数值的容器。当我们编写程序时,用变量来表示实际数据会更方便些。尤其是当我们需要多次使用某个数据时,使用变量pi
显然要比直接写数值3.141 592 653 589 793
方便得多。而且,之所以称它们为“变”量,就是因为它们所存储的数据在初始化之后仍然是可以改变的。另外,在编写代码时我们往往也可以用变量来代表某些程序运行前尚未知的数据,例如某个计算的结果。
变量的使用通常可分为以下两个步骤。
我们可以使用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语言中,变量名是区分大小写的。为了验证这一点,我们可以在JavaScript控制台中测试下列语句(每输入一行按一次Enter键):
var case_matters = 'lower';
var CASE_MATTERS = 'upper';
case_matters;
CASE_MATTERS;
为了减少按键的次数,在输入第三行时,我们可以先键入case
然后按Tab
键(或右方向键),控制台会自动将其补全为case_matters
。最后一行也是如此,我们只需先输入CASE
然后直接按Tab键。输入完成之后,最终结果如图2-1所示。
为方便起见,以后我们将用代码形式来代替截图。图2-1的例子可以表示如下:
> var case_matters = 'lower';
> var CASE_MATTERS = 'upper';
> case_matters;
"lower"
> CASE_MATTERS;
"upper"
如你所见,大于号(>)之后的内容就是我们输入的代码,而其余部分则是控制台输出的结果。需要强调的是,每当你看到此类示例时,强烈建议你亲自输入代码,然后可以进行一些实验性质的微调。这样才能有助于你更好地理解语言的工作方式。
图2-1
读者有时可能会看到某个表达式在控制台中的输出结果为undefined
。在大多数情况下这是完全可以忽略的。但你有没有想过,为什么这些表达式会输出undefined
呢?那是因为控制台在执行完我们输入的表达式之后,总是要输出该表达式的运行结果。但有一些表达式(例如var a = 1
;)是没有任何返回值的。在这种情况下,控制台就会隐式打印一个undefined
。相反,当一个表达式确实有返回值时,如之前的例子中的case_matters
或是1+1
之类的表达式,控制台就会将该表达式的实际返回值打印出来。当然,并不是所有的控制台都会在没有返回值时打印undefined
值,例如Firebug控制台就不会这样做。
所谓操作符,通常指的是能对一个或两个值(或变量)执行某种操作,并返回结果的符号。为了更清晰地表达该术语的含义,我们先来看一个具体的示例:
> 1 + 2;
3
这段代码包含了以下几点信息。
+
是一个操作符。1
和2
(输入值也称为操作数)。3
。1 + 2
这个整体称为表达式。在上述表达式中,1
和2
都是直接参与加法运算的。接下来我们要将它们换成变量,并再另外声明一个变量来存储运算结果。具体如下:
> var a = 1;
> var b = 2;
> a + 1;
2
> b + 2;
4
> a + b;
3
> var c = a + b;
> c;
3
在表2-1中,我们列出了一些基本的算术运算符。
表2-1
操作符 |
相关操作 |
代码示例 |
---|---|---|
|
加法运算 |
|
|
减法运算 |
|
|
乘法运算 |
|
|
除法运算 |
|
|
取模运算,即求除法运算的余数 |
取模运算对于测试一个整数的奇偶性很有用处,只需要让该数对2执行取模运算,返回1为奇数,返回0则为偶数 |
|
自增1运算 |
后置的++操作会先返回该值,然后再增1 前置的++操作会先将值增1,然后再返回 |
|
自减1运算 |
后置的― ―操作 前置的― ―操作 |
事实上,当我们输入var a = 1;
这样的语句时,所执行的也是一种独立的操作。这种操作是纯赋值操作,因而=
也称为简单赋值操作符(simple assignment operator)。
除此之外,JavaScript中还有一组由算术运算符和赋值操作符组合而成的操作符。我们称它们为复合操作符(compound operator)。这些操作符能让我们的代码显得更为紧凑。下面来看几个示例:
> 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中还有其他各种类型的操作符,我们将会在后面的章节中陆续看到。
最佳实践
表达式应始终是以分号为结束符的。尽管 JavaScript本身设有分号补全机制,即如果你忘了在某行表达式之后添加分号,该位置就会被隐式地补上一个分号,但这种机制同时也是出错的主要来源之一。所以,最好还是我们自己要记得在表达式结束之后明确地用分号来结束该表达式。换句话说,虽然> 1 + 1
与> 1 + 1;
都属于合法的表达式,但为了强调这一良好的编程习惯,本书将一律采用后一种形式。
我们在程序中所使用的任何值都是有类型的。JavaScript仅有以下几大基本数据类型。
1.数字——包括浮点数与整数,例如,1
、100
、3.14
。
2.字符串——包括由任意数量字符组成的序列,例如:"a"
、"one"
、"one 2 three"
。
3.布尔值——包括true
和false
。
4.undefined——当我们试图访问一个不存在的变量时,就会得到一个特殊值: undefined。除此之外,使用已声明却未赋值的变量也会如此,因为JavaScript会自动将变量在初始化之前的值设为undefined
。而undefined数据类型的值只有一个——undefined
。
5.null——这是另一种只包含一个值的特殊数据类型。所谓的null
值,通常是指没有值或空值,不代表任何东西。null与undefined最大的不同在于,被赋值为null
的变量通常被认为是已经定义了的,只不过它不代表任何东西。关于这一点,我们稍后会通过一些具体的示例来解释。
任何不属于上述5种基本数据类型的值都会被认为是一个对象。甚至有时候我们也会将null视为对象,这听起来有些尴尬——这是一个不代表任何东西的对象(东西)。我们将会在第4章中深入阐述对象的概念,现在我们只需要记住一点,JavaScript中的数据类型主要分为以下两个部分。
如果我们想知道某个变量或值的类型是什么,可以调用特殊操作符typeof
。该操作符会返回一个代表数据类型的字符串,以下是其可能返回的结果:
"number"
;"string"
;"boolean"
;"undefined"
;"object"
;"function"
。在接下来的几节中,我们将会在例子中逐一对5种基本数据类型使用typeof
操作。
最简单的数字类型当然就是整数了。如果我们将一个变量赋值为1
,并对其调用typeof
操作符,控制台就会返回字符串"number"
:
> var n = 1;
> typeof n;
"number"
> n = 1234;
> typeof n;
"number"
该例中有一点值得注意,即在第二次设置变量的值时,无须再用到var
语句了。
浮点数(即含小数部分的数字)显然也是数字类型的一种:
> var n2 = 1.23;
> typeof n2;
"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中,我们有好几种方式定义颜色,其中的两种方式如下。
rgb
(0,0,0)代表黑色、rgb(255,0,0)代表红色(红色达到最大值,而绿色和蓝色都为0值)。在JavaScript中,我们会用0x
前缀来表示一个十六进制值(hexadecimal value,简称为hex)。
> 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后面加1个0,也就是10
。同理,2e+3
的意思是在数字2后面加3个0,也就是2000
。例如:
> 1e1;
10
> 1e+1;
10
> 2e+3;
2000
> typeof 2e+3;
"number"
此外,我们也可以将2e+3理解为将数字2的小数点向右移三位。同理,2e-3
也就能被理解是将数字2的小数点向左移三位,如图2-2所示。
图2-2
例如:
> 2e-3;
0.002
> 123.456E-3;
0.123456
> typeof 2e-3;
"number"
在JavaScript中,还有一种称为Infinity
的特殊值。它所代表的是超出了JavaScript处理范围的数值。但Infinity
依然是一个数字,我们可以在控制台输入typeof
Infinity来证实
。当我们输入1e308
时,一切正常,但一旦将后面的308
改成309
就超出范围了。实践证明,JavaScript所能处理的最大值为1.7976931348623157e+308
,最小值为5e-324
。例如:
> Infinity;
Infinity
> typeof Infinity;
"number"
> 1e309;
Infinity
> 1e308;
1e+308
另外,任何数除以0的结果也为Infinity
:
> var a = 6 / 0;
> a;
Infinity
Infinity表示的是最大数(或者比最大数还要大的数),那么最小数该如何表示呢?答案是在Infinity之前加一个负号:
> var i = -Infinity;
> i;
-Infinity
> typeof i;
"number"
这是不是意味着我们可以得到双倍的Infinity
呢?毕竟我们可以从0加到Infinity
,也可以从0减到-Infinity
。事实上这是不可能的,因为即便将Infinity和-Infinity
相加,我们也不会得到0
,而是会得到一个称为NaN
(Not A Number的缩写,即不是数字)的东西。例如:
> Infinity - Infinity;
NaN
> -Infinity + Infinity;
NaN
另外,Infinity与其他任何操作数执行任何算术运算的结果也都等于Infinity
,例如:
> Infinity - 20;
Infinity
> -Infinity * 3;
-Infinity
> Infinity / 2;
Infinity
> Infinity – 99999999999999999;
Infinity
还记得之前见过的那个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
ES5中包含一个用来判断某个值是否为NaN
的全局方法isNaN()
。ES6提供了一个类似的方法Number.isNaN()
(要注意这不再是全局方法了)。
这两个方法之间的区别是,isNaN()
方法会在判断前将非数字类型的传入值进行类型转换。我们还是来看具体的示例,首先使用ES6提供的方法Number.isNaN()
来进行判断:
console.log(Number.isNaN('test')); //false : 字符串不是 NaN
console.log(Number.isNaN(123)); //false : 整数不是 NaN
console.log(Number.isNaN(NaN)); //true : NaN是NaN
console.log(Number.isNaN(123/'abc'));//true:123/'abc'结果是一个NaN
而ES5中的全局方法isNaN()
则会先尝试对传入值进行类型转换,之后再进行比较,所以结果可能会和上面的方法不同:
console.log(isNaN('test')); //true
总体上讲,Number.isNaN()
方法的判断结果要更准确一些。但是这两个方法都无法用来判断某个值是否为数字类型,它们只是将传入值与特殊值NaN
进行比较而已。如果你想进行准确的判断,Mozilla推荐使用以下的方法:
function isNumber(value) {
return typeof value==='number' && !Number.isNaN(value);
}
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"
像+
这样的双功能操作符可能会带来一些错误。因此,我们如果想执行拼接操作的话,最好确保其所有的操作数都是字符串。同样地,在执行数字相加操作时,我们也要确保其所有的操作数都是数字。至于如何做到这一点,我们将会在后续章节中详细讨论。
当我们将一个数字字符串用于算术运算中的操作数时,该字符串会在运算中被当作数字类型来使用。(由于加法操作符的歧义性,这条规则不适用于加法运算。)例如:
> 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"
在表2-2中,我们列出了一些具有特殊含义的字符串。
表2-2
字符串 |
含 义 |
示 例 |
---|---|---|
|
|
这样做是错误的,因为JavaScript会将I don 视为字符串,而其余部分则将会被视为无效代码。正确做法如下: 转义转义字符本身: |
|
换行符 |
|
|
回车符 |
以下所有语句: 结果都为: |
|
制表符 |
|
|
|
下面是作者的名字在保加利亚语中用西里尔字母的拼写: |
除此之外,还有一些很少被使用的特殊字符,例如:\b
(退格符)、\v
(纵向制表符)、\f
(换页符)等。
ES6中加入了模板字符串功能。如果你对其他一些编程语言有所了解,那么你应该知道,ES6中的模板字符串功能与Perl或Python当中的模板字符串功能类似。模板字符串允许你在通常的字符串之间插入表达式。关于模板字符串ES6提供了两种字面量:模板字面量和标签字面量。
模板字面量(template literal)是指嵌有表达式的单行或多行的字符串。例如,你肯定曾经写过类似的代码:
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提供的另外一种字面量称为标签字面量(tagged template literal)。标签字面量允许你用自定义的函数来格式化模板字符串。你可以通过在模板字符串之前添加函数名作为前缀来使用标签字面量,这一前缀表示将要被调用的函数。标签字面量中的函数需要提前定义才可使用,例如:
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}`
在标签函数中你能够获取到模板字符串的两种形式:
你可以通过raw
属性来获取模板字符串的原始内容:
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"
JavaScript中有3种逻辑运算符,它们都属于布尔运算,分别是:
!
——逻辑非(取反);&&
——逻辑与;||
——逻辑或。我们知道,如果某事为非真,它就为假。在JavaScript中,如果我们想描述某事物的非真状态,就可以考虑使用逻辑非运算符:
> var b = !true;
> b;
false
而如果我们对true
执行两次逻辑非运算的话,其结果就等于原值:
> var b = !!true;
> b;
true
如果我们对一个非布尔值执行逻辑运算,那么该值就会在计算过程中被转换为布尔值:
> var b = "one";
> !b;
false
如你所见,上例中的字符串"one"
是先被转换为布尔值true
再取反的,结果为false
。如果我们对它取反两次,结果就为true
。例如:
> var b = "one";
> !!b;
true
借助双重取反操作,我们可以很容易地将任何值转换为相应的布尔值。理解各种类型的值转换为相应布尔值的规则非常重要。除了下面所列出的特定值(它们将被转换为false
),其余大部分值在转换为布尔值时都为true
。
""
。null
。undefined
。0
。NaN
。false
。这6个值有时也会被我们称为falsy值,而其他值则被称为truthy值(包括字符串"0" " " "false"
等)。
接下来,让我们来看看另外两个操作符——逻辑与(&&
)和逻辑或(||
)的使用示例。当我们使用&&
时,当且仅当该操作的所有操作数为true
时,结果才为true
。而||
则只需要其中至少有一个操作数为true
,结果即为true
,例如:
> var b1 = true, b2 = false;
> b1 || b2;
true
> b1 && b2;
false
在表2-3中,我们列出了所有可能的情况及其相应结果。
表2-3
操 作 |
结 果 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
当然,我们也能连续执行若干逻辑操作,例如:
> 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
在这里,所有的逻辑或运算符的优先级都是相同的,只要其中任何一个操作数为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
(或者是6个falsy值中的任何一个),这段代码就不太可能如我们所愿了,例如:
> var mynumber = 0;
> var mynumber = mynumber || 10;
> mynumber;
10
在JavaScript中,还有另外一组以布尔值为返回值类型的操作符,即比较运算符。下面让我们通过表2-4来了解一下它们以及相关的示例。
表2-4
操作符 |
操作说明 |
代码示例 |
---|---|---|
|
相等运算符 当两个操作数相等时返回 |
|
|
严格相等运算符 当且仅当两个操作数的值相等且类型相同时返回 |
|
|
不相等运算符 当两个操作数不相等时返回 |
|
|
严格不相等运算符 当两个操作数的值不相等或类型不相同时返回 |
|
|
当且仅当左操作数大于右操作数时返回 |
|
|
当且仅当左操作数大于或等于右操作数时返回 |
|
|
当且仅当左操作数小于右操作数时返回 |
|
|
当且仅当左操作数小于或等于右操作数时返回 |
|
还有一件有趣的事情要提醒读者注意:NaN
不等于任何东西,甚至不等于它自己。例如:
> NaN == NaN;
false
当我们尝试使用一个不存在的变量时,控制台中就会产生以下错误消息:
> 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"
尽管undefined
和null
之间的差别微乎其微,但有时候很重要。例如,当我们对其分别执行某种算术运算时,结果就会截然不同:
> var i = 1 + undefined;
> i;
NaN
> var i = 1 + null;
> i;
1
这是因为null
和undefined
在被转换为其他基本类型时,其转换方法存在一定的区别,下面我们给出一些可能的转换类型。
> 1 * undefined;
> 1 * null;
0
> !!undefined;
false
> !!null;
false
> "value: " + null;
"value: null"
> "value: " + undefined;
"value: undefined"
ES6中引入了Symbol这种新的基本类型,其他的一些编程语言中也有类似的定义。Symbol看起来很像普通的字符串类型,但两者之间还是有许多显著的区别,我们来看示例:
var atom = Symbol()
注意,这里你并不需要使用new
关键字来初始化一个Symbol类型的变量,当你这样做时,你会得到一个错误:
var atom = new Symbol() // Symbol不是构造函数
Symbol同样也支持传参:
var atom = Symbol('atomic symbol')
当我们在一个大型项目中进行调试时,使用Symbol是非常有帮助的。
Symbol最重要的一个属性就是它是唯一且不可变的(这也是它存在的意义):
console.log(Symbol() === Symbol()) // false
console.log(Symbol('atom') === Symbol('atom')) // false
有关Symbol我们就先讨论到这里,在使用时你可以把Symbol类型的值作为属性键值或者唯一标识符。在本书的后续内容中我们会进行进一步的介绍。
现在,让我们来快速汇总一下目前为止所讨论过的内容。
NaN
、Infinity
、-Infinity
。true
和false
。null
。undefined
。true
,但以下6种falsy值除外:
" "
;null
;undefined
;0
;NaN
;false
。现在,我们对JavaScript中的基本数据类型已经有了一定的了解,是时候将注意力转向更有趣的数据结构——数组了。
那么究竟什么是数组呢?简而言之,它就是一个用于存储数据的列表。与一次只能存储一个数据的变量不同,我们可以用数组来存储任意数量的元素值。
我们可以用一对不包括任何内容的方括号来声明一个空数组变量,例如:
> var a = [];
如果我们想要定义一个有3个元素的数组,则可以这样做:
> var a = [1,2,3];
只要在控制台中输入相应的数组名,就能打印出该数组中的所有内容:
> a;
[1, 2, 3]
现在的问题是,我们应该如何访问数组中的各个元素呢?通常,元素在数组中的索引(下标)是从0开始编号的。也就是说,数组中首元素的索引(或者说位置)应该是0,第二个元素的索引是1,以此类推。表2-5中所展示的就是之前那个有3个元素的数组中的具体情况。
表2-5
索引 |
值 |
---|---|
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] = 'new';
"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]]
在该数组中,首元素a[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才被官方正式认为是标准的一部分。
除此之外,数组的使用方法还有很多(我们将会在第4章中详细介绍),现在先到此为止,请记住以下内容。
条件表达式是一种简单而强大的控制形式,它能够帮助我们控制一小段代码的执行走向。而循环则允许我们以较少的代码重复执行某个操作。接下来,我们将会学习以下内容。
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;
}
}
}
最佳实践
正如我们在之前内容中介绍过的那样,推荐在每一行代码之后加上分号。虽然当每行代码只包含单个表达式的时候不加分号也不会影响代码的语义,但使用分号仍是一种良好的开发习惯。每行只书写单个表达式,且用分号分隔,才能使代码具有良好的可读性。
对大括号内的代码使用缩进。一部分开发者喜欢使用单个制表符缩进,也有部分开发者使用4个空格或2个空格缩进。只要你的代码缩进方式保持一致,具体使用哪种缩进都不是问题。在前面的示例当中,最外层代码块使用2个空格缩进,里面一层使用4个空格缩进,最里层使用6个空格缩进。
始终使用大括号。虽然当某个代码块只包含一个表达式时可以省略大括号,但为了保持代码的可读性和可维护性,你应当保持一致使用大括号。
让我们先来看一个简单的if
条件表达式:
var result = '',a = 3;
if (a > 2) {
result = 'a is greater than 2';
}
如你所见,该表达式通常主要由以下几个部分组成:
if
语句;a
是否大于2
”;{}
内的代码块,这是当if
条件满足时该程序所要执行的部分。其中,条件部分(即括号内的部分)通常会返回布尔值,主要有以下几种形式:
!
、&&
、||
等;===
、!=
、>
等;除此之外,if
表达式中还可以有一个可选项,即else
。如果条件部分的表达式返回false
的话,我们可以执行后面else
子句中的代码块。例如:
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
代码块中再内嵌一个新的条件语句:
if (a === 1) {
if (b === 2) {
result = 'a is 1 and b is 2';
} else {
result = 'a is 1 but b is definitery 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 is not defined”。作为一个JavaScript高手,你肯定不会希望自己的代码有如此表现。其次,就算if(somevar)
返回的是false
,也并不意味着somevar
就一定没有定义,它也可以是任何一种被初始化为falsy
值(如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表达式。例如下面这段代码:
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
这里的?:操作符叫作三元运算符,因为它需要3个操作数。
当我们发现自己在if
表达式中使用了太多的else if
子句时,就应该考虑用switch
语句来替代if
语句了:
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
标记的默认条件代码块是可选的。如果其他case
条件都不为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
后面的代码相对于case
缩进。当然你也可以将case
相对于switch
缩进,但这样其实不会增加代码的可读性。
有时候,我们会希望故意省略一些break
语句,当然,这种叫作贯穿(fallthrough)的做法在实际应用中并不常见,因为它通常会被误认为是人为的遗漏,故而使用时往往需要在文档中加以说明。但从另一方面来说,如果我们确实有意让两个相邻的case
语句共享同一段代码的话,这样做并没有什么不妥。只不过,这不能改变相关的规则,即如果执行代码是写在case
语句之后的话,它依然应该以break
结尾。另外在缩进方面,break
是选择与case
对齐还是与相关的代码块对齐,完全取决于个人喜好,只要保持风格的一致性即可。
尽量使用default
条件,因为这可以使我们在switch
找不到任何匹配的情况下,依然能返回一些有意义的结果。
通过if…else
和switch
语句,我们可以在代码中采取不同的执行路径,好比我们处于十字路口时,可以根据某个具体的条件来选择自己的走向。然而,循环就完全是另一回事了,我们可以利用它使代码在返回主路径之前先去执行某些重复操作。至于重复的次数,则完全取决于我们设定在每次迭代之前(或之后)的条件值。
比如说,我们的程序通常都是在A点和B点之间运行,如果我们在这之间设置了一个条件C,而这个条件的值将会决定我们是否要进入循环L。我们进行一次迭代,然后再次回到C。一旦进入了循环,我们就必须在每次迭代完成之后对该条件进行重新求值,以判断是否要执行下一次迭代。总之,我们最终还是会回到通往B点的路径上来的。上述过程如图2-3所示。
图2-3
当某循环的条件永远为true
时,它就成了一个无限循环。这意味着代码将会被“永远”困在循环中。这无疑是一个逻辑上的错误,我们必须对此加以防范。
在JavaScript中,循环主要有以下4种类型:
while
循环;do...while
循环;for
循环;for...in
循环。while
循环是最简单的一种循环,它们通常是这样的:
var i = 0;
while (i < 10) {
i++;
}
while
语句主要分为两个部分:小括号中的条件和大括号中的代码块。当且仅当条件值为true
时,代码块才会被反复执行。
do...while
循环实际上是while
循环的一种轻微的变形,示例如下:
var i = 0;
do {
i++;
} while (i < 10);
在这里,do
语句后面先出现的是代码块,然后才是条件。条件出现在代码块之后,这意味着代码块无论如何都会被执行一次,然后再去对条件部分进行求值。
如果我们将上面两个示例中的i
初始化为11
而不是0
的话,第一个例子(while
循环)中的代码块将不会被执行,i
最终的值仍然是11
。而第二个例子(do…while
循环)中的代码块将会被执行一次,i
的值也会变为12
。
for循环
是使用最为广泛的循环类型,也是我们最应该掌握的内容。实际上,这也只需要掌握一点点语法知识。for
循环如图2-4所示。
图2-4
在条件C和代码块L的基础上,我们还需要增加以下两部分的内容。
最常用的for
循环模式主要包括以下内容。
i
。i
与循环边界值进行比对,如i < 100
。i
自增1
,如i++
。下面来看一个具体示例:
var punishment = '';
for (var i = 0; i < 100; i++) {
punishment += 'I will never do this again, ';
}
实际上,这3部分(初始化、循环条件、自增操作)都可以写成用逗号分隔的多重表达式。例如,我们可以重写一遍上面的例子,在其初始化部分中增加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, '){
// 这里没有内容
}
事实上,这3部分都是可选的,上面的例子也完全可以写成下面这样:
var i = 0, punishment = '';
for (;;) {
punishment += 'I will never do this again, ';
if (++i == 100) {
break;
}
}
尽管代码重写之后的工作方式与原来的相同,但它显得更长,可读性也更差了。我们完全可以用while
循环来取代它。但for
循环可以使代码更紧凑、更严谨。它的3个部分(初始化、循环条件、自增操作)泾渭分明,语法也更为纯粹。这些都有利于我们厘清程序的逻辑,从而避免类似于无限循环这样的麻烦。
另外,for
循环还可以嵌套。下面,我们来看一个嵌套循环的具体示例。假设要打印一个10行10列的星号字符串,那么我们就可以用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
循环的数组。
在下面的示例中,我们将遍历数组中的所有元素,并打印出当前所在的索引和元素值,如下:
//例子仅供参考
//for...in循环用于对象
//通常更适合用于数组
var a = [ 'a', 'b', 'c', 'x', 'y', 'z'];
var result = '\n';
for (var i in a) {
result += 'index: ' + i + ', value: ' + a[i] + '\n';
}
结果如下:
"
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引擎自动忽略,因此它们不会对程序产生任何影响。而当你几个月后重新访问这段代码,或将其转让给其他人维护时,这些注释就会显得非常重要。
注释的形式主要有以下两种。
//
开头并直至该行结束。/*
开头,并以*/
结尾,其中可以包括一行或多行内容。但要记住,注释首尾符之间的任何代码都将会被忽略。具体示例如下:
// 行开头
var a = 1; // anywhere on the line
/* 单行注释 */
/*
跨越多行的注释
*/
甚至,有些实用工具(例如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++;
(2)执行下面的语句后,v
的值会是什么?
> var v = v || 10;
如果将v
分别设置为100
、0
、null
,结果又是什么?
(3)编写一个打印乘法口诀表的脚本程序。提示:使用嵌套循环来实现。
在这一章中,我们学习了编写一个JavaScript程序所需要的基本组件。现在,你应该已经掌握了以下几种基本数据类型。
undefined
。null
。你也已经了解了一些基本的操作符。
+
、-
、*
、/
、%
。++
、--
。=
、+=
、-=
、*=
、/=
、%=
。typeof
、delete
。&&
、||
、!
。==
、===
、!=
、!==
、<
、>
、>=
、<=
。另外,我们还学习了如何使用数组来存储和访问数据。最后,我们还介绍了几种不同的控制程序流程的方法——条件(if...else
和switch
)和循环(while
、do...while
、for
、for...in
)。
本章的信息量确实不小,因此我们建议你通过练习巩固一下。在继续深入下一章的学习之前,我们需要给自己一些鼓励。
[1]三原色模式(RGB color model)是一种加色模型,指用3种原色──红色、绿色和蓝色的色光以不同的比例相加,可产生多种多样的色光。——译者注