1、什么是JavaScript?

简史

随着网络的普及,对客户端脚本语言的需求逐渐增长。 当时,即使网页的大小和复杂性不断增加,大多数Internet用户仍通过28.8 kbps的调制解调器进行连接。 使用户感到痛苦的是,简单表单验证所需的服务器往返次数众多。 想象一下,填写表单,单击“提交”按钮,等待30秒钟进行处理,然后看到一条消息,提示您忘记填写必填字段。 当时处于技术创新前沿的Netscape开始认真考虑开发客户端脚本语言以处理简单处理的问题。

1995年,一位名叫Brendan Eich的Netscape开发人员开始为发布Netscape Navigator 2开发一种名为Mocha(后来更名为LiveScript)的脚本语言。其目的是要在浏览器和服务器上同时使用它。 称为LiveWire。

Netscape与Sun Microsystems建立了开发联盟,以及时完成LiveScript的发布。 在Netscape Navigator 2正式发布之前,Netscape将LiveScript的名称更改为JavaScript,以利用Java从新闻界收到的嗡嗡声。

由于JavaScript 1.0如此受欢迎,因此Netscape在Netscape Navigator 3中发布了1.1版。新兴Web的流行程度达到了新的高度,并且Netscape已将自己定位为市场上的领先公司。 此时,Microsoft决定将更多资源投入到一个名为Internet Explorer的竞争浏览器中。 在Netscape Navigator 3发布后不久,Microsoft引入了Internet Explorer 3,该Internet Explorer 3带有一个名为JScript的JavaScript实现(为了避免Netscape可能出现许可问题)。 对于Microsoft来说,1996年8月迈入Web浏览器领域的这一重要步骤对Netscape而言是一个臭名昭著的日子,但是它也代表着JavaScript作为一种语言的发展迈出了重要的一步。

微软对JavaScript的实现意味着有两种不同的JavaScript版本在浮动:Netscape Navigator中的JavaScript和Internet Explorer中的JScript。 与C和许多其他编程语言不同,JavaScript没有规范其语法或功能的标准,并且三个不同的版本仅突出了此问题。 随着业界的担忧加剧,决定必须将语言标准化。

1997年,JavaScript 1.1作为提案提交给了欧洲计算机制造商协会(Ecma)。 分配了第39技术委员会(TC39)以“标准化通用的,跨平台的,与供应商无关的脚本语言的语法和语义”(www.ecma-international.org/memento/TC39.htm)。 来自Netscape,Sun,Microsoft,Borland,NOMBAS和其他对脚本的未来感兴趣的公司的程序员,TC39召开了数月的会议,以敲定ECMA-262,该标准定义了一种名为ECMAScript的新脚本语言(通常发音为“ ek-ma-script”)。

次年,国际标准化组织和国际电工委员会(ISO / IEC)也采用ECMAScript作为标准(ISO / IEC-16262)。 从那时起,浏览器就以不同程度的成功尝试将ECMAScript用作其JavaScript实现的基础。

JAVASCRIPT的实现

尽管经常将JavaScript和ECMAScript用作同义词,但JavaScript不仅限于ECMA-262中定义的内容。 实际上,一个完整的JavaScript实现由以下三个不同的部分组成(请参见图1-1):

  • 核心(ECMAScript)
  • 文档对象模型(DOM)
  • 浏览器对象模型(BOM)

ECMAScript

ECMA-262中定义的语言ECMAScript与网络浏览器无关。 实际上,该语言没有任何输入或输出方法。 ECMA-262将该语言定义为可以构建更强大的脚本语言的基础。 Web浏览器只是其中可能存在ECMAScript实现的一种主机环境。 主机环境提供ECMAScript的基本实现以及旨在与环境本身交互的实现扩展。诸如文档对象模型(DOM)之类的扩展使用ECMAScript的核心类型和语法来提供特定于环境的附加功能。其他主机环境包括NodeJS,服务器端JavaScript平台和日益淘汰的Adobe Flash。

ECMA-262如果不引用网络浏览器,究竟会指定什么? 在最基本的层次上,它描述了语言的以下部分:

  • 语法
  • 类型
  • 语句
  • 关键字
  • 保留字
  • 运算符
  • 全局对象

ECMAScript只是对语言的描述,该语言实现了规范中描述的所有方面。 JavaScript实现ECMAScript,Adobe ActionScript也实现。

ECMAScript版本

ECMAScript的不同版本定义为版本(指的是描述特定实现的ECMA-262版本)。ECMA-262的最新版本是2016年发布的第7版。ECMA-262的第一版与Netscape的JavaScript 1.1基本上相同,但是删除了对特定于浏览器的代码的所有引用,并做了一些小的更改:ECMA-262 需要支持Unicode标准(以支持多种语言),并且对象是平台无关的(Netscape JavaScript 1.1实际上具有不同的对象实现,例如Date对象,具体取决于平台)。 这是JavaScript 1.1和1.2不符合ECMA-262第一版的主要原因。

ECMA-262的第二版主要是社论。 该标准已更新,已与ISO / IEC-16262达成严格协议,并且没有任何添加,更改或遗漏。 ECMAScript实施通常不使用第二版来衡量一致性。

ECMA-262的第三版是对该标准的首次真正更新。 它提供了字符串处理,错误定义和数字输出的更新。 它还增加了对正则表达式,新的控制语句,try-catch异常处理和小的更改的支持,以更好地为国际化准备标准。 对许多人来说,这标志着ECMAScript作为一种真正的编程语言的到来。

ECMA-262的第四版是对该语言的全面修订。 为了响应JavaScript在Web上的普及,开发人员开始修订ECMAScript,以满足全球Web开发不断增长的需求。 作为响应,Ecma TC39再次召开会议,决定该语言的未来。 最终的规范基于第三版定义了几乎全新的语言。 第四版包括强类型变量,新语句和数据结构,真实类和经典继承以及与数据交互的新方法。

作为替代建议,TC39的一个小组委员会开发了一种名为“ ECMAScript 3.1”的规范,作为该语言的较小演变,该委员会认为第四版对于该语言而言实在太大了。 结果是提出了一个较小的建议,其中对ECMAScript进行了增量更改,可以在现有JavaScript引擎之上实施。 最终,ES3.1小组委员会赢得了TC39的支持,而ECMA-262的第四版在正式发布之前就被放弃了。

ECMAScript 3.1成为ECMA-262,第五版,并于2009年12月3日正式发布。第五版试图澄清第三版的模棱两可之处,并引入其他功能。 新功能包括一个用于解析和序列化JSON数据的本地JSON对象,用于继承和高级属性定义的方法,以及一个新的严格模式,该模式在某种程度上增强了ECMAScript引擎解释和执行代码的方式。 第五版于2011年6月进行了维护修订; 这仅是为了规范中的更正,没有引入任何新的语言或库功能。

ECMA-262的第六版(俗称ES6,ES2015或ES Harmony)已于2015年6月发布,可以说是自规范制定以来最重要的增强功能。 ES6添加了对类,模块,迭代器,生成器,箭头函数,promise,反射,代理和许多新数据类型的正式支持

ECMA-262的第七版(称为ES7或ES2016)于2016年6月发布。此修订版仅包含少量语法添加,例如Array.prototype.includes和幂运算符。

ECMA-262的第八版(称为ES8或ES2017)已于2017年1月完成。此修订版包括异步迭代,休息和传播属性,新的正则表达式功能的集合,Promise finally()catchall处理程序和模板文字 修订。

第九版ECMA-262仍在定稿中,但是在第3阶段它已经具有大量功能。它最重要的添加可能是动态导入ES6模块。

文档对象模型

文档对象模型(DOM)是XML的应用程序编程接口(API),已扩展为在HTML中使用。 DOM将整个页面映射为节点层次结构。 HTML或XML页面的每个部分都是一种节点,其中包含不同种类的数据。 考虑以下HTML页面:

1
2
3
4
5
6
7
8
<html>
    <head>
        <title>Sample Page</title>
    </head>
    <body>
        <p> Hello World!</p>
    </body>
</html>

可以使用DOM将这段代码绘制成节点的层次结构(请参见图1-2)。

通过创建代表文档的树,DOM使开发人员对其内容和结构的控制达到前所未有的水平。 可以使用DOM API轻松删除,添加,替换和修改节点。

为什么需要DOM

借助Internet Explorer 4和Netscape Navigator 4分别支持不同形式的动态HTML(DHTML),开发人员首次可以在不重新加载网页的情况下更改网页的外观和内容。 这代表了网络技术的巨大进步,但同时也是一个巨大的问题。 Netscape和Microsoft在开发DHTML时采取了不同的方式,从而结束了开发人员可以编写可由任何Web浏览器访问的单个HTML页面的时期。

决定必须采取一些措施来保留网络的跨平台性质。 担心的是,如果有人不控制Netscape和Microsoft,网络将发展为目标浏览器专有的两个不同的派系。 那时,负责创建Web通信标准的万维网联盟(W3C)开始研究DOM。

DOM级别

DOM级别1在1998年10月成为W3C的推荐。它由两个模块组成:DOM核心,它提供了一种映射基于XML的文档的结构的方式,从而可以轻松地访问和操作文档的任何部分, DOM HTML,通过添加特定于HTML的对象和方法扩展了DOM Core。

注意:请注意,DOM不是特定于JavaScript的,并且确实已经以许多其他语言实现。 但是,对于Web浏览器,已经使用ECMAScript实现了DOM,现在DOM构成了JavaScript语言的很大一部分。

DOM级别1的目标是映射文档的结构,而DOM级别2的目标则要广泛得多。 原始DOM的此扩展增加了对鼠标和用户界面事件(DHTML长期支持),范围和遍历(迭代DOM文档的方法)的支持,并通过对象界面支持层叠样式表(CSS)。 在级别1中引入的原始DOM Core也已扩展为包括对XML名称空间的支持。

浏览器对象模型

Internet Explorer 3和Netscape Navigator 3浏览器具有浏览器对象模型(BOM),该模型允许访问和操纵浏览器窗​​口。 使用BOM,开发人员可以在其显示页面的上下文之外与浏览器进行交互。 使BOM表真正独特且常常存在问题的原因是,它是JavaScript实现中唯一没有相关标准的部分。 随着HTML5的引入,这种情况发生了变化,HTML5试图将许多BOM编入正式规范的一部分。 多亏了HTML5,有关BOM的许多困惑已消除。

首先,BOM处理浏览器的窗口和框架,但是通常任何特定于浏览器的JavaScript扩展都被视为BOM的一部分。 以下是一些此类扩展:

  • 弹出新浏览器窗口的功能
  • 移动,调整和关闭浏览器窗口大小的功能
  • 导航器对象,提供有关浏览器的详细信息
  • 位置对象,提供有关浏览器中加载的页面的详细信息
  • 屏幕对象,提供详细信息 有关用户屏幕分辨率的信息
  • 性能对象,提供有关浏览器的内存消耗,导航行为和计时统计信息的详细信息
  • 对cookie的支持
  • 自定义对象(例如XMLHttpRequest和Internet Explorer的ActiveXObject)

由于很长一段时间以来,BOM都没有标准,因此每个浏览器都有自己的实现。有一些事实上的标准,例如具有窗口对象和导航器对象,但是每个浏览器都为这些对象和其他对象定义了自己的属性和方法。 现在有了HTML5,BOM的实现细节有望以更加兼容的方式增长。在“浏览器对象模型”一章中包含有关BOM的详细讨论。

JAVASCRIPT版本

作为原始Netscape的后代,Mozilla是唯一延续原始JavaScript版本编号顺序的浏览器供应商。 当将Netscape源代码分解成一个开源项目(名为Mozilla Project)时,JavaScript的最新浏览器版本是1.3。 (如前所述,版本1.4仅在服务器上实现。)随着Mozilla Foundation继续从事JavaScript的工作,添加了新功能,关键字和语法,JavaScript版本号也随之增加。

编号方案基于Firefox 4将具有JavaScript 2.0的思想,并且在此之前版本号的每次增加都表示JavaScript实现与2.0提案有多接近。 尽管这是最初的计划,但是JavaScript的发展是不可能的。 当前没有针对JavaScript 2.0的目标实现,并且这种样式化的版本控制在Firefox 4版本之后就停止了。

注意:必须注意,只有Netscape / Mozilla浏览器遵循此版本控制方案。 例如,Internet Explorer具有不同的JScript版本号。 这些JScript版本与上表中提到的JavaScript版本完全不对应。 此外,大多数浏览器都将JavaScript支持与ECMAScript遵从程度和DOM支持联系起来。

小结

JavaScript是一种旨在与网页交互的脚本语言,由以下三个不同的部分组成:

  • 在ECMA-262中定义的ECMAScript,提供核心功能
  • 文档对象模型(DOM),提供用于处理网页内容的方法和界面
  • 浏览器对象模型(BOM),提供与浏览器进行交互的方法和界面

在五个主要的Web浏览器(Internet Explorer,Firefox,Chrome,Safari和Opera)中,对JavaScript的三个部分的支持程度各不相同。 通常,在所有浏览器中对ECMAScript 5的支持都很好,并且对ECMAScript 6和7的支持正在增长。对DOM的支持各不相同,但是3级合规性越来越规范。 尽管假定存在一些共同点,但HTML5中编入的BOM可能因浏览器而异。

2、HTML中的JavaScript

将JavaScript引入网页后,网页上的主流语言即HTML立刻出现了问题。 作为有关JavaScript的原始工作的一部分,Netscape试图找出如何使JavaScript在HTML页面中共存,而又不破坏这些页面在其他浏览器中的呈现方式。 经过反复试验,错误和争议,最终做出了一些决定,并同意将通用脚本支持引入网络。 在网络的早期阶段所做的许多工作都幸存下来,并在HTML规范中正式化。

<Script>元素

将JavaScript插入HTML页面的主要方法是通过<script>元素。此元素是由Netscape创建的,并首先在Netscape Navigator 2中实现。后来被添加到正式的HTML规范中。<script>元素有六个属性

  • async-可选。表示脚本应立即开始下载,但不应阻止页面上的其他操作,例如下载资源或等待其他脚本加载。仅对外部脚本文件有效。
  • charset-可选。使用src属性指定的代码的字符集。很少使用此属性,因为大多数浏览器都不尊重它的价值。
  • crossorigin-可选。 为关联的请求配置CORS设置; 默认情况下,根本不使用CORS。crossorigin =“ anonymous”将配置文件请求不设置凭据标志。 crossorigin =“ use-credentials”将设置凭据标志,这意味着传出的请求将包含凭据
  • defer-可选。 表示可以安全地推迟执行脚本,直到完全解析并显示了文档内容为止。 仅对外部脚本有效。 Internet Explorer 7和更早版本也允许使用内联脚本。
  • integrity-可选。 通过对照提供的加密签名检查检索到的资源,允许验证子资源完整性(SRI)。 如果检索到的资源的签名与此属性指定的签名不匹配,则页面将错误并且脚本将不会执行。 这对于确保内容分发网络(CDN)不提供恶意有效负载非常有用。
  • language-已弃用。 最初指示代码块正在使用的脚本语言(例如“ JavaScript”,“ JavaScript1.2”或“ VBScript”)。 大多数浏览器都忽略此属性。 它不应该被使用。
  • src-可选。 表示包含要执行代码的外部文件。
  • type-可选。 替换language; 指示代码块使用的脚本语言的内容类型(也称为MIME类型)。 传统上,此值始终为“ text / javascript”,尽管“ text / javascript”和“ text / ecmascript”均已弃用。 JavaScript文件通常以“ application / x-javascript” MIME类型提供服务,即使在type属性中设置此名称也可能导致脚本被忽略。 在非Internet Explorer浏览器中可用的其他值是“ application / javascript”和“ application / ecmascript”。 如果值为模块,则将代码视为ES6模块,然后才有资格使用import和export关键字。

