JavaScript 程序员参考(一)
零、简介
在过去的十年中,JavaScript 的受欢迎程度有了很大的提高。JavaScript 最初用于创建交互式网页和处理基本的表单验证,现在是许多复杂 web 应用的主干。因此,能够很好地使用 JavaScript 编程的人在各种项目中都有很高的需求。如果你想从事网络技术,你应该了解 JavaScript。
本书旨在为 JavaScript 提供完整的参考,并涵盖该语言的基础知识。我们的总体目标是涵盖在任何规模的项目中使用 JavaScript 所需的所有主题。
这本书是给谁的?
这本书面向两类读者:一类是已经了解 JavaScript 并需要可靠参考的人,另一类是刚刚开始学习该语言并希望快速上手的人。在这两种情况下,我们都假设你至少有一个基本的编程背景。特别是第一章,假设你是从更传统的语言,如 C++或 Java,学习 JavaScript。
我们还假设您对 HTML 有基本的了解,包括语义标记和各种文档类型声明——尽管整本书中使用 HTML 的示例都是用 HTML 5 编写的。我们还假设您对 CSS 以及如何使用它来管理网页外观有基本的了解。
最后,我们假设您对 web 及其底层协议有基本的了解。
如果你一生中从未写过一行代码,或者如果你是 web 技术的新手,这可能不是最适合你的书。但是只要你对编程和 web 技术有基本的了解,这本书就可以帮助你学习 JavaScript。
概观
这本书分为两部分。第一部分致力于讲授 JavaScript 及其相关技术的基础知识。第二部分是参考文献。
- 第一章 针对的是从另一种语言来到 JavaScript 的程序员。JavaScript 是一种比大多数通用语言更加动态的语言,从这些语言迁移到 JavaScript 会带来特殊的挑战。首先,我们介绍 JavaScript 是什么以及它是如何产生的,然后我们深入探讨其他语言的程序员遇到的三个主要挑战:JavaScript 的对象继承和类的缺乏、它的作用域规则以及它的动态类型。所有这些特性在 JavaScript 中的工作方式与在其他语言中有很大不同,我们希望立即了解它们。我们通过提供一些使用我们所学的 JavaScript 中的常见模式来结束这一章。
- 第二章 是对 JavaScript 语言的总体参考。我们从头开始,从 JavaScript 的词汇结构开始,然后快速进入它的操作符,它如何处理变量,JavaScript 对对象、数组和函数的处理。我们通过查看 JavaScript 的流控制语句来结束这一章。第二章更详细地讲述了第一章中提到的一些事情。它们共同构成了对这门语言的坚实介绍,从基础到中间概念,如闭包。
- 第三章 涵盖了文档对象模型。虽然从技术上来说 DOM 不是 JavaScript 的一部分,但是我们在其中包含了一章,因为您将使用 JavaScript 完成的大量工作很可能会涉及到 DOM。这一章从 DOM 标准的简史和它是如何发展的开始。然后我们深入到细节:如何访问页面元素,如何操作它们(包括创建新元素和删除现有元素),以及 DOM 提供的事件模型(包括定制事件)。我们以跨浏览器策略的讨论来结束这一章,这些策略用于处理不同浏览器之间 DOM 实现的差异。
- 第四章 把我们在第一章、第二章、第三章中学到的东西都拿来用。我们已经把这一章分成了几节,每一节都涵盖了不同的内容。第一部分,使用 JavaScript,涵盖了使用 JavaScript 需要的东西。我们涵盖了基本的工作流程以及工具和调试技术。第二部分通过仔细研究浏览器如何加载和解析脚本,以及如何利用这一点来提高 JavaScript 应用的效率。第三部分介绍使用 XMLHTTP 对象的异步通信——也称为 AJAX。第四部分介绍了浏览器强加的一个重要的安全限制——单源策略——以及使用该策略并完成工作的一些技巧。在第五节中,我们提供了一个数据缓存的实际例子。第六部分是关于选择 JavaScript 库,第七部分涵盖了最流行的 JavaScript 库 jQuery。最后,我们用一个实际的例子来结束这一章,这个例子使用我们在这一章中学到的所有东西来构建你自己的库。
- 第五章 从本书的参考部分开始,涵盖了 JavaScript 的一部分对象。
- 第六章 为 JavaScript 的控制语句提供了参考。
- 第七章 讲的都是 JavaScript 操作符。
- 第八章 是 DOM 引用。
尽管它们是参考章节,但我们始终试图提供有用的、重要的示例。
本书中使用的约定
在本书中,代码以fixed-width font的形式呈现。代码示例和语法定义与其他文本分开,并使用相同的字体。此外,代码元素(如对象、原始值等)的内联引用也以相同的字体显示。
代码下载
所有的代码片段和例子都可以从http://www.apress.com/9781430246299下载。该下载包括书中所有的示例代码,以及一些没有包含在书中的额外代码。我们鼓励您下载代码,并在阅读文本时使用它。
一、JavaScript 基础知识
在这一章中,我们将采用一种不同于大多数编程语言参考资料的第一章的方法。大多数书都会深入到语言的语法和其他细节,但是我们在这里不打算这样做。JavaScript 是一种非常难学的语言,也是一种相对容易让人讨厌的语言,所以我们首先想探究为什么有些人会纠结于它,然后我们将提供一种不同的、更直观的方法来掌握这门语言。
我们将从研究学习和使用 JavaScript 的挑战开始。我们将通过研究该语言的进化史和实现来介绍一些背景知识。然后,有了这些信息,我们将研究 JavaScript 面临挑战的三个特定领域:继承隐喻 、作用域隐喻和类型隐喻。最后,我们将研究 JavaScript 中两种非常常见的模式——这是一个大多数书籍直到很久以后才涉及的主题,但是我们认为在本章结束时,您已经为处理这个主题做好了充分的准备。这些模式也是你在本章中学到的所有东西的很好的应用。
当我们阅读本章时,我们将讨论 JavaScript 的基本框架,但是我们鼓励你在这个阶段不要在语法和其他细节的考虑上陷入太多。我们将在后面的章节中讨论这些主题。现在,专注于我们将要描绘的更大的图景。
难学,更难爱
JavaScript 是很多人憎恨的目标。如果你在你最喜欢的搜索引擎中输入“讨厌 JavaScript”或“JavaScript 烂透了”,你会立即得到一页又一页的关于为什么这种语言很糟糕的文章。你可以自己阅读这些文章——我们鼓励你这样做——但是在阅读了几篇之后,你会注意到投诉中出现的一种模式。人们不喜欢 JavaScript 的几个关键点是:
- 它的对象和继承的实现——原型与类
- 其范围规则
- 对数据类型的处理
这是真的,JavaScript 做这三件事与许多普通语言完全不同。更糟糕的是,JavaScript 采用了类似于 C 或 Java 的语法和结构,这助长了一种可以理解的期望,即 JavaScript 应该像 C 或 Java 那样运行,但事实并非如此。(这是 JavaScript 作用域规则的一个特殊问题,我们将在本章后面更详细地讨论。)
还有,因为 JavaScript 很像 C,一个熟悉类 C 语言 (C,C++,Java,C#等)的程序员。)可以快速轻松地达到精通 JavaScript 的水平,而无需真正理解其内部工作原理。经常会遇到一些有才华的开发人员,他们已经使用 JavaScript 很多年了(甚至可能认为自己是 JavaScript 专家),但是他们对这种语言只有基本的了解,对它的真正力量几乎没有掌握。
所以 JavaScript 很容易被误解,很难掌握,并且三个重要的语言特性的实现有很大的不同。再加上不同浏览器之间不同的实现,难怪人们对这种语言评价不高。
以免我们把您从语言中吓跑,重要的是要认识到,很多时候这种低评价是由于误解了 JavaScript 的工作方式,或者试图应用其他语言的实践,而这些实践并不能很好地映射到 JavaScript 的行为。我们发现,开发人员越愿意学习 JavaScript,他们就越欣赏它。当然,这在某种程度上对任何语言都是正确的,但对 JavaScript 尤其如此。它的动态本质和真正的功能很难理解,但是一旦你理解了它,这种语言就开始呈现出很少语言所具有的美丽和简单。
我们教授 JavaScript 的方法旨在帮助您在我们开始讲述函数、数组和流控制等细节之前就形成对 JavaScript 的理解。我们也将非常详细地介绍这些内容,但在此之前,我们想正面解决人们对 JavaScript 感到困惑或困难的主要问题。这样做,我们希望你开始掌握 JavaScript 的旅程。精通的第一步是理解 JavaScript 的起源及其持续的发展。
JavaScript 是什么?
JavaScript 是一种编程语言,于 1995 年首次发布。尽管有它的名字,JavaScript 实际上和 Java 编程语言没有任何关系。从高层次来看,JavaScript 有几个显著的特性:
- 它是一种脚本语言 : JavaScript 程序是由解释器(或引擎)读取并执行的“脚本”。这与编译语言不同,在编译语言中,程序由编译器读取并翻译成可执行文件。(注意,JavaScript 引擎本身通常是用编译语言编写的。)用脚本语言编写的程序具有高度的可移植性,因为它们可以在任何已经为该语言构建了解释器的环境中运行。
- 是类 C:JavaScript 的基本语法和结构大量借鉴 C。
- 它是一种面向对象的语言:JavaScript 不同于大多数面向对象的语言,因为它的继承模型是基于原型的,而不是基于类的。
- 拥有一级函数 : JavaScript 函数是羽翼丰满的对象,有自己的属性和方法,可以作为参数传入其他函数,也可以从其他函数返回,赋给变量。
- 它是动态的:“动态编程语言”这个术语很宽泛,涵盖了很多特性。JavaScript 最动态的特性是其变量类型的实现(见下一点)及其
eval()方法和其他功能方面。 - 既有动态类型又有弱类型 : JavaScript 变量在解释时不进行类型检查(使 JavaScript 成为动态类型语言),混合类型的操作数之间如何发生运算取决于 JavaScript 内部的特定规则(使 JavaScript 成为弱类型语言)。
- 它是一个标准的实现:正如下一节所描述的,JavaScript 实际上是 ECMA-262 标准的一个实现,就像 C 编程语言受 ISO 标准管理一样。
这些主要特性结合起来使 JavaScript 变得有些独特。如果您对类似 C 的语言稍有了解,它们也有助于使 JavaScript 基础知识变得相当容易学习,因为您对 JavaScript 的语法或结构不会有什么问题。
JavaScript 也深受 Scheme 的影响,Scheme 是另一种函数式编程语言,是 Lisp 的一种方言。JavaScript 的许多设计原则都来自 Scheme,包括它的作用域。
那么 JavaScript 是如何拥有这种独特的特性组合的呢?
JavaScript 和 ECMA-262 标准的发展
如前所述,JavaScript 实际上是一个标准的实现。不过,事情并不是那样开始的。1995 年 9 月,Netscape 发布了 Navigator 浏览器的 2.0 版本,它有一个新的特性:一种面向对象的脚本语言,可以访问和操作页面元素。这种新的脚本语言由网景工程师 Brendan Eich 创建,最初代号为“Mocha”,最初发布时名为“LiveScript”此后不久,它被重新命名为“JavaScript”,以借助 Sun 的 Java 编程语言。
1996 年,网景向欧洲计算机制造商协会(或简称 ECMA )提交了 JavaScript 作为标准考虑,见http://www.ecma-international.org/memento/history.htm。由此产生的标准 ECMA-262 于 1997 年 6 月被采用。ECMA-262 恰当地定义了 ECMAScript 脚本语言,JavaScript 被认为是 ECMAScript 的一种“方言”。ECMAScript 的另一个值得注意的方言是 ActionScript 的版本 3 或更高版本。从技术上讲,Internet Explorer 不实现 JavaScript(出于版权考虑),而是实现微软自己的 ECMAScript 方言“JScript”
ECMAScript 的最新版本是 5.1,发布于 2011 年 6 月。从 ECMAScript 3 到 ECMAScript 5 的版本轨迹有一段有趣的政治历史,包括标准委员会(由 Brendan Eich 领导)和雅虎、微软和谷歌等行业利益相关者之间的分歧。我们不打算深入细节;可以说,最终各方都同意将 ECMAScript 5 作为一个统一的解决方案。
作为 ECMAScript 5 的一部分,ECMA International 发布了一套可由任何浏览器运行的一致性测试,并将显示浏览器支持哪些 ECMAScript 5 功能以及不支持哪些功能。该套件名为 Test262,可在http://test262.ecmascript.org/获得。请注意,运行全套测试可能需要几个小时,其中包含大约 11,500 个单独的测试。截至本文撰写时,没有一款浏览器在 Test262 中获得满分;目前最好的成绩属于 Safari 和 Internet Explorer,两者都只有 7 项测试不及格。Firefox 得分最低,目前未能通过 170 项测试(尽管这仍然是一个令人印象深刻的成就)。这些数字是在撰写本文时的数据,从现在到发表之前很可能会发生变化。我们鼓励您在自己喜欢的浏览器上运行测试套件,并探索在每种浏览器中失败的测试。这将让您对不同浏览器之间 JavaScript 实现的差异有所了解,以及它们到底有多小。
ECMAScript 的发展从第 6 版开始,代号为 ECMAScript Harmony。Harmony 还没有正式发布,在撰写本文时,还没有官方批准的发布日期。然而,规范草案都在http://wiki.ecmascript.org/doku.php?id=harmony:specification_drafts对公众开放,快速浏览它们表明,Harmony 将包含几个新特性,其中包括类的语法实现、函数的默认参数、新的字符串方法,以及在Math库中添加双曲三角函数。
许多浏览器制造商已经实现了一些 Harmony 特性,但是整体实现参差不齐,并且因制造商而异。在本书的大部分内容中,我们将把 JavaScript 作为 ECMAScript 5.1 的一种方言。在 ECMAScript 5 和 Harmony 重叠的地方,我们会注意到它们的区别,这样您就可以意识到潜在的支持陷阱。此外,在本书中,我们将使用“JavaScript”作为该语言及其实现的通用术语,除非我们需要引用特定的实现或标准本身。
由于 ECMA-262 的标准化影响,所有 JavaScript 的现代实现都非常相似。个别的实现会有所不同,特别是对于前沿的特性,但是核心标准是很好实现的。
JavaScript 实现 s
JavaScript 有几种不同的实现方式。例如,Adobe 的 Acrobat 文档系统实现了一个 JavaScript 版本,使用户能够在 Acrobat 文档中使用简单的脚本。JavaScript 引擎作为独立资源在 Windows、UNIX 和 Linux 上实现已经有一段时间了。在 1995 年首次引入 JavaScript 后不久,Netscape 在其企业服务器中包含了 JavaScript 的服务器端实现。今天,服务器端 JavaScript 最显著的实现是在 Node.js 软件系统中。
到目前为止,JavaScript 最常见的实现是在 web 浏览器中。web 浏览器的 JavaScript 引擎通常实现 ECMA-262 标准中规定的大多数功能。此外,浏览器经常用 ECMA 标准没有规定的其他特性来扩展 JavaScript。这些扩展中最值得注意的是文档对象模型(DOM ),它是由万维网联盟(W3C)维护的一个独立标准。重要的是要记住,DOM 和 JavaScript 是分开的、独立的标准,尽管 JavaScript 在浏览器中做的大部分工作都涉及到操纵 DOM。我们将在第三章更深入地讨论 DOM。
尽管 JavaScript 最初是一种基于浏览器的脚本语言,但是 JavaScript 的服务器端实现变得越来越普遍。在服务器端,JavaScript 实现将包括 ECMA-262 的大部分基本特性。而且,像基于浏览器的实现一样,服务器实现可以用其他特性扩展 JavaScript,比如库或框架。尽管服务器和浏览器的 JavaScript 实现在这些扩展特性上可能有所不同,但基本特性是相同的:无论是在浏览器中还是在服务器上实现,JavaScript Array对象都具有相同的方法和属性(当然,假设实现遵循 ECMA 标准)。
这使得您的 JavaScript 技能特别有价值。JavaScript 是少数同时拥有客户端和服务器端实现的语言之一,因此学习 JavaScript 是一项不错的投资。通过在服务器端使用 Node.js 以及客户端脚本,可以使用 JavaScript 作为主要语言来构建复杂的、数据驱动的应用,并提供丰富的用户交互。
可能在客户端和服务器端都使用 JavaScript 的两个最好的例子是微软的 Windows Azure 平台和 Windows 8 的 Windows 软件开发工具包(Windows SDK)。它们都支持使用 JavaScript 进行后端和前端实现,这使得用 JavaScript 构建 Windows 应用和利用微软平台的所有功能成为可能。
在本书中,我们不会讨论使用 Node.js 的服务器端 JavaScript,而是将重点放在 web 浏览器环境中的 JavaScript。
网络浏览器和 JavaScript
现代网络浏览器是复杂的软件。大多数人认为 web 浏览器是内容浏览器,可以说是“Web 上的窗口”。然而,对于理解 JavaScript 的程序员来说,web 浏览器变得更加强大:用户界面(UI) 平台。无论你是创建一个简单的网页还是一个复杂的数据驱动的应用,浏览器都是你的 UI 平台,JavaScript 是它使用的语言。
JavaScript 只是浏览器众多活动部件中的一个。在很高的层面上,浏览器由一堆独立的子程序(或称引擎)组成,每个子程序都有一个重要的功能:
- UI 引擎:呈现给用户的实际可视界面,有地址栏、渲染窗口、后退和前进按钮、书签工具栏等等。
- 浏览器引擎:在 UI 层和渲染引擎之间工作的控制器。
- 渲染引擎:负责读取 HTML 文档及其相关素材(比如图像和级联样式表)并决定它们的外观。呈现引擎是 DOM 存在的地方。
- 网络引擎:负责接入网络。
- 数据持久引擎:管理应用的持久层,这是存储 cookies 的地方,也是 web 数据库和本地存储等 HTML 5 新特性存在的地方。
- JavaScript 引擎:包括数据持久化、网络和渲染引擎的接口,可以观察和修改其中的任何一个或全部。
图 1-1 展示了 JavaScript 引擎是如何与浏览器的其他功能完全分离的,尽管它与浏览器的其他部分密切相关。
图 1-1 。浏览器引擎堆栈
关于浏览器版本的一句话
在本书中,我们将使用 HTML5 语法作为我们的 HTML 标记。因此,一些例子在没有实现 HTML5 特性的旧浏览器上运行时会有问题。本书中的大多数例子都已经在 Chrome 的最新稳定版本中进行了测试,但也应该可以在最新版本的 Safari 、Firefox 和 Internet Explorer 10 中运行。
网页中的 JavaScript
Web 浏览器加载 JavaScript 或者作为文档本身的内容块(内联脚本)或者作为单独加载的链接脚本文件。
内联脚本使用<script>标签表示:
<script>
/* Your JavaScript here */
</script>
链接的脚本也是使用<script>标签添加的:
<script src="js/init-document.js"></script>
这指示浏览器获取被引用的文件,并将其直接提供给 JavaScript 引擎。
注意你必须同时使用开始和结束标签。HTML5 标准不允许使用自结束标记(尽管有些浏览器可能允许)。
您可以在 HTML 文档的头或正文中的任何位置包含内联或链接脚本。
执行顺序
所以现在我们在网页中包含了 JavaScript,但是当浏览器加载并解析文档时,实际上发生了什么呢?
事实证明,web 浏览器有一个显而易见的特定解析顺序:浏览器从顶部开始解析 HTML 文档,然后一路向下,当特定的素材到达浏览器时就加载它们。这意味着一个脚本(无论是内联的还是链接的)只能引用一些东西(样式、其他脚本、HTML 元素等等)。)在文档中位于它之上。
考虑清单 1-1 中的简单 HTML 页面。
清单 1-1。 基本 HTML 模板
<!DOCTYPE html>
<html>
<head>
<title>JavaScript Developer's Guide</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
这将只是在浏览器中显示一个“Hello Word”消息。我们可以通过添加三个脚本来演示脚本执行的顺序,如清单 1-2 所示。
清单 1-2。 演示执行的顺序
<!DOCTYPE html>
<html>
<head>
<title>JavaScript Developer's Guide</title>
<script>
alert("This is the head.");
</script>
</head>
<body>
<script>
alert("This is the body, before the message.");
</script>
<h1>Hello World</h1>
<script>
alert("This is the body, after the message.");
</script>
</body>
</html>
当载入浏览器时,这个页面会首先弹出一个提示窗口,上面写着This is the head。请注意,浏览器窗口本身中还没有任何内容;浏览器尚未解析文档的其余部分。
接下来,浏览器将向下移动到正文。下一条警告消息将会出现,但是浏览器窗口仍然是空的。然后浏览器窗口会出现“Hello World”的标题,然后会出现最后一条提示信息。
这个例子不仅展示了执行的顺序,还展示了一个重要的事实,即 JavaScript 可能会阻止对文档的解析。在我们的例子中,我们使用了阻塞函数alert(),但是如果我们在头部加载一个复杂的脚本,需要一些时间来下载和解析,它将阻塞对文档其余部分的解析。类似地,正文中的脚本会导致显示整个文档的延迟。对于复杂的 JavaScript 应用,解析顺序和阻塞的组合会导致一些不良影响。我们将在第四章中探讨一些克服这些问题的技巧。
简短的题外话:理解和运行示例
正如我们提到的,我们不打算在这一章中涉及语法的细节——那是本书其余部分的内容。但是在我们深入研究之前,我们确实想了解一些关于语法和运行这些示例的重要细节:
- 变量声明:在这些例子中,我们将使用
var关键字来声明变量。语法很简单:var variableName声明变量variableName,并赋予它特殊的值undefined。可选地,您可以为您的变量提供一个值作为声明的一部分:var variableName = myValue将声明variableName并给它赋值myValue。在本章后面的“JavaScript 中的作用域”一节中,你会学到更多关于var关键字的知识,但是在深入之前,我们想简单地涉及一下。 - 点符号 : JavaScript 使用点符号访问对象上的属性和方法:
myObject.propertyName引用myObject上的propertyName,myObject.methodName()调用myObject上的methodName。 alert:浏览器为 JavaScript 提供了一个alert函数,提供了一种快速显示字符串的方法。当您调用alert方法并将一个字符串作为参数传递给它时,浏览器执行以下步骤:- a.它会暂停脚本的执行。
- b.它会弹出一个小窗口,显示您提供的字符串。弹出窗口包括一个标记为 OK 的按钮,单击该按钮可以关闭弹出窗口。
- c.当弹出窗口关闭时,浏览器会在警报后的下一条语句处继续执行脚本。
- 这使得 alert 成为一种轻松检查脚本变量和属性的方法。它还有一个优点,几乎可以在所有现存的浏览器上工作,甚至是非常旧的浏览器。
运行示例
有几种方法可以运行这些示例。最简单的方法可能是使用清单 1-1 中的模板,并在< H1>标签后添加一个< script>标签。然后将示例复制并粘贴到script标签中,保存文件,并将其加载到浏览器中。
许多浏览器还提供了 JavaScript 控制台,您可以使用它直接输入示例。但是,JavaScript 控制台会在您按 Enter 键时评估您的代码,我们的许多示例都被分成多行。我们不建议使用控制台,但如果你想尝试一下,在你最喜欢的浏览器上访问控制台(通常 Control-或 Option-Shift-J 是键盘快捷键,但也有所不同);例如:
- 在 Chrome 中,你可以通过“定制和配置谷歌浏览器”菜单访问控制台。选择工具
JavaScript 控制台。您还会看到用于访问控制台的键盘快捷键(Windows 为 Control-Shift-J)。您可以在此处直接键入代码示例。
- 在 Firefox 中,选择工具
Web Developer
错误控制台。您可以在标有“代码”的框中键入代码示例。如果要使用控制台,请确保在按 Enter 键之前键入完整的有效语句。要了解 JavaScript 中的语句是由什么组成的,你可以跳到第二章。
JavaScript 的三个困难特性
正如我们提到的,人们发现 JavaScript 的三个主要特性有问题:它实现继承的方式,它实现变量范围的方式,以及它实现数据类型的方式。我们不会回避这些特性,而是直接进入它们。
原型遗传
JavaScript 是一种面向对象的语言,但是,与大多数面向对象的语言不同,它的继承基于原型而不是类。这种差异经常被误解,并且很难解释。
最大的区别是在 JavaScript 中没有类这种东西。您可以用 JavaScript 构建类仿真,但是现成的 JavaScript 没有类。只有对象,你从其他对象实例化新对象。
JavaScript 中的继承是通过每个对象的一个特殊属性来处理的,这个属性叫做prototype。prototype属性引用它从其父对象继承的所有属性和方法——包括它的prototype。当您试图访问一个对象的属性或方法时,JavaScript 首先查看它是否存在于本地副本中。如果没有,JavaScript 会检查prototype。如果在prototype中没有找到请求的条目,它会检查prototype's prototype,依此类推,一直到继承链的顶端。
您可以覆盖prototype中的属性和方法。这将从本质上打破原型链,因此对象和任何从它实例化的子对象将继承覆盖,并且不再进一步搜索原型链。
在某种程度上,prototype链可以被认为是一个单向链表。prototype是对列表中前一个元素的引用。
原型继承的一个主要方面是,如果你改变一个对象的继承属性或方法,它的子对象也会反映这种改变,即使在它们被创建之后。这是因为子原型都引用了父属性和方法。
原型继承的另一个主要方面是,您可以更改任何全局对象的prototype,从而向它们添加您自己的属性和方法——甚至覆盖它们现有的属性和方法。但是请注意,覆盖全局对象的现有属性和方法可能是危险的。请记住,那些属性和方法是由标准定义的,所以如果您让它们做其他事情,那么您可能会失去遵循标准的好处。因此,通常认为覆盖这些属性和方法是不好的做法,除非您非常小心自己在做什么。
清单 1-3 提供了一个非常简单的原型继承的例子。
清单 1-3。 原型继承的简单例子
var myParent = {
a: 10,
b: 50
}
var myChild = Object.create(myParent);
var myGrandChild = Object.create(myChild);
alert(myGrandChild.a); // will alert 10
myParent.a = 20;
alert(myGrandChild.a); // will alert 20
alert(myChild.a); // will alert 20
一会儿我们会多谈一点关于Object.create 的语法;现在,只需关注脚本正在做的事情:首先,它创建一个具有属性a和b的父对象,然后从该父对象创建一个子对象,并从子对象创建一个孙对象。我们现在有三个对象,每个都继承自其父对象。当我们检查孙儿对象的值a时,JavaScript 遍历prototype链,直到在父对象中找到属性。
由于prototype只是一个引用,向父对象添加属性会立即使它们在子对象中可用,如清单 1-4 所示。
清单 1-4。 给父节点添加一个属性使其对子节点可用
var myParent = {
a: 10,
b: 50
}
var myChild = Object.create(myParent);
var myGrandChild = Object.create(myChild);
myParent.c = "hello";
alert(myChild.c); // will alert "hello"
alert(myGrandChild.c); // will alert "hello"
这个例子类似于清单 1-3 ,但是我们在实例化了子对象之后给父对象添加了一个新的属性。
在一些浏览器中,你甚至可以直接检查prototype,因为它们在对象上提供了一个__proto__属性,你可以通过控制台查看(见图 1-2 )。
图 1-2 。在 Chrome 的控制台中查看原型
在图 1-2 中,我们看到myGrandChild是一个对象,展开后看到它有一个__proto__属性。当我们展开它时,我们看到它有另一个__proto__属性,当我们展开它时,我们找到了a和b属性。还有另一个__proto__属性,它引用全局对象 Object…因此,我们所有的对象都继承了全局对象 Object 的所有属性和方法。
原型继承的想法很简单,但是却很容易被误解。更复杂的问题是,在早期版本的 JavaScript 中,从其他对象创建新对象的方法和语法,如清单 1-5 所示,非常类似于传统继承语言的语法。
清单 1-5。 旧语法用于创建新对象和修改原型
function myObject() {}; // constructor function
var myInstance = new myObject; // instantiate a new instance
alert(myInstance.prop1); // will alert "undefined" because it doesn't exist
myObject.prototype.prop1 = "Here I am";
alert(myInstance.prop1); // will alert "Here I am"
这不仅有些不雅,关键字new还让人们从古典继承的角度思考问题,这只会让问题更加混乱。并且它要求任何你计划用来创建子对象的对象都是一个函数。
ECMAScript 5 在全局对象上定义了一个新属性:Object.create()。此方法将对象作为参数,并返回一个以参数对象为原型的新对象。这种语法更加整洁,如清单 1-6 所示,也有助于澄清继承链,并且消除了直接访问prototype property的需要。
清单 1-6。 改进创建新对象的语法
var myObject = {};
var myInstance = Object.create(myObject);
alert(myInstance.prop1); // will alert "undefined" because it doesn't exist
myObject.prop1 = "Here I am";
alert(myInstance.prop1); // will alert "Here I am"
这种新方法适用于现代浏览器,但是如果你发现自己在使用一个不支持这个标准版本的旧 JavaScript 引擎(最明显的是 Internet Explorer 8 和更早的版本),你总是可以使用清单 1-7 中的代码片段来提供相同的功能。
清单 1-7。 一种为不存在的对象添加创建方法的方法
if (typeof Object.create !== 'function') {
Object.create = function (o) {
function F() {}
F.prototype = o;
return new F();
};
}
清单 1-7 检查Object.create是否存在,如果不存在,就把它添加到全局对象 Object 中。这是一个安全扩展全局对象的好例子。
垫片
清单 1-7 是所谓的填充或多填充的一个例子,这些术语指的是为特定环境添加缺失功能或修复不正确实现的小脚本。清单 1-7 弥补了 JavaScript 的一个缺点;还有针对各种 CSS 问题甚至 HTML 问题的垫片。
JavaScript 库中通常包含垫片——事实上,许多库最初只是各种垫片的集合。
JavaScript 中的作用域
JavaScript 的另一个经常被误解和中伤的特性是它的作用域:JavaScript 如何限制和允许访问它的变量。因为 JavaScript 在许多方面与 C 语言非常相似,所以很自然地认为它使用了类似 C 语言的块级作用域,清单 1-8 给出了一个例子。
***清单 1-8。***C 中的块级作用域
#include <stdio.h>
int main() {
int x = 1;
printf("%d, ", x); // 1
if (1) {
int x = 2;
printf("%d, ", x); // 2
}
printf("%d\n", x); // 1
}
在 C 中,每个代码块(if语句、for循环等)。)是它自己的作用域:在一个作用域中定义的变量在另一个作用域中不可用。假设 JavaScript 采用块级作用域是合乎逻辑的,因为它使用的语法非常类似于 C。。但事实并非如此。
相反,JavaScript 使用所谓的函数作用域,这意味着作用域是由函数声明的。函数中定义的变量在该函数中的任何地方都是可用的,甚至在其他块中,如if语句、for循环或嵌套函数。作为示范,下面的清单创建了一个函数范围,并在:中测试变量
清单 1-9。 演示嵌套的功能范围
function testScope() {
var myTest = true;
if (true) {
var myTest = "I am changed!"
}
alert(myTest);
}
在清单 1-9 中显示的例子创建了一个简单的testScope函数。在这个函数中,我们声明了一个变量myTest,这给了它在函数中任何地方都可用的范围。然后我们在一个if语句块中重新声明这个变量,并给它一个不同的值。最后,我们测试看看结果是什么:脚本将提醒I am changed!
在 C 或另一种具有块级作用域的语言中,一个类似的例子会警告true,因为if语句中的myTest重新声明在作用域上将被限制到该块。
如果我们试图在testScope函数之外访问myTest变量,将会失败,如清单 1-10 所示。
清单 1-10。 演示功能范围
function testScope() {
var myTest = true;
if (true) {
var myTest = "I am changed!"
}
alert(myTest);
}
testScope(); // will alert "I am changed!"
alert(myTest); // will throw a reference error, because it doesn't exist outside of the function
在testScope函数之外,myTest不存在。你可以让它存在,如清单 1-11 所示。
清单 1-11。 展示全局范围
var myTest = true;
function testScope() {
if (true) {
var myTest = "I am changed!"
}
alert(myTest);
}
testScope(); // will alert "I am changed!"
alert(myTest); // will alert "I am changed!"
通过在testScope函数之外定义myTest变量,它变得随处可用。这就是所谓的全球范围。全局函数和变量随处可用。这是 JavaScript 的一个非常强大的特性,但是很容易被滥用。一般来说,混淆全局范围被认为是不好的做法,主要是因为它会导致同名变量在脚本执行时互相碰撞对方的值,从而导致各种难以调试的问题。相反,建议尽可能将变量限制在私有范围内。
限制范围
到目前为止,我们已经用关键字var小心地声明了我们的新变量。但是var关键字在 JavaScript 中是可选的;你可以简单地通过提供一个值来声明一个新变量,如清单 1-12 所示。
清单 1-12。 声明一个没有 var 关键字的变量
var myNewVar = 1; // Using var to declare a variable.
myOtherNewVar = 2; // var is optional.
alert(myNewVar); // will alert 1
alert(myOtherNewVar); // will alert 2
然而,当你声明一个没有var关键字的变量时,JavaScript 假设你的意思是你在一个更高的作用域中定义了这个变量,并且你想要访问这个变量。因此 JavaScript 将查找包含范围,看看变量是否是使用关键字var声明的。如果不是,JavaScript 会继续查找作用域链,直到到达全局作用域。如果它到达了全局范围,仍然没有找到使用var关键字的声明,JavaScript 将为你把变量赋给全局范围,如清单 1-13 所示。
清单 1-13。 杂糅全局范围
function testScope() {
myTest = true; // now myTest is global.
alert(myTest);
}
testScope(); // will alert "true"
alert(myTest); // will alert "true" as well, because now myTest is global.
JavaScript 的这个特性叫做隐含全局作用域。它基本上意味着没有明确限定范围的变量被假定为全局变量。
为了限制变量的范围,在声明中使用var关键字,如清单 1-14 所示。使用var关键字指示 JavaScript 将变量的范围限制为当前变量。这可以防止意外弄乱全局范围。
清单 1-14。 用 var 限制范围
function testScope() {
var myTest = true;
function testNestedScope() {
var myTest = false;
alert(myTest);
}
testNestedScope();
alert(myTest);
}
testScope(); // will alert false, and then true.
在清单 1-14 中,我们在不同的范围内定义了两个不同的myTest变量。在testNestedScope函数中,myTest有一个覆盖更高作用域的局部定义。这可以防止两个不同名称的变量互相取值。
你可能想知道如果我们交换testNestedScope函数中的两行会发生什么,如清单 1-15 所示——换句话说,如果我们在一个变量被定义在给定的作用域之前尝试访问它,会发生什么?
清单 1-15。 在给定范围内定义变量之前访问变量
function testScope() {
var myTest = true;
function testNestedScope() {
alert(myTest);
var myTest = false;
}
testNestedScope();
alert(myTest);
}
testScope(); // will alert "undefined", and then true.
清单 1-15 将警告undefined然后是true。为什么呢?也就是说,testNestedScope里的第一条线为什么不报警true?毕竟myTest在更高的范围内设置为true,那么为什么在那里不可用呢?
原因是我们通过用var关键字定义变量testNestedScope来限制myTest的范围。但是当我们在给它一个值之前访问它时,它被设置为“未定义”所以这段代码相当于清单 1-16 中的代码。
清单 1-16。 更明确的等价于清单 1-15 中的
function testScope() {
var myTest = true;
function testNestedScope() {
var myTest;
alert(myTest);
myTest = false;
}
testNestedScope();
alert(myTest);
}
testScope(); // will alert "undefined", and then true.
清单 1-16 通过在变量作用域的最开始明确声明一个没有值的变量,说明了在清单 1-15 中发生了什么。在 JavaScript 中,在给定范围内声明的任何变量在该范围内的任何地方都是可用的,甚至在它被赋值之前。JavaScript 的这个特性通常被称为提升:变量被“提升”到声明它的作用域的开始。由于提升的原因,在 JavaScript 中,在变量作用域的开始显式声明变量通常被认为是一个好的做法,即使你很久以后才访问它们。
关闭
在大多数语言中,一旦一个函数返回,它的所有局部变量都被解除分配——从内存中移除并且不再可用。在 JavaScript 中,这不是必须发生的。由于 JavaScript 的动态特性和作用域规则,您可以编写这样的代码,即使在函数执行完毕后,函数中的局部变量仍然可用。考虑清单 1-17 中的例子。
清单 1-17。
function greet(myName) {
var myAlertString = "Hello " + myName; // Local variable
function doAlert() {
alert(myAlertString);
}
return doAlert; // return the new function
}
var greetKitty = greet("Kitty"); // greetKitty is now a function
greetKitty(); // will alert "Hello Kitty"
清单 1-17 是一个有些做作的例子,这里有一些不寻常的事情,所以让我们一次看一个。第一件奇怪的事情是我们从函数中返回一个函数。这似乎有点奇怪,但在 JavaScript 中并不罕见。请记住,在 JavaScript 中,函数是对象,所以您可以返回它们,甚至可以像其他对象一样轻松地将它们赋给变量。
我们的greet函数将一个名称作为参数,在一个局部变量中将它连接成一个字符串,然后定义一个局部函数来警告该字符串。然后它返回本地函数。当我们调用greet函数时,我们将返回的函数赋给一个变量,然后执行返回的函数。
这种情况的特别之处在于,当我们将其结果赋给greetKitty变量时,greet函数被调用并完全执行。在大多数语言中,myAlertString变量会被释放并且不可用。但在 JavaScript 中,它仍然存在,因为我们已经创建了一个特定的情况,它需要保持在那里,以便当我们执行返回的函数时,一切都将按预期进行。换句话说,返回的函数及其直接的非局部函数作用域都被保留,即使创建它们的函数已经结束运行。这是 JavaScript 中变量作用域的副作用:解释器会维护一个作用域,直到不再需要它。
这也适用于我们创建的私有范围。清单 1-18 通过对另一只猫说“你好”来演示这一点。
清单 1-18。 一个保持着隐私的范围
function greet(myName) {
var myAlertString = "Hello " + myName; // Local variable
function doAlert() {
alert(myAlertString);
}
return doAlert; // return the new function
}
var greetKitty = greet("Kitty"); // greetKitty is now a function
greetKitty(); // will alert "Hello Kitty"
var greetMax = greet("Max"); // greetMax is now a function
greetMax(); // will alert "Hello Max"
greetKitty(); // will alert "Hello Kitty"
greetMax和greetKitty函数都可以访问它们自己维护的作用域,这些作用域对于彼此和全局作用域都是私有的。
这个特殊例子的结果是由 JavaScript 的作用域规则造成的。如果我们允许使用一个全局变量,如清单 1-19 所示,那么我们就不再为每个函数保留一个私有的范围。
清单 1-19。 使用全局变量
function greet(myName) {
myAlertString = "Hello " + myName; // Now a global variable
function doAlert() {
alert(myAlertString);
}
return doAlert; // return the new function
}
var greetKitty = greet("Kitty"); // greetKitty is now a function
greetKitty(); // will alert "Hello Kitty"
var greetMax = greet("Max"); // greetMax is now a function
greetMax(); // will alert "Hello Max"
greetKitty(); // will alert "Hello Max"
var greetLenore = greet("Lenore");
greetLenore(); // will alert "Hello Lenore"
greetKitty(); // will alert "Hello Lenore"
greetMax(); // will alert "Hello Lenore"
在清单 1-19 中,我们通过不对myAlertString变量施加范围限制来改变这种情况。这允许 JavaScript 暗示它是一个全局变量,每次调用greet函数时都会被覆盖。
即使在父函数已经执行完之后,仍然保持一个函数及其父作用域被称为闭包。闭包是 JavaScript 极其重要和强大的特性,我们将在本书中广泛使用它们。
因为您可以使用它们来加强隐私,所以闭包对于封装功能和管理范围非常有用。它们在一些最常见的 JavaScript 模式中扮演着重要的角色,我们将在本章后面介绍这些模式。
闭包非常强大,但是它们有一个重要的缺点:因为闭包需要浏览器为函数及其作用域保留分配的内存,一旦不再需要闭包,浏览器有时可能不会将所有的内存返回给系统。这种情况的主要症状是内存泄漏:随着脚本继续在浏览器中执行,浏览器消耗越来越多的内存,最终耗尽所有可用内存。内存泄漏假设浏览器不应该消耗越来越多的内存,或者消耗得比应该消耗的要快。
老版本的浏览器由于关闭而存在严重的内存泄漏问题。现代浏览器效率更高,但仍有问题。您应该在浏览器运行您的脚本时监控它的内存使用情况,以确保它没有问题。
属于那种软弱的类型,嗯?
正如我们前面提到的,JavaScript 是弱类型的。这意味着,如果表达式中存在类型不匹配问题,JavaScript 将根据自己的规则解决它。以清单 1-20 中的代码为例。
清单 1-20。 弱打字演示
var myNumber = 5; // Integer
var myString = "7"; // String
var myResult = myNumber + myString; // Type mismatch: integer + string = what?
alert(myResult); // Will alert "57"
解析这个脚本时,静态类型语言会抛出一个错误。但是 JavaScript 自己解决了类型不匹配的问题,允许程序继续运行而不会崩溃。
许多人认为弱类型是一个缺点,事实上,对于 JavaScript 开发新手来说,弱类型很容易犯很多错误。但是,一旦掌握了 JavaScript 的类型规则,您就可以充分利用这一特性,创建比用等效的强类型语言创建的脚本更小、更优雅的脚本。
基本数据类型和原语
我们将通过回顾 JavaScript 的基本数据类型来开始我们对 JavaScript 类型的探索。是的,尽管它是弱类型的,JavaScript 实际上有数据类型,只是比典型的数据类型更广泛。四种基本数据类型是:
- 布尔:为真或为假的变量或表达式。
- 数字:JavaScript 中所有的数字都是 64 位浮点数。
- 字符串:任意字符的字符串。
- 对象:属性和方法的集合。
JavaScript 使用这些数据类型作为所有类型管理的基础。要确定 JavaScript 中任何东西的类型,使用typeof操作符,如清单 1-21 所示(参见第七章了解关于typeof操作符的全部细节)。
清单 1-21。 使用 typeof 运算符
var myArrayOfThings = ["hello", 5, true, {}];
for (var i = 0; i < myArrayOfThings.length; i++) {
alert(typeof myArrayOfThings[i]);
}
alert(typeof myArrayOfThings);
这将依次警告“字符串”、“数字”、“布尔”、“对象”和“对象”。(是的,在 JavaScript 中,数组就是对象。)注意,typeof将为函数返回“Function ”,尽管在 JavaScript 中没有函数类型。
另外,JavaScript 有原语的概念:非对象简单值。JavaScript 原语是构建更复杂数据类型的基础。他们是
- 布尔:关键字
true和false本身就是布尔原语。 - Null :关键字
null。 - 数:一个数本身就是一个数本原。
- 字符串:用引号括起来的字符串是字符串原语。
- 未定义的:一个特殊的值,代表一个用
var关键字创建的变量,但是没有给它赋值。
需要注意的是,在 JavaScript 中,任何不是原语的东西都是对象。例如,函数就是对象。
您还可以在 JavaScript 中的原语和对象之间进行转换。你会注意到布尔、数字和字符串原语有匹配的全局对象(见第五章关于布尔、数字和字符串全局对象以及如何使用它们的细节)。考虑在清单 1-22 中显示的例子。
清单 1-22。 如此等等,到底是不是原始人?
var myString = "hello there" // primitive
alert(myString.length); // will alert 11\. . .but length is a property of the String object
alert(typeof myString); // will alert "string"
这段代码片段提醒11,字符串的长度。但是为什么呢?如果“hello there”确实是一个原语,我们怎么能访问myString.length?
我们之所以能做到这一点,是因为在幕后,JavaScript 正在将我们的原始值转换为其关联的对象,从而使我们能够访问 String 的所有属性和方法。这种转换是短暂的,这就是为什么typeof myString仍然产生“字符串”而不是“对象”
这种幕后的转换在 JavaScript 中经常发生,这也是充分理解 JavaScript 如何做到这一点非常重要的另一个原因。
JavaScript 中的类型转换
现在我们已经定义了所有的基础知识,我们可以看看 JavaScript 实际上是如何处理类型不匹配的。JavaScript 有一组函数用于处理从一种类型到另一种类型的转换:toPrimitive()、toNumber()和toBoolean()。这些函数是抽象的,意味着它们是 JavaScript 内部工作的私有函数,不能被脚本直接调用。
toPrimitive()方法接受一个input参数,也可以接受一个可选的preferredType参数。它将非基元(也就是说,对象)转换为最接近的相关基元类型。如果input参数可以分解成多个原始类型,那么preferredType参数可以用来指定选择哪一个。根据input参数类型:,表 1-1 总结了toPrimitive()遵循的规则
表 1-1。规则为原始
input参数类型 | 结果 |
|---|---|
| 目标 | 如果valueOf()返回一个原语,则返回该原语;否则,如果toString返回原始值,则返回该值;否则,抛出一个错误 |
| 其他一切 | 无变化 |
toNumber()方法接受一个input参数并试图将其转换成一个数字,如表 1-2 所示。
表 1-2。【toNumber 规则
input参数类型 | 结果 |
|---|---|
| 布尔代数学体系的 | 1如果为真,+0如果为假 |
| 空 | +0 |
| 数字 | 无转换 |
| 目标 | toNumber(toPrimitive(object)) |
| 线 | 类似于parseInt()(参见第五章的中的“其他全局函数和变量”以获得对parseInt()的完整解释),除非原始值包含除数字、单个小数或前导+或-以外的任何内容,否则它返回NaN |
| 不明确的 | NaN |
toBoolean()方法接受一个input参数,并试图将其转换为true或false,如表 1-3T6 所示。
表 1-3。托布尔的规则
input参数类型 | 结果 |
|---|---|
| 布尔代数学体系的 | 无转换 |
| 空 | false |
| 数字 | 如果–0、+0或NaN,返回false;否则,返回true |
| 目标 | true |
| 线 | 如果字符串为空,则返回false;否则,返回true |
| 不明确的 | false |
脚本中最常发生类型转换的地方是在评估一个if (Expression) Statement条件时,以及使用==比较时。在有条件的情况下,使用toBoolean()将表达式简化为布尔值。==的类型转换算法是 ECMA-262 标准定义的简单算法,在表 1-4 中有概述。
表 1-4。类型转换算法为==运算符
| x 的类型 | y 的类型 | 结果 |
|---|---|---|
| 空 | 不明确的 | true |
| 不明确的 | 空 | true |
| 数字 | 线 | x == toNumber(y) |
| 线 | 数字 | toNumber(x) == y |
| 布尔代数学体系的 | 任何的 | toNumber(x) == y |
| 任何的 | 布尔代数学体系的 | x = toNumber(y) |
| 字符串或数字 | 目标 | x == toPrimitive(y) |
| 目标 | 字符串或数字 | toPrimitive(x) == y |
从这个算法中有几个重要的收获:首先,null和undefined彼此相等,其他都不相等,其次,最终所有其他的都被简化为数字以便于比较。
尽管这个算法实际上非常简单,但许多人不理解它,结果发现==的行为令人困惑。人们发现这非常令人困惑,以至于一个普遍推荐的 JavaScript 编码最佳实践是避免使用==(和!=),而是始终使用===(和!==)。清单 1-23 是一个例子的版本,这个例子经常被引用作为在 JavaScript 中避免使用==的理由。
***清单 1-23。***JavaScript 中令人困惑的类型转换
if ("Primitive String") {
alert("Primitive String" == true);
alert("Primitive String" == false);
}
这段代码将首先向false发出警报,然后再次向false发出警报,因此不难理解为什么这可能会导致人们沮丧地举手投降。让我们一步一步来看:
- 在
if语句中,我们看到 JavaScript 在"Primitive String"上应用toBoolean(),其计算结果为true,因此执行移动到代码块中。 - 我们将算法应用于
"Primitive String" == true,它告诉我们检查"Primitive String" == toNumber(true),这与"Primitive String" == 1相同。 - 我们查
toNumber("Primitive String") == 1,和NaN == 1一样,都是false。 - 我们对
"Primitive String" == false进行同样的处理,在应用了几次类似于步骤 2 的算法后,我们得到了NaN == false,也就是false。
现在我们理解了规则,清单 1-23 的结果实际上非常有意义。
考虑一下清单 1-24 中显示的通用代码模式,其中我们试图为函数中的参数提供一个默认值。
清单 1-24。 一个常见的错误:检查某物是否未定义或为空
function myFunction(arg1) {
// Check if arg1 wasn't provided
if ((arg1 === undefined) || (arg1 === null)) {
// provide default value for arg1 here
}
// Continue with function. . .
}
清单 1-24 中常见的错误表明对一个最基本的类型转换规则缺乏理解:null和undefined彼此相等,除此之外别无其他。清单 1-25 展示了编写这段代码的一种更好的方法。
清单 1-25。
function myFunction(arg1) {
// Check if arg1 wasn't provided
if (arg1 == null) {
// provide default value for arg1 here
}
// Continue with function. . .
}
您本来可以检查arg1 == undefined,但是因为undefined是一个变量,所以它有两个缺点:首先,与undefined比较需要范围链查找,这通常没什么大不了的,但是如果您深埋在范围中和/或在一个长循环中实现检查,它可能会影响性能;其次,它的值有可能被意外覆盖(这种情况很少发生,但确实会发生)。和null比更安全。
放在一起:两种常见的模式
每种语言都有常用的模式,JavaScript 也不例外。JavaScript 的通用模式利用了我们在本章中讨论的一个或多个特性,所以它们是这些特性的优秀的实际例子,也说明了它们有多么强大。第一种模式是一种语法模式,您将会在 JavaScript 中经常看到,并且您自己也会多次使用。第二种是同样非常常见的实现模式,它将帮助您开始将 JavaScript 组织成可管理的模块。
立即执行函数表达式
在本章的其他例子中,我们已经在函数中创建了函数,然后执行它们。有没有可能定义一个函数,然后立即执行它,而不必单独调用它?
在 JavaScript 中,调用一个函数的符号是在函数名后(或在它被赋值的变量名后)放置一对圆括号——参见第二章了解更多关于这种区别的细节。所以,如果我们只是在函数声明后放一对括号,会执行它吗?例如,清单 1-26 中的代码可以工作吗?
清单 1-26。 试图立即调用一个函数
function greet(myName) {
var myAlertString = "Hello " + myName; // Local variable
function doAlert() {
alert(myAlertString);
}()
}
greet("Kitty");
答案是不,这个不行。当 JavaScript 解释器看到关键字function时,它认为后面是一个要添加到作用域中的声明,而不是要计算的表达式。你必须明确地告诉解释器你的函数是一个要被求值的表达式,你可以用圆括号把它括起来,如清单 1-27 所示。
清单 1-27。 立即调用功能
function greet(myName) {
var myAlertString = "Hello " + myName; // Local variable
(function doAlert() {
alert(myAlertString);
})()
}
greet("Kitty");
这实际上会像你所期望的那样工作。你甚至不必命名你的函数,如清单 1-28 所示。
清单 1-28。 立即调用匿名功能
(function() {
// do stuff here
})();
您还可以将变量传递到调用中,如清单 1-29 所示。
清单 1-29。 传递变量到一个立即被调用的匿名函数中
(function(var1, var2) {
// do stuff here
})(myExternalVar1, myExternalVar2);
立即调用的匿名函数非常有用,因为它们提供了一种利用闭包和管理作用域的方法。表达式中的所有内容都是私有的,除非你明确地将某些内容返回到全局范围,这样可以更容易地将全局范围清除掉不必要的混乱。立即调用的匿名函数模式在整个 JavaScript 开发中使用,尤其是在模块模式中。
模块模式
假设您正与许多其他开发人员一起开发一个大型 JavaScript 应用。您需要一种方法来封装代码段,以便它们可以有一个私有的名称空间,这样您就可以避免与现有代码的冲突。你会怎么做?当然是模块模式。
模块模式使用一个立即调用的函数为所有封装的代码创建一个闭包。您可以拥有私有成员,甚至可以发布公共 API。基本模式如清单 1-30 所示。
清单 1-30。 模块模式
var Module = (function() {
var _privateVariable = "This is private",
_otherPrivateVariable = "So is this",
public = {}; // This object will be returned
function privateMethod() {
alert("This method is private as well");
}
public.publicProperty = "This is a public property";
public.publicMethod = function() {
alert("This is a public method");
}
return public;
})()
alert(Module._privateVariable); // will alert "undefined"
// Module.privateMethod(); // would throw an error if we let it run
alert(Module.publicProperty); // will alert "This is a public property"
Module.publicMethod(); // will alert "This is a public method"
模块模式是使用闭包来管理范围的一个很好的例子。在模块中,有一个独立的私有作用域,不会被修改。
这还不是全部。您甚至可以通过直接调用的函数来重新处理模块,从而轻松地扩展模块。你所要做的就是将原始模块作为一个参数传递给新的立即被调用的函数,如清单 1-31 所示。
清单 1-31。 扩展模块
var Module = (function(oldModule) {
oldModule.newMethod = function() {
alert("This is a new method!");
}
return oldModule;
})(Module)
你也可以在模块上创建子模块,如清单 1-32 所示。
清单 1-32。 创建子模块
Module.sub = (function() {
var _privateSubVariable = "This is a private variable in the submodule",
public = {};
public.publicSubVariable = "This is a public variable in the submodule";
return public;
})();
因为它充分利用了 JavaScript 的动态特性,所以模块模式非常灵活。事实上,如果您看一看现代 JavaScript 库(例如 jQuery)的源代码,您会发现其中许多都是使用这种模式构建的。
摘要
在这一章中,我们正面解决了人们认为 JavaScript 最难的问题。我们没有回避许多人因为这些事情不喜欢 JavaScript 的事实,我们解释了 JavaScript 的历史和持续的演变,以便您理解 JavaScript 是如何结束的。阅读完本章后,您现在应该了解以下内容:
- 人们在学习 JavaScript 时最头疼的三件事是作用域、继承和类型。
- JavaScript 的继承是原型的,而不是基于类的。
- JavaScript 的范围是基于函数而不是代码块的。
- JavaScript 的作用域和函数性质允许您创建闭包。
- JavaScript 以非常具体和明确定义的方式处理类型。
您现在还应该熟悉立即执行的函数表达式和模块模式,以及模块模式如何使用闭包来维护范围和加强隐私。
有了这一章,你就可以更深入地研究 JavaScript 的具体细节了。在下一章中,我们将涵盖我们在本章中忽略的细节,从表达式和语句到对象,一直到函数和流控制。
二、JavaScript 螺母和螺丝
在第一章中,我们讲述了 JavaScript 的一些基础知识。我们深入研究了一些人们在学习语言时会纠结的概念。不过,我们并没有真正把语言作为一个整体来处理,这就是我们现在要做的。在这一章中,我们将深入到我们在第一章中忽略的细节中,并了解这门语言的具体细节。我们还将更详细地讨论我们在《??》第一章中提到的一些事情。
这一章将为你提供 JavaScript 语言的坚实基础,并且以一种既能让语言新手理解,又能让有经验的 JavaScript 开发人员有价值的参考的方式来完成。我们的希望是,随着您 JavaScript 开发技能的进步,您会参考这一章来提醒自己一些基础知识,并更深入地钻研特定的主题。
我们将首先回顾一些格式化 JavaScript 代码的基本问题,尤其是与本书中的例子相关的问题。然后我们将讨论表达式和语句,这是 JavaScript 的两个最基本的构件,所有的 JavaScript 程序都是从这两个构件构建的。有了这个基础,我们就可以讨论用操作符创建更复杂的语句了。我们将讨论变量以及如何在 JavaScript 程序中管理它们。然后我们将讨论对象和数组,这将为您提供所有其他内容的构建模块。然后我们将深入讨论函数:它们是什么,以及如何制作它们,我们将获得一些关于 JavaScript 动态本质的重要见解。最后,我们将讨论如何用条件和循环来控制我们的程序。
到本章结束时,你应该对 JavaScript 的词汇结构和语法有一个坚实的理解,并且应该对使用它的基本结构进行流控制和功能感到舒适。
注在本章中,我们将引用甚至直接引用 ECMA-262 标准,其当前版本是 ECMAScript 语言规范 5.1 版。我们鼓励您在
www.ecma-international.org/ecma-262/5.1/探索标准本身(它也提供了一个可下载的 PDF 版本的链接),因为这是扩展您对 JavaScript 理解的一个极好的方式。
格式化 JavaScript 代码
格式化代码是众多不可避免地导致一屋子愤怒的开发人员相互争吵的主题之一。(有一次,我看到有人在关于空格缩进和制表符缩进的争论中差点把椅子扔了出去。)尽管这是一个敏感的话题,但如果没有至少为未来的争论打下基础,以及定义我们将在本书中使用的约定,这种引用将是不负责任的。
一般来说,JavaScript 使用类似 C 的格式。最值得注意的是,JavaScript 使用花括号({ } ) 来表示代码块,比如循环或逻辑流控制。
JavaScript 还使用两种样式的注释分隔符。双斜杠(//)是单行分隔符,表示从该点到行尾的一切都是注释。JavaScript 还使用/*来表示多行注释的开始和*/来表示结束。包含在这些分隔符中的任何内容,不管新行是什么,都被视为注释。
空白,包括缩进,在很大程度上是不重要的。引用 ECMA-262 标准的第 7.2 节:“空白字符用于提高源文本的可读性,并将标记(不可分割的词汇单元)彼此分开,但在其他方面并不重要。”JavaScript 不在乎你是否用制表符或空格缩进,甚至根本不在乎你是否缩进。类似地,JavaScript 对新行没有要求。事实上,为了减小文件大小,通过删除所有空格并将所有内容放在一行来“压缩”JavaScript 是很常见的(参见第四章了解更多关于压缩 JavaScript 的信息)。
JavaScript 使用分号(; ) 来终止语句。然而,分号可以被认为是可选的,因为 JavaScript 解释器实践自动分号插入(ASI),这意味着它们试图通过在需要时自动插入分号来纠正没有分号将不起作用的代码。因此,您可以选择不使用许多(甚至任何)分号来编写您的 JavaScript,而是依赖 ASI。传统上,显式使用分号来终止语句被认为是一种最佳实践。然而,随着 CoffeeScript 等新的元脚本语言的出现,许多人现在更喜欢编写尽量少用分号的简洁代码,而是尽可能依赖 ASI。
从实践的角度来看,这两种方法都是可以接受的,因为这两种方法都有助于生成一致的、功能性的代码。然而,正如任何涉及编程风格的事情一样,最近有许多关于显式使用分号还是依赖 ASI 的激烈争论。
依靠 ASI
ASI 遵循 ECMA-262 标准(第 5.1 版第 7.9 节)中规定的一套明确的规则。如果您想编写不带分号的 JavaScript,我们鼓励您回顾一下这个标准,这样您就能确切地知道您在做什么。我们不会在这里详细讨论这些规则,但是如果你想依赖 ASI,有一些重要的事情要记住。
一般来说,如果 JavaScript 引擎遇到一个新行(或者一个花括号,尽管 ASI 主要是为新行而调用的),这个新行用来分隔本来不属于一起的标记,JavaScript 就会插入一个分号——但是只有在为了创建语法上有效的代码(解释器可以成功解析和执行的代码)而需要这样做的时候。但是解释器并不关心代码在执行时是否会导致错误。它只关心代码能否被执行。
为了说明这一点,考虑清单 2-1 中的两行 JavaScript 代码。
清单 2-1。 不带分号的 JavaScript
myResult = argX - argY
myFunction()
如果解释器遇到这个代码,它将确定确实需要一个分号来使这个代码起作用,并且它将插入一个分号(清单 2-2 ):
清单 2-2。 清单 2-1 上 ASI 的结果
myResult = argX - argY;
myFunction()
另一方面,考虑清单 2-3 中的两行代码。
清单 2-3。 更多 JavaScript 不带分号
myResult = argX - argY
[myResult].myProperty = "foo"
在这种情况下,解释器不会插入分号,因为即使有新的一行,也不需要分号来使代码起作用。相反,解释器会假设我们指的是你在清单 2-4 中看到的内容。
清单 2-4。 解释器认为清单 2-3 是什么意思
myResult = argX - argY[myResult].myProperty = "foo";
如果您实际运行这个例子,您的浏览器将抛出一个引用错误,抱怨一个无效的赋值。=操作符是 JavaScript 的赋值操作符,JavaScript 期望赋值的形式是左操作数取右操作数的值。在这个例子中,JavaScript 甚至无法确定左操作数的含义,更不用说使用结果赋值了。
这是一个人为的例子,但是它确实暴露了依赖 ASI 时的主要考虑:为了有效地使用它,您必须理解规则,而显式地使用分号是毫无疑问的。你不仅要理解这些规则,任何和你一起工作的人也必须理解它们。
保持一致
每个程序员对编程风格都有自己的个人见解,这没问题;重要的是选择一种做事方式并保持一致。一致编写的代码比用多种括号样式、不一致的缩进规则和变量命名约定编写的代码更容易阅读和理解。为此,在本书中,为了保持一致性,我们采用了以下风格:
- 分号:我们显式使用分号(而不是依赖 ASI)。
- 括号:我们使用所谓的“一个真正的括号样式”,其中,左括号和它们的关联语句放在同一行,右括号和它们的关联语句在同一行。
- 变量命名:一般来说,属性是名词,方法是动词。在一些例子中,我们依赖于“匈牙利符号”的变体,其中变量名以它们的类型或功能性的指示为前缀(例如,
intCounter或strMessage),只是为了在例子中更加明确变量的用途或角色。
这些特殊的选择并不意味着比其他人更好。当决定在项目中使用哪种风格时,您应该选择最适合您、您的团队和您的情况的风格。一致性是最重要的。
表达和陈述
表达式和语句是理解 JavaScript 的第一步,因为它们是 JavaScript 程序的基本构件。表达式和语句之间的区别很简单,但是很微妙。
表情
从概念上讲,表达式就像口语中的单词或短语。它们是程序最简单的组成部分。在 JavaScript 中,表达式是解析为一个值的任何代码段。由于文字表达式的计算结果是实际值,JavaScript 支持与变量相同的表达式类型:布尔、数字、字符串和对象。表达式可以很简单,只是陈述一个值,也可以是数学或逻辑运算,如清单 2-5 所示:
清单 2-5。 JavaScript 文字表达式
10 // Literal expression, resolves to 10
"Hello World" // Literal expression, resolves to the string "Hello World"
3+7 // Mathematical expression, resolves to 10
也可以写复合表达式。复合表达式是表达式中的一个(或多个)项是另一个表达式的表达式。复合表达式可以根据需要任意复杂和嵌套,如清单 2-6 所示:
清单 2-6。 复合表情
(3+7)/(5+5) // evaluates to 1
Math.sqrt(100) // evaluates to 10
你最常遇到表达式的地方之一是在条件语句中,如清单 2-7 所示。
清单 2-7。 条件句中的复合表达式
if ((myString === "Hello World") && (myNumber > 10)) {
// conditional code here
}
在本例中,我们有一个由两个表达式组成的复合表达式,一个测试myString的值,另一个测试myNumber的值,其值为 true 或 false。这些表达式包含在一个逻辑 AND 表达式中,因此如果两个表达式的计算结果都为 true,则条件代码将执行。(稍后我们将更多地讨论嵌套的多重表达式;现在,只要把注意力集中在每个单独的表达式上,就像它所代表的布尔值一样。)
最后,尽管表达式可以独立存在,如清单 2-8 所示,但这样的表达式通常不是很有用。
清单 2-8。 一个不太有用的字面表达
var myNumber = 10,
myOtherNumber = 20;
"hello world"; // um, okay?
if (myOtherNumber > myNumber) {
alert("Condition was true!"); // will alert because conditional is true
}
这段代码将在不抛出错误的情况下执行,并发出警告“条件为真!”不出所料。清单 2-8 的第三行文字表达式完全有效,尽管它没有做任何有用的事情。为了实际做一些事情,文字表达式通常与操作符结合在一起:赋值(使用=操作符)、条件(使用逻辑操作符)等等。
关于表达式(甚至复合表达式)的底线是它们只表示值。如果你想用这些值做任何事情,你需要使用一个语句。
语句
在 JavaScript 中,语句是执行特定动作的一个或多个表达式的集合。回到口语类比,如果表达式是单词和短语,那么语句就是完整的句子。从概念上讲,最简单的语句类型是具有副作用的表达式,例如变量赋值或简单的数学运算。参见清单 2-9 中的一些例子。
清单 2-9。 简单语句
var x = 5, // variable assignment, a statement
y = 3,
z = x + y; // mathematical operation, also a statement
有时这些简单的语句被称为表达式语句,以强调它们本质上是具有副作用的表达式。然而,这个术语会混淆表达式和语句之间的微妙区别,所以在本书中我们不会使用它。
就像 JavaScript 有复合表达式一样,它也有复合语句。复合语句是代码块中语句的集合,通常用花括号括起来。复合语句的优秀例子是if语句和循环,如清单 2-10 所示。
清单 2-10。 if 语句和循环是复合语句
if (expression) {
// conditional statement--often a compound statement because it contains multiple statements.
}
for (expression) {
// repeated statement--often a compound statement because it contains multiple statements.
}
但是,请注意,并不是花括号中的每一段代码都一定是一条语句。例如,对象文字是表达式,而不是语句,尽管是用括号括起来的多个表达式,正如你在清单 2-11 中看到的。
清单 2-11。 一个对象字面上不是一个语句
{
prop1: "value",
prop2: "value2"
}
然而,只要你记住表达式(甚至复合表达式)代表的是值而不是别的,那么对象文字不是语句的事实就应该很清楚了,因为对象文字只是实际对象值的规范。有关对象文字的详细信息,请参阅本章后面的“对象”一节。
运算符
操作符对表达式执行操作,这也许并不奇怪。运算符对操作数执行它们的功能(“运算”)。大多数 JavaScript 操作符都是二进制的,也就是说它们有两个操作数,通常是这样的:
operand1 operator operand2
JavaScript 中最常用的二元运算符可能是赋值运算符=。其他例子包括数学运算符和大多数逻辑运算符。
一些 JavaScript 操作符是一元的,这意味着它们只有一个操作数;例如:
operand operator
或者
operator operand
操作数和操作符的顺序取决于所讨论的操作符,有时还取决于你想对操作符做什么。示例包括逻辑“非”运算符或数学“非”运算符。
此外,JavaScript 还有一个三元运算符,称为条件运算符 。它接受三个操作数并执行条件测试:
conditionalExpression ? valueIfTrue : valueIfFalse
条件运算符允许您编写比显式使用if-then-else语句更简洁的代码,并且可以在任何使用标准运算符的地方使用。
JavaScript 操作符分为以下几大类:
- 算术运算符:对其操作数进行算术运算,如加、乘等。
- 赋值操作符:修改变量,要么赋值,要么根据特定规则改变它们的值。
- 按位运算符:将它们的操作数视为一组 32 位,并在该上下文中执行它们的运算。
- 比较运算符:比较它们的操作数,并根据比较结果是否为真返回一个逻辑值(真或假)。
- 逻辑运算符:对操作数执行逻辑运算,通常用于连接多个比较。
- 字符串运算符:对两个字符串进行运算,比如串联。
- 其他运算符:不属于以上任何一类的运算符。此类别包括条件运算符和运算符,如 void 运算符和逗号运算符。
在本章中,我们不打算详细讨论每一个操作符;该参考资料可在第七章中找到。然而,这里有一个重要的操作符概念我们想要讨论:优先级。
优先级
如果在一个语句中有多个操作符,如何确定它们的执行顺序?你严格从左到右评价他们吗?还有其他规则吗?根据操作符及其操作数的不同,不同的执行顺序会产生不同的结果,所以有一个处理这个问题的标准方法是很重要的。
考虑清单 2-12 中涉及数学运算符的例子。
清单 2-12。 单个语句中的多个数学运算符
var myVar = 5 + 7 * 3 + 4 - 2 * 8;
alert(myVar); // what will this alert?
如果你从左到右评估清单 2-12 中的语句,执行每一个操作,你会得到 304。然而,这个例子实际上警告了 14,因为根据一组称为优先级的规则,一些操作符在其他操作符之前被评估。在这个例子中,乘法比加法或减法具有更高的优先级,所以该语句实际上是如清单 2-13 所示进行求值的,清单 2-13 使用圆括号通过将实际求值的运算组合在一起来明确表示优先级。
清单 2-13。 用括号明确表示优先顺序
var myVar = ((5 + (7 * 3)) + 4) - (2 * 8);
alert(myVar); // will alert 14
碰巧的是,JavaScript 中的数学运算符优先级遵循数学本身的优先级规则:首先计算圆括号或方括号中的项,然后是指数和根,然后是乘法和除法,最后是加法和减法。
清单 2-14 提供了另一个例子,只涉及加法和减法,两个运算符具有相同的优先级。
清单 2-14。 多个优先级相同的运算符
var myVar = 5 + 6 - 7 + 10;
alert(myVar); // what will this alert?
myVar值多少?这取决于您执行操作的顺序。如果你从左到右评估它,它将是 14,如果你评估它为(5+6)-(7+10),它将是-6。
当您有多个优先级相同的运算符时,它们将根据它们的结合性进行计算:或者从左到右,或者从右到左。数学运算符的话,都是从左到右求值,所以myVar的值是 14。
因为 JavaScript 不仅仅有数学运算符,它还有比数学更复杂的优先级规则,正如你在表 2-1 中看到的。
表 2-1。JavaScript 中的运算符优先级
理解运算符优先级很重要;否则,您的语句可能会产生意想不到的结果。尽管如此,许多 JavaScript 最佳实践和风格指南都建议,对于包含多个操作符的复杂语句,应该用括号明确说明您想要的优先级。一般来说,这使得代码更可读,维护更容易,尽管如果你有一个非常复杂的语句,你可能会有很多括号。在这种情况下,将单个语句分解成一条或多条语句可能是有价值的,这样可以充分明确并减少括号的总数。
变量
广义地说,变量是一个带有关联值的命名存储位置。您可以通过使用名称来访问与存储位置关联的值。每种语言都有自己的变量实现:如何声明它们,它们的范围是什么,以及如何管理它们。
在 JavaScript 中声明变量
在 JavaScript 中,变量是使用var关键字声明的,如清单 2-15 所示。
清单 2-15。 在 JavaScript 中声明变量
var myVar = 1;
您也可以根据需要简单地访问变量,而不需要使用var关键字正式声明它们(清单 2-16 )。
清单 2-16。 通过访问创建一个新变量
var myVar = 1;
myOtherVar = 2;
这两种声明变量的方式在语法上都是有效的,但是它们对于变量的作用域有不同的含义(在下一节中描述)。
一次声明许多变量是常见的做法。您可以对每个变量使用var关键字,或者您可以使用一次var关键字并用逗号分隔变量声明。将每个变量声明放在自己的行上也是常见的做法,如清单 2-17 所示,以提高可读性。
清单 2-17。 一次性声明多个变量
var myObject = {},
intCounter = 0,
strMessage = "",
isVisible = true;
JavaScript 风格指南通常建议在给定范围的开始声明该范围内的所有变量,主要是因为这有助于防止变量范围错误的问题。它还有助于 JavaScript 代码压缩器,它将获取变量列表并对每个项目运行搜索和替换,以将变量名更改为单字母或双字母名称,从而进一步减小文件的大小。
理解 JavaScript 中的变量范围
正如每种语言都有创建变量的规则一样,每种语言都有控制变量访问位置的规则。这就是所谓的变量范围。基本上,作用域规则决定了这个问题的答案,“如果我在这里创建这个变量,我还能在哪里访问它?”变量作用域对于任何语言来说都是一个重要的概念,因为它几乎影响到语言的方方面面,从调试到优化。
在第一章的中提到,JavaScript 有函数作用域:当你使用var关键字正式声明一个变量时,它的作用域被限制在当前函数作用域以及当前函数作用域中包含的所有函数作用域。换句话说,如果你在一个给定的作用域内声明一个变量,你将能够在一个子作用域内访问它,但不能在任何包含它的作用域内访问它。清单 2-18 提供了一个例子来说明这个概念。
***清单 2-18。***JavaScript 中的功能范围
function myFunction() {
var myVariable = "Here"; // myVariable is now limited in scope to myFunction and any scopes we create within myFunction
// Create a new function within myFunction to demonstrate scope nesting
function myInternalFunction() {
alert(myVariable);
}
myInternalFunction(); // call myInternalFunction when myFunction is called
}
myFunction(); // will alert "Here"
alert(myVariable); // will throw an error; myVariable is not defined outside of myFunction().
当你在一个特定的作用域中声明一个变量时,这个作用域通常被称为这个变量的局部作用域。当您在一个函数中嵌套另一个函数时,您创建了嵌套的函数作用域,通常被称为作用域链。
每当您在程序中访问一个变量时,JavaScript 引擎都会在当前范围内查找,看它是否在当前范围内定义。如果在那里找不到定义,它就向上找到包含它的作用域,以此类推,沿着链向上找到程序的最顶层作用域。这通常被称为范围链查找,或者有时仅仅是范围查找。
任何 JavaScript 程序的最顶层作用域被称为全局作用域。在全局作用域中声明的任何变量对程序中的所有作用域都是可用的,如清单 2-19 所示。
***清单 2-19。***JavaScript 中的全局作用域
var myVariable = "This is a global variable";
function myFunction() {
myVariable = "Global variable has been changed inside a function";
alert(myVariable);
}
alert(myVariable); // will alert "This is a global variable"
myFunction(); // will alert "Global variable has been changed inside a function"
alert(myVariable); // will alert "Global variable has been changed inside a function"
您总是可以通过在特定的函数范围内重新声明变量来覆盖更高范围的声明。这实质上创建了一个新的变量,其作用域仅限于该函数作用域;这通常被称为局部范围优先级。为了演示局部作用域优先级,请参见清单 2-20 。
清单 2-20。 局部范围优先
var myVariable = "This is a global variable";
function myFunction() {
var myVariable = "Global variable has been overridden inside a function";
alert(myVariable);
}
alert(myVariable); // will alert "This is a global variable"
myFunction(); // will alert "Global variable has been overridden inside a function"
alert(myVariable); // will alert "This is a global variable"
由于局部作用域的优先性,JavaScript 变量(和函数声明,将在本章后面描述)在它们的作用域块的开始处立即可用,不管它们是否已经被定义。如果您试图在 JavaScript 变量初始化之前访问它们,您将得到一个未定义的值,但是它们会在那里,脚本不会抛出错误。这可能是相当出乎意料的行为,尤其是在覆盖已经在更高作用域中声明的变量的情况下,如清单 2-21 所示。
清单 2-21。 局部范围凌驾上级范围
function testScope() {
var myTest = true; // myTest is now present in this top level scope.
function testNestedScope() { // Create a sub-scope within the main scope
alert(myTest); // Access myTest...but from which scope?
var myTest = false; // Redefine myTest in this sub-scope.
}
testNestedScope();
alert(myTest);
}
testScope(); // will alert "undefined", and then true.
当我们执行这个例子时,它首先警告“未定义”,然后警告“真”第一个警告的出现是因为,在testNestedScope()函数中,我们重新定义了变量myTest,因此它现在在该范围内。这使得它的新值在该范围内的任何地方都可用,有效地从该函数内更高范围的任何地方擦除了该变量的值。这被称为提升:一个变量声明(不是它的赋值,只是它的声明)被自动“提升”到它的包含范围的开始。换句话说,当创建一个新的作用域时,JavaScript 会在做任何事情之前立即声明所有的局部变量,包括赋值和函数调用。结果,清单 2-21 被解析,就好像它被写成了清单 2-22 中的所示。
清单 2-22。 明确地提升变量
function testScope() {
var myTest = true;
function testNestedScope() {
var myTest;
alert(myTest);
myTest = false;
}
testNestedScope();
alert(myTest);
}
testScope(); // will alert "undefined", and then true.
由于变量提升,许多 JavaScript 最佳实践和风格指南建议在访问变量之前,在它们的作用域的开始定义所有变量,从而显式地说明提升无形地做了什么。
如果访问一个变量而没有使用var关键字声明它,JavaScript 仍然会执行范围链查找。如果它到达了全局范围,仍然没有找到变量声明,它将假定该变量在范围上是全局的,并将它添加到那里。这被称为隐含全局作用域,清单 2-23 中显示了一个例子。
清单 2-23。 隐含全局范围
function myFunction() {
myVariable = "Declared in function, default global scope";
alert(myVariable);
}
alert(typeof myVariable); // will alert "undefined" because it wasn't created yet
myFunction(); // will alert " Declared in function, default global scope "
alert(myVariable); // will alert "Declared in function, default global scope "
关于变量作用域的细节,包括像闭包这样的相关主题,请参见第一章中的“JavaScript 中的作用域”一节。
在 JavaScript 中管理变量
JavaScript 试图让程序员尽可能容易地管理变量。一旦声明了一个变量,就不需要显式地取消声明来释放内存——事实上,JavaScript 没有提供这样做的机制。解释器将自己管理变量,当所有的引用和闭包都完全结束时,释放它们的内存。
正如在第一章中提到的,JavaScript 是一种弱类型语言,这意味着它将根据一组特定的规则来管理表达式中的变量类型不匹配。因为 JavaScript 不断地在幕后管理变量类型,所以理解 JavaScript 的一个最重要的方面就是理解它是如何管理类型的,所以一定要仔细复习第一章。概括地说,JavaScript 有四种广泛的数据类型:
- 布尔型:真或假值。
- 数字:JavaScript 中所有的数字都是 64 位浮点数。
- 字符串:任意字符的字符串。
- 对象:属性和方法的集合。
此外,JavaScript 采用了原语的概念:非对象的简单变量,它们本身可以是布尔值、数字或字符串。任何不是原语的东西都是对象——尽管 JavaScript 会透明地将原语转换成它们相关的对象类型,并在需要时再转换回来。
在复制变量时,JavaScript 对原语和对象的处理是不同的。原语直接从一个变量实例传递到另一个变量实例。另一方面,对象是通过引用传递的:设置一个新变量等于一个现有的对象,并不将该对象大规模复制到新变量中;相反,它只是使新变量成为指向原始对象的指针。参见清单 2-24 中的示例。
清单 2-24。 直接赋值图元和引用对象
var myObject = {};
var myOtherObject = myObject; // myOtherObject is now a reference to myObject
myObject.bar = "bar"; // This changes myObject directly
myOtherObject.foo = "foo"; // This changes myObject via reference
alert(myObject.foo); // will alert "foo"
alert(myOtherObject.bar); // will alert "bar"
var myInt = 5; // Primitive
var myOtherInt = myInt; // myOtherInt is now its own primitive, there is no reference
myOtherInt++;
myInt--;
alert(myOtherInt); // will alert 6
alert(myInt); // will alert 4
var myPrimitiveString = "My Primitive String";
var myOtherPrimitiveString = myPrimitiveString;
myOtherPrimitiveString += " is now longer."
alert(myOtherPrimitiveString); // Will alert "My Primitive String is now longer."
alert(myPrimitiveString); // Will alert "My Primitive String"
因为 JavaScript 透明地管理类型不匹配,有时,正如你在清单 2-25 中看到的,很容易混淆什么是原语,什么是对象:
清单 2-25。 同一数据类型的对象和原语之间的类型转换
var myStringObject = new String("This is an object");
var myOtherStringObject = myStringObject;
myOtherStringObject += " which I just changed into a primitive"; // Type change, so no longer a reference!
alert(myStringObject); // will alert "This is an object"
alert(myOtherStringObject); // will alert "This is an object which I just changed into a primitive"
如果两个对象在内存中引用同一个对象,它们将在相等检查中返回相等,即使这两个对象在其他方面是相同的,如清单 2-26 所示。
清单 2-26。 对象只有在内存中引用同一个对象时才相等
var myObject = {};
var myOtherObject = {};
var myThirdObject = myObject;
alert(myObject == myThirdObject); // will alert "true"
alert(myOtherObject == myThirdObject); // will alert "false"
alert(myObject == myOtherObject); // will alert "false"
JavaScript 只提供引用对象的方法;没有复制对象的方法。但是,如果需要的话,遍历一个对象并将其所有方法和属性复制到一个新对象中并不难。
目标
几乎在所有面向对象的编程语言中,对象都是属性的集合,JavaScript 也不例外。属性可以是原语,也可以是其他对象,包括函数。JavaScript 对象可以是任意深度的,这意味着您可以拥有具有作为对象的属性的对象,而这些对象又具有作为对象的属性,依此类推,直到您希望的深度。
继承
正如在第一章中详细介绍的,JavaScript 使用原型继承而不是类。每个对象都有一个特殊的原型属性,该属性充当指向创建它的对象的指针。当您试图访问对象上的属性时,解释器会检查当前对象中是否存在所需的属性。如果属性不存在,解释器检查原型。如果属性不存在,解释器检查原型的原型,依此类推,直到它找到属性或者到达原型链的末尾并返回一个错误。(参见第一章了解原型继承的细节和例子。)
访问属性和枚举
JavaScript 提供了两种访问对象属性的方法,如清单 2-27 所示。
清单 2-27。 在 JavaScript 中访问对象属性
var myObject = {};
myObject.property1 = "This is property1"; // access via dot notation
myObject["property2"] = 5; // access via square brackets
alert(myObject["property1"]); // will alert "This is property1"
alert(myObject.property2); // will alert 5
ECMA-262 标准规定这两种方法完全相同:
-
属性通过名称访问,使用点符号:
MemberExpression.IdentifierName CallExpression.IdentifierName -
或括号标注:
MemberExpression[ Expression ] CallExpression[ Expression ] -
点符号通过下面的句法转换来解释:
MemberExpression.IdentifierName -
的行为与的行为相同
-
与类似
CallExpression.IdentifierName -
的行为与的行为相同
-
其中
<identifier-name-string>是一个字符串文字,包含与 IdentifierName 相同的 Unicode 转义序列处理后的字符序列。
ECMA-262 版本 5.1,第 11.2.1 节,“属性访问器”
这种双重符号的好处是,您可以使用方括号符号轻松地以编程方式访问对象属性,而不必知道所有属性的名称。作为一个例子,考虑需要枚举一个对象的所有属性。你不知道它们是什么,所以你不能用点符号来访问它们。相反,您只需查询对象的每个属性,并使用括号访问它们的值,如清单 2-28 所示。
***清单 2-28。***JavaScript 中枚举对象的传统方法
// Assuming the existence of targetObject, which has many unknown properties:
var thing,
strMessage = "";
for (thing in targetObject) {
strMessage += "targetObject." + thing + " = " + targetObject[thing] + "\n";
}
alert(strMessage); // will alert all of the properties in targetObject
在清单 2-28 中,我们使用一个for循环遍历targetObject中的所有属性(参见本章后面的“循环”一节,了解关于for循环的细节)。我们构建一个包含每个属性及其相关值的字符串,每行一个,然后警告该字符串。这将只包括对象的非继承属性。这是 JavaScript 中枚举属性的传统方法。在新版 JavaScript 中,可以使用不同的方法枚举对象。在 ECMA-262 的版本 5 中,全局Object对象有了两个新方法:Object.keys()和Object.getOwnPropertyNames()。(参见第五章了解这两种方法的详细信息及其区别。)现在我们可以枚举一个对象,如清单 2-29 所示。
***清单 2-29。***JavaScript 中枚举对象的新方法
// Assuming the existence of myObject, which has many unknown properties:
var arrKeys = Object.keys(myObject),
strMessage = "",
i = 0,
arrKeysLength = arrKeys.length;
for (i = 0; I , arrKeysLength; i++) {
strMessage += "myObject." + arrKeys[i] + " = " + myObject[arrKeys[i]] + "/n";
}
alert(strMessage);
创建对象
JavaScript 有三种创建对象的主要方式:使用构造函数、使用文字符号或使用Object.create()。
使用构造函数
创建新 JavaScript 对象的传统方法是创建一个构造函数,并根据需要用它来创建新对象。要创建一个构造函数,你只需像平常一样创建一个函数,并根据需要添加属性,如清单 2-30 所示。
清单 2-30。 基本构造函数
function myConstructor() {
this.property1 = "foo";
this.property2 = "bar";
this.method1 = function() {
alert("Hello World!");
}
}
您会注意到,在这个构造函数中,我们使用了this关键字向对象添加新的属性。关于函数中关键字this微妙之处的细节将在本章后面的“函数”一节中提供在构造函数的上下文中,关键字this指的是由构造函数创建的对象。
要从构造函数创建一个新的实例,使用new操作符,如清单 2-31 所示。
清单 2-31。 从构造函数创建新的实例
var myObject = new myConstructor();
myObject.method1(); // will alert "Hello World!"
new 运算符执行以下步骤:
- 它创建一个新的空对象,该对象继承自操作数的原型,
- 它将那个新对象设置为操作数的执行范围(因此在操作数内,
this关键字引用新的空对象), - 它调用操作数,因此操作数可以根据需要修改新对象,
- 它返回操作数返回的值,或者如果操作数没有返回任何内容,它会自动返回在步骤 1 中创建的新对象以及在步骤 3 中修改的操作数。
如果您是以 Java 或 C++等基于类的语言为背景来学习 JavaScript,您可能会想,“嘿,这看起来有点像类!”你是对的,这个方法表面上确实类似于类。您可以继续沿着这条路走下去,结合使用这种方法和其他方法来完全模拟 JavaScript 中的类。但是,我们鼓励您在使用 JavaScript 时尝试放弃类的概念,以便更好地利用语言的动态特性。
使用文字
在 JavaScript 中创建对象的另一种方式是使用文字符号。文字符号是一种在创建过程中为对象提供文字值的方法。在 JavaScript 中,文字符号是非常常见的,我们将在本章中多次提到它。
要按字面意思创建一个对象,首先像平常一样用var关键字定义它,如清单 2-32 所示,然后用花括号将属性括起来,属性应该是用逗号分隔的键/值对。
清单 2-32。 使用文字符号创建对象
var myObjectLiteral = {
property1: "one",
property2: "two",
method1: function() {
alert("Hello World!");
}
}
myObjectLiteral.method1(); // will alert "Hello World!"
因为我们已经创建了对象,所以我们可以立即使用它。因此,文字符号是在 JavaScript 中创建单例的最佳方式。以这种方式创建的对象仍然可以在以后根据需要通过添加属性和方法来扩展,如清单 2-33 所示。
清单 2-33。 扩展一个对象
myObjectLiteral.property3 = "New property"; // adds a new property to the previously created object
使用 Object.create( )
最后,最新版本的 JavaScript 提供了创建新对象的第三种方法:全局Object对象上的create()方法。如清单 2-34 所示,该方法将一个对象作为其参数,并返回一个以参数对象为原型的新对象。
清单 2-34。 使用 Object.create()创建新对象
var myObjectLiteral = {
property1: "one",
property2: "two",
method1: function() {
alert("Hello world!");
}
}
var myChild = Object.create(myObjectLiteral);
myChild.method1(); // will alert "Hello world!"
这个方法可以从任何对象创建一个新的对象,甚至是一个构造函数。
我应该使用哪种方法?
使用哪种方法在很大程度上是个人喜好的问题,尽管有时选择会受到惯例或情况的支配。例如,一些 JavaScript 库大量使用了Object.create()。或者,如果您正在使用 JSON 做大量的工作,您可能会发现使用文字来管理您的单例更有意义。如果你真的觉得你的 JavaScript 程序需要类行为,那么构造函数是最简单的方法。
数组
与大多数语言一样,在 JavaScript 中,数组本质上是索引数据结构,每个索引都有一个值。JavaScript 数组索引都是从 0 开始的,所以数组的第二项实际上索引是 1,数组的长度等于最后一个索引+ 1。
动态长度
为了与 JavaScript 的动态特性保持一致,它的数组具有动态长度。这意味着您可以在数组中添加和删除项目,并且数组的长度会根据需要而改变。因此,在向数组中添加或从中移除项时,不会产生边界错误。如果你试图访问一个不存在的元素,解释器将返回undefined而不是抛出一个错误。JavaScript 中一个常见的错误是试图从一个不存在的数组中检索某个东西,导致了undefined,然后试图用那个值做一些事情,而没有先检查它是否未定义。根据您试图对该值做什么,解释器可能会在该点抛出错误,但不会在数组被越界访问时抛出错误。
JavaScript 数组有一个length属性,它包含一个表示数组长度的数字。随着元素在数组中的添加和删除,这个数字会根据需要增加或减少。也可以直接设置length属性,如清单 2-35 所示;这样做将根据需要从数组末尾移除现有项或将未定义的项添加到数组末尾。
***清单 2-35。***JavaScript 数组的动态长度
var arrColors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];
alert(arrColors.length); // will alert 7
arrColors.length = 10; // adds three new elements to the array, each set to "undefined"
alert(arrColors[8]); // will alert "undefined"
arrColors.length = 6; // arrColors is now ["red", "orange", "yellow", "green", "blue", "indigo"]
访问和分配值
在 JavaScript 中,数组可以包含任何有效的数据类型:对象、函数、布尔值等等。数据类型也可以在数组中混合,这意味着数组可以由一个对象、一个布尔值、一个数字和一个字符串组成。
使用为对象描述的方括号符号访问数组值:数组的名称,后跟一组包含所需值的索引的方括号;例如,在清单 2-36 ,myArray[2]将访问数组myArray中的第三项。
清单 2-36。 访问数组
var myArray = new Array();
myArray[0] = "foo";
myArray[1] = "bar";
myArray[3] = 4;
alert(myArray.length); // will alert 4
alert(myArray[2]); // will alert "undefined"
var testVar = myArray[498]; // testVar is now "undefined" and no error will be thrown
alert(testVar); // will alert "undefined"
数组实际上是 JavaScript 对象的特例。你可以像添加任何其他对象一样给数组添加属性,如清单 2-37 所示。
***清单 2-37。***赋值
var myArray = new Array();
myArray[0] = "foo"; // assign "foo" to the first element of the array
myArray[1] = "bar"; // assign "bar" to the second element of the array
myArray["foo"] = "bar"; // create the property "foo" on myArray and give it the value of "bar"
alert(myArray.length); // will alert 2
myArray["2"] = 7; // assign 7 to the third element of the array
alert(myArray.length); // will alert 3
var strMyIndex = "3";
myArray[strMyIndex] = 8; // will assign 8 to the fourth element of the array
alert(myArray.length); // will alert 4
清单 2-37 展示了 JavaScript 如何将一个非数字索引的类型强制转换成一个数字值,如果可以的话,然后使用它作为一个索引。否则,它将使用提供的值作为数组对象本身的新属性的键。
由于这种行为,人们常说 JavaScript 有“关联数组”这并不完全正确,因为 JavaScript 数组不能有非数字索引。如果你用一个非数字索引向一个数组添加东西,如清单 2-38 所示,你只是简单地把它作为一个属性添加到数组对象本身,而不是把元素添加到数组。
清单 2-38。 数组元素对属性
var myArray = new Array();
myArray["foo"] = "bar"; // adds a property, not a new element
myArray["new"] = "old"; // adds a property, not a new element
alert(myArray.length); // will alert 0, because no elements have actually been added to the array.
myArray[0] = 0; // Adds a new element to the array
alert(myArray.length); // will alert 1
创建数组
在 JavaScript 中创建数组有两种方法:使用全局Array对象作为构造函数,如清单 2-39 所示,或者使用文字符号,如清单 2-40 所示。
清单 2-39。 用构造函数创建数组
var myArrayObject = new Array(4); // creates an array with 4 undefined elements
var myOtherArray = new Array(4, 2, 5, 2, 7); // creates an array with those values
alert(myArrayObject.length); // will alert 4
alert(myOtherArray.length); // will alert 5
清单 2-40。 用文字符号创建数组
var myLiteralArray = []; // Creates an array of length 0 with no elements
var myOtherArray = [1, "foo", {}, true]; // Creates an array of length 4 with a number, a string, an object, and a boolean
当使用Array对象作为构造函数时,您可以提供一个可选的单个数值,这将导致构造函数返回一个用指定数量的槽初始化的数组。每个槽将被设置为“未定义”如果您提供多个以逗号分隔的参数,构造函数将返回一个数组,每个参数作为一个索引值,从 0 开始按顺序排列。
使用文字符号创建数组类似于使用文字符号创建对象。
清单 2-40 展示了在一个数组中可以有多种数据类型。甚至可以将对象作为数组值,就像可以将数组作为对象的属性一样。
JavaScript 也支持多维数组,作为数组的数组,如清单 2-41 所示。
清单 2-41。 多维数组
var row1 = [0, 1, 2];
var row2 = [3, 4, 5];
var row3 = [6, 7, 8];
var array3by3 = [row1, row2, row3];
alert(array3by3[2][1]); // will alert 7
迭代数组
因为数组是数字索引的,所以对它们最常见的操作之一就是按顺序遍历它们的成员,通常是对每个成员做一些事情。迭代数组最常见的方式是在for循环中,如清单 2-42 所示。(有关for循环以及如何优化它们的详细信息,请参见本章后面的“for Loops”。)
清单 2-42。 使用 for 循环迭代数组
var myColors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];
for (var i = 0; i < myColors.length; i++) {
alert(myColors[i]); // will alert each color one at a time
}
在这个例子中,for循环将继续,直到迭代器i到达myColors.length -1。每次循环时,JavaScript 都会提醒存储在该索引中的值。这是目前为止最常见的数组迭代模式,而且速度非常快,即使对于非常大的数组也是如此。
您可能会尝试使用一个for-in循环,就像我们在枚举对象时所做的那样,但是请记住数组可以有属性和值,并且一个for-in循环将遍历所有这些项。此外,也不能保证循环会按顺序遍历所有索引值,或者一次遍历所有索引值。
通常,您会希望对数组中的每个元素做一些事情。如果你确定你的数组中没有元素是未定义的,你可以使用稍微不同版本的for循环,如清单 2-43 所示。
清单 2-43。 另一种迭代数组的方式
var myColors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];
for ( var i = 0, color; color = myColors[i]; i++) {
// Inside of the loop, the variable color will contain the value at the current index
alert(color); // will alert each color one at a time
}
这种方法的优点是,在循环中,变量color已经被设置为当前索引处的一个值,省去了您自己获取它的麻烦。注意,如果其中一个数组元素未定义,那么你的变量在循环中也是未定义的。
在新版本的 JavaScript 中,数组有一个forEach()方法,可以用来迭代数组,如清单 2-44 所示。该方法将函数表达式作为参数,并对每个数组元素执行一次该函数。函数表达式可以接受一个可选参数,该参数将被设置为当前索引处的数组值。
清单 2-44。 迭代数组的第三种方式
var myColors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];
myColors.forEach(function(color) {
alert(color); // will alert each of the colors, one at a time
});
如果你有一个数组,你想在迭代时修改它,你需要注意不要跳过元素。考虑清单 2-45 中提供的例子。
清单 2-45。 迭代中修改数组
var myColors = ["red", "orange", "green", "green", "blue", "indigo", "violet"];
for (var i = 0; i < myColors.length; i++) {
if (myColors[i] === "green") {
myColors.splice(i, 1); // the splice() method removes the item at index i (see Chapter 5 for details on the splice() method)
}
}
在本例中,我们从“red”开始,一次一个地检查myColors数组的每个成员。当循环到达i = 2时,myColors[i]将变成“绿色”,条件将导致该元素从数组中移除。因此,数组将从 7 个元素变为 6 个元素,第二个“绿色”元素将从索引 3 变为索引 2。然后,根据for循环功能(见本章后面的“循环”),索引将递增,从 2 到 3,循环将继续。这将导致第二个“绿色”元素被遗漏。
每当你在迭代时修改一个数组,你必须考虑这种可能性。有两种处理方法。一种是递减if语句内的计数器i,这样如果出现匹配,一个元素被弹出数组,i就会递减 1,然后递增 1,这样就避免了跳过这个元素。
另一个更好的解决方案是反向迭代数组,如清单 2-46 所示。
清单 2-46。 反向迭代数组
var myColors = ["red", "orange", "green", "green", "blue", "indigo", "violet"];
for (var i = myColors.length - 1; i >= 0; i--) {
if (myColors[i] === "green") {
myColors.slice(i, 1); // the slice method removes the specified element from the array.
}
}
在清单 2-46 中,我们从数组的末尾开始向后工作。首先我们测试index i = 6,然后是i = 5,依此类推。在i = 3,我们遇到一个“绿色”元素,它将从数组中移除。这将使数组长度减少 1,并且“blue”元素(及其后面的所有元素)的索引将减少 1。然后,计数器将减 1,到i = 2,我们将点击数组中的另一个“绿色”元素。通过反向遍历数组,可以避免手动管理计数器。
数组方法和属性
数组有几个方法和属性来管理它们的元素。例如,在本章中,我们使用了数组的length属性。还有其他几个属性和方法;有关所有数组方法和属性的详细描述,以及示例,请参见第五章。
功能
函数是可重用的代码块,可以从程序的其他区域调用。在 JavaScript 中,函数也是一级对象,这意味着它们可以像语言中的任何其他对象一样被操作:它们可以有属性和方法,可以从函数返回,可以作为参数传递,等等。JavaScript 中函数的对象特性是理解语言动态特性的最重要的关键之一。
JavaScript 提供了两种创建新函数的方法:通过声明和通过表达式。
函数声明
JavaScript 提供了一个function关键字,可以用来声明函数。它的工作方式类似于声明变量的关键字var,在相同的上下文中考虑它是很有用的。根据 ECMA-262 标准,函数声明的形式如下:
function Identifier (FormalParameterList optional) { FunctionBody}
FormalParameterList是可选的(JavaScript 函数不需要有参数)。
这将创建一个名为Identifier()的函数,这个函数在它的父作用域和它自己的作用域中都可见。清单 2-47 显示了一个简单的函数声明。
清单 2-47。 简单函数声明
function saySomething(strMessage, strTarget) {
alert(strMessage + " " + strTarget);
}
saySomething("Hello", "world"); // will alert "Hello world"
注意,因为函数名在它自己的作用域内可用,所以函数可以调用它自己,允许递归。清单 2-48 提供了一个例子,说明我们可以很容易地实现阶乘的数学概念,其中一个数 N!= N(N–1)(N–2)。。。(N-(N–1))。
清单 2-48。 递归函数
function factorial(number) {
if (number <=1) {
return 1
} else {
return number * factorial(number - 1);
}
}
alert(factorial(5)); // will alert 120
像变量声明一样,函数声明被提升到其作用域的开始。事实上,它们在所有其他语句之前被解析和评估,这意味着它们将在其定义的范围内立即可用,甚至在代码中定义它们之前。(有关提升的完整解释,请参见本章前面的“理解 JavaScript 中的变量范围”。)作为示范,考虑在清单 2-49 中出现的常见 JavaScript 面试问题。
清单 2-49。 常见面试问题演示函数声明吊装
function myFunction() {
function myInternalFunction() {
return 10;
}
return myInternalFunction();
function myInternalFunction() {
return 20;
}
}
alert(myFunction()); // What will this alert?
如果你回答“它会提醒 20”,那么恭喜你,你被录用了!函数myInternalFunction()定义了两次,第二次用第一次替换。在两个定义中间访问函数并不重要,因为定义被提升到了其作用域的顶部。它相当于清单 2-50 中的。
清单 2-50。 相当于清单 2-49 中的
function myFunction() {
function myInternalFunction() {
return 10;
}
function myInternalFunction() {
return 20;
}
return myInternalFunction();
}
alert(myFunction()); // What will this alert?
因为函数声明被提升到其作用域的顶部,所以您可以在声明它们之前访问它们。你可以,但是,不代表你就应该;许多 JavaScript 风格指南建议不要这样做,因为这会导致代码混乱。不管你是否使用它,函数声明提升是存在的,当你决定函数的范围时,你应该记住这一点。
函数表达式
在 JavaScript 中创建函数的另一种方法是使用函数表达式。与任何表达式一样,函数表达式代表一个值;在函数表达式的情况下,值是一个函数对象。通常,函数表达式被赋值给变量,这样就可以访问它们了。
根据 ECMA-262 标准,函数表达式的形式为:
function Identifier optional (FormalParameterList optional) { FunctionBody }
这反过来又会产生类似
var myFunction = function foo() {
// function body here
}
您会注意到Identifier是可选的。JavaScript 允许创建未命名的函数表达式,称为匿名函数 。匿名函数在 JavaScript 中很常见。创建函数表达式时不提供标识符是很常见的,因为变量是调用函数的一种方式。大多数情况下,除非函数需要调用自身,否则会将匿名函数作为函数表达式的一部分赋给变量:
var myFunction = function() {
// function body
}
您还会注意到,这个定义看起来几乎与函数声明的定义(在上一节中介绍过)完全相同。这意味着可以将完全相同的代码用作函数声明或函数表达式,这取决于上下文:
function myFunction() {
// function body
}
一般来说,在赋值(比如变量赋值)或表达式(比如作为另一个函数的参数提供的匿名函数)的上下文中,或者如果没有Identifier,那么解释器将假设代码是一个函数表达式。否则,代码将是函数体(或全局上下文)的一部分,并将被解释为函数声明。清单 2-51 展示了每一个例子。
清单 2-51。 函数声明与函数表达式
// This is not an assignment, there is an Identifier, and it's in the
// global scope, so it's a function declarationfunction myFunction() {
// function body
}
// This is part of an assignment, so it is a function expression
var myOtherFunction = function foo() {
// function body
}
// Part of a new expression, so it is a function expression
new function myThirdFunction() {
// function body
// This is part of a function body, so it is a function declaration
function myInternalFunction() {
// internal function body
}
}
赋值函数表达式就像变量声明一样被提升,但是只提升它们的声明表达式,而不是它们的赋值表达式。作为一个例子,考虑另一个常见的 JavaScript 面试问题,如清单 2-52 所示。
清单 2-52。 吊装为函数表达式
function myTestFunction() {
var myInternalFunction = function() {
return "Hello World.";
}
return myInternalFunction();
var myInternalFunction = function() {
return "Second Definition.";
}
}
alert(myTestFunction()); // What will this alert?
在这个例子中,代码将警告“Hello Word”第二个赋值表达式没有被提升,所以myInternalFunction()的赋值是返回字符串“Hello World”。
调用函数
到目前为止,本书中我们一直在调用函数,但我们从未真正定义具体的语法。语法很重要,因为在 JavaScript 中实际上有几种调用函数的方法,如何调用函数将决定它的执行上下文。
在 JavaScript 中,当您调用一个函数时,该函数会收到一个指向其执行上下文的指针,该指针将被设置为关键字this,可以在函数内部访问该关键字。
在 JavaScript 中,有三种方法可以调用函数:
- 使用函数调用者
(),它与函数和方法(附属于对象的函数)一起工作 - 使用
new关键字,就像构造一个新对象一样 - 使用
apply()和call()方法
使用调用程序调用函数
在 JavaScript 中,函数 invoker 是一对括号,()。任何计算为函数的表达式都可以使用调用程序来调用。要将参数传递给被调用的函数,需要将它们作为逗号分隔的列表包含在括号中。
当您使用 invoker 调用函数时,函数的执行上下文被设置为window对象。当您调用一个方法时,执行上下文被设置为父对象。清单 2-53 提供了一些例子。
清单 2-53。 测试函数和方法的执行上下文
var myObject = {
myMethod : function() {
alert(this === myObject); // Test to see if this does indeed refer to the parent object of a method.
}
}
function myGlobalFunction() {
alert(this === window); // Test to see if this refers to the window for functions
function mySubFunction() {
alert(this === window); // Test to see if this refers to window as well.
};
mySubFunction();
}
// Invoke our tests
myObject.myMethod(); // will alert "true"
myGlobalFunction(); // will alert "true" and then alert "true" again.
在清单 2-53 中,我们用一个方法建立了一个对象,在这个方法中我们测试了一下this关键字是否确实指向了父对象。然后我们创建一个全局函数,测试它的关键字是否被设置为window,并定义它自己的子函数。子函数测试它的关键字是否也设置为窗口对象。
就方法而言,让this引用父对象是 JavaScript 面向对象范式的主要特征之一。它使您能够轻松地访问和修改父对象及其方法。
调用函数作为构造函数
一个函数也可以通过使用new操作符来调用,如清单 2-54 所示。任何计算结果为函数的表达式都可以用这种方式调用。
清单 2-54。 使用 new 关键字调用函数
function myFunction() {
alert('Hello world!');
}
new myFunction; // will alert "Hello world!"
尽管您可以使用new操作符调用任何函数,但它是用来构造新对象的。当您以这种方式调用函数时,JavaScript 会创建一个新的空对象,该对象从操作数继承其原型,并将其设置为函数的执行上下文。因此,当您构建一个构造函数时,this关键字将引用您正在创建的新对象。然后,您的构造函数可以显式返回新对象,或者 new 运算符会自动为您返回它。
这个调用方法给了你创建任何你需要的任意对象构造函数的能力,如清单 2-55 所示。
清单 2-55。 构造函数用来构造一个对象
// A common convention for constructors is to capitalize their first letter.
function Kitty() {
this.soft = true;
this.temperature = "warm";
this.vocalize = function() {
alert('Purr, purr, purr');
}
}
var myKitty = new Kitty; // create a new kitty.
alert(myKitty.soft); // will alert true
alert(myKitty.temperature); // will alert "warm"
myKitty.vocalize(); // will alert "Purr, purr, purr"
在这个例子中,当我们使用new关键字调用Kitty()函数时,JavaScript 首先创建一个新的空对象,并将其作为执行上下文传递给函数。然后,函数执行,将属性soft和temperature以及方法vocalize()添加到对象中。最后,返回该对象,以便将其赋给myKitty变量。
这个语法看起来非常像从一个类中实例化一个对象的语法。但是不要忘记:JavaScript 没有类,只有对象。如果你有基于类的面向对象语言的背景,不要让这种熟悉的语法欺骗你,让你以为你在和类打交道。
使用 apply()和 call() 调用函数
最后,所有的 JavaScript 函数都有两个方法可以用来调用它们:apply()和call()。这些方法允许我们为函数指定任何我们想要的上下文。
apply()和call()方法有相似的语法:
myFunction.apply(thisContext, arrArgs);
myFunction.call(thisContext, arg1, arg2, arg3, ..., argN);
这两种方法都带有一个thisContext参数,它是一个对象或引用,指定函数的执行上下文。如果不指定参数,JavaScript 将在全局上下文中执行该函数,就像您传递了对window对象的引用一样。
这两种方法的区别在于如何为函数指定参数。使用apply()方法,在数组中指定参数,使用call()方法,以逗号分隔的列表形式提供参数。否则,这两种方法的工作方式完全相同。
清单 2-56 提供了一个使用这些方法调用函数的例子。
清单 2-56。 使用 call()和 apply()来调用函数
var contextObject = {
testContext: 10
}
var otherContextObject = {
testContext: "Hello World!"
}
var testContext = 15; // Global variable
function testFunction() {
alert(this.testContext);
}
testFunction(); // This will alert 15
testFunction.call(contextObject); // Will alert 10
testFunction.apply(otherContextObject); // Will alert "Hello World!"
在清单 2-56 中,我们创建了两个上下文对象,它们的相同属性被设置为不同的值:一个被设置为数字 10,另一个被设置为字符串“Hello World!”。然后,我们创建一个与属性同名的全局变量,并将其设置为 15。最后,我们创建一个函数来提醒this.testContext的值。
当我们使用 invoker 调用函数时,执行上下文是 window 对象,因此函数会警告全局变量的值。当我们使用call()方法并提供第一个上下文对象作为新的执行上下文时,该函数会提醒该对象的属性值。类似地,当我们使用apply()方法并提供另一个上下文对象时,该函数警告该属性。
条件句
JavaScript 为代码的条件执行提供了一套相当标准的特性:测试表达式,并根据结果执行指定的语句块。
if 语句
JavaScript 中if语句的基本形式是:
if (conditionExpression) { statementBlock }
显而易见,如果conditionExpression评估为真,那么statementBlock中的代码将被执行。如果语句块中只有一行代码,括号是可选的。
JavaScript if语句可以使用else关键字扩展,允许逻辑分支:
if (conditionExpression) {
statementBlock1
} else {
statementBlock2
}
这里,如果conditionExpression评估为真,statementBlock1将执行;否则,statementBlock2就会执行。
您可以通过这种方式将if语句链接在一起:
if (conditionExpression1) {
statementBlock1
} else if (conditionExpression2) {
statementBlock2
} else if (conditionExpression3) {
statementBlock3
} else {
statementBlock4
}
在这个例子中,如果conditionExpression1为真,那么statementBlock1将执行,程序的控制将移动到链的末端。如果conditionExpression1为假,那么conditionExpression2将被求值。如果是真的,statementBlock2将执行,然后程序的控制权将再次移动到链的末端。因此,statementBlock4只有在conditionExpression1、conditionExpression2和conditionExpression3的计算结果都为假时才会执行;否则,它将永远不会执行。参见清单 2-7 中的if...else语句链示例:
清单 2-57。 一个 if-then-else 块
function alertGenres(authorName) {
if (authorName === "Neil Gaiman") {
alert("Fantasy");
} else if (authorName === "Octavia Butler") {
alert("Science Fiction");
} else if (authorName === "Roger Zelazny") {
alert("Science Fiction and Fantasy");
} else {
alert("Unknown author.");
}
}
alertGenres("Roger Zelazny"); // will alert "Science Fiction and Fantasy"
alertGenres("Arthur C. Clarke"); // will alert "Unknown author."
在清单 2-57 中,我们构建了一个简单的函数来测试一个作者的名字,如果它识别出这个名字,就会提醒这个作者所写的流派的名字。如果作者未被识别,该函数会警告“未知作者”
切换报表
switch语句为大量的if-else-if-else-if-else链提供了一种替代方案,在测试单个conditionExpression并可能产生多个结果时特别有用。以下是switch语句的格式:
switch (expression) {
case result1:
statementBlock1
[break;]
case result2:
statementBlock2
[break;]
case result3:
statementBlock3
[break;]
default:
statementBlockDefault
}
在一个case语句中,表达式应该解析为一个case标签。这反过来将导致执行移动到该标签,并从该点开始执行statementBlock。一个可选的break语句将停止块的执行。如果表达式没有解析出一个匹配的标签,那么解释器将寻找一个默认标签,如果存在的话,将执行其相关的statementBlock。
为了演示,清单 2-58 提供了一个简单的例子。
清单 2-58。 一个开关语句的琐碎例子
var myColor = "yellow";
switch (myColor) {
case "red":
alert("myColor was set to red");
break;
case "yellow":
alert("myColor was set to yellow");
case "green":
alert("myColor was set to green");
default:
alert("myColor was an unknown color");
}
在清单 2-58 中,myColor解析为“红色”,因此switch语句将警告“myColor 被设置为红色”。因为“红色”代码块包括一个break语句,所以switch语句的执行结束,程序在右括号后继续。如果myColor被改为设置为“黄色”,那么switch语句将移动到“黄色”情况,并警告“我的颜色被设置为黄色”。然后,因为“黄色”代码块没有break语句,程序将执行“绿色”情况,然后是默认情况。
环
任何编程语言都需要执行的最常见的任务通常是重复性或迭代性的任务。例如,假设您想检查给定 HTML 页面上的所有链接,看看其中是否有包含特定文本值的链接。JavaScript 有一组循环语句来处理这些情况。
一般来说,循环是一个重复执行的语句块,直到满足指定的条件。JavaScript 有四种基本的循环:for循环、for-in循环、while循环和do循环。
对于循环
大概最常用的循环语句是for循环。一般来说,for循环反复执行它的语句块,直到指定的条件为假。此时,程序会在循环结束后执行下一条语句。一个for循环将在第一次执行它的语句块之前执行它的条件检查,所以有可能(如果第一次条件检查为假)语句块永远不会执行。
for循环的语法是:
for (initialExpression; conditionExpression; incrementExpression)
statementBlock
一个for循环执行如下:
- 执行
initialExpression。通常这是用来定义和初始化计数变量,但它可以是任何有效的表达式。initialExpression也是可选的。 - 对
conditionExpression进行评估。如果表达式评估为真,那么statementBlock将执行。如果表达式的计算结果为假,循环将终止而不执行statementBlock。conditionExpression在技术上是可选的;如果你忽略它,解释器假设条件总是真的,因此总是执行循环,这意味着你必须手动中断循环。 - 在
statementBlock执行之后,incrementExpression被执行。这个表达式也是可选的。一旦执行完毕,就重复步骤 2。
清单 2-59 提供了一个简单的例子来演示循环执行。
清单 2-59。 Simple为循环
for (var i =0; i < 10; i++) {
alert(i); // will alert 0 through 9 one at a time
}
在此示例中,循环执行如下:
- 变量
i被声明并设置为 0。 - 循环检查
i < 10是否存在,如果存在,则执行其语句块(在这种情况下,警告i的值)。 i增加 1,重复步骤 2。
请注意,因为每次循环执行时都要计算conditionExpression,如果它是一个开销很大的语句,那么它可能会在您的程序中导致严重的性能问题,尤其是当循环被执行数百次或数千次时。让conditionExpression尽可能简单是个好主意。例如,考虑清单 2-60 中的循环。
清单 2-60。 潜贵循环
for (var i = 0; i < someArray.length; i++) {
// do things
}
在清单 2-60 中,在每次循环开始时,我们检查someArray数组的length属性。这并不是很贵,但是如果someArray有几千件商品,它就能加起来。清单 2-61 提供了一个简单的优化。
清单 2-61。 为循环优化
var someArrayLength = someArray.length,
i = 0;
for (i = 0; i < someArrayLength; i++) {
// do things
}
在清单 2-61 中,我们将数组的长度存储在一个变量中,从而简化了conditionExpression。我们还将i的变量声明移到了循环之外,以明确定义它的作用域。这些都是微小的改进,但是如果阵列很长,好处会很快增加。
for-in 循环
JavaScript 还有一个for-in循环语句。此构造专门用于枚举对象属性:
for (var variable in objectExpression) { statementBlock }
任何计算为对象的表达式都可以在objectExpression中使用;大多数情况下,它只是某种物体。在每次迭代中,对象的一个属性将被赋给变量,然后执行statementBlock。清单 2-62 中显示了一个例子。有关使用for-in循环枚举对象属性的更多信息和例子,请参阅本章前面的“对象”一节。
清单 2-62。 列举一个对象
var myObject = {
prop1: "value1",
prop2: "value2",
prop3: true,
prop4: 100
}
var strAlert = "";
for (var prop in myObject) {
strAlert += prop + " : " + myObject[prop] + "\n";
}
alert(strAlert);
这个例子将提醒
prop1 : value1
prop2 : value2
prop3 : true
prop4 : 100
while 循环
JavaScript 也有while循环,只要它们的条件评估为真,就执行它们的语句块:
while (conditionExpression)
statementBlock
与for循环一样,每次在执行statementBlock之前都会测试条件。如果在第一次循环中,条件评估为假,那么statementBlock将永远不会执行。清单 2-63 提供了一个例子。
清单 2-63。 琐碎而循环
var counter = 0;
while (counter < 10) {
alert(counter);
counter++;
}
在这个例子中,counter变量在循环中递增,所以当它最终达到 10 时,循环将终止。这个例子将导致从 0 到 9 的数字报警,一次一个。
做循环
与while循环类似,do循环在其条件评估为真时执行其语句块:
do
statementBlock
while (conditionExpression);
与while循环和for循环不同,在do循环中,statementBlock在conditionExpression求值之前第一次被执行。因此,do循环的statementBlock将总是至少执行一次。清单 2-64 提供了一个例子。
清单 2-64。 琐碎做循环
var counter = 0;
do {
alert(counter);
counter++;
} while (counter < 10)
和前面的例子一样,这个例子将从 0 到 9 发出警报,一次一个。
摘要
在本章中,我们讨论了使用 JavaScript 的基础知识:
- JavaScript 表达式是计算值的代码段,语句是实现特定目标的表达式块。
- JavaScript 有几种不同的操作符:算术、赋值、按位、比较、逻辑和字符串,还有一些不属于这些类别。
- JavaScript 有特定的优先级来决定在有多个操作符的语句中哪些操作符应该先执行。
- 用
var关键字声明的变量的作用域限于当前作用域;通过访问简单声明的变量被认为是全局变量。 - 您可以使用点符号或方括号来访问对象的属性。
- 可以用文字符号、构造函数或
Object.create()方法创建对象。 - JavaScript 数组是动态的,不会抛出越界错误。
- 您可以通过表达式或声明来创建函数。
- 调用函数的方式决定了它的执行范围。
- 您可以使用
call()方法或apply()方法来指定函数的执行范围。 - JavaScript 同时支持
if-then-else和switch条件。 - JavaScript 支持几种循环类型:
for循环、for-in循环、while循环和do循环。
在下一章,我们将深入探讨文档对象模型:用 JavaScript 表示 HTML 页面。我们将应用本章的知识来创建动态网页、动画和其他效果。