第15章 Web浏览器中的JavaScript

  • 15.1 网络编程基础
    • 15.1.1 HTML script 标签中的JavaScript
      • 模块
      • 指定脚本类型
      • 脚本运行时:异步和延迟
      • 按需加载脚本
    • 15.1.2 文档对象模型(DOM)
    • 15.1.3 Web浏览器中的全局对象
    • 15.1.4 脚本共享一个命名空间
    • 15.1.5 JavaScript程序的执行
      • 客户端JavaScript线程模型
      • 客户端JavaScript时间轴
    • 15.1.6 程序输入和输出
    • 15.1.7 程序错误
    • 15.1.8 Web安全模型
      • JavaScript不能做什么
      • 同源策略
      • 跨站点脚本
  • 15.2 事件
    • 15.2.1 事件类别
    • 15.2.2 注册事件处理程序
      • 设置目标对象的事件处理程序属性
      • 设置HTML标签的事件处理程序属性
      • addEventListener()
    • 15.2.3 事件处理程序调用
      • 事件处理程序参数
      • 事件处理程序上下文
      • 处理程序返回值
      • 调用顺序
    • 15.2.4 事件传播
    • 15.2.5 事件取消
    • 15.2.6 分发自定义事件
  • 15.3 编写文档脚本
    • 15.3.1 选择文档元素
      • 使用CSS选择器选择元素
      • 其他元素选择方法
      • 预选元素
    • 15.3.2 文档结构和遍历
      • 作为节点树的文档
    • 15.3.3 属性
      • HTML属性作为元素属性
      • class属性
      • 数据集属性
    • 15.3.4 元素内容
      • 元素内容为HTML
      • 纯文本形式的元素内容
    • 15.3.5 创建、插入和删除节点
    • 15.3.6 示例:生成目录

JavaScript语言创建于1994年,其明确目的是在web浏览器显示的文档中启用动态行为。自那以后,这种语言有了很大的发展,与此同时,web平台的范围和功能也在爆炸性地增长。今天,JavaScript程序员可以把web看作是一个功能齐全的应用程序开发平台。Web浏览器专门用于显示格式化的文本和图像,但与本机操作系统一样,浏览器还提供其他服务,包括图形、视频、音频、网络、存储和线程。JavaScript是一种使web应用程序能够使用web平台提供的服务的语言,本章将演示如何使用这些服务中最重要的一种。

本章从web平台的编程模型开始,解释脚本是如何嵌入到HTML页面中的(§15.1),以及JavaScript代码是如何由事件异步触发的(§15.2)。简介后面的部分是核心JavaScript API,它使您的web应用程序能够:

  • 控制文档内容(§15.3)和样式(§15.4)
  • 确定文档元素在屏幕上的位置(§15.5)
  • 创建可重用的用户界面组件(§15.6)
  • 绘制图形(§15.7和§15.8)
  • 播放并生成声音(§15.9)
  • 管理浏览器导航和历史记录(§15.10)
  • 通过网络交换数据(§15.11)
  • 将数据存储在用户计算机上(§15.12)
  • 使用线程执行并行计算(§15.13)

客户端JavaScript
在这本书中,在网络上,你会看到术语“客户端JavaScript”。这个术语只是编写在web浏览器中运行的JavaScript的同义词,它与运行在web服务器上的“服务器端”代码形成鲜明对比。

这两个“端”字是指将web服务器和web浏览器分开的网络连接的两端,web软件开发通常需要在“两端”编写代码。客户端和服务器端通常也被称为“前端”和“后端”。

这本书的前几版试图全面地涵盖web浏览器定义的所有JavaScript API,结果,这本书在十年前已经太长了。web API的数量和复杂性在不断增长,我认为试图在一本书中涵盖它们已经没有意义了。在第七版中,我的目标是明确介绍JavaScript语言,并深入介绍如何在Node和web浏览器中使用JavaScript语言。本章不能涵盖所有的web API,但它详细介绍了最重要的API,以便您立即开始使用它们。而且,在了解了这里介绍的核心API之后,您应该能够在需要时使用新的API(如§15.15中总结的API)。

Node只有一个实现和一个权威的文档源。相比之下,Web API是由主要的Web浏览器供应商达成一致的,而权威文档采用的是为实现API的C++程序员而设计的规范,而不是使用JavaScript程序员的规范。幸运的是,Mozilla的“MDN网络文档”项目是一个可靠而全面的web API文档源1

遗留API
自JavaScript首次发布以来的25年中,浏览器供应商一直在为程序员添加属性和API。其中许多API现在已经过时。它们包括:

  • 从未被其他浏览器供应商标准化和/或从未实现过的专有API。微软的IE定义了很多这样的API。有些(比如innerHTML属性)被证明是有用的,并且最终被标准化了。其他方法(比如attachEvent()方法)已经过时多年了。
  • 效率低下的API(如document.write()方法)对性能的影响非常严重,以至于不再可以接受它们的使用。
  • 过时的API早已被新的API所取代,以达到同样的目的。一个允许设置文档背景颜色的示例document.bgColor。随着CSS的出现,document.bgColor变成了一个没有实际用途的奇特的特例。
  • 设计拙劣的API被更好的API所取代。在web的早期,标准委员会以一种与语言无关的方式定义了关键的文档对象模型API,以便在Java程序中使用相同的API来处理XML文档,并在JavaScript程序中使用同一API来处理HTML文档。这就导致了一个不太适合JavaScript语言的API,它具有web程序员并不特别关心的属性。从早期的设计错误中恢复需要几十年的时间,但是今天的web浏览器支持一个大大改进的文档对象模型。

浏览器供应商可能需要在可预见的将来支持这些遗留API,以确保向后兼容性,但本书不再需要对它们进行文档记录,也不需要您了解它们。web平台已经成熟和稳定,如果你是一个经验丰富的web开发人员,还记得这本书的第四或第五版,那么你可能会忘记很多过时的知识,就像你有新的东西需要学习一样。

15.1 网络编程基础

本节将解释用于web的JavaScript程序是如何构造的,它们是如何加载到web浏览器中的,它们如何获得输入,如何生成输出,以及它们如何通过响应事件异步运行。

15.1.1 HTML script 标签中的JavaScript

Web浏览器显示HTML文档。如果您希望web浏览器执行JavaScript代码,那么HTML文档中必须包含(或引用)代码,而这正是HTML<script>标签所做的。

JavaScript代码可以在HTML文件中的<script>和</script>标签之间显示。例如,这里是一个HTML文件,其中包含一个带有JavaScript代码的script标签,动态更新文档的一个元素,使其行为类似于数字时钟:

<!DOCTYPE html> <!-- 这是一个HTML5文件 -->
<html>
<!-- 根元素 --><head><!-- 标题,脚本和样式可以在这里 --><title>数字时钟</title><style>/* 时钟的CSS样式表 */#clock {/* 样式应用于id=“clock”的元素 */font: bold 24px sans-serif;/* 使用粗体大字体 */background: #ddf;/* 在浅蓝灰色的背景上。 */padding: 15px;/* 用一些空白把它围起来 */border: solid black 2px;/* 和一个实体黑色边框 */border-radius: 10px;/* 有圆角。 */}</style>
</head><body><!-- 主体保存文档的内容。 --><h1>数字时钟</h1> <!-- 显示标题。 --><span id="clock"></span> <!-- 我们将在这个元素中插入时间。 --><script>// 定义函数以显示当前时间function displayTime() {let clock = document.querySelector("#clock"); // 获取id=“clock”的元素let now = new Date();                         // 获取当前时间clock.textContent = now.toLocaleTimeString(); // 在时钟中显示时间}displayTime()                    // 马上显示时间setInterval(displayTime, 1000);  // 然后每秒钟更新一次。</script>
</body></html>

尽管JavaScript代码可以直接嵌入到<script>标签中,但更常见的做法是使用<script>标签的src属性(attribute)来指定包含JavaScript代码的文件的URL(绝对URL或相对于显示的HTML文件URL的URL)。如果我们从这个HTML文件中取出JavaScript代码并将其存储在它自己的scripts/digital_clock.js文件中,则<script>标签可能使用如下所示引用这个文件:

<script src="scripts/digital_clock.js"></script>

JavaScript文件包含纯JavaScript,没有<script>标签或任何其他HTML。按照惯例,JavaScript代码的文件名以.js结尾。

带有src属性的<script>标签的行为就像指定的JavaScript文件的内容直接出现在<script>和</script>标签之间。请注意,即使指定了src属性,HTML文档中也需要结束符</script>标签:HTML不支持<script/>标签。

使用src属性有许多优点:

  • 它简化了你的HTML文件,允许你删除大量的JavaScript代码,也就是说,它有助于保持内容和行为的分离。
  • 当多个网页共享相同的JavaScript代码时,使用src属性可以只维护该代码的一个副本,而不必在代码更改时编辑每个HTML文件。
  • 如果一个JavaScript代码文件由多个页面共享,则只需下载一次,由使用它的第一个页面下载,后续页面可以从浏览器缓存中检索该文件。
  • 由于src属性采用任意URL作为其值,所以来自一个web服务器的JavaScript程序或web页面可以使用其他web服务器导出的代码。很多网络广告都依赖于这个事实。

模块

§10.3记录了JavaScript模块并涵盖了它们的导入和导出指令。如果您使用模块编写了JavaScript程序(并且没有使用代码打包工具将所有模块组合到单个非模块JavaScript文件中),则必须使用具有type=“module”属性的<script>标签加载程序的顶层模块。如果您这样做,那么您指定的模块将被加载,它导入的所有模块都将被加载,并且(递归地)加载它们导入的所有模块。完整详情见§10.3.5。

指定脚本类型

在web早期,人们认为浏览器可能有一天会实现JavaScript以外的语言,程序员在其<script>标签中添加了language=“javascript”和type=“application/javascript”等属性。这完全没有必要。JavaScript是web的默认(也是唯一)语言。language属性已弃用,在<script>标签上使用type属性只有两个原因:

  • 指定脚本是一个模块
  • 将数据嵌入网页而不显示(见§15.3.4)

脚本运行时:异步和延迟

当JavaScript第一次被添加到web浏览器中时,还没有用于遍历和操作已呈现文档的结构和内容的API。JavaScript代码影响文档内容的唯一方法是在文档加载过程中动态生成该内容。它通过使用document.write()方法将HTML文本注入到脚本所在的文档中。

document.write()的使用不再被认为是一种好的风格,但事实上它是可能的,这意味着当HTML解析器遇到<script>元素时,它必须在默认情况下运行脚本,以确保它在继续解析和呈现文档之前不会输出任何HTML。这会显著降低网页的解析和呈现速度。

幸运的是,这种默认的同步或阻塞脚本执行模式并不是唯一的选择。<script>标签可以具有defer和async属性,这会导致脚本以不同的方式执行。这些是布尔属性,它们没有值;它们只需要出现在<script>标签上。请注意,这些属性仅在与src属性一起使用时才有意义:

<script defer src="deferred.js"></script>
<script async src="async.js"></script>

defer和async属性都是告诉浏览器链接脚本不使用document.write()生成HTML输出的方法,因此浏览器可以在下载脚本的同时继续解析和呈现文档。defer属性使浏览器推迟脚本的执行,直到文档被完全加载和解析并准备好进行操作。async属性使浏览器尽快运行脚本,但在下载脚本时不会阻止文档解析。如果<script>标签同时具有这两个属性,则async属性优先。