使用<script>元素有两种方法:将JavaScript代码直接嵌入到页面中,或包含来自外部文件的JavaScript。

小结

使用<script>元素将JavaScript插入HTML页面。 该元素可用于将JavaScript嵌入HTML页面,使其与标记的其余部分保持内联,或包括存在于外部文件中的JavaScript。 以下是要点:

  • 要包含外部JavaScript文件,必须将src属性设置为要包含的文件的URL,该URL可以是与包含页面位于同一服务器上的文件,也可以是位于完全不同的域中的一个文件。
  • 所有<script>元素均按照它们在页面上出现的顺序进行解释。 只要不使用defer和async属性,必须完全解释<script>元素中包含的代码,然后才能开始下一个<script>元素中的代码。
  • 对于非延迟脚本,浏览器必须先完成<script>元素内的代码解释,然后才能继续呈现页面的其余部分。 因此,通常在页面末尾,主要内容之后以及结束</body>标记之前包含<script>元素。
  • 您可以使用defer属性将脚本的执行推迟到文档渲染完成之后。 延迟脚本始终按照指定的顺序执行。
  • 您可以使用async属性指示一个脚本无需等待其他脚本,也不必阻止文档呈现。 异步脚本不能保证以它们在页面中出现的顺序执行。

通过使用<noscript>元素,您可以指定仅当浏览器不提供脚本支持时才显示内容。如果在浏览器中启用了脚本,则不会呈现<noscript>元素中包含的任何内容。

3、语言基础

语法

ECMAScript的语法大量借鉴了C语言和其他类似C的语言,例如Java和Perl。熟悉此类语言的开发人员应该可以轻松地掌握ECMAScript较为宽松的语法。

区分大小写

要理解的第一个概念是,所有内容都区分大小写。 变量,函数名和运算符均区分大小写,这意味着名为test的变量与名为Test的变量不同。 同样,typeof不能是函数的名称,因为它是关键字(在下一节中介绍); 但是,typeof是一个完全有效的函数名称。

标识符

标识符是变量,函数,属性或函数参数的名称。标识符可以是一个或多个以下格式的字符:

  • 第一个字符必须是字母,下划线(_)或美元符号($)。
  • 所有其他字符可以是字母,下划线,美元符号或数字。

标识符中的字母可以包括扩展的ASCII或Unicode字母字符,例如À和Æ,尽管不建议这样做。

按照惯例,ECMAScript标识符使用驼峰式大小写,这意味着第一个字母是小写字母,每个其他单词都用大写字母偏移,如下所示:

  • firstSecond
  • myCar
  • doSomethingImportant

尽管未严格执行此操作,但遵循遵循此格式的内置ECMAScript函数和对象被视为最佳实践。

注意:关键字,保留字,true,false和null不能用作标识符。有关更多详细信息,请参见稍后出现的“关键字和保留字”部分。

注释

ECMAScript对单行注释和块注释均使用C样式注释。单行注释以两个正斜杠字符开头,例如:

1
// single line comment

块注释以正斜杠和星号(/ )开头,以反斜杠( /)结束,如以下示例所示:

1
2
/* This is a multi-line
comment */

严格模式

ECMAScript 5引入了严格模式的概念。 严格模式是JavaScript的另一种解析和执行模型,其中ECMAScript 3的某些不稳定行为已得到解决,并且为不安全的活动抛出了错误。 要为整个脚本启用严格模式,请在顶部包括以下内容:

1
"use strict";

尽管这看起来像是未分配给变量的字符串,但这是一种杂语,它告诉支持JavaScript的引擎更改为严格模式。语法是专门选择的,以免破坏ECMAScript 3语法。

您还可以通过在函数主体顶部包括编译指示来指定仅在严格模式下执行的函数:

1
2
3
4
function doSomething() {
    "use strict";
    // function body
}

严格模式改变了JavaScript执行方式的许多部分,因此,整本书都指出了严格的模式区别。 所有现代浏览器均支持严格模式。

陈述

ECMAScript中的语句以分号终止,尽管省略分号会使解析器确定语句末尾的位置。

即使在语句末尾不需要分号,也应始终包括一个分号。包括分号有助于防止遗漏错误,例如不完成您键入的内容,并允许开发人员通过删除多余的空格来压缩ECMAScript代码(当行不以分号结尾时,这种压缩会导致语法错误)。 在某些情况下,包括分号也可以提高性能,因为解析器会尝试通过在它们似乎属于的地方插入分号来纠正语法错误。

可以使用C样式语法将多个语句组合到代码块中,以左花括号({)开始,以右花括号(})结尾:

1
2
3
4
if (test) {
    test = false;
    console.log(test);
}

控制语句(例如if)仅在执行多个语句时才需要代码块。 但是,最好将代码块始终与控制语句一起使用,即使只有一条语句要执行.

将代码块用于控制语句可以使意图更清晰,并且在需要进行更改时出现错误的可能性较小。

关键字和保留字

ECMA-262描述了一组保留的关键字,它们具有特定的用途,例如指示控制语句的开始或结束或执行特定的操作。根据规则,关键字是保留的,不能用作标识符或属性名称。 第六版ECMA-262的关键字完整列表如下:

break do in typeof
case else instanceof var
catch export new void
class extends return while
const finally super with
continue for switch yield
debugger function this
default if throw
delete import try

该规范还描述了一组将来的保留字,它们不能用作标识符或属性名。尽管保留字在语言中没有任何特定用法,但保留它们以备将来用作关键字。

以下是ECMA-262(第六版)中定义的将来保留字的完整列表:

  • 一律保留:enum
  • 在严格模式下保留:implements package public interface protected static let private
  • 在模块代码中保留:await

这些词可能仍不能用作标识符,但现在可以用作对象中的属性名称。一般来说,最好避免同时使用关键字和保留字作为标识符和属性名称,以确保与过去和将来的ECMAScript版本兼容。

变量

ECMAScript变量是松散类型的,这意味着变量可以保存任何类型的数据。 每个变量只是一个值的命名占位符。 可以使用三个关键字来声明变量:var(在所有ECMAScript版本中可用)以及const和let(在ECMAScript 6中引入)。

“var”关键字

要定义变量,请使用var运算符(请注意var是关键字),后跟变量名称(如前所述,为标识符),如下所示:

1
var message;

这段代码定义了一个名为message的变量,可以用来保存任何值。(不进行初始化,它将保留未定义的特殊值,这将在下一节中讨论。)ECMAScript实现变量初始化,因此可以定义变量并同时设置其值,如下例所示:

1
var message = "hi";

在此,message定义为保留字符串值“ hi”。 进行初始化不会将变量标记为字符串类型; 它只是将值分配给变量。 仍然不仅可以更改存储在变量中的值,还可以更改值的类型,例如:

1
2
var message = "hi";
message = 100; // legal, but not recommended

在此示例中,变量message首先被定义为具有字符串值“ hi”,然后被数字值100覆盖。尽管不建议切换变量包含的数据类型,但这在ECMAScript中是完全有效的。

var声明范围

重要的是要注意,使用var运算符定义变量可以使其在定义该函数的函数范围内是局部的。例如,使用var在函数内部定义变量意味着该函数退出后立即销毁该变量,如下所示:

1
2
3
4
5
function test() {
    var message = "hi"; // local variable
}
test();
console.log(message); // error!

在此,message变量是在使用var的函数中定义的。 该函数称为test(),该函数创建变量并分配其值。 之后,该变量立即被销毁,因此本示例中的最后一行会导致错误。 但是,可以通过如下简单地省略var运算符来全局定义变量

1
2
3
4
5
function test() {
    message = "hi"; // global variable
}
test();
console.log(message); // "hi"

通过从示例中删除var运算符,消息变量将变为全局变量。 一旦调用了test()函数,该变量即被定义,并且一旦执行便可以在函数外部访问。

注意:尽管可以通过省略var运算符来定义全局变量,但不建议使用此方法。 局部定义的全局变量难以维护,并且会引起混乱,因为是否有意省略var尚不明显。 当为未声明的变量分配值时,严格模式将引发ReferenceError。

如果需要定义多个变量,则可以使用一个语句来完成,用逗号分隔每个变量(和可选的初始化),如下所示:

1
2
3
var message = "hi",
    found = false,
    age = 29;

在此,定义并初始化了三个变量。 由于ECMAScript是松散类型的,因此可以将使用不同数据类型的变量初始化组合为一个语句。 尽管没有必要插入换行符和使变量缩进,但这有助于提高可读性。

在严格模式下运行时,不能定义名为eval或arguments的变量。这样做会导致语法错误。

var声明提升

使用var时,可能会发生以下情况,因为使用该关键字声明的变量被提升到函数作用域的顶部:

1
2
3
4
5
function foo() {
    console.log(age);
    var age = 26;
}
foo(); // undefined

这不会引发错误,因为ECMAScript运行时在技术上将其视为这样:

1
2
3
4
5
6
function foo() {
    var age;
    console.log(age);
    age = 26;
}
foo(); // undefined

这是“提升”,解释器将所有变量声明拉到其作用域的顶部。 它还允许您使用冗余的var声明而不会受到惩罚:

1
2
3
4
5
6
7
function foo() {
    var age = 16;
    var age = 26;
    var age = 36;
    console.log(age);
}
foo(); // 36

“let”声明

let的操作几乎与var相同,但是有一些重要的区别。 最值得注意的是let是块作用域的,而var是函数作用域的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if (true) {
    var name = 'Matt';
    console.log(name); // Matt
}
console.log(name); // Matt
if (true) {
    let age = 26;
    console.log(age); // 26
}
console.log(age); // ReferenceError: age is not defined

在这里,age变量不能在if块之外引用,因为它的范围不会扩展到该块之外。 块作用域严格来说是函数作用域的子集,因此任何适用于var声明的作用域限制也将适用于let声明。

let声明还不允许在块范围内进行任何多余的声明。这样做将导致错误:

1
2
3
4
5
var name;
var name;

let age;
let age; // SyntaxError; identifier 'age' has already been declared

当然,JavaScript引擎将跟踪用于变量声明的标识符及其在其中声明的块作用域,因此使用相同标识符的嵌套的行为与您期望的一样,没有错误,因为没有发生重新声明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var name = 'Nicholas';
console.log(name); // 'Nicholas'
if (true) {
    var name = 'Matt';
    console.log(name); // 'Matt'
}
let age = 30;
console.log(age); // 30
if (true) {
    let age = 26;
    console.log(age); // 26
}

声明冗余错误不是顺序的函数,如果let与var混合,则不会受到影响。不同的关键字不会声明变量的不同类型,它们只是指定变量在相关范围内的存在方式。

1
2
3
4
5
var name;
let name; // SyntaxError

let age;
var age; // SyntaxError

时间死区

let区别于var的另一个重要行为是,let声明不能以假定提升的方式使用:

1
2
3
4
5
6
7
// name is hoisted
console.log(name); // undefined
var name = 'Matt';

// age is not hoisted
console.log(age); // ReferenceError: age is not defined
let age = 26;

解析代码时,JavaScript引擎仍会知道稍后在块中出现的let声明,但是在实际声明发生之前,将无法以任何方式引用这些变量。 在声明之前发生的执行段称为“时间死区”,任何对这些变量的引用尝试都将引发ReferenceError。

全局声明

与var关键字不同,在全局上下文中使用let声明变量时,变量不会像使用var那样附加到窗口对象。

1
2
3
4
5
var name = 'Matt';
console.log(window.name); // 'Matt'

let age = 26;
console.log(window.age); // undefined

但是,let声明仍将在全局块作用域内发生,该作用域将在页面的生存期内保持不变。 因此,必须确保页面不会尝试重复声明,以避免引发SyntaxError。

有条件的声明

当使用var声明变量时,由于提升了声明,因此JavaScript引擎会很乐意将冗余声明合并到作用域顶部的单个声明中。由于let声明的作用域是块,因此无法检查是否曾经声明过let变量,只有在没有声明时才有条件地声明它。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<script>
    var name = 'Nicholas';
    let age = 26;
</script>

<script>
    // Suppose this script is unsure about what has already been declared in the page.
    // It will assume variables have not been declared.
    var name = 'Matt';
    // No problems here, since this will be handled as a single hoisted declaration.
    // There is no need to check if it was previously declared.
    let age = 36;
    // This will throw an error when 'age' has already been declared.
</script>

使用 try/catch 语句或 typeof 运算符不是解决方案,因为条件块中的let声明将作用于该块。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script>
    let name = 'Nicholas';
    let age = 36;
</script>

<script>
    // Suppose this script is unsure about what has already been declared in the page.
    // It will assume variables have not been declared.
    if (typeof name !== 'undefined') {
        let name;
    }
    // 'name' is restricted to the if {} block scope,
    // so this assignment will act as a global assignment
    name = 'Matt';
    
    try (age) {
        // If age is not declared, this will throw an error
    }
    catch(error) {
        let age;
    }
    // 'age' is restricted to the catch {} block scope,
    // so this assignment will act as a global assignment
    age = 26;
</script>

因此,您不能依靠带有此新ES6声明关键字的条件声明模式。

注意: 不能使用let进行条件声明是一件好事,因为条件声明是代码库中的错误模式。 这使得很难理解程序流程。 如果您发现自己已经达到了这种模式,那么很有可能找到一种更好的方式编写它。

循环中的let声明

在let出现之前,for循环定义涉及使用迭代器变量,该变量的定义会在循环主体之外流出:

1
2
3
4
for (var i = 0; i < 5; ++i) {
    // do loop things
}
console.log(i); // 5

切换到let声明时,这不再是问题,因为iterator变量将仅作用于for循环块:

1
2
3
4
for (let i = 0; i < 5; ++i) {
    // do loop things
}
console.log(i); // ReferenceError: i is not defined

使用var时,经常遇到的问题是迭代器变量的单数声明和修改:

1
2
3
4
5
for (var i = 0; i < 5; ++i) {
    setTimeout(() => console.log(i), 0)
}
// You might expect this to console.log 0, 1, 2, 3, 4
// It will actually console.log 5, 5, 5, 5, 5

发生这种情况是因为循环退出时,其迭代器变量仍设置为导致循环退出的值:5。稍后执行超时时,它们引用该变量,因此console.log其最终值。

使用let声明循环迭代器时,JavaScript引擎实际上会在每次循环迭代时声明一个新的迭代器变量。 每个setTimeout都引用该单独的实例,因此它将console.log期望的值:执行该循环迭代时迭代器变量的值。

1
2
3
4
for (let i = 0; i < 5; ++i) {
    setTimeout(() => console.log(i), 0)
}
// console.logs 0, 1, 2, 3, 4

这种逐项声明的行为适用于所有样式的for循环,包括for-in和for-of循环。

“const”声明

const的行为与let的行为相同,但有一个重要的区别-必须使用一个值对其进行初始化,并且在声明后不能重新定义该值。 尝试修改const变量将导致运行时错误。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const age = 26;
age = 36; // TypeError: assignment to a constant

// const still disallows redundant declaration
const name = 'Matt';
const name = 'Nicholas'; // SyntaxError
// const is still scoped to blocks
const name = 'Matt';
if (true) {
    const name = 'Nicholas';
}
console.log(name); // Matt

const声明仅相对于对其所指向的变量的引用而强制执行。如果const变量引用了一个对象,则它不会违反const约束来修改该对象内部的属性。

1
2
const person = {};
person.name = 'Matt'; // ok

即使JavaScript引擎正在for循环中创建let迭代器变量的新实例,并且即使const变量的行为与let变量相似,也不能使用const声明for循环迭代器:

1
for (const i = 0; i < 10; ++i) {} // TypeError: assignment to constant variable

但是,如果要声明未修改的for循环变量,则允许使用const,这恰好是因为每次迭代都声明了一个新变量。 这对于for-of和for-in循环尤为重要:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let i = 0;
for (const j = 7; i < 5; ++i) {
    console.log(j);
}
// 7, 7, 7, 7, 7
for (const key in {a: 1, b: 2}) {
    console.log(key);
}
// a, b
for (const value of [1,2,3,4,5]) {
    console.log(value);
}
// 1, 2, 3, 4, 5

声明样式和最佳做法

ECMAScript 6中let和const的引入以提高声明范围和语义精度的形式在客观上为该语言带来了更好的工具。 众所周知,var声明的异常行为导致JavaScript社区由于其引起的所有问题而拖延了多年的发展。 在引入这些新关键字之后,出现了一些可以提高代码质量的越来越常见的模式。

不要使用var

使用let和const,大多数开发人员将发现他们不再需要在任何地方的代码库中使用var。由于对变量范围,声明局部性和const正确性的精心管理,将变量声明限制为仅让let和const出现的模式将有助于提高代码库质量。

喜欢const超过let

使用const声明允许浏览器运行时强制执行常量变量,以及使用静态代码分析工具来预见非法的重新分配操作。因此,许多开发人员认为默认情况下将变量声明为const对他们有利,除非他们知道他们需要在某个时候重新分配其值。这使开发人员可以更具体地推断出他们知道永远不会改变的值,并在代码执行尝试执行意外的值重新分配的情况下快速检测意外行为。

数据类型

ECMAScript中有六种简单的数据类型(也称为原始类型):Undefined,Null,Boolean,Number,String和Symbol。 Symbol是ECMAScript 6中新引入的。还有一个称为Object的复杂数据类型,它是名称-值对的无序列表。 由于无法在ECMAScript中定义自己的数据类型,因此所有值都可以表示为这七个值之一。 仅具有七个数据类型似乎太少而无法完全表示数据。 但是,ECMAScript的数据类型具有动态方面,使单个数据类型的行为都像多个数据类型。

typeof运算符

由于ECMAScript是松散类型的,因此需要一种方法来确定给定变量的数据类型。 typeof运算符提供该信息。 在值上使用typeof运算符将返回以下字符串之一:

  • 如果值未定义,则为“undefined”
  • “boolean”(如果值是布尔值)
  • “string”(如果值是字符串)
  • “number”(如果值是数字)
  • “object”(如果值是对象(函数以外)或 null)
  • “function”(如果值是函数)
  • “symbol”(如果值是Symbol)

typeof运算符的调用方式如下:

1
2
3
4
let message = "some string";
console.log(typeof message); // "string"
console.log(typeof(message)); // "string"
console.log(typeof 95); // "number"

在此示例中,变量(message)和数字文字均传递到typeof运算符中。请注意,由于typeof是运算符而不是函数,因此不需要括号(尽管可以使用它们)

请注意,在某些情况下typeof似乎返回了一个令人困惑但在技术上正确的值。 调用typeof null返回值“ object”,因为特殊值null被认为是空对象引用。

注意: 从技术上讲,函数在ECMAScript中被视为对象,并不代表其他数据类型。 但是,它们确实具有一些特殊的属性,这需要通过typeof运算符区分函数和其他对象。

Undefined类型

未定义类型只有一个值,即特殊值undefined。 使用var或let声明变量但未初始化变量时,将为其分配undefined值,如下所示:

1
2
let message;
console.log(message == undefined); // true

在此示例中,声明变量message时不对其进行初始化。 与undefined的文字值比较时,两者相等。 此示例与以下示例相同:

1
2
let message = undefined;
console.log(message == undefined); // true

此处,变量message已显式初始化为undefined。这是不必要的,因为默认情况下,任何未初始化的变量都将获得undefined的值。

注意: 通常来说,您绝对不应将变量明确设置为undefined。 字面量undefined值主要用于比较,直到第三版ECMA-262才添加,以帮助形式化空对象指针(null)和未初始化变量之间的差异。

请注意,包含undefined值的变量与根本没有定义的变量不同。 考虑以下:

1
2
3
4
5
let message; // this variable is declared but has a value of undefined
// make sure this variable isn't declared
// let age
console.log(message); // "undefined"
console.log(age); // causes an error

在此示例中,第一个console.log显示变量消息,该消息为“undefined”。 在第二个console.log中,未声明的变量age被传递到console.log()函数中,这会导致错误,因为尚未声明该变量。 未声明的变量只能执行一个有用的操作:您可以在其上调用typeof(在未声明的变量上调用delete不会导致错误,但这不是很有用,实际上在严格模式下会引发错误)。

当对未初始化的变量进行调用时,typeof运算符将返回“ undefined”,但对未声明的变量进行调用时,也会返回“ undefined”,这可能会造成混淆。 考虑以下示例:

1
2
3
4
5
let message; // this variable is declared but has a value of undefined
// make sure this variable isn't declared
// let age
console.log(typeof message); // "undefined"
console.log(typeof age); // "undefined"

在这两种情况下,在变量上调用typeof都会返回字符串“ undefined”。 从逻辑上讲,这是有道理的,因为即使在技术上存在很大差异,也无法使用任何一个变量执行实际操作。

注意: 即使未初始化的变量会自动分配为undefined的值,建议始终初始化变量。 这样,当typeof返回“undefined”时,您会知道这不是因为尚未声明给定变量,而只是未对其进行初始化。

undefined的值是false的;因此,您可以在需要的地方进行更简洁的检查。但是请记住,许多其他可能的值也是虚假的,因此在需要测试确切值undefined而不是虚假值的情况下要小心:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let message; // this variable is declared but has a value of undefined
// 'age' is not declared
if (message) {
    // This block will not execute
}
if (!message) {
    // This block will execute
}
if (age) {
    // This will throw an error
}

Null类型

Null类型是仅具有一个值的第二种数据类型:特殊值null。 从逻辑上讲,空值是一个空的对象指针,这就是为什么在以下示例中typeof传递空值时会返回“对象”的原因:

1
2
let car = null;
console.log(typeof car); // "object"

当定义一个打算稍后用于保存对象的变量时,建议将变量初始化为null,而不是其他任何东西。 这样,您可以显式检查值null,以确定该变量以后是否已用对象引用填充,例如以下示例:

1
2
3
if (car != null) {
    // do something with car
}

值undefined是null的派生,因此ECMA-262将它们定义为表面上相等,如下所示:

1
console.log(null == undefined); // true

在空值和未定义值之间使用相等运算符(==)始终返回true,但请记住,==运算符将转换其操作数以进行比较(本章稍后将详细介绍)。

即使null和undefined相关联,它们的用法也有很大不同。 如前所述,永远不要将变量的值显式设置为undefined,但对于null则不适用。 任何时候期望对象但不可用,都应在其位置使用null。 这有助于将null范式保留为空对象指针,并进一步将其与undefined区分开。

空类型是false的。 因此,您可以在需要的地方进行更简洁的检查。 但是请记住,许多其他可能的值也是false的,因此在需要测试确切的null值而不是false的值的情况下要小心:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let message = null;
let age;

if (message) {
    // This block will not execute
}
if (!message) {
    // This block will execute
}
if (age) {
    // This block will not execute
}
if (!age) {
    // This block will execute
}

Boolean类型

布尔类型是ECMAScript中最常用的类型之一,只有两个文字值:true和false。 这些值与数值不同,因此true不等于1false不等于0。将布尔值分配给变量如下:

1
2
let found = true;
let lost = false;

请注意,布尔文字true和false区分大小写,因此True和False(以及其他大写和小写字母的混合形式)作为标识符有效,但对于布尔值无效。

尽管只有两个文字布尔值,但是所有类型的值在ECMAScript中都具有等效的布尔值。 要将值转换为其等效的布尔值,将调用特殊的Boolean()强制转换函数,如下所示:

1
2
let message = "Hello world!";
let messageAsBoolean = Boolean(message);

在此示例中,字符串message被转换为布尔值并存储在messageAsBoolean中。可以在任何类型的数据上调用Boolean()强制转换函数,并且始终返回布尔值。何时将值转换为true或false的规则取决于数据类型以及实际值。下表概述了各种数据类型及其特定的转换:

理解这些转换很重要,因为流控制语句(例如if语句)会自动执行此布尔转换,如下所示:

1
2
3
4
let message = "Hello world!";
if (message) {
    console.log("Value is true");
}

在此示例中,将显示console.log,因为字符串消息将自动转换为其等效的布尔值(true)。 重要的是要了解由于这种自动转换而在流控制语句中使用的变量。 错误地使用对象而不是布尔值可以极大地改变应用程序的流程。

Number类型

ECMAScript中最有趣的数据类型可能是数字,它使用IEEE–754格式表示整数和浮点值(在某些语言中也称为双精度值)。 为了支持各种类型的数字,有几种不同的数字文字格式。

最基本的数字文字格式是十进制整数格式,可以直接输入。

整数也可以表示为八进制(基数8)或十六进制(基数16)文字。 对于八进制文字,第一个数字必须为零(0),后跟八进制数字序列(数字0至7)。如果在文字中检测到超出此范围的数字,那么将忽略前导零,并将该数字视为十进制,如以下示例所示:

1
2
3
let octalNum1 = 070; // octal for 56
let octalNum2 = 079; // invalid octal - interpreted as 79
let octalNum3 = 08; // invalid octal - interpreted as 8

在严格模式下运行时,八进制文字无效,并且会导致JavaScript引擎引发语法错误。

要创建十六进制文字,您必须使前两个字符为0x(不区分大小写),后接任意数量的十六进制数字(0至9,以及A至F)。 字母可以大写或小写。 这是一个例子:

1
2
let hexNum1 = 0xA; // hexadecimal for 10
let hexNum2 = 0x1f; // hexadecimal for 31

使用八进制或十六进制格式创建的数字在所有算术运算中均视为十进制数字。

注意: 由于数字在JavaScript中的存储方式,实际上可能有一个正零(+0)和负零(–0)值。 在所有情况下,正零和负零被认为是等效的,但为清楚起见,在本文中对其进行了注明。

要定义浮点值,必须包含一个小数点,并且在小数点后至少要有一个数字。 尽管小数点前不需要整数,但建议使用。 这里有些例子:

1
2
3
let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // valid, but not recommended

因为存储浮点值使用的内存是存储整数值的两倍,所以ECMAScript始终在寻找将值转换为整数的方法。如果小数点后没有数字,则该数字变为整数。 同样,如果要表示的数字是整数(例如1.0),则它将转换为整数,如本例所示:

1
2
let floatNum1 = 1.; // missing digit after decimal - interpreted as integer 1
let floatNum2 = 10.0; // whole number - interpreted as integer 10

对于非常大或非常小的数字,可以使用e表示法表示浮点值。E表示用来表示一个数字,该数字应乘以10等于给定幂。 ECMAScript中的电子注释格式为数字(整数或浮点数),后跟大写或小写字母E,再乘以10的幂。 考虑以下:

1
let floatNum = 3.125e7; // equal to 31250000

在此示例中,即使floatNum使用e表示法以更紧凑的形式表示,也等于31,250,000。 该符号本质上说:“取3.125乘以10^7。”

e符号也可以用于表示非常小的数字,例如0.00000000000000003,可以将其更简洁地写为3e-17。 默认情况下,ECMAScript会将小数点后至少六个零的任何浮点值转换为e表示法(例如,0.0000003变为3e–7)。

浮点值的精度最高为小数点后17位,但算术计算中的精度远不如整数。 例如,将0.1和0.2相加将产生0.30000000000000004而不是0.3。 这些小的舍入误差使得很难测试特定的浮点值。考虑以下示例:

1
2
3
if (a + b == 0.3) { // avoid!
    console.log("You got 0.3.");
}

在这里,测试了两个数字的总和,看是否等于0.3。 这将适用于0.05和0.25以及0.15和0.15。 但是,如前所述,如果应用于0.1和0.2,则此测试将失败。因此,永远不要测试特定的浮点值。

注意: 必须了解,舍入误差是浮点算术在基于IEEE-754的数字中完成的方式的副作用,并且并非ECMAScript独有。使用相同格式的其他语言也存在相同的问题。

取值范围

由于内存限制,并非世界上所有数字都可以用ECMAScript表示。ECMAScript中可以表示的最小数字存储在Number.MIN_VALUE中,并且在大多数浏览器中为5e–324。 最大的数字存储在Number.MAX_VALUE中,并且在大多数浏览器中为1.7976931348623157e + 308。 如果计算得出的数字无法用JavaScript的数字范围表示,则该数字会自动获得Infinity的特殊值。 任何不能表示的负数是–Infinity(负无穷大),任何不能表示的正数就是Infinity(正无穷大)

如果计算返回正或负的Infinity,则该值不能再用于任何进一步的计算,因为Infinity没有用于计算的数字表示形式。 为了确定一个值是否是有限的(即它出现在最小值和最大值之间),有一个isFinite()函数。 仅当参数在最小值和最大值之间时,此函数才返回true,如以下示例所示:

1
2
let result = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(result)); // false

尽管很少会进行超出有限数值范围之外的值的计算,但是在进行非常大或非常小的计算时,这是可能的并且应受到监视。