请注意,延迟脚本按照它们在文档中出现的顺序运行。异步脚本在加载时运行,这意味着它们可能会无序执行。

默认情况下,带有type=“module”属性的脚本将在文档加载后执行,就像它们具有defer属性一样。您可以使用async属性覆盖此默认值,这将导致在加载模块及其所有依赖项后立即执行代码。

对于直接包含在HTML中的代码,async和defer属性的一个简单替代方法是将脚本放在HTML文件的末尾。这样,脚本可以在文档内容被解析并准备好操作之前运行。

按需加载脚本

有时,您可能有一些JavaScript代码,当第一次加载文档时不使用这些代码,并且只有在用户执行某些操作(如单击按钮或打开菜单)时才需要该代码。如果您使用模块开发代码,您可以使用import()按需加载模块,如§10.3.6所述。

如果不使用模块,只需在需要加载脚本时向文档添加<script>标签,就可以按需加载JavaScript文件:

// 从指定的URL异步加载和执行脚本
// 返回一个在脚本加载后解决的Promise。
function importScript(url) {return new Promise((resolve, reject) => {let s = document.createElement("script"); // 创建<script>元素s.onload = () => { resolve(); };          // 加载后解决Promises.onerror = (e) => { reject(e); };        // 失败时拒绝s.src = url;                              // 设置脚本URLdocument.head.append(s);                  // 将<script>添加到文档});
}

这个importScript()函数使用DOM API(§15.3)创建一个新的<script>标签并将其添加到文档<head>中。它使用事件处理程序(§15.2)来确定脚本何时成功加载或何时加载失败。

15.1.2 文档对象模型(DOM)

客户端JavaScript编程中最重要的对象之一是Document对象,它表示在浏览器窗口或选项卡中显示的HTML文档。用于处理HTML文档的API称为文档对象模型,或DOM,§15.3将对此进行详细介绍。但是DOM是客户端JavaScript编程的核心,因此值得在这里介绍它。

HTML文档包含一个嵌套的HTML元素,形成一个树。考虑以下简单的HTML文档:

<html><head><title>Sample Document</title>
</head><body><h1>An HTML Document</h1><p>This is a <i>simple</i> document.
</body></html>

顶层的<html>标签包含<head>和<body>标签。<head>标签包含一个<title>标签。并且<body>标签包含<h1>和<p>标签。<title>和<h1>标签包含文本字符串,<p>标签包含两个文本字符串,它们之间有一个<i>标签。

DOM API映射HTML文档的树结构。对于文档中的每个HTML标签,都有一个对应的JavaScript元素对象,对于文档中每次运行的文本,都有一个对应的Text(文本)对象。Element(元素)和Text(文本)类以及Document(文档)类本身都是更通用的Node(节点)类的子类,节点对象被组织成树状结构,JavaScript可以使用DOM API查询和遍历这些树结构。本文档的DOM表示如图15-1所示。


图15-1. HTML文档的树表示

如果您还不熟悉计算机编程中的树结构,那么知道它们借用了家谱中的术语是很有帮助的。节点正上方的节点是该节点的父节点。另一个节点正下方一个级别的节点是该节点的子节点。处于同一级别且具有相同父级的节点是兄弟节点。另一个节点下任意数量级别的节点集都是该节点的后代。父节点、祖父母节点和节点上方的所有其他节点都是该节点的祖先。

DOM API包括创建新元素和文本节点的方法,以及将它们作为其他元素对象的子节点插入到文档中的方法。还有一些方法可以在文档中移动元素并完全删除它们。虽然服务器端应用程序可能通过使用console.log()编写字符串来生成纯文本输出,但是客户端JavaScript应用程序可以通过使用DOM API构建或操作文档树文档来生成格式化的HTML输出。

每个HTML标签类型都对应一个JavaScript类,并且文档中的每个标签都由该类的一个实例表示。例如,<body>标签由HTMLBodyElement的实例表示,<table>标签由HTMLTableElement的实例表示。JavaScript元素对象具有与标签的HTML属性相对应的属性。例如,表示<img>标签的HTMLImageElement的实例具有与标签的src属性相对应的src属性。src属性的初始值是HTML标签中显示的属性值,使用JavaScript设置此属性会更改HTML属性的值(并导致浏览器加载和显示新图像)。大多数JavaScript元素类只是镜像HTML标签的属性,但有些类定义了其他方法。例如,HTMLAudioElement和HTMLVideoElement类定义了play()和pause()等方法来控制音频和视频文件的播放。

15.1.3 Web浏览器中的全局对象

每个浏览器窗口或选项卡都有一个全局对象(§3.7)。在该窗口中运行的所有JavaScript代码(在工作线程中运行的代码除外;请参见§15.13)都共享这个全局对象。不管文档中有多少脚本或模块,这都是正确的:一个文档的所有脚本和模块共享一个全局对象;如果一个脚本定义了该对象的属性,则该属性对所有其他脚本也可见。

全局对象是JavaScript的标准库定义的地方:parseInt()函数、Math对象、Set类等等。在web浏览器中,全局对象还包含各种web API的主要入口点。例如,document属性表示当前显示的文档,fetch()方法发出HTTP网络请求,Audio()构造函数允许JavaScript程序播放声音。

在web浏览器中,全局对象有双重作用:除了定义内置类型和函数外,它还表示当前的web浏览器窗口,并定义诸如history(§15.10.2)之类的属性,history表示窗口的浏览历史,innerWidth保存窗口的像素宽度。这个全局对象的一个属性名为window,它的值是全局对象本身。这意味着您可以简单地键入window来引用客户端代码中的全局对象。当使用特定于窗口的属性时,通常最好包含一个window.前缀:例如,window.innerWidth比innerWidth更清晰。

15.1.4 脚本共享一个命名空间

对于模块,在模块的顶层(即任何函数或类定义之外)定义的常量、变量、函数和类对模块是私有的,除非它们被显式导出,在这种情况下,它们可以被其他模块选择性地导入。(请注意,代码打包工具也支持模块的这个属性。)

但是,对于非模块脚本,情况则完全不同。如果脚本中的顶层代码定义了常量、变量、函数或类,则该声明将对同一文档中的所有其他脚本可见。如果一个脚本定义函数f(),另一个脚本定义类c,那么第三个脚本可以调用函数并实例化类,而不必执行任何操作来导入它们。因此,如果不使用模块,文档中的独立脚本共享一个名称空间,其行为方式就好像它们都是单个较大脚本的一部分。这对于小程序来说可能很方便,但是对于较大的程序来说,避免命名冲突的需要可能会成为问题,特别是当一些脚本是第三方库时。

这个共享名称空间的工作方式有一些历史上的怪癖。顶层的var和function声明在共享全局对象中创建属性。如果一个脚本定义了顶级函数f(),那么同一文档中的另一个脚本可以调用f()或window.f()。另一方面,在顶层使用ES6声明const、let和class时,不会在全局对象中创建属性。但是,它们仍然是在一个共享的命名空间中定义的:如果一个脚本定义了一个类C,其他脚本将能够用new C()创建该类的实例,但不能用new window.C()创建。

总结一下:在模块中,顶级声明的作用域是模块,可以显式导出。但是,在非模块脚本中,顶级声明的作用域是包含文档的,并且声明由文档中的所有脚本共享。旧的var和函数声明是通过全局对象的属性共享的。更新的const、let和class声明也被共享并具有相同的文档范围,但它们不作为JavaScript代码可以访问的任何对象的属性存在。

15.1.5 JavaScript程序的执行

在客户端JavaScript中没有程序的正式定义,但是可以说JavaScript程序由文档中或从文档引用的所有JavaScript代码组成。这些独立的代码共享一个全局窗口对象,这使它们能够访问表示HTML文档的同一个底层Document对象。不是模块的脚本还共享一个顶级命名空间。

如果网页包含一个被嵌入的框架(使用<iframe>元素),则被嵌入文档中的JavaScript代码与包含被嵌入文档的文档中的代码具有不同的全局对象和文档对象,可以将其视为一个单独的JavaScript程序。不过,请记住,JavaScript程序的边界并没有正式的定义。如果包含文档和被包含文档都是从同一个服务器加载的,那么一个文档中的代码可以与另一个文档中的代码进行交互,如果需要,可以将它们视为单个程序的两个交互部分。§15.13.6解释了JavaScript程序如何在<iframe>中运行的JavaScript代码之间收发消息。

可以将JavaScript程序的执行看作是分两个阶段执行的。在第一阶段,加载文档内容,并运行来自<script>元素(内联脚本和外部脚本)的代码。脚本通常按照它们在文档中出现的顺序运行,尽管这个默认顺序可以通过我们描述的async和defer属性进行修改。任何单个脚本中的JavaScript代码都是自上而下运行的,当然,还包括JavaScript的条件语句、循环和其他控制语句。有些脚本在第一阶段并不做任何事情,而是定义在第二阶段使用的函数和类。其他脚本可能在第一个阶段执行重要的工作,而在第二个阶段什么也不做。想象一下,在文档末尾有一个脚本,它在文档中找到所有<h1>和<h2>标签,并通过在文档开头生成和插入目录来修改文档。这完全可以在第一阶段完成。(请参见§15.3.6中的具体示例。)

一旦加载了文档并运行了所有脚本,JavaScript执行将进入第二阶段。这个阶段是异步和事件驱动的。如果一个脚本要参与第二个阶段,那么在第一个阶段它必须完成的一件事就是注册至少一个事件处理程序或其他异步调用的回调函数。在这个事件驱动的第二阶段,web浏览器调用事件处理程序函数和其他回调来响应异步发生的事件。事件处理程序通常在响应用户输入(鼠标单击、击键等)时调用,但也可能由网络活动、文档和资源加载、运行时间或JavaScript代码中的错误触发。事件和事件处理程序在§15.2中有详细描述。

在事件驱动阶段,首先发生的一些事件是“DOMContentLoaded”和“load”事件。“DOMContentLoaded”在HTML文档被完全加载和解析后被触发。当文档的所有外部资源(如图像)也已完全加载时,将触发“load”事件。JavaScript程序通常使用这些事件之一作为触发器或启动信号。常见的情况是,程序的脚本定义函数,但除了注册事件处理程序函数之外,不执行任何操作,以便在事件驱动的执行阶段开始时由“load”事件触发。正是这个“load”事件处理程序,然后操纵文档并执行程序应该执行的任何操作。请注意,在JavaScript编程中,事件处理程序函数(如这里描述的“加载”事件处理程序)注册其他事件处理程序是很常见的。

JavaScript程序的加载阶段相对较短:理想情况下不到一秒钟。一旦加载了文档,事件驱动阶段将持续到文档通过web浏览器显示的时间。因为这个阶段是异步的和事件驱动的,所以可能会有很长一段时间没有执行JavaScript,中间是由用户或网络事件触发的突发活动。接下来我们将更详细地介绍这两个阶段。

客户端JavaScript线程模型

JavaScript是一种单线程语言,单线程执行使编程更加简单:您可以编写代码,并保证两个事件处理程序永远不会同时运行。您可以在没有其他线程试图同时修改文档内容的情况下操作文档内容,而且在编写JavaScript代码时,您不必担心锁、死锁或竞争条件。

单线程执行意味着web浏览器在脚本和事件处理程序执行时停止响应用户输入。这给JavaScript程序员带来了负担:这意味着JavaScript脚本和事件处理程序不能运行太长时间。如果脚本执行计算密集型任务,它将在文档加载过程中引入延迟,并且在脚本完成之前,用户将看不到文档内容。如果事件处理程序执行计算密集型任务,浏览器可能变得无响应,可能导致用户认为它已崩溃。

web平台定义了一种称为“web worker”的受控并发形式。web worker是一个后台线程,用于执行计算密集型任务,而不冻结用户界面。运行在web worker线程中的代码不能访问文档内容,不能与主线程或其他工作线程共享任何状态,并且只能通过异步消息事件与主线程和其他工作线程通信,因此主线程无法检测到并发性,并且web worker不会更改基本的JavaScript程序的单线程执行模型。有关网络安全线程机制的详细信息,请参见§15.13。

客户端JavaScript时间轴

我们已经看到JavaScript程序开始于脚本执行阶段,然后过渡到事件处理阶段。这两个阶段可以进一步细分为以下步骤:

  1. web浏览器创建一个Document对象并开始解析web页面,在解析HTML元素及其文本内容时向文档添加元素对象和文本节点。document.readyState属性在此阶段具有值“loading”。
  2. 当HTML解析器遇到不具有async、defer或type=“module”属性的<script>标签时,它会将该脚本标签添加到文档中,然后执行脚本。脚本是同步执行的,当脚本下载(如果需要)并运行时,HTML解析器会暂停。像这样的脚本可以使用document.write()将文本插入到输入流中,当解析器继续时,该文本将成为文档的一部分。像这样的脚本通常只定义函数并注册事件处理程序以供以后使用,但它可以遍历和操作文档树,因为它此时已经存在。也就是说,没有async或defer属性的非模块脚本可以看到它们自己的<script>标签和前面的文档内容。
  3. 当解析器遇到设置了async属性的<script>元素时,它开始下载脚本文本(如果脚本是一个模块,它还会递归地下载脚本的所有依赖项),并继续解析文档。脚本将在下载后尽快执行,但解析器不会停止并等待下载。他们可以看到自己的<script>标签和它前面的所有文档内容,并且可能有权也可能无权访问其他文档内容。
  4. 当文档被完全解析后,document.readyState属性将更改为“interactive”。
  5. 任何设置了defer属性的脚本(以及没有async属性的模块脚本)将按照它们在文档中出现的顺序执行。此时也可以执行async脚本。延迟脚本可以访问完整的文档,并且不能使用document.write()方法。
  6. 浏览器在Document对象上触发一个“DOMContentLoaded”事件。这标志着从同步脚本执行阶段过渡到程序执行的异步、事件驱动阶段。但是,请注意,此时可能仍有一些async脚本尚未执行。
  7. 此时文档已完全解析,但浏览器可能仍在等待其他内容(如图像)加载。当所有这些内容加载完毕,并且所有async脚本都已加载和执行时,document.readyState属性将更改为“complete”,web浏览器将对窗口对象触发“load”事件。
  8. 从现在起,事件处理程序将异步调用,以响应用户输入事件、网络事件、计时器过期等。

15.1.6 程序输入和输出

与任何程序一样,客户端JavaScript程序处理输入数据以生成输出数据。有多种可用的输入:

  • 文档本身的内容,JavaScript代码可以使用DOM API访问这些内容(§15.3)。
  • 用户输入,以事件的形式,例如HTML<button>元素上进行鼠标点击(或触摸屏点击),或文本输入到HTML<textrarea>元素中。§15.2演示了JavaScript程序如何响应用户事件。
  • 正在显示的文档的URL可以作为document.URL提供给客户端JavaScript。如果将此字符串传递给URL()构造函数(§11.9),则可以轻松访问URL的path、query和fragment部分。
  • HTTP“Cookie”请求头的内容作为document.cookie可供客户端代码使用。服务器端代码通常使用cookie来维护用户会话,但客户端代码也可以在必要时读取(和写入)Cookies。更多详情见§15.12.2。
  • 全局navigator属性提供对有关web浏览器、运行在其上的操作系统以及每个浏览器的功能的信息的访问。例如,navigator.userAgent是标识web浏览器的字符串,navigator.language是用户的首选语言,navigator.hardwareConcurrency返回web浏览器可用的逻辑CPU数。类似地,全局screen属性通过screen.width和screen.height属性提供对用户显示大小的访问。在某种意义上,这些navigator和screen对象对于web浏览器就像环境变量对于Node程序一样。

客户端JavaScript通常在需要时生成输出,方法是使用DOM API(§15.3)操作HTML文档,或者使用更高级的框架(如React或Angular)来操作文档。客户端代码还可以使用console.log()和相关方法(§11.8)生成输出。但此输出仅在web开发人员控制台中可见,因此在调试时很有用,但对于用户可见的输出则不可用。

15.1.7 程序错误

与直接在操作系统上运行的应用程序(如Node应用程序)不同,web浏览器中的JavaScript程序不会真正“崩溃”。如果JavaScript程序运行时发生异常,并且您没有catch语句来处理,则开发人员控制台中将显示一条错误消息,但任何已注册的事件处理程序都会继续运行并响应事件。

如果您想定义在发生此类未捕获异常时调用的最后一个错误处理程序,请将Window对象的onerror属性设置为错误处理程序函数。当一个未捕获的异常沿着调用堆栈向上传播,并且开发人员控制台中将显示一条错误消息时,将使用三个字符串参数调用window.onerror函数。window.onerror的第一个参数是描述错误的消息。第二个参数是一个字符串,它包含导致错误的JavaScript代码的URL。第三个参数是发生错误的文档中的行号。如果onerror处理程序返回true,它将告诉浏览器该处理程序已处理错误,并且无需进一步操作,换句话说,浏览器不应显示自己的错误消息。

当一个Promise被拒绝并且没有.catch()函数来处理它时,这种情况非常类似于未处理的异常:程序中的意外错误或逻辑错误。您可以通过定义window.onunhandledrejection函数或使用window.addEventListener()为“unhandledrejection”事件注册处理程序来检测此情况。传递给这个处理程序的事件对象将有一个promise属性,其值是被拒绝的promise对象,而reason属性的值是传递给.catch()函数的值。与前面描述的错误处理程序一样,如果对未处理的拒绝事件对象调用preventDefault(),则它将被视为已处理,并且不会在开发人员控制台中导致错误消息。

通常不需要定义onerror或onunhandledrejection处理程序,但如果您希望向服务器报告客户端错误(例如,使用fetch()函数发出HTTP POST请求),以便获取有关用户浏览器中发生的意外错误的信息,则它可以作为遥测机制非常有用。

15.1.8 Web安全模型

网页可以在您的个人设备上执行任意JavaScript代码这一事实具有明显的安全隐患,浏览器供应商一直在努力平衡两个相互竞争的目标:

  • 定义功能强大的客户端API以支持有用的web应用程序
  • 防止恶意代码读取或更改您的数据,损害您的隐私,欺骗您,或浪费您的时间

下面的小节简要介绍了作为JavaScript程序员应该注意的安全限制和问题。

JavaScript不能做什么

Web浏览器抵御恶意代码的第一道防线就是它们根本不支持某些功能。例如,客户端JavaScript不提供任何方法来写入或删除客户端计算机上的任意文件或列出任意目录。这意味着JavaScript程序不能删除数据或植入病毒。

同样,客户端JavaScript没有通用的联网功能。客户端JavaScript程序可以发出HTTP请求(§15.11.1)。另一个被称为WebSockets(§15.11.3)的标准定义了一个与专用服务器通信的类套接字API。但这两个API都不允许对更广泛的网络进行非中介访问。通用internet客户端和服务器不能用客户端JavaScript编写。

同源策略

同源策略是对Java脚本代码可以与哪些web内容进行交互的全面安全限制。它通常在网页包含<iframe>元素时发挥作用。在这种情况下,同源策略控制一个框架中JavaScript代码与其他框架内容的交互。特别是,脚本只能读取与包含脚本的文档具有相同来源的窗口和文档的属性。

文档的来源定义为从中加载文档的URL的协议、主机和端口。从不同的web服务器加载的文档具有不同的来源。通过同一主机的不同端口加载的文档具有不同的来源。而且加载了http:protocol的文档与使用https:protocol加载的文档具有不同的来源,即使它们来自同一个web服务器。浏览器通常将每个file:URL视为一个单独的来源,这意味着如果您正在开发一个显示来自同一服务器的多个文档的程序,您可能无法使用file:URL在本地测试它,并且必须在开发期间运行静态web服务器。

重要的是要理解脚本本身的来源与同源策略无关:重要的是包含嵌入脚本的文档的来源。例如,假设主机a托管的脚本(使用<script>元素的src属性)包含在主机B提供服务的网页中。该脚本的来源是主机B,并且该脚本对包含它的文档的内容具有完全访问权限。如果这个文档包含一个<iframe>,该iframe包含来自主机B的第二个文档,则该脚本还可以完全访问该第二个文档的内容。但是,如果顶级文档包含另一个<iframe>,该iframe显示来自主机C的文档(甚至来自主机a的文档),则同源策略将生效,并阻止脚本访问此嵌套文档。

同源策略也适用于脚本化的HTTP请求(见§15.11.1)。JavaScript代码可以向加载包含文档的web服务器发出任意的HTTP请求,但它不允许脚本与其他web服务器通信(除非这些web服务器选择使用CORS,如下面所述)。

同源策略给使用多个子域的大型网站带来了问题。例如,源为“orders.example.com”的脚本可能需要从“example.com”上的文档中读取属性。为了支持此类多域网站,脚本可以通过将“document.domain”设置为域后缀来更改其来源。所以一个源为”https://orders.example.com“的脚本可以通过将“document.domain”设置为“example.com”将其来源更改为”https://example.com“,但该脚本无法将“document.domain”设置为“orders.example”、“ample.com”或“com”。

放松同源策略的第二种技术是跨源资源共享(Cross-Origin Resource Sharing,CORS),它允许服务器决定他们愿意为哪些源服务。CORS用一个新的Origin:请求头和一个新的Access-Control-Allow-Origin响应头扩展了HTTP。它允许服务器使用报头显式列出可能请求文件的源,或者使用通配符,并允许任何站点请求文件。浏览器尊重这些CORS头文件,除非它们存在,否则不会放松同源限制。

跨站点脚本

跨站点脚本(XSS)是一类安全问题的术语,其中攻击者将HTML标签或脚本注入目标网站。客户端JavaScript程序员必须了解并防范跨站点脚本。

如果网页动态生成文档内容是基于用户提交的数据,但是不首先通过删除任何嵌入的HTML标签来“净化”这些数据,则网页容易受到跨站点脚本的攻击。作为一个简单的示例,请考虑以下使用JavaScript按名字问候用户的网页:

<script>let name = new URL(document.URL).searchParams.get("name");
document.querySelector('h1').innerHTML = "Hello " + name;</script>

这个两行脚本从文档URL的“name”查询参数提取输入。然后它使用DOM API将一个HTML字符串注入到文档中的第一个<h1>标签中。此页面旨在通过如下URL调用:

http://www.example.com/greet.html?name=David

当这样使用时,它会显示文本“Hello David”。但是请考虑使用以下查询参数调用它时会发生什么情况:

name=%3Cimg%20src=%22x.png%22%20οnlοad=%22alert(%27hacked%27)%22/%3E

当对URL转义参数进行解码时,此URL将导致将以下HTML注入到文档中:

Hello <img src=“x.png” οnlοad=“alert(‘hacked’)”/>

图像加载后,onload属性中的JavaScript字符串被执行。全局alert()函数的作用是:显示一个模式对话框。一个单独的对话框相对来说是良性的,但它表明在这个站点上可以执行任意代码,因为它显示未经净化的HTML。

之所以称为跨站点脚本攻击,是因为涉及多个站点。站点B包含到站点A的精心编制的链接(如前一个示例中的链接)。如果站点B能够说服用户单击该链接,他们将被带到站点A,但该站点现在将运行来自站点B的代码。该代码可能会损坏页面或导致页面故障。更危险的是,恶意代码可以读取站点A存储的cookie(可能是账号或其他个人识别信息),并将这些数据发送回站点B。注入的代码甚至可以跟踪用户的按键并将数据发送回站点B。

一般来说,防止XSS攻击的方法是在使用任何不受信任的数据创建动态文档内容之前删除HTML标签。通过将不受信任的输入字符串中的特殊HTML字符替换为等效的HTML实体,可以修复前面显示的greet.html文件:

name = name.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;").replace(/\//g, "&#x2F;")

解决XSS问题的另一种方法是构建web应用程序,使不受信任的内容始终显示在<iframe>中,并将sandbox属性设置为禁用脚本和其他功能。

跨站点脚本是一个有害的漏洞,其根源深入到web体系结构中。深入了解这个漏洞是值得的,但进一步的讨论超出了本书的范围。有许多在线资源可以帮助您抵御跨站点脚本。

15.2 事件

客户端JavaScript程序使用异步事件驱动编程模型。在这种编程风格中,每当文档或浏览器或与之相关的元素或对象发生有趣的事情时,web浏览器就会生成一个事件。例如,web浏览器在完成加载文档、用户将鼠标移到超链接上或用户敲击键盘上的键时生成事件。如果JavaScript应用程序关心特定类型的事件,它可以注册一个或多个函数,以便在该类型的事件发生时调用。请注意,这并不是web编程所独有的:所有具有图形用户界面的应用程序都是这样设计的:它们坐在那里等待与之交互(即,它们等待事件发生),然后做出响应。

在客户端JavaScript中,事件可以发生在HTML文档中的任何元素上,这使得web浏览器的事件模型比Node的事件模型复杂得多。我们从一些重要的定义开始本节,这些定义有助于解释事件模型:

事件类型

  • 此字符串指定发生的事件类型。例如,“mousemove”类型表示用户移动了鼠标。“keydown”类型表示用户按下键盘上的某个键。“load”类型表示文档(或其他资源)已从网络加载完毕。因为事件的类型只是一个字符串,它有时被称为事件名,实际上,我们使用这个名称来标识我们所讨论的事件的类型。

事件目标

  • 这是发生事件或与事件关联的对象。当我们谈到一个事件时,我们必须同时指定类型和目标。例如,window上的load事件或<button>元素上的click事件。窗口、文档和元素对象是客户端JavaScript应用程序中最常见的事件目标,但有些事件是在其他类型的对象上触发的。例如,一个Worker对象(一种线程,包含在§15.13中)是当Worker线程向主线程发送消息时发生的“message”事件的目标。

事件处理程序或事件侦听器

  • 此函数用于处理或响应事件2。应用程序使用web浏览器注册其事件处理程序函数,指定事件类型和事件目标。当指定目标上发生指定类型的事件时,浏览器将调用处理程序函数。当为一个对象调用事件处理程序时,我们称浏览器已“激发”、“触发”或“调度”事件。注册事件处理程序的方法有很多种,§15.2.2和§15.2.3解释了处理程序注册和调用的详细信息。

事件对象

  • 此对象与特定事件关联,并包含有关该事件的详细信息。事件对象作为参数传递给事件处理程序函数。所有事件对象都有一个指定事件类型的type属性和一个指定事件目标的target属性。每个事件类型为其关联的事件对象定义一组属性。例如,与鼠标事件相关联的对象包括鼠标指针的坐标,与键盘事件关联的对象包含有关按下的键和按下的修改键的详细信息。许多事件类型只定义一些标准属性,例如type和target,并且不包含太多其他有用的信息。对于这些事件,重要的是事件的发生,而不是事件的细节。

事件传播

  • 这是浏览器决定在哪个对象上触发事件处理程序的过程。对于特定于单个对象的事件,例如窗口对象上的“load”事件或Worker对象上的“message”事件,不需要传播。但是,当HTML文档中的元素发生某些类型的事件时,它们会在文档树上传播或“冒泡”。如果用户将鼠标移动到超链接上,则鼠标mousemove事件首先会在定义该链接的<a>元素上触发。然后,它会被触发到包含元素上:可能是<p>元素、一个<section>元素和文档对象本身。在文档或其他容器元素上注册单个事件处理程序有时比在感兴趣的每个元素上注册处理程序更方便。事件处理程序可以停止事件的传播,这样它就不会继续冒泡,也不会触发包含元素的处理程序。处理程序通过调用事件对象的方法来实现这一点。在另一种形式的事件传播(称为事件捕获)中,在容器元素上特别注册的处理程序有机会在将事件传递到实际目标之前拦截(或“捕获”)事件。事件冒泡和捕获在§15.2.4中有详细说明。

有些事件具有与它们关联的默认操作。例如,当超链接上发生单击事件时,默认操作是浏览器跟随链接并加载新页面。事件处理程序可以通过调用事件对象的方法来防止此默认操作。这有时被称为“取消”事件,并在§15.2.5中进行了说明。

15.2.1 事件类别

客户端JavaScript支持大量的事件类型,本章无法涵盖所有这些类型。不过,将事件分组到一些常规类别中,以说明支持的事件的范围和广泛的多样性是很有用的:

设备相关输入事件

  • 这些事件直接绑定到特定的输入设备,如鼠标或键盘。它们包括诸如“mousedown”、“mousemove”、“mouseup”、“touchstart”、“touchmove”、“touchend”、“keydown”和“keyup”等事件类型。

设备无关输入事件

  • 这些输入事件不直接绑定到特定的输入设备。例如,“click”事件表示链接或按钮(或其他文档元素)已激活。这通常是通过鼠标点击来完成的,但也可以通过键盘或(在触摸敏感设备上)点击来完成。“input”事件是“keydown”事件的一个独立于设备的替代项,它支持键盘输入以及诸如剪切粘贴和用于象形文字的输入方法等替代方法。“pointerdown”、“pointermove”和“pointerup”事件类型是与设备无关的鼠标和触摸事件的替代品。它们适用于鼠标型指针、触摸屏以及笔式或手写笔式输入。

用户界面事件

  • UI事件是更高级别的事件,通常在HTML表单元素上,这些元素定义web应用程序的用户界面。它们包括“focus”事件(当文本输入字段获得键盘焦点时)、“change”事件(当用户更改表单元素显示的值时)和“submit”事件(当用户单击表单中的提交按钮时)。

状态更改事件

  • 有些事件不是由用户活动直接触发的,而是由网络或浏览器活动触发的,它们表示某种生命周期或状态相关的变化。在文档加载结束时,分别对窗口和文档对象触发的“load”和“DOMContentLoaded”事件可能是这些事件中最常用的(参见“客户端JavaScript时间轴”)。当网络连接发生变化时,浏览器在窗口对象上触发“online”和“offline”事件。浏览器的历史管理机制(§15.10.4)触发“popstate”事件以响应浏览器的后退按钮。

API特定事件

  • 许多由HTML和相关规范定义的web API包括它们自己的事件类型。HTML<video>和<audio>元素定义了一长串相关事件类型,如“waiting”、“playing”、“seeking”、“volumechange”等等,您可以使用它们自定义媒体播放。一般来说,异步的web平台API是在向JavaScript添加Promise之前开发的,它们基于事件并定义API特定的事件。例如,IndexedDB API(§15.12.3)在数据库请求成功或失败时触发“success”和“error”事件。尽管用于发出HTTP请求的新fetch() API(§15.11.1)是基于Promise的,但是它所替代的XMLHttpRequest API定义了许多特定于API的事件类型。

15.2.2 注册事件处理程序

注册事件处理程序有两种基本方法。第一种,从web早期开始,是在作为事件目标的对象或文档元素上设置一个属性。第二种(更新和更通用的)技术是将处理程序传递给对象或元素的addEventListener()方法。

设置目标对象的事件处理程序属性

注册事件处理程序的最简单方法是将事件目标的属性设置为所需的事件处理程序函数。按照惯例,事件处理程序属性的名称由单词“on”组成,后跟事件名称:onclick、onchange、onload、onmouseover等等。请注意,这些属性名称区分大小写,并且全部用小写字母书写3,即使事件类型(如“mousedown”)由多个单词组成。以下代码包括两个此类事件处理程序注册:

// 将Window对象的onload属性设置为函数。
// 这个函数是事件处理程序:它在文档加载时被调用。
window.onload = function () {// 查找<form>元素let form = document.querySelector("form#shipping");// 在表单上注册事件处理程序函数,在提交表单之前将调用该函数。// 假设isFormValid()在其他地方定义。form.onsubmit = function (event) { // 当用户提交表单时if (!isFormValid(this)) { // 检查表单输入是否有效event.preventDefault(); // 如果无效,就阻止表单提交。}};
};

事件处理程序属性的缺点是,它们的设计是基于这样一个假设:事件目标对于每种类型的事件最多只能有一个处理程序。使用addEventListener()注册事件处理程序通常更好,因为该技术不会覆盖任何以前注册的处理程序。

设置HTML标签的事件处理程序属性

文档元素的事件处理程序属性也可以直接在HTML文件中定义为相应HTML标签上的属性。(用JavaScript在Window元素上注册的处理程序可以用HTML中的<body>标签上的属性来定义)这种技术在现代web开发中通常是不受欢迎的,但是它是可能的,本文将其记录在这里,因为您仍然可以在现有代码中看到它。

将事件处理程序定义为HTML属性时,属性值应为JavaScript代码的字符串。该代码应该是事件处理程序函数的主体,而不是完整的函数声明。也就是说,您的HTML事件处理程序代码不应该用大括号括起来并以function关键字作为前缀。例如:

<button onclick="console.log('Thank you');">Please Click</button>

如果一个HTML事件处理程序属性包含多个JavaScript语句,则必须记住用分号分隔这些语句,或者将属性值分隔多行。

当您指定一个JavaScript代码字符串作为HTML事件处理程序属性的值时,浏览器会将字符串转换为一个函数,其工作方式如下:

function(event) {with (document) {with (this.form || {}) {with (this) {/* 你的代码在这里 */}}}
}

event参数表示处理程序代码可以将当前事件对象作为event引用。with语句意味着处理程序的代码可以直接引用目标对象、包含<form>和包含文档对象的属性,就像它们是作用域中的变量一样。在严格模式下禁止使用with语句(§5.6.3),但是HTML属性中的JavaScript代码从不严格。以这种方式定义的事件处理程序在定义了意外变量的环境中执行。这可能是令人困惑的bug的来源,也是避免用HTML编写事件处理程序的一个很好的理由。

addEventListener()

任何可以作为事件目标的对象(包括窗口和文档对象)以及所有文档元素都定义了一个名为addEventListener()的方法,您可以使用该方法为该目标注册事件处理程序。addEventListener()接受三个参数。第一个是为其注册处理程序的事件类型。事件类型(或名称)是一个字符串,不包括设置事件处理程序属性时使用的“on”前缀。addEventListener()的第二个参数是在发生指定类型的事件时应调用的函数。第三个参数是可选的,将在下面解释。

下面的代码为<button>元素上的“click”事件注册了两个处理程序。注意使用的两种技术之间的差异:

<button id="mybutton">Click me</button>
<script>let b = document.querySelector("#mybutton");
b.onclick = function() { console.log("Thanks for clicking me!"); };
b.addEventListener("click", () => { console.log("Thanks again!"); });</script>

以“click”作为第一个参数调用addEventListener()不会影响onclick属性的值。在这段代码中,单击一个按钮将两条消息记录到开发人员控制台。如果我们先调用addEventListener()然后设置onclick,我们仍然会以相反的顺序记录两条消息。更重要的是,可以多次调用addEventListener()为同一对象上的同一事件类型注册多个处理程序函数。当一个事件发生在一个对象上时,为该类型事件注册的所有处理程序都将按注册顺序调用。对具有相同参数的同一对象多次调用addEventListener()没有任何影响,处理程序函数只保留注册一次,重复调用不会改变调用处理程序的顺序。

addEventListener()与removeEventListener()方法成对出现,该方法需要相同的两个参数(加上可选的第三个参数),但从对象中移除事件处理程序函数,而不是添加它。临时注册一个事件处理程序,然后很快将其删除,这通常很有用。例如,当您得到一个“mousedown”事件时,您可以为“mousemove”和“mouseup”事件注册临时事件处理程序,这样您就可以看到用户是否拖动了鼠标。然后在“mouseup”事件到达时注销这些处理程序。在这种情况下,事件处理程序移除代码可能如下所示:

document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);

addEventListener()的第三个可选参数是布尔值或对象。如果传递true,则处理程序函数将注册为捕获事件处理程序,并在事件分派的不同阶段调用。我们将在§15.2.4中介绍事件捕获。如果在注册事件侦听器时传递第三个参数true,则如果要移除处理程序,则还必须将true作为第三个参数传递给removeEventListener()。

注册捕获事件处理程序只是addEventListener()支持的三个选项之一,您也可以传递一个显式指定所需选项的对象,而不是传递单个布尔值:

document.addEventListener("click", handleClick, {capture: true,once: true,passive: true
});

如果Options对象的capture属性设置为true,则事件处理程序将注册为捕获处理程序。如果该属性为false或被省略,则处理程序将不捕获。

如果Options对象的once属性设置为true,那么事件侦听器将在触发一次后自动删除。如果此属性为false或被省略,则不会自动删除处理程序。

如果Options对象的passive属性设置为true,则表示事件处理程序永远不会调用preventDefault()来取消默认操作(请参见§15.2.5)。这对于移动设备上的触摸事件尤其重要,如果用于“touchmove”事件的事件处理程序可以阻止浏览器的默认滚动操作,则浏览器无法实现平滑滚动。这个passive属性提供了一种注册这种类型的潜在破坏性事件处理程序的方法,但是它让web浏览器知道它可以在事件处理程序运行时安全地开始它的默认行为,例如滚动。平滑的滚动对于一个好的用户体验非常重要,以至于Firefox和Chrome在默认情况下会使“touchmove”和“mousewheel”事件变为passive。因此,如果您真的想注册一个为这些事件之一调用preventDefault()的处理程序,那么应该显式地将passive属性设置为false。

也可以将Options对象传递给removeEventListener(),但只有capture属性相关。删除侦听器时不需要指定once或passive,这些属性将被忽略。

15.2.3 事件处理程序调用

注册事件处理程序后,当指定对象上发生指定类型的事件时,web浏览器将自动调用它。本节详细描述事件处理程序调用,解释事件处理程序参数、调用上下文(this值)和事件处理程序返回值的含义。

事件处理程序参数

事件处理程序以事件对象作为其单个参数来调用。事件对象的属性提供有关事件的详细信息:

type

  • 发生的事件的类型。

target

  • 发生事件的对象。

currentTarget

  • 对于传播的事件,此属性是注册当前事件处理程序的对象。

timeStamp

  • 表示事件发生时间但不代表绝对时间的时间戳(以毫秒为单位)。您可以通过从第二个事件的时间戳中减去第一个事件的时间戳来确定两个事件之间经过的时间。

isTrusted

  • 如果事件是由web浏览器本身调度的,则此属性为true;如果事件是由JavaScript代码调度的,则此属性为false。

特定类型的事件具有附加属性。例如,鼠标和指针事件具有clientX和clientY属性,它们指定事件发生的窗口坐标。

事件处理程序上下文

通过设置属性注册事件处理程序时,看起来就像是在目标对象上定义新方法:

target.onclick = function() { /* 处理程序代码 */ };

因此,将事件处理程序作为定义它们的对象的方法来调用并不奇怪。也就是说,在事件处理程序的主体中,this关键字引用注册事件处理程序的对象。

即使使用addEventListener()注册处理程序,也会将目标作为其this值来调用。但是,对于定义为箭头函数的处理程序,这不起作用:箭头函数的this值始终与定义它们的作用域相同。

处理程序返回值

在现代JavaScript中,事件处理程序不应该返回任何内容。您可能会看到在旧代码中返回值的事件处理程序,并且返回值通常是向浏览器发出的信号,指示它不应执行与事件关联的默认操作。例如,如果表单中提交按钮的onclick处理程序返回false,则web浏览器将不提交表单(通常是因为事件处理程序确定用户的输入未通过客户端验证)。

阻止浏览器执行默认操作的标准和首选方法是对事件对象调用preventDefault()方法(§15.2.5)。

调用顺序

一个事件目标可以为特定类型的事件注册多个事件处理程序。当该类型的事件发生时,浏览器将按注册顺序调用所有处理程序。有趣的是,即使您混合使用addEventListener()注册的事件处理程序和在对象属性(如onclick)上注册的事件处理程序,也是如此。

15.2.4 事件传播

当事件的目标是窗口对象或其他独立对象时,浏览器只需调用该对象上的适当处理程序即可响应该事件。然而,当事件目标是文档或文档元素时,情况会更复杂。

在调用在目标元素上注册的事件处理程序之后,大多数事件都会在DOM树上“冒泡”。将调用目标父级的事件处理程序。然后调用在目标的祖父母上注册的处理程序。这将一直持续到Document对象,然后再延伸到Window对象。事件冒泡提供了一种替代方法,不需要在许多单独的文档元素上注册处理程序:相反,您可以在一个公共祖先元素上注册一个处理程序并在那里处理事件。例如,您可以在<form>元素上注册“change”处理程序,而不是为表单中的每个元素注册“change”处理程序。

文档元素上发生的大多数事件都会冒泡。值得注意的例外是“focus”、“blur”和“scroll”事件。文档元素上的“load”事件会冒泡,但它在文档对象处停止冒泡,并且不会传播到窗口对象。(窗口对象的“load”事件处理程序仅在整个文档已加载时触发。)

事件冒泡是事件传播的第三个“阶段”。对目标对象本身的事件处理程序的调用是第二个阶段。第一个阶段甚至在调用目标处理程序之前发生,称为“捕获”阶段。回想一下addEventListener()接受可选的第三个参数。如果这个参数是true,或者{capture:true},则事件处理程序注册为捕获事件处理程序,以便在事件传播的第一阶段进行调用。事件传播的捕获阶段与冒泡阶段相反。首先调用窗口对象的捕获处理程序,然后调用文档对象的捕获处理程序,然后调用主体对象的捕获处理程序,依此类推,直到事件目标的父级的捕获事件处理程序被调用为止。不会调用在事件目标本身上注册的捕获事件处理程序。

事件捕获提供了一个在事件传递到目标之前查看事件的机会。捕获事件处理程序可以用于调试,也可以与下一节中描述的事件取消技术一起使用,以筛选事件,从而永远不会实际调用目标事件处理程序。事件捕获的一个常见用途是处理鼠标拖动,其中鼠标运动事件需要由被拖动的对象处理,而不是由拖动对象所在的文档元素来处理。

15.2.5 事件取消

浏览器响应许多用户事件,即使您的代码没有响应:当用户在超链接上单击鼠标时,浏览器会跟随链接。如果HTML文本输入元素具有键盘焦点并且用户键入键,浏览器将输入用户的输入。如果用户在触摸屏设备上移动手指,浏览器将滚动。如果为此类事件注册事件处理程序,则可以通过调用事件对象的preventDefault()方法阻止浏览器执行其默认操作。(除非使用passive选项注册了处理程序,这会使preventDefault()无效。)

取消与事件关联的默认操作只是事件取消的一种。我们还可以通过调用事件对象的stopPropagation()方法来取消事件的传播。如果在同一对象上定义了其他处理程序,则仍将调用其余处理程序,但在调用stopPropagation()之后,不会调用任何其他对象上的事件处理程序。stopPropagation()在捕获阶段、事件目标本身和冒泡阶段工作。stopImmediatePropagation()的工作原理与stopPropagation()类似,但它还会阻止调用在同一对象上注册的任何后续事件处理程序。

15.2.6 分发自定义事件

客户端JavaScript的事件API是一个相对强大的API,您可以使用它来定义和分发自己的事件。例如,假设您的程序周期性地需要执行长时间的计算或发出网络请求,并且当此操作处于挂起状态时,其他操作是不可能的。您希望通过显示“旋转器”来指示应用程序正忙,从而让用户知道这一点。但是繁忙的模块不需要知道旋转器应该显示在哪里。相反,该模块可能只是分发一个事件来宣布它正忙,然后在它不再忙时分发另一个事件。然后,UI模块可以为这些事件注册事件处理程序,并采取任何适当的UI操作来通知用户。

如果JavaScript对象有addEventListener()方法,那么它就是一个“事件目标”,这意味着它还有一个dispatchEvent()方法。您可以使用CustomEvent()构造函数创建自己的事件对象,并将其传递给dispatchEvent()。CustomEvent()的第一个参数是指定事件类型的字符串,第二个参数是指定事件对象属性的对象。将此对象的detail属性设置为表示事件内容的字符串、对象或其他值。如果计划在文档元素上分发事件并希望它在文档树中冒泡,请在第二个参数中添加 bubbles:true:

// 分发一个自定义事件,以便UI知道我们很忙
document.dispatchEvent(new CustomEvent("busy", { detail: true }));// 执行网络操作
fetch(url).then(handleNetworkResponse).catch(handleNetworkError).finally(() => {// 在网络请求成功或失败后,分发另一个事件让UI知道我们不再忙了。document.dispatchEvent(new CustomEvent("busy", { detail: false }));});// 在其他地方,你可以在程序中注册一个“busy”事件的处理程序,
// 并用它来显示或隐藏旋转器,让用户知道。
document.addEventListener("busy", (e) => {if (e.detail) {showSpinner();} else {hideSpinner();}
});

15.3 编写文档脚本

客户端JavaScript用于将静态HTML文档转换为交互式web应用程序。因此,编写网页内容的脚本实际上是JavaScript的核心目的。

每个窗口对象都有一个引用文档对象的Document属性。Document对象表示窗口的内容,它是本节的主题。但是,Document对象并不是独立的。它是DOM中用于表示和操作文档内容的中心对象。

DOM在§15.1.2中引入。本节将详细解释API。它包括:

  • 如何从文档中查询或选择单个元素。
  • 如何遍历文档,以及如何查找任何文档元素的祖先、兄弟姐妹和后代。
  • 如何查询和设置文档元素的属性。
  • 如何查询、设置和修改文档内容。
  • 如何通过创建、插入和删除节点来修改文档结构。

15.3.1 选择文档元素

客户端JavaScript程序通常需要操作文档中的一个或多个元素。全局document属性引用Document对象,Document对象具有head和body属性,分别引用<head>和<body>标签的Element对象。但是,如果一个程序想要操作嵌入在文档中更深层的元素,则必须以某种方式获取或选择引用这些文档元素的Element对象。

使用CSS选择器选择元素

CSS样式表有一种非常强大的语法,称为选择器,用于描述文档中的元素或元素集。DOM方法querySelector()和querySelectorAll()允许我们在文档中查找与指定CSS选择器匹配的一个或多个元素。在介绍这些方法之前,我们将从一个关于CSS选择器语法的快速教程开始。

CSS选择器可以通过标签名、id属性的值或class属性中的单词来描述元素:

div // <div>元素
#nav // id=“nav”的元素
.warning // class属性中包含“warning”的任何元素

#字符用于根据id属性进行匹配,并且.字符用于基于class属性进行匹配。也可以根据更一般的属性值选择元素:

p[lang="fr"] // 法语段落:<p lang=“fr”>
*[name="x"] // 任何具有name=“x”属性的元素

请注意,这些示例将标签名选择器(或标签名通配符 * )与属性选择器组合在一起。也可以采用更复杂的组合:

span.fatal.error // 任何class中有"fatal""error"的<span>
span[lang="fr"].warning // 任何<span>,法语,class中有"warning"

选择器还可以指定文档结构:

#log span // id=“log”的元素后代中的任何<span>
#log>span // id=“log”的元素子结点中的任何<span>
body>h1:first-child // <body>的第一个<h1>子结点
img + p.caption // 紧接在<img>之后的class属性中有"caption"的<p>元素
h2 ~ p // 任何跟在<h2>之后并且是其兄弟的<p>

如果两个选择器之间用逗号分隔,则表示我们选择了与其中任一个选择器匹配的元素:

button, input[type="button"] // 所有 <button> 和 <input type="button"> 元素

如您所见,CSS选择器允许我们按类型、ID、类、属性和文档中的位置引用文档中的元素。querySelector()方法以CSS选择器字符串为参数,返回它在文档中找到的第一个匹配元素,如果没有匹配,则返回null:

// 找到属性id=“spinner”的HTML标签的文档元素
let spinner = document.querySelector("#spinner");

querySelectorAll()类似,但它返回文档中所有匹配的元素,而不仅仅返回第一个元素:

// 查找<h1>、<h2>和<h3>标签的所有元素对象
let titles = document.querySelectorAll("h1, h2, h3");

querySelectorAll()的返回值不是元素对象的数组。相反,它是一个类似数组的对象,称为NodeList。NodeList对象有一个length属性,可以像数组一样被索引,所以可以用传统的for循环遍历它们。NodeList也是可迭代的,因此也可以将它们用于for/of循环。如果要将NodeList转换为真正的数组,只需将其传递给Array.from()。

如果文档中没有与指定选择器匹配的元素,则querySelectorAll()返回的NodeList的length属性将设置为0。

querySelector()和querySelectorAll()由Element类和Document类实现。当对元素调用时,这些方法将只返回该元素的后代元素。

注意CSS定义了::first-line和::first-letter伪元素。在CSS中,这些匹配部分是文本节点而不是实际元素。如果与querySelectorAll()或querySelector()一起使用,则它们将不匹配。另外,许多浏览器会拒绝返回:link和:visited伪类的匹配项,因为这可能会暴露用户的浏览历史信息。

另一个基于CSS的元素选择方法是closest()。此方法由Element类定义,并将选择器作为其唯一参数。如果选择器与调用它的元素匹配,它将返回该元素。否则,它返回选择器匹配的最近的祖先元素,如果没有匹配则返回null。在某种意义上,closest()与querySelector()相反:closest()从一个元素开始并在树中查找该元素上方的匹配项,而querySelector()则从一个元素开始并在树中查找该元素下方的匹配项。当您在文档树的高层注册了事件处理程序时,closest()非常有用。例如,如果您正在处理一个“click”事件,您可能想知道它是否是一个点击了超链接的事件。event对象将告诉您目标是什么,但目标可能是链接中的文本,而不是超链接的<a>标签本身。事件处理程序可以像这样查找最近的包含超链接:

// 查找最近的包含href属性的<a>标签。
let hyperlink = event.target.closest("a[href]");

下面是另一种使用closest()的方法:

// 如果元素e在HTML列表元素内,则返回true
function insideList(e) {return e.closest("ul,ol,dl") !== null;
}

相关方法matches()不返回祖先或后代:它只测试元素是否与CSS选择器匹配,如果匹配,则返回true,否则返回false:

// 如果e是HTML标题元素,则返回true
function isHeading(e) {return e.matches("h1,h2,h3,h4,h5,h6");
}

其他元素选择方法

除了querySelector()和querySelectorAll()之外,DOM还定义了许多现在或多或少已经过时的旧元素选择方法。您可能仍然会看到其中一些方法(尤其是getElementById())正在使用中,但是:

// 按id查找元素。参数只是id,没有CSS选择器前缀#。
// 类似document.querySelector("#sect1")
let sect1 = document.getElementById("sect1");// 查找具有name=“color”属性的所有元素(如表单复选框)。
// 类似document.querySelectorAll('*[name="color"]');
let colors = document.getElementsByName("color");// 在文档中查找所有<h1>元素。
// 类似于document.querySelectorAll("h1")
let headings = document.getElementsByTagName("h1");// getElementsByTagName()也在元素上定义。
// 获取sect1元素中的所有<h2>元素。
let subheads = sect1.getElementsByTagName("h2");// 查找所有具有类“tooltip”的元素
// 类似于document.querySelectorAll(".tooltip")
let tooltips = document.getElementsByClassName("tooltip");// 查找所有具有类“侧边栏”的sect1的后代
// 类似于sect1.querySelectorAll(".sidebar")
let sidebars = sect1.getElementsByClassName("sidebar");

与querySelectorAll()一样,此代码中的方法返回一个NodeList(getElementById()除外,它返回一个元素对象)。但是,与querySelectorAll()不同,这些旧的选择方法返回的NodeList是“动态的”,这意味着如果文档内容或结构发生更改,列表的长度和内容可能会更改。

预选元素

由于历史原因,Document类定义了访问某些类型节点的快捷属性。例如,images、forms和links属性可以方便地访问文档的<img>、<form>和<a>元素(但仅限于具有href属性的<a>标签)。这些属性引用HTMLCollection对象,它们非常类似于NodeList对象,但是它们还可以通过元素ID或名称进行索引。例如,使用document.forms属性,您可以访问<form id=“address”>标签:

document.forms.address;

一个更过时的用于选择元素的API是document.all属性,它类似于文档中所有元素的HTMLCollection。document.all已弃用,您不应再使用它。

15.3.2 文档结构和遍历

一旦从文档中选择了一个元素,您有时需要找到文档结构上相关的部分(父结点、兄弟结点、孩子节点)。当我们主要关注文档的元素而不是其中的文本(以及它们之间的空白也是文本)时,有一个遍历API允许我们将文档视为元素对象的树,而忽略同时也是文档一部分的文本节点。此遍历API不涉及任何方法;它只是元素对象的一组属性,允许我们引用给定元素的父结点、兄弟结点和孩子结点:

parentNode

  • 元素的此属性引用元素的父结点,它是另一个元素或文档对象。

children

  • 此NodeList包含元素的子元素结点,但不包括文本节点(和注释节点)等非元素子结点。

childElementCount

  • 子元素结点的数目。返回与children.length相同的值。

firstElementChild, lastElementChild

  • 这些属性引用元素的第一个子元素和最后一个子元素。如果元素没有子元素,则它们为null。

nextElementSibling, previousElementSibling

  • 这些属性引用紧靠在元素之前或之后的同级元素,如果没有此类同级元素,则为null。

使用这些元素属性,可以使用以下表达式之一引用文档第一个子元素的第二个子元素:

document.children[0].children[1]
document.firstElementChild.firstElementChild.nextElementSibling

(在标准HTML文档中,这两个表达式都引用文档的<body>标签。)

以下两个函数演示如何使用这些属性递归地对文档元素执行深度优先遍历,并且每个元素调用指定函数:

// 递归地遍历文档或元素e,对e及其每个后代调用函数f
function traverse(e, f) {f(e); // 对e调用f()for(let child of e.children) { // 对子结点进行迭代traverse(child, f); // 对每一个进行递归}
}function traverse2(e, f) {f(e); // 对e调用f()let child = e.firstElementChild; // 按链表形式进行遍历while(child !== null) {traverse2(child, f); // 递归child = child.nextElementSibling;}
}

作为节点树的文档

如果要遍历文档或文档的某个部分而不想忽略文本节点,则可以使用在所有节点对象上定义的不同属性集。这将允许您查看元素、文本节点甚至注释节点(它们表示文档中的HTML注释)。

所有节点对象都定义以下属性:

parentNode

  • 此节点的父节点,或者对于没有父节点的Document对象这样的节点为null。

childNodes

  • 一个只读节点列表,包含节点的所有子节点(不仅仅是元素子节点)。

firstChild, lastChild

  • 节点的第一个子节点和最后一个子节点,如果节点没有子节点,则为null。

nextSibling, previousSibling

  • 节点的下一个和上一个同级节点。这些属性连接双链接列表中的节点。

nodeType

  • 一个指定节点类型的数字。文档节点的值为9。元素节点的值为1。文本节点的值为3。注释节点的值为8。

nodeValue

  • 文本或注释节点的文本内容。

nodeName

  • 元素的HTML标签名,转换为大写。

使用这些节点属性,可以使用如下表达式引用文档第一个子节点的第二个子节点:

document.childNodes[0].childNodes[1]
document.firstChild.firstChild.nextSibling

假设所讨论的文档如下:

<html><head><title>Test</title></head><body>Hello World!</body></html>

第一个子结点的第二个子结点是<body>元素。它的nodeType为1,nodeName为“BODY”。

但是请注意,这个API对文档文本的变化非常敏感。例如,如果通过在<html>和<head>标签之间插入一个换行符来修改文档,则表示换行符的文本节点将成为第一个子节点的第一个子节点,第二个子节点是<head>元素而不是<body>元素。

为了演示这种基于节点的遍历API,下面是一个返回元素或文档中所有文本的函数:

// 返回元素e的纯文本内容,递归到子元素中。
// 此方法的工作方式与textContent属性类似
function textContent(e) {let s = ""; // 这里的文本进行累加for (let child = e.firstChild; child !== null; child = child.nextSibling) {let type = child.nodeType;if (type === 3) { // 如果是文本节点s += child.nodeValue; // 将文本内容添加到字符串中。} else if (type === 1) { // 如果是元素节点s += textContent(child); // 那么递归。}}return s;
}

这个函数只是一个演示,实际上,您只需编写e.textContent就可以获得e元素的文本内容。

15.3.3 属性

HTML元素由一个标签名和一组名为属性的名称/值对组成。例如,定义超链接的<a>元素使用其href属性的值作为链接的目标。

Element类定义了用于查询、设置、测试和删除元素属性的常规getAttribute()、setAttribute()、hasAttribute()和removeAttribute()方法。但是HTML元素的属性值(对于标准HTML元素的所有标准属性)可以作为表示这些元素的HTMLElement对象的属性使用,并且通常将它们作为JavaScript属性使用比调用getAttribute()和相关方法要容易得多。

HTML属性作为元素属性

表示HTML文档元素的Element对象通常定义映射元素的HTML属性的读/写属性。元素定义通用HTML属性的属性,如id、title、lang和dir,以及事件处理程序属性(如onclick)。元素特定的子类型定义特定于这些元素的属性。例如,要查询图像的URL,可以使用表示<img>元素的HTMLElement的src属性:

let image = document.querySelector("#main_image");
let url = image.src; // src属性是图像的URL
image.id === "main_image" // => true; 我们按id查询图像

类似地,您可以使用如下代码设置<form>元素的表单提交属性:

let f = document.querySelector("form"); // 文档中的第一个<form>元素
f.action = "https://www.example.com/submit"; // 设置要提交到的URL。
f.method = "POST"; // 设置HTTP请求类型。

对于某些元素,例如<input>元素,一些HTML属性名映射到不同名称的属性。例如,<input>的HTML value属性由JavaScript defaultValue属性映射。<input>元素的JavaScript value属性包含用户的当前输入,但是对value属性的更改不会影响defaultValue属性和HTML的value属性。

HTML属性不区分大小写,但JavaScript属性名区分大小写。要将属性名转换为JavaScript属性,请使用小写形式。但是,如果属性的长度超过一个单词,则将每个单词的第一个字母放在第一个大写字母之后:例如defaultChecked和tabIndex。但是,像onclick这样的事件处理程序属性是一个例外,并且是用小写字母编写的。

一些HTML属性名是JavaScript中的保留字。对于这些,一般规则是在属性名前面加上“html”。例如,HTML for属性(属于<label>元素)成为JavaScript htmlFor属性。“class”是JavaScript中的保留字,非常重要的HTML class属性是此规则的一个例外:它在JavaScript代码中变成了className。

表示HTML属性的属性通常具有字符串值。但是当属性是布尔值或数值(例如<input>元素的defaultChecked和maxLength属性)时,属性是布尔值或数字而不是字符串。事件处理程序属性的值始终是函数(或null)。

请注意,此用于获取和设置属性值的基于属性的API没有定义从元素中移除属性的任何方法。尤其是,delete运算符不能用于此目的。如果需要删除属性,请使用removeAttribute()方法。

class属性

HTML元素的class属性是一个特别重要的属性。它的值是一个以空格分隔的CSS类列表,这些CSS类应用于元素并影响如何使用CSS设置样式。因为class是JavaScript中的保留字,所以这个属性的值可以通过Element对象的className属性获得。className属性可以设置并以字符串形式返回class属性的值。但是class属性的名称不好:它的值是一个CSS类的列表,而不是一个类,在客户端JavaScript编程中,通常希望在该列表中添加或删除单个类名,而不是将列表作为单个字符串处理。

因此,元素对象定义了一个classList属性,该属性允许您将class属性视为列表。classList属性的值是可迭代的类数组对象。尽管属性的名称是classList,但它的行为更像一组类,并定义add()、remove()、contains()和toggle()方法:

// 当我们想让用户知道我们很忙时,我们会显示一个旋转器。
// 为此,我们必须删除“hidden”类并添加“animated”类(假设样式表配置正确)。
let spinner = document.querySelector("#spinner");
spinner.classList.remove("hidden");
spinner.classList.add("animated");

数据集属性

将附加信息附加到HTML元素有时很有用,通常JavaScript代码将选择这些元素并以某种方式操作它们。在HTML中,任何名称为小写并以前缀“data-”开头的属性都被认为是有效的,您可以将它们用于任何目的。这些“数据集属性”不会影响它们出现的元素的表示,它们定义了一种标准的方法来附加额外的数据,而不会影响文档的有效性。

在DOM中,元素对象有一个dataset属性,该属性引用一个对象,该对象的属性与去掉前缀的数据属性相对应。因此,dataset.x将保存data-x属性的值。用连字符连接的属性映射到驼峰风格的属性名称:属性data-section-number成为属性dataset.sectionNumber。

假设HTML文档包含以下文本:

<h2 id="title" data-section-number="16.1">Attributes</h2>

然后您可以编写这样的JavaScript来访问该分区号:

let number = document.querySelector("#title").dataset.sectionNumber;

15.3.4 元素内容

再看看图15-1所示的文档树,问问自己<p>元素的“内容”是什么。有两种方法可以回答这个问题:

  • 内容是HTML字符串“This is a simple document”。
  • 内容是纯文本字符串“This is a simple document”。

这两个答案都是有效的,而且每个答案都有自己的用处。接下来的部分将解释如何使用元素内容的HTML表示和纯文本表示。

元素内容为HTML

读取元素的innerHTML属性将返回该元素的内容作为标记字符串。在元素上设置此属性将调用web浏览器的解析器,并将元素的当前内容替换为新字符串的解析表示形式。您可以通过打开开发人员控制台并键入以下命令来进行测试:

document.body.innerHTML = "<h1>Oops</h1>";

你会看到整个网页消失了,取而代之的是一个标题“Oops”。Web浏览器非常擅长解析HTML,而设置innerHTML通常相当高效。但是,请注意,使用+=运算符将文本附加到innerHTML属性效率是比较低的,因为它既需要序列化步骤将元素内容转换为字符串,又需要解析步骤将新字符串转换回元素内容。

在使用这些HTML API时,不要在文档中插入用户输入,这一点非常重要。如果这样做,就允许恶意用户将自己的脚本注入到应用程序中。有关详细信息,请参阅第425页的“跨站点脚本”。

元素的outerHTML属性与innerHTML类似,只是其值包括元素本身。查询outerHTML时,该值包括元素的开始和结束标记。在元素上设置outerHTML时,新内容将替换元素本身。

一个相关的元素方法是insertAdjacentHTML(),它允许您在指定元素附近插入任意HTML标记字符串。标记作为第二个参数传递给这个方法,“相邻”的确切含义取决于第一个参数的值。第一个参数应该是一个字符串,其中一个值是“beforebegin”、“afterbegin”、“beforeend”或“afterend”。这些值对应于图15-2所示的插入点。


图15-2. insertAdjacentHTML()的插入点

纯文本形式的元素内容

有时,您希望以纯文本的形式查询元素的内容,或者将纯文本插入文档(而不必转义HTML标记中使用的尖括号和&号)。标准的方法是使用textContent属性:

let para = document.querySelector("p"); // 文件中的第一个<p>元素
let text = para.textContent; // 获取段落内容
para.textContent = "Hello World!"; // 修改这段文字

textContent属性由Node类定义,因此它适用于文本节点和元素节点。对于元素节点,它查找并返回元素所有后代中的所有文本。

Element类定义了一个与textContent类似的innerText属性。innerText有一些异常和复杂的行为,比如试图保留表格格式。但是,它没有正规地规范,也没有在浏览器之间兼容地实现,因此不应该再使用它。

<script>元素中的文本
内嵌代码的<script>元素(即那些没有src属性的元素)有一个text属性,可以用来检索它们的文本。浏览器从不显示<script>元素的内容,HTML解析器忽略脚本中的尖括号和&号。这使得<script>元素成为嵌入任意文本数据以供应用程序使用的理想位置。只需将元素的type属性设置为某个值(例如“text/x-custom-data”),以明确说明脚本不是可执行的JavaScript代码。如果您这样做,JavaScript解释器将忽略该脚本,但元素将存在于文档树中,其text属性将向您返回数据。

15.3.5 创建、插入和删除节点

我们已经了解了如何使用HTML和纯文本字符串查询和更改文档内容。我们还看到,我们可以遍历一个文档来检查由它组成的单个元素和文本节点。也可以在单个节点级别更改文档。Document类定义了用于创建元素对象的方法,而Element和Text对象具有用于插入、删除和替换树中节点的方法。

使用Document类的createElement()方法创建一个新元素,并使用其append()和prepend()方法将文本字符串或其他元素附加到其中:

let paragraph = document.createElement("p"); // 创建一个空的<p>元素
let emphasis = document.createElement("em"); // 创建一个空的<em>元素
emphasis.append("World"); // 向<em>元素添加文本
paragraph.append("Hello ", emphasis, "!"); // 将文本和<em>添加到<p>
paragraph.prepend("¡"); // 在<p>开头添加更多文本
paragraph.innerHTML // => "¡Hello <em>World</em>!"

append()和prepend()接受任意数量的参数,可以是节点对象或字符串。字符串参数将自动转换为文本节点。(可以使用document.createTextNode()显式创建文本节点,但很少有理由这样做。)append()将参数添加到孩子列表元素末尾。prepend()在孩子列表的开头添加参数。

如果要将元素或文本节点插入包含元素的孩子列表的中间,则append()或prepend()都不适用。在这种情况下,应该获取对兄弟节点的引用,并调用before()在该兄弟节点之前插入新内容,或者调用after()在该兄弟节点之后插入新内容。例如:

// 使用class=“greetings”查找标题元素
let greetings = document.querySelector("h2.greetings");// 现在在标题后面插入新段落和一条横线
greetings.after(paragraph, document.createElement("hr"));

与append()和prepend()一样,after()和before()接受任意数量的字符串和元素参数,并在将字符串转换为文本节点后将它们全部插入文档中。append()和prepend()只在元素对象上定义,而after()和before()同时适用于元素和文本节点:可以使用它们插入相对于文本节点的内容。

请注意,元素只能插入文档中的一个位置。如果某个元素已经在文档中,而您将其插入到其他位置,则该元素将被移动到新位置,而不是被复制:

// 我们在这个元素之后插入了paragraph,但是现在我们移动它,使它出现在元素之前
greetings.before(paragraph);

如果确实要复制元素,请使用cloneNode()方法,传递true以复制其所有内容:

// 制作一个段落的副本,并将其插入greetings元素之后
greetings.after(paragraph.cloneNode(true));

可以通过调用元素或文本节点的remove()方法从文档中移除元素或文本节点,也可以通过调用replaceWith()来替换它。remove()不带参数,replaceWith()接受任意数量的字符串和元素,就像before()和after()一样:

// 从文档中删除greetings元素并将其替换为paragraph元素
// (如果paragraph已经插入到文档中,则将其从当前位置移动)。
greetings.replaceWith(paragraph);// 现在删除paragraph。
paragraph.remove();

DOM API还定义了上一代用于插入和删除内容的方法。appendChild()、insertBefore()、replaceChild()和removeChild()比此处显示的方法更难使用,因此不应该需要这些方法。

15.3.6 示例:生成目录

例15-1展示了如何动态地为文档创建目录。它演示了前面几节中描述的许多文档脚本技术。这个例子注释得很好,您应该可以很容易地理解代码。

例15-1. 使用DOM API生成目录

/*** TOC.js: 为文档创建目录。** 此脚本在触发DOMContentLoaded事件时运行,并自动生成文档的目录。* 它不定义任何全局符号,因此不应与其他脚本冲突。** 当这个脚本运行时,它首先查找id为“TOC”的文档元素。* 如果没有这样的元素,它会在文档的开头创建一个元素。* 接下来,该函数查找所有<h2>到<h6>标记,将它们视为章节标题,* 并在TOC元素中创建一个目录。该函数将章节编号添加到每个章节标题,* 并将标题包装在命名锚中,以便TOC可以链接到它们。* 生成的锚点的名称以“TOC”开头,因此您应该避免在自己的HTML中使用这个前缀。** 生成的TOC中的条目可以用CSS设置样式。所有条目都有一个类“TOCEntry”。* 条目也有一个与章节标题级别相对应的类。<h1>标记生成类“TOCLevel1”的条目,* <h2>标记生成类“TOCLevel2”的条目,依此类推。* 插入标题中的章节编号具有“TOCSectNum”类。** 您可以将此脚本与以下样式表一起使用:**   #TOC { border: solid black 1px; margin: 10px; padding: 10px; }*   .TOCEntry { margin: 5px 0px; }*   .TOCEntry a { text-decoration: none; }*   .TOCLevel1 { font-size: 16pt; font-weight: bold; }*   .TOCLevel2 { font-size: 14pt; margin-left: .25in; }*   .TOCLevel3 { font-size: 12pt; margin-left: .5in; }*   .TOCSectNum:after { content: ": "; }** 要隐藏章节编号,请使用以下命令:**   .TOCSectNum { display: none }**/
document.addEventListener("DOMContentLoaded", () => {// 找到TOC容器元素。// 如果没有,请在文档开头创建一个。let toc = document.querySelector("#TOC");if (!toc) {toc = document.createElement("div");toc.id = "TOC";document.body.prepend(toc);}// 查找所有章节标题元素。这里我们假设文档标题使用<h1>并且文档中的部分用<h2>到<h6>标记。let headings = document.querySelectorAll("h2,h3,h4,h5,h6");// 初始化一个数组来跟踪章节编号。let sectionNumbers = [0,0,0,0,0];// 现在遍历我们找到的章节标题元素。for(let heading of headings) {// 如果标题在TOC容器中,则跳过该标题。if (heading.parentNode === toc) {continue;}// 找出它是什么级别的标题。// 减去1,因为<h2>是一级标题。let level = parseInt(heading.tagName.charAt(1)) - 1;// 增加此标题级别的章节编号,并将所有较低的标题级别编号重置为零。sectionNumbers[level-1]++;for(let i = level; i < sectionNumbers.length; i++) {sectionNumbers[i] = 0;}// 现在合并所有标题级别的章节编号,生成类似2.3.1的章节编号。let sectionNumber = sectionNumbers.slice(0, level).join(".");// 将章节编号添加到章节标题中。// 我们把数字放在一个<span>中,使其具有样式。let span = document.createElement("span");span.className = "TOCSectNum";span.textContent = sectionNumber;heading.prepend(span);// 将标题包装在命名锚中,以便我们可以链接到它。let anchor = document.createElement("a");let fragmentName = `TOC${sectionNumber}`;anchor.name = fragmentName;heading.before(anchor);    // 在标题前插入锚anchor.append(heading);    // 将标题移动到锚内// 现在创建一个指向此章节的链接。let link = document.createElement("a");link.href = `#${fragmentName}`;     // 链接目标// 将标题文本复制到链接中。这是innerHTML的安全使用,// 因为我们没有插入任何不受信任的字符串。link.innerHTML = heading.innerHTML;// 将链接放在基于级别的可设置样式的div中。let entry = document.createElement("div");entry.classList.add("TOCEntry", `TOCLevel${level}`);entry.append(link);// 并将div添加到TOC容器中。toc.append(entry);}
});

译者注:以下部分是自己添加的
现在我们创建一个html,来测试一下上面的js文件,先创建一个含有标题的文件toctest.html:

<!DOCTYPE html>
<html><head><title>toc test</title>
</head><body><h2>章节1</h2><h3>子章节1</h3><h3>子章节2</h3><h2>章节2</h2><h3>子章节1</h3><h3>子章节2</h3><h2>章节3</h2><h3>子章节1</h3><h3>子章节2</h3>
</body></html>

显示效果如下:

现在我们把样式和js文件放入html中:

<!DOCTYPE html>
<html><head><title>toc test</title><style>#TOC {border: solid black 1px;margin: 10px;padding: 10px;}.TOCEntry {margin: 5px 0px;}.TOCEntry a {text-decoration: none;}.TOCLevel1 {font-size: 16pt;font-weight: bold;}.TOCLevel2 {font-size: 14pt;margin-left: .25in;}.TOCLevel3 {font-size: 12pt;margin-left: .5in;}.TOCSectNum:after {content: ": ";}</style><script src="./TOC.js"></script>
</head><body><h2>章节1</h2><h3>子章节1</h3><h3>子章节2</h3><h2>章节2</h2><h3>子章节1</h3><h3>子章节2</h3><h2>章节3</h2><h3>子章节1</h3><h3>子章节2</h3>
</body></html>

再看一下效果:


  1. 本书以前的版本有一个广泛的参考部分,包括JavaScript标准库和web API。它在第七版中被删除了,因为MDN已经使它过时了:今天,在MDN上查找东西比翻阅一本书要快得多,而且我以前在MDN的同事在保持在线文档的最新性方面做得比这本书更好。 ↩︎

  2. 一些源代码(包括HTML规范)根据处理程序和侦听器的注册方式在技术上对它们进行了区分。在这本书中,我们把这两个词当作同义词。 ↩︎

  3. 如果您使用React框架来创建客户端用户界面,这可能会让您大吃一惊。React对客户端事件模型做了一些小的更改,其中之一是在React中,事件处理程序属性名是用驼峰风格编写的:onClick、onMouseOver等等。然而,当以本地方式使用web平台时,事件处理程序属性完全以小写形式编写。 ↩︎

《JavaScript权威指南第7版》第15章 Web浏览器中的JavaScript 15.1 15.2 15.3相关推荐

  1. 翻译:《JavaScript 权威指南(第5版)》第一章(一)

    声明:翻译只有一个目的:学习用途.若有版权问题请及时联系本人. 本贴文根据篇幅将第一章的翻译分为两个部分,这是第一部分的内容. Chapter 1. Introduction to JavaScrip ...

  2. 【JavaScript权威指南(第七版)】之阅读学习总结

    写在前面 最近借着空闲时间断断续续两个月看完了<JavaScript权威指南(第七版)>,<JavaScript权威指南>一直以来被称为"犀牛书",前面的第 ...

  3. JavaScript权威指南(第6版)

    JavaScript权威指南(第6版) 编辑推荐 经典权威的JavaScript工具书 本书是程序员学习核心JavaScript语言和由Web浏览器定义的JavaScript API的指南和综合参考手 ...

  4. JavaScript权威指南(第5版)pdf

    下载地址:网盘下载 内容简介 编辑 第5版针对Ajax和Web 2.0技术进行了全新的改版.和上一版相比,更新的内容较多,总体上接近整个篇幅的1/2,而这也正是本书姗姗来迟的原因之一.具体来说,第5版 ...

  5. 《JavaScript权威指南第四版》 电子版 电子书下载

    JavaScript权威指南第四版 图书评价:★★★★☆ 图书语言:简体图书 图书大小:19.11MB 图书格式:PDF 图书作者:David Flanagan 更新日期:2006-05-23 下载次 ...

  6. JavaScript权威指南(第6版)(中文版).pdf

    pdf 电子版书籍, 百度云盘:[JavaScript权威指南(第6版)(中文版)] 提取密码:b0tf

  7. JavaScript权威指南 第6版 中文版 pdf

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 下载地址 ...

  8. JavaScript权威指南(第6版)中文版pdf

    JavaScript权威指南(第6版)中文版pdf JavaScript权威指南(第6版)中文版pdf 百度网盘下载链接:点击下载

  9. 《JavaScript权威指南第7版》第14章 元编程

    第14章 元编程 14.1 属性特性 (Property Attributes) 14.2 对象扩展性 14.3 prototype特性(原型特性) 14.4 内置Symbol 14.4.1 Symb ...

最新文章

  1. 微信公众平台开发,自定义菜单,群发消息,网页授权(3)
  2. 计算机机房双电源供电,超高效数据机房可靠性浅析——走进腾讯青浦云计算中心...
  3. ajax 下拉刷新 上拉加载更多,局部刷新iscroll控件的具体使用(下拉刷新,上拉加载更多)...
  4. 15个精心挑选的 jQuery 下拉菜单制作教程
  5. [转]大数据环境搭建步骤详解(Hadoop,Hive,Zookeeper,Kafka,Flume,Hbase,Spark等安装与配置)
  6. 土地利用覆被变化的概念_【能源+气候】生物质能碳捕集与封存技术在气候变化下的减排潜力...
  7. react里面的this_React 为啥要绑定this
  8. php解析二级json,PHP Json 解析
  9. hadoop1.2.1伪分布式搭建
  10. 按键精灵html代码,最新按键精灵脚本代码大全 按键精灵命令运行方法
  11. Android手机修改wifi图标,安卓手机怎么改wifi密码?
  12. html怎样设置图片的圆角矩形,怎么把矩形变成圆角 ps怎么在原来的矩形中改成圆角...
  13. B. Shifting Sort(rotate函数旋转应用)
  14. 打造淘宝极简包的轻量化框架
  15. java导出excel下载后文件损坏无法修复
  16. 2022无线蓝牙耳机选哪个?盘点超热门的蓝牙耳机品牌推荐
  17. Linux设备驱动编程第三版-笔记
  18. 三年又三年,我朋友都生娃了《打工人的故事》
  19. 五大列级庄_五大名庄背后的1855分级
  20. 验证码、文件上传和中间件

热门文章

  1. STM32 VCAP引脚爬坑
  2. 微信小程序设置灰色滤镜
  3. android rom修改小白有福了
  4. 有什么网站是python做的_自学python有什么网站
  5. 【sqli-labs】 对于less34 less36的宽字节注入的一点深入
  6. 软件定义雷达(SDR)和软件化雷达(SR)
  7. 解决Visual Studio的IIS与无法启动调试器问题
  8. nginx下discuz x3的伪静态规则
  9. Java日期转换SimpleDateFormat格式大全
  10. GPIO寄存器的地址怎么寻找?