注意: 您还可以通过访问Number.NEGATIVE_INFINITYNumber.POSITIVE_INFINITY获取正负无穷大的值。 如您所料,这些属性分别包含值–Infinity和Infinity。

NaN

有一个特殊的数值,称为NaN,是Not Number的缩写,用于指示用于返回数字的操作何时失败(与引发错误相反)。 例如,将任何数字除以0通常会导致其他编程语言出错,从而导致代码执行中断。 在ECMAScript中,将数字除以0将返回NaN,这将允许其他处理继续进行。

NaN值具有几个独特的属性。 首先,任何涉及NaN的操作都始终返回NaN(例如NaN / 10),这在多步计算的情况下可能会出现问题。 其次,NaN不等于任何值,包括NaN。 例如,以下返回false:

1
console.log(NaN == NaN); // false

因此,ECMAScript提供了isNaN()函数。 该函数接受单个参数,该参数可以是任何数据类型,以确定该值是否为“非数字”。 将值传递给isNaN()时,会尝试将其转换为数字。 一些非数字值会直接转换为数字,例如字符串“10”或布尔值。 任何不能转换为数字的值都会导致该函数返回true。考虑以下:

1
2
3
4
5
console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false - 10 is a number
console.log(isNaN("10")); // false - can be converted to number 10
console.log(isNaN("blue")); // true - cannot be converted to a number
console.log(isNaN(true)); // false - can be converted to number 1
数值转换

可以使用三个函数将非数值转换为数字:Number()强制转换函数,parseInt()函数和parseFloat()函数。 第一个函数Number()可以用于任何数据类型另外两个函数专门用于将字符串转换为数字。这些功能对同一输入的反应不同。

String类型

String数据类型表示零个或多个16位Unicode字符的序列。 可以使用双引号(“),单引号(')或反引号(`)来描述字符串。

Symbol类型

ECMAScript 6中的新增功能是Symbol数据类型。符号是原始值,符号实例是唯一且不可变的。符号的目的是成为对象属性的有保证的唯一标识符,而不会冒属性冲突的危险。

尽管它们似乎与私有属性具有某些相似之处,但是符号并不旨在提供私有属性行为(尤其是因为对象API提供了容易发现符号属性的方法)。 取而代之的是,符号旨在用作唯一的令牌,可用于使用字符串以外的其他键来键入特殊属性。

使用Symbol函数实例化符号。因为它是它自己的原始类型,所以typeof运算符会将一个符号标识为symbol。

1
2
let sym = Symbol();
console.log(typeof sym); // symbol

调用该函数时,可以提供一个可选字符串,该字符串可用于在调试时标识符号实例。 您提供的字符串与符号的定义或标识完全分开:

1
2
3
4
5
6
7
8
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();

let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');

console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false

符号没有文字字符串语法,这对于它们的用途至关重要。 规范符号操作方式的规范允许您创建一个新的Symbol实例,并使用它在对象上键入新的属性,并确保您不会覆盖现有的对象属性-不管它是使用字符串还是使用Symbol作为对象。

1
2
3
4
5
let genericSymbol = Symbol();
console.log(genericSymbol); // Symbol()

let fooSymbol = Symbol('foo');
console.log(fooSymbol); // Symbol(foo);

重要的是,Symbol函数不能与new关键字一起使用。 这样做的目的是避免符号对象包装器,如Boolean,String和Number可能的那样,它们支持构造函数的行为并实例化原始包装器对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let myBoolean = new Boolean();
console.log(typeof myBoolean); // "object"

let myString = new String();
console.log(typeof myString); // "object"

let myNumber = new Number();
console.log(typeof myNumber); // "object"

let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor

如果您想使用对象包装器,可以使用Object()函数:

1
2
3
let mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol);
console.log(typeof myWrappedSymbol); // "object"

对象类型

ECMAScript中的对象开始时是非特定的数据和功能组。通过使用new运算符创建对象,后跟要创建的对象类型的名称。开发人员通过创建Object类型的实例并向其添加属性和/或方法来创建自己的对象,如下所示:

1
let o = new Object();

此语法类似于Java,尽管ECMAScript要求仅在向构造函数提供参数时才使用括号。如果没有参数,则可以安全地省略括号(尽管不建议这样做)。

Object实例本身并不是很有用,但是要理解这些概念很重要,因为与Java中的java.lang.Object类似,ECMAScript中的Object类型是派生所有其他对象的基础。 Object类型的所有属性和方法也存在于其他更特定的对象上。

每个Object实例具有以下属性和方法:

  • 构造函数-用于创建对象的函数。在前面的示例中,构造函数是Object()函数。
  • hasOwnProperty(propertyName)-指示给定属性是否存在于对象实例(而不是原型)上。必须将属性名称指定为字符串(例如o.hasOwnProperty(“ name”))。
  • isPrototypeof(object)-确定对象是否是另一个对象的原型。(原型将在第5章中讨论。)
  • propertyIsEnumerable(propertyName)-指示是否可以使用for-in语句枚举给定的属性(在本章后面讨论)。 与hasOwnProperty()一样,属性名称必须是字符串。
  • toLocaleString()-返回适合于执行环境的语言环境的对象的字符串表示形式。
  • toString()-返回对象的字符串表示形式。
  • valueOf()-返回对象的字符串,数字或等效的布尔值。它通常返回与toString()相同的值。

因为对象是ECMAScript中所有对象的基础,所以每个对象都具有这些基本属性和方法。 第5章和第6章介绍了这种情况的具体发生方式。

注意: 从技术上讲,ECMA-262中对象的行为不一定适用于JavaScript中的其他对象。浏览器环境中存在的对象(例如,浏览器对象模型(BOM)和文档对象模型(DOM)中的对象)被视为主机对象,因为它们是由主机实现提供和定义的。 主机对象不受ECMA-262的管辖,因此,可能会也可能不会直接从Object继承。

运算符

ECMA-262描述了一组可用于操纵数据值的运算符。 运算符的范围从数学运算(例如加法和减法)和按位运算符到关系运算符和相等运算符。 运算符在ECMAScript中是独特的,因为它们可用于各种值,包括字符串,数字,布尔值甚至对象。 当在对象上使用时,运算符通常调用valueOf()和/或toString()方法来检索可以使用的值。

一元运算符

仅对一个值进行运算的运算符称为一元运算符。 它们是ECMAScript中最简单的运算符。

按位运算符

布尔运算符

乘法运算符

求幂运算符

加法运算符

相等运算符

语句

小结

JavaScript的核心语言功能在ECMA-262中定义为名为ECMAScript的伪语言。 ECMAScript包含完成基本计算任务所需的所有基本语法,运算符,数据类型和对象,尽管它没有提供获取输入或产生输出的方法。了解ECMAScript及其复杂性对于全面了解Web浏览器中实现的JavaScript至关重要。 以下是ECMAScript的一些基本元素:

  • ECMAScript中的基本数据类型为Undefined,Null,Boolean,Number,String和Symbol。
  • 与其他语言不同,整数和浮点值没有单独的数据类型;Number类型代表所有数字。
  • 还有一个复杂的数据类型Object,这是该语言中所有对象的基本类型。
  • 严格模式限制了语言中某些容易出错的部分。
  • ECMAScript提供了许多C和其他类似C的语言可用的基本运算符,包括算术运算符,布尔运算符,关系运算符,相等运算符和赋值运算符。
  • 这些语言的功能是流控制语句从其他语言中大量借用,例如if语句,for语句和switch语句。

ECMAScript中的函数与其他语言中的函数有所不同:

  • 无需指定函数的返回值,因为任何函数都可以随时返回任何值。
  • 未指定返回值的函数实际上会返回特殊值undefined。

4、变量,范围和内存

与其他语言相比,ECMA-262中定义的JavaScript变量的性质非常独特。松散类型的变量实际上只是在特定时间特定值的名称。由于没有规则定义变量必须保存的数据类型,因此变量的值和数据类型可以在脚本的生存期内更改。尽管这是一个有趣,强大且有问题的功能,但与变量相关的复杂性却很多。

基本值和引用值

ECMAScript变量可能包含两种不同类型的数据:原始值和参考值。 基本值是简单的原子数据,而参考值是可以由多个值组成的对象。

将值分配给变量后,JavaScript引擎必须确定它是原始值还是参考值。 上一章讨论了六个基本类型:未定义,空,布尔,数字,字符串和符号。 据说这些变量是按值访问的,因为您正在操纵存储在变量中的实际值。

参考值是存储在内存中的对象。 与其他语言不同,JavaScript不允许直接访问内存位置,因此不允许直接操纵对象的内存空间。 当您操作一个对象时,您实际上是在对该对象进行引用,而不是实际的对象本身。 因此,据说这些值是通过引用访问的。

注意: 在许多语言中,字符串由对象表示,因此被视为引用类型。ECMAScript打破了这一传统。

动态特性

原始值和参考值的定义类似:创建变量并为其分配值。将这些值存储在变量中后,您可以执行的操作却大不相同。使用参考值时,可以随时添加,更改或删除属性和方法。 考虑以下示例:

1
2
3
let person = new Object();
person.name = "Nicholas";
console.log(person.name); // "Nicholas"

在此创建一个对象并将其存储在变量person中。接下来,添加一个名为name的属性,并为其分配字符串值“ Nicholas”。从那时起,就可以访问新属性,直到对象被销毁或该属性被显式删除为止。

原始值不能添加属性,即使这样做不会导致错误。这是一个例子:

1
2
3
let name = "Nicholas";
name.age = 27;
console.log(name.age); // undefined

在这里,在字符串名称上定义了一个名为age的属性,并将其赋值为27。但是,在下一行,该属性已消失。只有引用值可以具有动态定义的属性,以供以后使用。

请注意,可以仅使用原始文字形式来完成原始类型的实例化。 如果要使用new关键字,JavaScript将创建一个Object类型,但其行为类似于原始类型。 这是区分两者的示例:

1
2
3
4
5
6
7
8
let name1 = "Nicholas";
let name2 = new String("Matt");
name1.age = 27;
name2.age = 26;
console.log(name1.age); // undefined
console.log(name2.age); // 26
console.log(typeof name1); // string
console.log(typeof name2); // object

复制变量值

除了存储方式不同外,原始值和参考值在从一个变量复制到另一个变量时的行为也不同。 将原始值从一个变量分配给另一个变量时,将创建存储在变量对象上的值,并将其复制到新变量的位置。考虑以下示例:

1
2
let num1 = 5;
let num2 = num1;

此处,num1包含值5。当num2初始化为num1时,它也获得值5。该值与num1中存储的值完全分开,因为它是该值的副本。

这些变量中的每一个现在都可以单独使用而没有副作用。此过程如图4-1所示。

当引用值从一个变量分配给另一个变量时,存储在变量对象上的值也将复制到新变量的位置。区别在于此值实际上是指向存储在堆中的对象的指针。操作完成后,两个变量将指向完全相同的对象,因此对一个变量的更改会反映在另一个变量上,如以下示例所示:

1
2
3
4
let obj1 = new Object();
let obj2 = obj1;
obj1.name = "Nicholas";
console.log(obj2.name); // "Nicholas"

在此示例中,变量obj1填充有对象的新实例。 然后将该值复制到obj2中,这意味着两个变量现在都指向同一对象。 在obj1上设置属性名称后,以后可以从obj2对其进行访问,因为它们都指向同一对象。 图4-2显示了变量对象上的变量与堆上的对象之间的关系。

参数传递

ECMAScript中的所有函数参数均按值传递。 这意味着将函数外部的值复制到函数内部的参数中,就像将值从一个变量复制到另一个变量一样。 如果该值是原始变量,则其行为就像原始变量副本;如果该值是引用,则其行为就像参考变量副本。对于开发人员来说,这常常是一个困惑点,因为变量既可以通过值访问,也可以通过引用访问,但是参数仅通过值传递。

当参数通过值传递时,该值将复制到局部变量(命名参数,在ECMAScript中为arguments对象中的插槽)。 当参数通过引用传递时,值在内存中的位置将存储到局部变量中,这意味着对局部变量的更改将反映在函数外部。 (这在ECMAScript中是不可能的。)请考虑以下示例:

1
2
3
4
5
6
7
8
function addTen(num) {
    num += 10;
    return num;
}
let count = 20;
let result = addTen(count);
console.log(count); // 20 - no change
console.log(result); // 30

在这里,函数addTen()具有一个参数num,该参数本质上是一个局部变量。调用时,变量count作为参数传递。 此变量的值为20,将其复制到参数num中以在addTen()中使用。在函数中,参数num的值通过加10进行了更改,但这不会更改函数外部存在的原始变量计数。参数num和变量count无法相互识别;它们只是恰好具有相同的价值。如果num已通过引用传递,则count的值将更改为30以反映函数内部所做的更改。使用数字等原始值时,这一事实很明显,但是使用对象时,事情并不清楚。以这个为例:

1
2
3
4
5
6
function setName(obj) {
    obj.name = "Nicholas";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"

在此代码中,将创建一个对象并将其存储在变量person中。 然后将该对象传递到setName()方法中,然后将其复制到obj中。在函数内部,obj和person都指向同一对象。结果是,即使obj是通过值传递到函数中的,obj也通过引用来访问对象。当在函数内部的obj上设置name属性时,此更改将反映在函数外部,因为它指向的对象全局存在于堆中。许多开发人员错误地认为,当全局更改反映对象的局部更改时,这意味着已通过引用传递了参数。为了证明对象是按值传递的,请考虑以下修改后的代码:

1
2
3
4
5
6
7
8
function setName(obj) {
    obj.name = "Nicholas";
    obj = new Object();
    obj.name = "Greg";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"

此示例与上一个示例之间的唯一变化是,已在setName()中添加了两行,将obj重新定义为具有不同名称的新对象。当person传递给setName()时,其name属性设置为“ Nicholas”。 然后,将变量obj设置为新对象,并将其name属性设置为“ Greg”。 如果person通过引用传递,则person将自动更改为指向名称为“Greg”的对象。但是,当再次访问person.name时,其值是“Nicholas”,表明即使参数的值在函数内部发生了变化,原始引用仍保持不变。当obj在函数内部被覆盖时,它将成为指向局部对象的指针。函数完成执行后,该局部对象将被销毁。

注意: ECMAScript中的函数参数只不过是局部变量。

小结

可以在JavaScript变量中存储两种类型的值:基本值和引用值。基本值具有六种基本数据类型之一:Undefined,Null,Boolean,Number,String和Symbol。基本值和引用值具有以下特征:

  • 基本值具有固定内存大小,因此存储在栈的内存中。
  • 将基本值从一个变量复制到另一个变量将创建该值的第二个副本。
  • 引用值是对象,并存储在堆的内存中。
  • 包含引用值的变量实际上仅包含指向对象的指针,而不包含对象本身。
  • 将引用值复制到另一个变量仅复制指针,因此两个变量最终都引用同一对象。
  • typeof运算符确定值的原始类型,而instanceof运算符用于确定值的引用类型。

所有变量(基本变量和引用变量)都存在于执行上下文(也称为作用域)中,该上下文确定变量的生存期以及代码的哪些部分可以访问它。执行上下文可以总结如下:

  • 执行上下文全局存在(称为全局上下文),在函数内部以及在块内部。
  • 每次输入新的执行上下文时,它都会创建一个范围链以搜索变量和函数。
  • 函数或块局部的上下文不仅可以访问该范围内的变量,还可以访问任何包含其的上下文和全局上下文的变量。
  • 全局上下文只能访问全局上下文中的变量和函数,而不能直接访问局部上下文中的任何数据。
  • 变量的执行上下文有助于确定何时释放内存

JavaScript是垃圾回收的编程环境,开发人员无需担心内存分配或回收问题。 JavaScript的垃圾回收例程可以总结如下:

  • 超出上下文的值将自动标记为回收,并在垃圾回收过程中将其删除。
  • 主要的垃圾收集算法称为“标记清除”算法,该算法会标记当前未使用的值,然后返回以回收该内存。
  • 另一种算法是引用计数,该算法跟踪特定值有多少个引用。JavaScript引擎不再使用此算法,但是由于在JavaScript中访问了非本地JavaScript对象(例如DOM元素),它仍然会影响Internet Explorer。
  • 当代码中存在循环引用时,引用计数会导致问题。
  • 取消引用变量不仅有助于循环引用,而且通常还有助于垃圾回收。 为了帮助进行内存回收,在不再需要全局对象,全局对象的属性和循环引用时,都应取消引用。

5、基本引用类型

引用值(对象)是特定引用类型的实例。在ECMAScript中,引用类型是用于将数据和功能组合在一起的结构,通常被错误地称为类。尽管从技术上讲是一种面向对象的语言,但是ECMAScript缺少一些传统上与面向对象编程相关联的基本构造,包括类和接口。 引用类型有时也称为对象定义,因为它们描述了对象应具有的属性和方法。

注意: 即使引用类型与类相似,但这两个概念也不相同。为避免混淆,本章其余部分不使用“类”一词。

同样,对象被认为是特定引用类型的实例。通过使用new运算符后跟构造函数来创建新对象。构造函数只是一个函数,其目的是创建一个新对象。考虑以下代码行:

1
let now = new Date();

此代码创建日期引用类型的新实例,并将其存储在变量now中。使用的构造函数是Date(),它将创建仅具有默认属性和方法的简单对象。ECMAScript提供了许多本机引用类型,例如Date,以帮助开发人员执行常见的计算任务。

注意: 函数是一种引用类型,但是对于本章而言,它们的作用范围太广,因此整整一章专门讨论它们。请参阅第十章“函数”。

小结

JavaScript中的对象称为引用值,可以使用几种内置引用类型来创建特定类型的对象,如下所示:

  • 引用类型类似于传统的面向对象编程中的类,但实现方式有所不同。
  • Date类型提供有关日期和时间的信息,包括当前日期和时间以及计算。
  • RegExp类型是ECMAScript中支持正则表达式的接口,提供最基本和一些高级的正则表达式功能。

JavaScript的独特方面之一是,函数实际上是Function类型的实例,意味着函数是对象。 因为函数是对象,所以函数具有可用于增强其行为方式的方法。

由于存在原始包装类型,因此可以像对待对象一样访问JavaScript中的原始值。 共有三种原始包装器类型:布尔值,数字和字符串。 它们都具有以下特征:

  • 每个包装器类型都映射到相同名称的原始类型。
  • 在读取模式下访问原始值时,将实例化原始包装器对象,以便可以将其用于处理数据。
  • 一旦执行包含原始值的语句,包装对象就会被销毁。

在代码执行开始时还存在两个内置对象:Global和Math。在大多数ECMAScript实施中无法访问Global对象; 但是,Web浏览器将其实现为窗口对象。全局对象包含所有全局变量和函数作为属性。 Math对象包含有助于复杂数学计算的属性和方法。

6、集合引用类型

Object类型

到目前为止,大多数参考值示例都使用了Object类型,这是ECMAScript中最常用的类型之一。 尽管Object实例没有太多功能,但它们非常适合在应用程序周围存储和传输数据。

有两种方法可以显式创建Object的实例。第一种是将new运算符与Object构造函数一起使用,如下所示:

1
2
3
let person = new Object();
person.name = "Nicholas";
person.age = 29;

另一种方法是使用对象文字表示法。对象文字符号是对象定义的简化形式,旨在简化创建具有众多属性的对象。例如,以下使用对象文字符号定义了与上一个示例相同的人对象:

1
2
3
4
let person = {
    name: "Nicholas",
    age: 29
};

在此示例中,左花括号({)表示对象文字的开头,因为它出现在表达式上下文中。 ECMAScript中的表达式上下文是期望值(表达式)的上下文。赋值运算符指示下一个期望值,因此左花括号指示表达式的开始。如果出现在语句上下文中(例如,如果遵循if语句条件),则相同的花括号表示block语句的开始。

接下来,指定name属性,后跟一个冒号,然后是该属性的值。逗号用于分隔对象字面量中的属性,因此在字符串“Nicholas”之后有一个逗号,但在值29之后没有逗号,因为age是对象中的最后一个属性。在非常老的浏览器中,在最后一个属性之后包含逗号会导致错误,但是所有现代浏览器都支持该错误。

在使用对象文字表示法时,属性名称也可以指定为字符串或数字,例如在以下示例中:

1
2
3
4
5
let person = {
    "name": "Nicholas",
    "age": 29,
    5: true
};

本示例将生成一个具有name属性,age属性和5属性的对象。请注意,数字属性名称会自动转换为字符串

通过使用花括号之间的空格为空,也可以使用对象文字符号创建仅具有默认属性和方法的对象,例如:

1
2
3
let person = {}; // same as new Object()
person.name = "Nicholas";
person.age = 29;

此示例与本节中的第一个示例等效,尽管看起来有些奇怪。 仅当您要指定可读性的属性时,才建议使用对象文字符号。

注意: 通过对象文字符号定义对象时,实际上不会调用Object构造函数。

尽管可以使用两种方法来创建Object实例,但是开发人员倾向于使用对象文字表示法,因为它需要较少的代码并以可视方式封装所有相关数据。实际上,对象文字已成为将大量可选参数传递给函数的一种首选方式,例如以下示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function displayInfo(args) {
    let output = "";
    if (typeof args.name == "string"){
        output += "Name: " + args.name + "\n";
    }
    if (typeof args.age == "number") {
        output += "Age: " + args.age + "\n";
    }
    alert(output);
}

displayInfo({
    name: "Nicholas",
    age: 29
});

displayInfo({
    name: "Greg"
});

在这里,函数displayInfo()接受一个名为args的参数。该参数可能带有一个名为name或age的属性,或两者兼有或都不存在。设置该函数以使用typeof运算符测试每个属性的存在,然后构造一条消息以根据可用性显示。 然后调用此函数两次,每次使用对象常量中指定的不同数据。该功能在两种情况下均正常工作。

注意: 当有大量可选参数可以传递到函数中时,最好使用这种参数传递模式。一般而言,命名参数更易于使用,但是当存在许多可选参数时,它们可能变得笨拙。最好的方法是对需要的参数使用命名参数,并使用对象文字包含多个可选参数。

尽管通常使用点符号来访问对象属性,这是许多面向对象的语言所共有的,但是也可以通过方括号来访问属性。当使用括号表示法时,将在括号之间放置一个包含属性名称的字符串,如下例所示:

1
2
alert(person["name"]); // "Nicholas"
alert(person.name); // "Nicholas"

从功能上讲,两种方法之间没有区别。括号表示法的主要优点是,它允许您使用变量进行属性访问,如以下示例所示:

1
2
let propertyName = "name";
alert(person[propertyName]); // "Nicholas"

当属性名称包含可能是语法错误或关键字/保留字的字符时,也可以使用括号表示法。 例如:

1
person["first name"] = "Nicholas";

由于名称“first name”包含空格,因此您不能使用点符号来访问它。但是,属性名称可以包含非字母数字字符-您只需要使用括号符号即可访问它们。

一般而言,点号是首选,除非必须使用变量才能通过名称访问属性

注意: “对象,类和面向对象的程序设计”一章详细介绍了对象类型。

Array类型

在对象类型之后,数组类型可能是ECMAScript中使用最多的类型。ECMAScript数组与大多数其他编程语言中的数组有很大不同。与其他语言一样,ECMAScript数组是数据的有序列表,但是与其他语言不同,它们可以在每个插槽中保存任何类型的数据。 这意味着可以创建一个数组,该数组的第一个位置包含一个字符串,第二个位置包含一个数字,第三个位置包含一个对象,依此类推。ECMAScript数组还可以动态调整大小,并自动增长以容纳添加到其中的任何数据。

可以通过几种基本方式创建数组。一种是使用Array构造函数,如以下行所示:

1
let colors = new Array();

如果知道数组中的项目数,则可以将计数传递到构造函数中,然后将使用该值自动创建length属性。 例如,以下代码创建一个初始长度值为20的数组:

1
let colors = new Array(20);

Array构造函数也可以传递应该包含在数组中的项目。以下创建具有三个字符串值的数组:

1
let colors = new Array("red", "blue", "green");

可以通过将单个值传递给构造函数来创建一个数组。这有点棘手,因为仅提供一个数字参数始终会创建具有给定数量的项的数组,而任何其他类型的参数都会创建一个包含指定值的单项数组。这是一个例子:

1
2
let colors = new Array(3); // create an array with three items
let names = new Array("Greg"); // create an array with one item, the string "Greg"

使用Array构造函数时,可以省略new运算符。 如您在此处看到的,结果相同:

1
2
let colors = Array(3); // create an array with three items
let names = Array("Greg"); // create an array with one item, the string "Greg"

创建数组的第二种方法是使用数组文字符号。 通过使用方括号并在其之间放置逗号分隔的项目列表来指定数组文字,如本示例所示:

1
2
3
let colors = ["red", "blue", "green"]; // Creates an array with three strings
let names = []; // Creates an empty array
let values = [1,2,]; // Creates an array with 2 items

在此代码中,第一行创建一个包含三个字符串值的数组。 第二行使用空方括号创建一个空数组。 第三行显示了在数组文字中的最后一个值之后留下逗号的效果:values是一个包含值1和2的两项数组。

注意: 与对象一样,使用数组文字符号创建数组时不会调用Array构造函数。

Array构造函数在ES6中还引入了两个额外的静态方法来创建数组:from()和of()。from()用于将类似数组的构造转换为数组实例,而of()用于将参数集合转换为数组实例。

Array.from()的第一个参数是一个“arrayLike”对象,该对象是可迭代的或具有长度属性和索引元素的任何对象。可以以多种不同方式使用此类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Strings will be broken up into an array of single characters
alert(Array.from("Matt")); // ["M", "a", "t", "t"]

// Sets and Maps can be converted into an new array instance using from()
const m = new Map().set(1, 2)
                   .set(3, 4);
const s = new Set().add(1)
                   .add(2)
                   .add(3)
                   .add(4);

alert(Array.from(m)); // [[1, 2], [3, 4]]
alert(Array.from(s)); // [1, 2, 3, 4]

// Array.from() performs a shallow copy of an existing array
const a1 = [1, 2, 3, 4];
const a2 = Array.from(a1);
alert(a1); // [1, 2, 3, 4]
alert(a1 === a2); // false

// Any iterable object can be used
const iter = {
    *[Symbol.iterator]() {
        yield 1;
        yield 2;
        yield 3;
        yield 4;
    }
};
alert(Array.from(iter)); // [1, 2, 3, 4]

// The arguments object can now easily be casted into an array:
function getArgsArray() {
    return Array.from(arguments);
}
alert(getArgsArray(1, 2, 3, 4)); // [1, 2, 3, 4]

// from() will happily use a custom object with required properties
const arrayLikeObject = {
    0: 1,
    1: 2,
    2: 3,
    3: 4,
    length: 4
};
alert(Array.from(arrayLikeObject)); // [1, 2, 3, 4]

Array.from()还接受第二个可选的map函数参数。这样,您无需先创建中间数组就可以增加新数组的值,如果使用Array.from().map()进行了相同操作,便会出现这种情况。 第三个可选参数指定map函数内部的this的值。 覆盖的此值不适用于箭头函数:

1
2
3
4
5
const a1 = [1, 2, 3, 4];
const a2 = Array.from(a1, x => x**2);
const a3 = Array.from(a1, function(x) {return x**this.exponent}, {exponent: 2});
alert(a2); // [1, 4, 9, 16]
alert(a3); // [1, 4, 9, 16]

Array.of()会将参数列表转换为数组。这用于替换使用异常笨拙的Array.prototype.slice.call(arguments)将参数对象转换为数组的ES6之前的通用方法:

1
2
alert(Array.of(1, 2, 3, 4)); // [1, 2, 3, 4]
alert(Array.of(undefined)); // [undefined]

阵列孔

使用数组文字初始化数组可让您使用顺序逗号创建“空洞”。ECMAScript将逗号之间的索引处的值视为一个孔,并且ES6规范完善了如何处理这些孔。

可能会如下创建孔阵列:

1
2
3
const options = [,,,,,]; // Creates an array with 5 items
alert(options.length); // 5
alert(options); // [,,,,,]

ES6中引入的方法和迭代器的行为与早期ECMAScript版本中存在的方法不同。ES6的添加将普遍将这些孔视为值为undefined的现有条目:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const options = [1,,,,5];
for (const option of options) {
    alert(option === undefined);
}
// false
// true
// true
// true
// false

const a = Array.from([,,,]); // Array of 3 holes created with ES6's Array.from()
for (const val of a) {
    alert(val === undefined);
}
// true
// true
// true

alert(Array.of(...[,,,])); // [undefined, undefined, undefined]

for (const [index, value] of options.entries()) {
    alert(value);
}
// 1
// undefined
// undefined
// undefined
// 5

相反,ES6之前可用的方法将倾向于忽略这些漏洞,尽管确切的行为可能在方法之间略有不同:

1
2
3
4
5
6
7
const options = [1,,,,5];

// map() will skip the holes entirely
alert(options.map(() => 6)); // [6, undefined, undefined, undefined, 6]

// join() treats holes as empty strings
alert(options.join('-')); // "1----5"

注意: 由于它们的异常行为和性能问题,请避免在代码中使用数组孔。最好使用显式的undefined代替孔

索引到数组

要获取和设置数组值,请使用方括号并提供值的从零开始的数字索引,如下所示:

1
2
3
4
let colors = ["red", "blue", "green"]; // define an array of strings
alert(colors[0]); // display the first item
colors[2] = "black"; // change the third item
colors[3] = "brown"; // add a fourth item

方括号内提供的索引表示正在访问的值。 如果索引小于数组中的项目数,则它将返回存储在相应项目中的值,因为在此示例中colors [0]显示为“红色”。 设置值的方式与在指定位置替换值的方式相同。 如果将值设置为数组末尾的索引,例如本示例中的colors [3],则数组长度将自动扩展为该索引加1(因此该长度为4,因为该示例 使用的索引是3)。

数组中的项目数存储在length属性中,该属性始终返回0或更大,如以下示例所示:

1
2
3
4
let colors = ["red", "blue", "green"]; // creates an array with three strings
let names = []; // creates an empty array
alert(colors.length); // 3
alert(names.length); // 0

长度的一个独特特征是它不是只读的。通过设置length属性,您可以轻松地从数组末尾删除项目或向数组末尾添加项目。考虑以下示例:

1
2
3
let colors = ["red", "blue", "green"]; // creates an array with three strings
colors.length = 2;
alert(colors[2]); // undefined

在这里,数组颜色从三个值开始。将长度设置为2会删除最后一个项目(在位置2),从而使其不再可以使用colors [2]进行访问。 如果将长度设置为大于数组中项目数的数字,则每个新项目都将填充为undefined,如本示例所示:

1
2
3
let colors = ["red", "blue", "green"]; // creates an array with three strings
colors.length = 4;
alert(colors[3]); // undefined

即使仅包含三个项目,此代码也会将colors数组的长度设置为4。 位置3在数组中不存在,因此尝试访问其值会导致返回未定义的特殊值。

length属性也有助于将项目添加到数组的末尾,如以下示例所示:

1
2
3
let colors = ["red", "blue", "green"]; // creates an array with three strings
colors[colors.length] = "black"; // add a color (position 3)
colors[colors.length] = "brown"; // add another color (position 4)

数组中的最后一项始终位于位置 length – 1,因此下一个可用的空槽位于位置长度。 每次在数组中的最后一项之后添加一项,都会自动更新length属性以反映更改。 这意味着colors [colors.length]在此示例的第二行中为位置3和最后一行中的位置4分配一个值。 将项目放置在当前数组大小之外的位置时,系统会自动计算新的长度,方法是在该位置加1,如下所示:

1
2
3
let colors = ["red", "blue", "green"]; // creates an array with three strings
colors[99] = "black"; // add a color (position 99)
alert(colors.length); // 100

在此代码中,colors数组的值插入到位置99中,导致新的长度为100(99 +1)。 其他所有项目(位置3至98)实际上都不存在,因此在访问时返回undefined。

注意: 数组最多可以包含4,294,967,295个项目,对于几乎所有编程需求而言,这应该足够了。如果您尝试添加多于该数字,则会发生异常。尝试创建一个初始大小接近此最大值的数组可能会导致长时间运行的脚本错误。

检测数组

经典的ECMAScript问题是确定给定对象是否为数组。 当处理单个网页以及单个全局范围时,instanceof运算符可以很好地工作

1
2
3
if (value instanceof Array){
    // do something on the array
}

instanceof的一个问题是它假设一个全局执行上下文。 如果要处理网页中的多个框架,则实际上是在处理两个不同的全局执行上下文,因此要处理两个版本的Array构造函数。 如果要将数组从一帧传递到第二帧,则该数组的构造函数与在第二帧中本机创建的数组不同

要变通解决此问题,ECMAScript提供了**Array.isArray()**方法。 此方法的目的是确定给定值是否为数组,而不管其创建时所在的全局执行上下文如何。 考虑以下示例:

1
2
3
if (Array.isArray(value)){
    // do something on the array
}

迭代器方法

在ES6中,数组原型上公开了三种新方法,可让您检查数组的内容:keys(),values()和entrys()。 **keys()**将返回数组索引的迭代器,**values()**将返回数组元素的迭代器,**entries()**将返回索引/值对的迭代器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const a = ["foo", "bar", "baz", "qux"];

// Because these methods return iterators, you can funnel their contents into array instances using Array.from()
const aKeys = Array.from(a.keys());
const aValues = Array.from(a.values());
const aEntries = Array.from(a.entries());

alert(aKeys); // [0, 1, 2, 3]
alert(aValues); // ["foo", "bar", "baz", "qux"]
alert(aEntries); // [[0, "foo"], [1, "bar"], [2, "baz"], [3, "qux"]

ES6解构意味着现在很容易在循环内拆分键/值对:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const a = ["foo", "bar", "baz", "qux"];
for (const [idx, element] of a.entries()) [
    alert(idx);
    alert(element);
}
// 0
// foo
// 1
// bar
// 2
// baz
// 3
// qu

注意: 尽管它们已包含在ES6规范中,但截至2017年底,一些现代浏览器尚未实现其中一些方法

复制和填充方法

ES6中的新增功能是fill()和copyWithin()这两种方法,它们分别允许在数组内部批量填充和复制。 两种方法都具有相似的函数签名,因为它们允许您使用包含的开始索引和包含的结束索引来指定现有数组实例内的范围。 使用此方法的数组将永远不会调整大小

fill()方法允许您将相同的值插入到现有数组的全部或一部分中。指定可选的起始索引会指示填充从该索引开始,除非提供了终止索引,否则填充将继续到数组的末尾。负索引从数组的末尾开始解释。 另一种思考的方式是,负索引具有添加到它们的数组长度以计算正索引:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const zeroes = [0, 0, 0, 0, 0];

// Fill the entire array with 5
zeroes.fill(5);
alert(zeroes); // [5, 5, 5, 5, 5]
zeroes.fill(0); // reset

// Fill all indices >=3 with 6
zeroes.fill(6, 3);
alert(zeroes); // [0, 0, 0, 6, 6]
zeroes.fill(0); // reset

// Fill all indices >= 1 and < 3 with 7
zeroes.fill(7, 1, 3);
alert(zeroes); // [0, 7, 7, 0, 0];
zeroes.fill(0); // reset

// Fill all indices >=1 and < 4 with 8
// (-4 + zeroes.length = 1)
// (-1 + zeroes.length = 4)
zeroes.fill(8, -4, -1);
alert(zeroes); // [0, 8, 8, 8, 0];

fill()默默地忽略超出数组边界,长度为零或向后移动的范围:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const zeroes = [0, 0, 0, 0, 0];

// Fill with too low indices is noop
zeroes.fill(1, -10, -6);
alert(zeroes); // [0, 0, 0, 0, 0]

// Fill with too high indices is noop
zeroes.fill(1, 10, 15);
alert(zeroes); // [0, 0, 0, 0, 0]

// Fill with reversed indices is noop
zeroes.fill(2, 4, 2);
alert(zeroes); // [0, 0, 0, 0, 0]

// Fill with partial index overlap is best effort
zeroes.fill(4, 3, 10)
alert(zeroes); // [0, 0, 0, 4, 4]

与fill()不同,copyWithin()会执行一些数组的迭代浅表副本,并覆盖从提供的索引处开始的现有值。 但是,它对于开始和结束索引使用相同的约定:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
let ints,
    reset = () => ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
reset();

// Copy the contents of ints beginning at index 0 to the values beginning at index 5.
// Stops when it reaches the end of the array either in the source
// indices or the destination indices.
ints.copyWithin(5);
alert(ints); // [0, 1, 2, 3, 4, 0, 1, 2, 3, 4]
reset();

// Copy the contents of ints beginning at index 5 to the values beginning at index 0.
ints.copyWithin(0, 5);
alert(ints); // [5, 6, 7, 8, 9, 5, 6, 7, 8, 9]
reset();

// Copy the contents of ints beginning at index 0 and ending at index 3 to values
// beginning at index 4.
ints.copyWithin(4, 0, 3);
alert(ints); // [0, 1, 2, 3, 0, 1, 2, 7, 8, 9]
reset();

// The JS engine will perform a full copy of the range of values before inserting,
// so there is no danger of overwrite during the copy.
ints.copyWithin(2, 0, 6);
alert(ints); // [0, 1, 0, 1, 2, 3, 4, 5, 8, 9]
reset();

// Support for negative indexing behaves identically to fill() in that negative
// indices are calculated relative to the end of the array
ints.copyWithin(-4, -7, -3);
alert(ints); // [0, 1, 2, 3, 4, 5, 3, 4, 5, 6]

fill()默默地忽略超出数组边界,长度为零或向后移动的范围:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
let ints,
reset = () => ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
reset();

// Copy with too low indices is noop
ints.copyWithin(1, -15, -12);
alert(ints); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
reset()

// Copy with too high indices is noop
ints.copyWithin(1, 12, 15);
alert(ints); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
reset();

// Copy with reversed indices is noop
ints.copyWithin(2, 4, 2);
alert(ints); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
reset();

// Copy with partial index overlap is best effort
ints.copyWithin(4, 7, 10)
alert(ints); // [0, 1, 2, 3, 7, 8, 9, 7, 8, 9];

类型数组

ECMAScript 6中的新增功能,类型数组是一种用于将二进制数据有效传递到本机库的结构。JavaScript中没有实际的“TypedArray”类型,而是指包含数字类型的专用数组的集合。要了解如何使用类型化数组,先了解其预期用途将很有帮助。

使用ArrayBuffers

Float32Array实际上是一种“视图”类型,它允许JavaScript运行时访问分配的内存块ArrayBuffer。ArrayBuffer是所有类型的数组和视图引用的基本单位。

注意: TypedArrayBuffer是ArrayBuffer的变体,可以在执行上下文之间传递而不执行复制。有关这种类型的说明,请参见第27章“Workers”。

ArrayBuffer是一个普通的JavaScript构造函数,可用于在内存中分配特定数量的字节。

1
2
const buf = new ArrayBuffer(16); // Allocates 16 bytes of memory
alert(buf.byteLength); // 16

创建ArrayBuffer后就无法调整大小。但是,您可以使用slice()将现有ArrayBuffer的全部或部分复制到新实例中:

1
2
3
const buf1 = new ArrayBuffer(16);
const buf2 = buf1.slice(4, 12);
alert(buf2.byteLength); // 8

ArrayBuffer在某些方面类似于C++ malloc(),但有几个值得注意的例外:

  • 当malloc()分配失败时,它将返回一个空指针。 如果ArrayBuffer分配失败,则会引发错误
  • malloc()调用可以利用虚拟内存,因此分配的最大大小仅受可寻址系统内存的限制。ArrayBuffer分配不能超过Number.MAX_SAFE_INTEGER(2 ^ 53)字节。
  • 成功的malloc()调用不执行实际地址的初始化。声明ArrayBuffer会将所有位初始化为0。
  • 在调用free()或程序退出之前,系统无法使用malloc()分配的堆内存。通过声明ArrayBuffer分配的堆内存仍在垃圾回收中-无需手动进行内存管理。

只能通过引用缓冲区实例来读取或写入ArrayBuffer的内容。 要在内部读取或写入数据,必须在视图中进行。 视图有不同类型,但是它们都引用存储在ArrayBuffer中的二进制数据。

Map类型

在ECMAScript 6规范之前,可以通过使用Object(对象属性用作键,而属性引用值)来有效,轻松地在JavaScript中实现键/值存储。 但是,这种实现方式并非没有缺陷,因此TC39委员会认为适合为真正的键/值存储定义规范。

ECMAScript 6中新添加了Map,它是一种新的集合类型,它将真正的键/值行为引入了该语言。 它提供的功能与Object类型提供的功能有很多重叠,但是在选择使用的Object和Map类型之间存在细微的差异。

基本API

空的Map用new关键字实例化:

1
const m = new Map();

如果希望在初始化Map时填充它,则构造函数可以选择接受一个可迭代的对象,期望它包含键/值对数组。 可迭代参数中的每对将以其迭代顺序插入新创建的Map中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Initialize map with nested arrays
const m1 = new Map([
    ["key1", "val1"],
    ["key2", "val2"],
    ["key3", "val3"]
]);
alert(m1.size); // 3

// 使用自定义迭代器初始化Map
const m2 = new Map({
    [Symbol.iterator]: function*() {
        yield ["key1", "val1"];
        yield ["key2", "val2"];
        yield ["key3", "val3"];
    }
});
alert(m2.size); // 3

// Map expects values to be key/value whether they are provided or not
const m3 = new Map([[]]);
alert(m3.has(undefined)); // true
alert(m3.get(undefined)); // undefined

可以在使用set()初始化之后添加键/值对,使用get()和has()查询,使用size属性计数,并使用delete()和clear()删除键/值对:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const m = new Map();

alert(m.has("firstName")); // false
alert(m.get("firstName ")); // undefined
alert(m.size); // 0

m.set("firstName", "Matt")
 .set("lastName", "Frisbie");

alert(m.has("firstName")); // true
alert(m.get("firstName")); // Matt
alert(m.size); // 2

m.delete("firstName"); // deletes only this key/value pair

alert(m.has("firstName")); // false
alert(m.has("lastName")); // true
alert(m.size); // 1

m.clear(); // destroys all key/value pairs in this Map instance

alert(m.has("firstName")); // false
alert(m.has("lastName")); // false
alert(m.size); // 0

set()方法返回Map实例,因此可以将多个set操作链接在一起,包括在初始声明中:

1
2
3
4
5
6
const m = new Map().set("key1", "val1");

m.set("key2", "val2")
 .set("key3", "val3");

alert(m.size); // 3

与只能使用数字或字符串作为键的Object不同,Map可以使用任何JavaScript数据类型作为键。它使用“SameValueZero”比较操作(在ECMAScript规范中定义,但在实际语言中不可用),并且与使用严格的对象等效性检查键匹配是否可进行比较。与对象一样,对值中包含的内容也没有限制。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const m = new Map();

const functionKey = function() {};
const symbolKey = Symbol();
const objectKey = new Object();

m.set(functionKey, "functionValue");
m.set(symbolKey, "symbolValue");
m.set(objectKey, "objectValue");

alert(m.get(functionKey)); // functionValue
alert(m.get(symbolKey)); // symbolValue
alert(m.get(objectKey)); // objectValue

// SameValueZero checks mean separate instances will not collide
alert(m.get(function() {})); // undefined

与严格对等相同,当键和值的内容或属性发生更改时,用于键和值的对象和其他“集合”类型将保持不变:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const m = new Map();

const objKey = {},
      objVal = {},
      arrKey = [],
      arrVal = [];

m.set(objKey, objVal);
m.set(arrKey, arrVal);

objKey.foo = "foo";
objVal.bar = "bar";
arrKey.push("foo");
arrVal.push("bar");

alert(m.get(objKey)); // {bar: "bar"}
alert(m.get(arrKey)); // ["bar"]

使用SameValueZero操作可能会导致意外冲突:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const m = new Map();

const a = 0/"", // NaN
      b = 0/"", // NaN
      pz = +0,
      nz = -0;

alert(a === b); // false
alert(pz === nz); // true

m.set(a, "foo");
m.set(pz, "bar");

alert(m.get(b)); // foo
alert(m.get(nz)); // bar

注意: SameValueZero操作是ECMAScript规范的新增功能。Mozilla文档站点上有关于它和其他ECMAScript相等约定的出色文章:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness.

小结

JavaScript中的对象称为引用值,可以使用几种内置引用类型来创建特定类型的对象,如下所示:

  • 引用类型类似于传统的面向对象编程中的类,但实现方式有所不同。
  • 对象类型是所有其他引用类型继承基本行为的基础。
  • Array类型表示值的有序列表,并提供用于操纵和转换值的功能。
  • 类型化数组包含一系列不同的引用类型,这些引用类型涉及内存中数字的类型管理。
  • Date类型提供有关日期和时间的信息,包括当前日期和时间以及计算。
  • RegExp类型是ECMAScript中支持正则表达式的接口,提供最基本和一些高级的正则表达式功能。

JavaScript的独特方面之一是,函数实际上是Function类型的实例,意味着函数是对象。 因为函数是对象,所以函数具有可用于增强其行为方式的方法。

由于存在原始包装类型,因此可以像对待对象一样访问JavaScript中的原始值。 共有三种原始包装器类型:布尔值,数字和字符串。 它们都具有以下特征:

  • 每个包装器类型都映射到相同名称的原始类型。
  • 在读取模式下访问原始值时,将实例化原始包装器对象,以便可以将其用于处理数据。
  • 一旦执行包含原始值的语句,包装对象就会被销毁。

在代码执行开始时还存在两个内置对象:Global和Math。在大多数ECMAScript实施中无法访问Global对象; 但是,Web浏览器将其实现为窗口对象。 全局对象包含所有全局变量和函数作为属性。 Math对象包含有助于复杂数学计算的属性和方法。

ECMAScript 6引入了一些集合类型:Map,WeakMap,Set和WeakSet。 这些为组织应用程序数据以及简化内存管理提供了新的可能性。

7、迭代器和生成器

小结

迭代实际上是每种编程语言都遇到的一种模式。ECMAScript 6规范通过在语言中引入两个正式的概念(迭代器和生成器)来正式包含迭代的概念。

迭代器是可以由任何对象实现的接口,并允许连续访问其生成的值。任何实现Iterable接口的功能都具有符号。iterator属性,它引用默认的迭代器。 默认的迭代器的行为就像一个迭代器工厂:一个函数,该函数在被调用时会产生一个实现Iterator接口的对象。

迭代器通过next()方法强制继承值,该方法返回IteratorObject。 该对象包含一个done属性,一个布尔值(指示是否有更多可用值)以及一个value属性,其中包含从迭代器提供的当前值。该接口可以通过重复调用next()来手动使用,也可以由本机可迭代使用方(例如for … of循环)自动使用。

生成器是一种特殊的函数,在调用时会生成生成器对象。该生成器对象实现了Iterable接口,因此可以在需要可迭代的任何地方使用。生成器的独特之处在于它们支持yield关键字,该关键字用于暂停生成器功能的执行。 yield关键字还可用于通过next()方法接受输入和输出。当带有星号时,yield关键字将用于序列化与之配对的可迭代对象。

8、对象,类和面向对象的编程

小结

在代码执行期间的任何时候都可以创建和扩充对象,从而使对象成为动态的而非严格定义的实体。以下模式用于创建对象:

  • 工厂模式使用一个简单的函数创建一个对象,分配属性和方法,然后返回该对象。 当构造器模式出现时,该模式就不再受欢迎。
  • 使用构造器模式,可以定义可以使用new运算符创建自定义引用类型的方式,就像创建内置对象实例一样。但是,构造器模式确实有一个缺点,即它的成员(包括函数)都不会被重用。由于可以使用松散类型的方式编写函数,因此没有理由不能被多个对象实例共享。
  • 原型模式考虑了这一点,使用构造函数的prototype属性分配应共享的属性和方法。 构造函数/原型组合模式使用构造函数定义实例属性,使用原型模式定义共享属性和方法。

JavaScript中的继承主要使用原型链接的概念来实现。 原型链涉及将构造函数的原型分配为另一种类型的实例。 这样做时,子类型采用类似于基于类的继承的方式假定超类型的所有属性和方法。 原型链的问题在于,所有继承的属性和方法在对象实例之间共享,这使其不适合单独使用。 构造函数窃取模式避免了这些问题,它从子类型的构造函数内部调用了父类型的构造函数。 这允许每个实例具有其自己的属性,但是强制仅使用构造函数模式来定义类型。 最受欢迎的继承模式是组合继承,它使用原型链继承共享的属性和方法,并使用构造函数窃取来继承实例属性。

还有以下替代继承模式:

  • 原型继承无需预先定义的构造函数即可实现继承,实质上是对给定对象执行浅表克隆操作。然后可以进一步增加运算结果。
  • 密切相关的是寄生继承,这是一种基于另一个对象或某些信息创建对象,对其进行扩充并返回的模式。 此模式也已重新用于结合继承,以消除与调用超类型构造函数的次数有关的效率低下的问题。
  • 寄生组合继承被认为是实现基于类型的继承的最有效方法。

ECMAScript 6中的新功能是引入了类,这些类在很大程度上是现有的基于原型的概念的语法包装。该语法使该语言能够优雅地定义向后兼容的类,并且可以从内置或自定义类继承。类优雅地弥合了对象实例,对象原型和对象类之间的鸿沟。

9、代理与反思

小结

代理是ECMAScript 6规范中最令人兴奋和动态的添加之一。尽管它们没有向后编译支持,但它们启用了元编程和抽象的全新领域,而以前是不存在的。

从高层次看,代理是真实JavaScript对象的透明虚拟化。 创建代理后,您可以定义一个包含陷阱的处理程序对象,陷阱是几乎所有基本JavaScript运算符和方法都会遇到的拦截点。这些陷阱处理程序允许您修改这些基本方法的操作方式,尽管它们受陷阱不变式约束。

代理旁边还有Reflect API,它提供了一套方法,这些方法相同地封装了每个陷阱正在拦截的行为。Reflect API可以被视为基本操作的集合,这些基本操作是几乎所有JavaScript对象API的构造块。

代理的效用几乎是无限的,它使开发人员可以运用优雅的新模式,例如(但不限于)跟踪属性访问,隐藏属性,防止对属性的修改或删除,功能参数验证,构造函数参数验证,数据约束力和可观察性。

10、函数

小结

函数是JavaScript编程中有用且通用的工具。ECMAScript 6引入了强大的语法,使您可以更有效地使用它们。

  • 函数表达式与函数声明不同。函数声明需要名称,而函数表达式则不需要。没有名称的函数表达式也称为匿名函数。
  • 箭头函数是ES6中的新功能,与函数表达式相似,但有一些重要区别。
  • JavaScript函数中的参数和参数非常灵活。arguments对象以及ES6中新的散布运算符允许完全动态的定义和调用。
  • 在内部,函数公开了一些对象和引用,这些对象和引用为您提供有关如何调用该函数,在何处调用该函数以及最初传递给它的信息。
  • 引擎将通过尾调用来优化功能,以保留栈空间。
  • 在幕后,闭包的作用域链包含一个变量对象,包含函数和全局上下文。
  • 通常,函数完成执行后会破坏函数的作用域及其所有变量。
  • 从该函数返回闭包时,其范围将保留在内存中,直到闭包不再存在为止。
  • 可以立即创建并调用一个函数,执行其中的代码,但永远不要留下对该函数的引用。
  • 这将导致函数内部的所有变量被破坏,除非将它们专门设置为包含作用域中的变量。
  • 即使JavaScript没有私有对象属性的正式概念,闭包也可以用来实现可访问包含范围内定义的变量的公共方法。
  • 可以访问私有变量的公共方法称为特权方法。
  • 可以使用构造函数或原型模式在自定义类型上实现特权方法,而使用模块或模块扩展模式可以在单例上实现特权方法。

11、承诺和异步功能

小结

长期以来,在单线程JavaScript运行时内部掌握异步行为一直是一项艰巨的任务。随着ES6的Promise和ES7的 async/await 的引入,ECMAScript中的异步构造得到了极大的增强。Promise和async/await不仅启用了以前难以实现或无法实现的模式,而且还带来了一种全新的JavaScript编写方式,该方式更加简洁,简短,易于理解和调试。

构建承诺是为了提供围绕异步代码的简洁抽象。它们可以表示异步执行的代码块,但也可以表示异步计算的值。 在需要序列化异步代码块的情况下,它们特别有用。 承诺是一个令人愉快的可塑结构:它们可以序列化,链接,组合,扩展和重组。

异步函数是将promise范式应用于JavaScript函数的结果。它们引入了在不阻塞执行主线程的情况下挂起函数执行的功能。它们在编写可读的以承诺为中心的代码以及管理异步代码的序列化和并行化方面都非常有用。它们是现代JavaScript工具箱中最重要的工具之一。

12、浏览器对象模型

小结

浏览器对象模型(BOM)基于窗口对象,该窗口对象表示浏览器窗口和可见页面区域。 窗口对象是ECMAScript全局对象的两倍,因此所有全局变量和函数都将成为其属性,并且所有本机构造函数和函数最初都存在于其上。本章讨论了BOM的以下元素:

  • 要引用其他窗口对象,有几个窗口指针。
  • location对象允许通过编程方式访问浏览器的导航系统。通过设置属性,可以逐段或完全更改浏览器的URL。
  • replace()方法允许导航到新的URL并替换浏览器历史记录中当前显示的页面。
  • navigator对象提供有关浏览器的信息。提供的信息类型在很大程度上取决于所使用的浏览器,尽管某些通用属性(例如userAgent)在所有浏览器中都可用。

BOM中可用的其他两个对象执行的功能非常有限。screen对象提供有关客户端显示的信息。此信息通常用于网站的指标收集。history对象提供了对浏览器历史记录堆栈的有限浏览,允许开发人员确定历史记录堆栈中有多少个站点,并使他们能够返回或前进到历史记录中的任何页面,以及修改历史记录堆栈。

13、客户端检测

小结

客户端检测是JavaScript中最具争议的主题之一。由于浏览器的差异,通常有必要基于浏览器的功能来分叉代码。有几种检测客户端的方法,但以下两种最常用:

  • 功能检测-在使用特定浏览器功能之前进行测试。例如,脚本可以在调用函数之前检查其是否存在。这种方法使开发人员不必担心特定的浏览器类型和版本,从而使他们能够专注于功能是否存在。功能检测无法准确检测特定的浏览器或版本。
  • 用户代理检测-通过查看浏览器的用户代理字符串来识别浏览器。useragent字符串包含有关浏览器的大量信息,通常包括浏览器,平台,操作系统和浏览器版本。用户代理字符串的开发已有很长的历史,浏览器供应商试图欺骗网站,使他们认为自己是另一个浏览器。用户代理检测可能很棘手,特别是在处理Opera屏蔽其用户代理字符串的功能时。即使这样,useragent字符串也可以确定正在使用的渲染引擎以及运行它的平台,包括移动设备和游戏系统。

在决定使用哪种客户端检测方法时,最好先使用功能检测。奇怪的检测是确定代码应该如何进行的第二选择。用户代理检测被认为是客户端检测的最后选择,因为它非常依赖于用户代理字符串。

浏览器还提供了围绕它的软件和硬件的日益广泛的画面。通过屏幕和导航器对象,可以得出关于操作系统,浏览器,硬件,设备位置,电池状态以及各种其他主题的极其准确的想法。

14、文档对象模型

文档对象模型(DOM)是HTML和XML文档的应用程序编程接口(API)。DOM将文档表示为节点的分层树,从而使开发人员可以添加,删除和修改页面的各个部分。 从Netscape和Microsoft的早期动态HTML(DHTML)创新中发展而来,DOM现在是一种真正的跨平台,独立于语言的表示和处理标记页面的方式。

DOM级别1在1998年10月成为W3C推荐,为基本文档结构和查询提供了接口。本章重点介绍DOM的功能和用法,因为它与浏览器中的HTML页面和DOM JavaScript API有关。

注意: 请注意,所有DOM对象在Internet Explorer 8及更早版本中均由COM对象表示。 这意味着这些对象的行为或功能与本机JavaScript对象不同。

小结

文档对象模型(DOM)是与语言无关的API,用于访问和处理HTML和XML文档。DOM级别1将HTML和XML文档表示为节点的层次结构,可以使用JavaScript对其进行操作以更改基础文档的外观和结构。

DOM由一系列节点类型组成,如下所述:

  • 基本节点类型是Node,它是文档各个部分的抽象表示; 所有其他类型都继承自Node。
  • Document类型代表整个文档,并且是层次结构的根节点。 在JavaScript中,文档对象是Document的实例,它允许以多种不同方式查询和检索节点。
  • 元素节点表示文档中的所有HTML或XML元素,可用于操纵其内容和属性。
  • 存在其他节点类型,用于文本内容,注释,文档类型,CDATA部分和文档片段。

尽管使用<script>和<style>元素通常会很复杂,但是DOM访问在大多数情况下都能按预期工作。 由于这些元素分别包含脚本和样式信息,因此它们在浏览器中的处理方式通常不同于其他元素。

关于DOM可能最重要的了解是它如何影响整体性能。DOM操作是可以在JavaScript中完成的一些最昂贵的操作,其中NodeList对象特别麻烦。NodeList对象是“活动的”,这意味着每次访问该对象时都会运行查询。由于这些问题,最好最大程度地减少DOM操作的数量。

引入了MutationObserver来代替性能较差的MutationEvent。它允许使用相对简单的API进行高效且精确的DOM突变监控。

15、DOM扩展

小结

尽管DOM指定了用于与XML和HTML文档进行交互的核心API,但是有一些规范为标准DOM提供了扩展。许多扩展都是基于专有扩展的,后来随着其他浏览器开始模仿其功能,这些扩展已成为事实上的标准。本章涵盖的三个规范是:

  • Selectors API,它定义了两种基于CSS选择器的用于检索DOM元素的方法:querySelector(),querySelectorAll()和matchs()。
  • 元素遍历,它在DOM元素上定义其他属性,以允许轻松遍历下一个相关的DOM元素。 由于对DOM中空白的处理(在元素之间创建文本节点),因此需要这样做。
  • HTML5,它提供了对标准DOM的大量扩展。 其中包括事实上的标准(例如innerHTML)的标准化,以及用于处理焦点管理,字符集,滚动等的其他功能。

当前DOM扩展的数量很少,但是几乎可以肯定的是,随着网络技术的不断发展,该数量将继续增长。浏览器仍在尝试专有扩展,如果成功,它们可能会最终成为伪标准或合并到未来版本的规范中。

16、DOM级别2和3

小结

DOM级别2规范定义了几个模块,这些模块增强了DOM级别1的功能。DOM级别2核心引入了几种与各种DOM类型上的XML名称空间相关的新方法。 这些更改仅在XML或XHTML文档中使用时才相关。它们在HTML文档中没有用。与XML名称空间无关的方法包括以编程方式创建Document的新实例以及启用DocumentType对象的功能。

DOM级别2样式模块指定如何与有关元素的样式信息进行交互,如下所示:

  • 每个元素都有一个与之关联的样式对象,可用于确定和更改内联样式。
  • 要确定元素的计算样式,包括适用于该元素的所有CSS规则,可以使用一种名为getComputedStyle()的方法。
  • 也可以通过document.styleSheets集合访问样式表。

DOM级别2遍历和范围模块指定了与DOM结构进行交互的不同方式,如下所示:

  • 使用NodeIterator或TreeWalker处理遍历以执行DOM树的深度优先遍历。
  • NodeIterator界面很简单,仅允许以一步为单位进行向前和向后移动。 TreeWalker接口支持相同的行为,并在DOM结构上沿所有其他方向移动,包括父母,兄弟姐妹和孩子。
  • 范围是一种选择DOM结构的特定部分以某种方式增强它的方法。
  • 范围的选择可用于在保留格式良好的文档结构的同时删除文档的某些部分或克隆文档的某些部分。

17、事件

小结

事件是JavaScript与网页绑定的主要方式。 最常见的事件在DOM Level 3事件规范或HTML5中定义。 即使有针对基本事件的规范,许多浏览器也超出了规范范围,并实现了专有事件,以使开发人员可以更好地了解用户交互。 一些专有事件与特定设备直接相关。

围绕事件有一些内存和性能方面的考虑。 例如:

  • 最好限制页面上事件处理程序的数量,因为它们会占用更多内存,并使页面对用户的响应能力降低。
  • 通过利用事件冒泡,可以使用事件委托来限制事件处理程序的数量。
  • 最好删除在页面卸载前添加的所有事件处理程序。

可以使用JavaScript在浏览器中模拟事件。DOM 2级和3级事件规范提供了对所有事件的模拟,从而可以轻松模拟所有已定义的事件。也可以通过结合使用其他技术来模拟键盘事件。Internet Explorer 8和更早版本还支持事件模拟,尽管通过不同的界面也是如此。

事件是JavaScript中最重要的主题之一,对它们如何工作及其对性能的影响的良好理解至关重要。

18、用Canvas做动画和绘图

小结

requestAnimationFrame是一个简单而优雅的工具,它允许JavaScript进入浏览器的呈现周期,以便有效地执行页面的可视化操作。

HTML5 <canvas>元素提供了一个JavaScript API,可以动态创建图形。图形是在特定的上下文中创建的,当前有两种。第一个是2D上下文,它允许原始绘图操作:

  • 设置填充和描边的颜色和图案
  • 绘制矩形
  • 绘制路径
  • 绘制文字
  • 创建渐变和图案

第二个是称为WebGL的3D上下文。WebGL是OpenGL ES 2.0的浏览器端口,OpenGL ES 2.0是游戏开发人员经常用于计算机图形的语言。 与2D上下文相比,WebGL允许进行更强大的图形处理,从而提供:

  • 用OpenGL着色语言(GLSL)编写的顶点和片段着色器
  • 支持类型化数组,将数组中包含的数据类型限制为特定的数值类型
  • 纹理创建和处理

现在,<canvas>标记本身已得到广泛支持,并且在所有主要浏览器的最新版本中都可用。

19、表单脚本

小结

尽管HTML和Web应用程序自诞生以来发生了巨大变化,但Web表单基本上保持不变。 JavaScript可用于扩充现有的表单字段,以提供新的功能和可用性增强。为此,表单和表单字段具有用于JavaScript的属性,方法和事件。以下是本章介绍的一些概念:

  • 您可以使用多种标准和非标准方法来选择文本框中的所有文本或仅部分文本
  • 所有浏览器都采用Firefox与文本选择进行交互的方式,这使其成为真正的标准。
  • 通过侦听键盘事件并检查插入的字符,可以更改文本框以允许或禁止某些字符。

所有浏览器都支持剪贴板事件,包括复制,剪切和粘贴。 其他浏览器中的剪贴板事件实现在浏览器供应商之间差异很大。

当文本框的内容必须限制为某些字符时,挂钩剪贴板事件对于阻止粘贴事件很有用。

选择框也经常使用JavaScript进行控制。多亏了DOM,操作选择框比以前容易得多。 可以添加,删除选项,将选项从一个选择框移至另一个选择框,或使用标准DOM技术对其进行重新排序

通过使用包含空白HTML文档的iframe处理富文本格式编辑。通过将文档的designMode属性设置为“ on”,您可以使页面可编辑,并且它就像文字处理程序一样。您还可以将元素集用作可编辑的。默认情况下,您可以切换粗体和斜体等字体样式,并使用剪贴板操作。JavaScript可以通过使用execCommand()方法来访问某些功能,并可以通过使用queryCommandEnabled(),queryCommandState()和queryCommandValue()方法获取有关文本选择的信息。由于以这种方式构建格式文本编辑器不会创建表单字段,因此,如果要将HTML提交到服务器,则需要将iframe或contenteditable元素中的HTML复制到表单字段中。

20、JavaScript API

小结

HTML5除了定义新的标记规则外,还定义了几个JavaScript API。这些API旨在提供更好的Web界面,可以与桌面应用程序的功能相媲美。本章介绍的API如下:

  • 使用Atomics API,您可以保护代码免受多线程内存访问模式导致的竞争条件的影响。
  • postMessage()API提供了跨不同来源的文档之间发送消息的功能,同时保持了同源策略的安全性。
  • 使用Encoding API,您可以在字符串和缓冲区之间无缝转换(这是一种越来越常见的模式)。
  • File API为您提供了用于发送,接收和读取大型二进制对象的强大工具。
  • 媒体元素<audio>和<video>具有用于与音频和视频进行交互的自己的API。 并非所有浏览器都支持所有媒体格式,因此请使用canPlayType()方法来正确检测浏览器支持。
  • Drag-and-Drop API使您可以轻松地指示元素是可拖动的,并且可以像操作系统对放置所做的那样做出响应。您可以创建自定义可拖动元素并放置目标。
  • Notifications API为您提供了一种与浏览器无关的向用户呈现交互式图块的方式。
  • Streams API提供了一种全新的增量读取,写入和处理数据的方式。
  • Timing API提供了一种强大的方法来测量浏览器内部和周围的延迟。
  • Web Components API为元素可重用性和封装引入了巨大的飞跃。
  • Web加密API进行加密操作,例如随机数生成,加密和签名消息等一等成员。

21、错误处理和调试

小结

JavaScript的错误处理对于当今复杂的Web应用程序至关重要。 无法预期错误可能发生的位置以及如何从错误中恢复可能导致不良的用户体验,并可能使用户沮丧。 默认情况下,大多数浏览器不会向用户报告JavaScript错误,因此您需要在开发和调试时启用错误报告。但是,在生产中,不应以这种方式报告任何错误。

可以使用以下方法来防止浏览器对JavaScript错误作出反应:

  • 在可能发生错误的地方可以使用try-catch语句,这使您有机会以适当的方式响应错误,而不是允许浏览器处理错误。
  • 另一种选择是使用window.onerror事件处理程序,该事件处理程序将接收尝试捕获未处理的所有错误(仅Internet Explorer,Firefox和Chrome)。

应该检查每个Web应用程序,以确定可能发生错误的位置以及如何处理这些错误。

  • 需要提前确定是什么致命错误或非致命错误
  • 之后,可以对代码进行评估以确定最可能发生错误的位置。由于以下因素,JavaScript中通常会发生错误:
    • 类型强制
    • 数据类型检查不足
    • 发送到服务器或从服务器接收的数据不正确

Internet Explorer,Firefox,Chrome,Opera和Safari均具有浏览器随附的JavaScript调试器,也可以作为附件下载。每个调试器都提供了设置断点,控制代码执行以及在运行时检查变量值的功能。

22、JavaScript中的XML

小结

JavaScript中对XML和相关技术有大量支持。 不幸的是,由于早期缺乏规范,常见功能有几种不同的实现。 DOM级别2提供了一个API,用于创建空的XML文档,但不用于解析或序列化。 浏览器实现了两种新类型来处理XML解析和序列化,如下所示:

  • DOMParser类型是一个简单的对象,它将XML字符串解析为DOM文档。
  • XMLSerializer类型执行相反的操作,将DOM文档序列化为XML字符串。

DOM 3级引入了针对XPath API的规范,该规范已由所有主要浏览器实现。该API使JavaScript能够对DOM文档运行任何XPath查询并检索结果,而不管其数据类型如何。

最新的相关技术是XSLT,它没有定义用于其用途的API的公共规范。Firefox创建了XSLTProcessor类型来通过JavaScript处理转换。

23、JSON

小结

JSON是一种轻量级的数据格式,旨在轻松表示复杂的数据结构。 该格式使用JavaScript语法的子集来表示对象,数组,字符串,数字,布尔值和null。即使XML可以处理相同的工作,JSON也不那么冗长,并且在JavaScript中具有更好的支持。 而且,所有浏览器都很好地支持本机JSON对象。

ECMAScript 5定义了一个本机JSON对象,该对象用于将对象序列化为JSON格式并将JSON数据解析为JavaScript对象。 JSON.stringify()和JSON.parse()方法分别用于这两个操作。 这两种方法都有许多选项,可让您更改默认行为以过滤或修改流程。

24、网络请求和远程资源

小结

Ajax是一种无需刷新当前页面即可从服务器检索数据的方法。Ajax具有以下特征:

  • 负责Ajax增长的主要对象是XMLHttpRequest(XHR)对象。
  • 此对象由Microsoft创建,并且首先在Internet Explorer 5中引入,作为从JavaScript中从服务器检索XML数据的一种方式
  • 从那时起,Firefox,Safari,Chrome和Opera都复制了实现,并且W3C编写了定义XHR行为的规范,使XHR成为Web标准
  • 尽管实现上存在一些差异,但是XHR对象的基本用法在所有浏览器中都相对标准化,因此可以安全地在Web应用程序中使用。

XHR的主要限制之一是同源策略,该策略将通信限制为使用同一端口和同一协议的同一域。 除非使用批准的跨域解决方案,否则任何尝试访问这些限制之外的资源的行为都会导致安全错误。该解决方案称为跨域资源共享(CORS),并通过XHR对象提供本机支持。图像ping和JSONP是跨域通信的其他技术,尽管它们不如CORS健壮。

引入Fetch API是对现有XHR对象的端对端替换。该API提供了卓越的基于承诺的结构,更直观的界面以及对Stream API的一流支持。

Web套接字是与服务器的全双工双向通信通道。与其他解决方案不同,Web套接字不使用HTTP,而是使用旨在快速传送少量数据的自定义协议。这需要使用其他Web服务器,但具有速度优势。

25、客户端存储

小结

Web Storage定义了两个用于保存数据的对象:sessionStorage和localStorage。前者严格用于在浏览器会话中保存数据,因为一旦关闭浏览器,数据就会被删除。 后者用于跨会话持久化数据。

IndexedDB是类似于SQL数据库的结构化数据存储机制。 数据存储在对象存储中,而不是将数据存储在表中。 通过定义键然后添加数据来创建对象存储。 游标用于查询对象存储中的特定数据,并且可以创建索引以更快地查找特定属性。

有了所有这些选项,就可以使用JavaScript在客户端计算机上存储大量数据。由于数据缓存未加密,因此请不要存储敏感信息。

26、模组

小结

模块模式仍然是用于管理复杂性的永恒工具。 它允许开发人员创建隔离逻辑的段,声明这些段之间的依赖关系,并将它们连接在一起。而且,该模式已被证明可以优雅地扩展到任意复杂度并跨平台。

多年来,该生态系统围绕着针对服务器环境的模块系统CommonJS和针对延迟受限的客户端环境的模块系统AMD之间有争议的二分法发展。 两种系统都经历了爆炸性的增长,但是为每种系统编写的代码在许多方面都不一致,并且经常会产生不正确的样板。 而且,这两种系统都不是由浏览器本地实现的,并且由于这种不兼容性,出现了大量的工具,允许在浏览器中使用模块模式。

ECMAScript 6规范中包含了一个针对浏览器模块的全新概念,该概念同时兼顾了两者的优点,并将其组合为更简单的声明性语法。 浏览器越来越多地提供对本机模块利用的支持,但也提供了强大的工具来弥合对ES6模块的边际支持和全面支持之间的差距。

27、工作程序

小结

工作程序允许您运行不会阻止用户界面的异步JavaScript。 这对于复杂的计算和数据处理非常有用,否则这些计算和数据处理会占用大量时间并干扰用户使用页面的能力。 始终为工作程序提供他们自己的执行环境,并且只能通过异步消息传递与工作程序通信。

工作程序可以是专用的,这意味着它们与单个页面相关联,也可以是共享的,这意味着相同来源的任何页面都可以与单个工作程序建立连接。

服务人员旨在允许网页表现得像本机应用程序。 服务工作者也是一种工作者,但是他们的行为更像是网络代理,而不是单独的浏览器线程。 它们可以充当高度可定制的网络缓存,还可以为渐进式Web应用程序启用推送通知。

28、最佳实践

小结

随着JavaScript开发的成熟,出现了最佳实践。以前被视为业余爱好的人现在是合法职业,因此,他们经历了对可维护性,性能和部署的研究,这些研究通常是针对其他编程语言完成的。

JavaScript的可维护性必须部分与以下代码约定有关:

  • 其他语言的代码约定可以用来确定何时注释以及如何缩进,但是JavaScript需要一些特殊的约定来弥补该语言的松散类型性质。
  • 因为JavaScript必须与HTML和CSS共存,所以让每个对象完全定义其目的也很重要:JavaScript应该定义行为,HTML应该定义内容,而CSS应该定义外观
  • 这些职责的混合使用会导致难以调试的错误和维护问题。

随着Web应用程序中JavaScript数量的增加,性能变得越来越重要。 因此,您应牢记以下几点:

  • JavaScript执行所花费的时间直接影响网页的整体性能,因此不能忽略其重要性。
  • 许多基于C语言的性能建议也适用于与循环性能有关的JavaScript,并使用switch语句代替if。
  • 要记住的另一件重要事情是,DOM交互非常昂贵,因此您应该限制DOM操作的数量。

该过程的最后一步是部署。 以下是本章中讨论的一些关键点:

  • 为了帮助部署,您应该建立一个将JavaScript文件合并为少量文件(最好是一个文件)的构建过程。
  • 拥有构建过程还会使您有机会在源代码上自动运行其他过程和过滤器。 例如,您可以运行JavaScript验证程序以确保代码没有语法错误或潜在问题。
  • 压缩程序可以在部署之前使文件尽可能小。
  • 结合HTTP压缩可确保JavaScript文件尽可能小,并且对整体页面性能的影响最小。