文档对象模型
当你打开一个网页时,你的浏览器会检索网页的 HTML 文本并解析它,就像我们来自 第 12 章 的解析器解析程序一样。浏览器构建了文档结构的模型,并使用该模型在屏幕上绘制页面。
这种文档的表示是 JavaScript 程序在其沙箱中可用的玩具之一。它是一个可以读取或修改的数据结构。它充当实时数据结构:当它被修改时,屏幕上的页面会更新以反映这些更改。
文档结构
你可以将 HTML 文档想象成一组嵌套的盒子。像 <body>
和 </body>
这样的标签包含其他标签,而其他标签反过来包含其他标签或文本。这是来自 上一章 的示例文档
html> <head> <title>My home page</title> </head> <body> <h1>My home page</h1> <p>Hello, I am Marijn and this is my home page.</p> <p>I also wrote a book! Read it <a href="https://eloquent.javascript.ac.cn">here</a>.</p> </body> </html><
浏览器用来表示文档的数据结构遵循这种形状。对于每个盒子,都存在一个对象,我们可以与它交互以了解例如它代表的 HTML 标签以及它包含的盒子和文本。这种表示被称为文档对象模型,简称DOM。
全局绑定 document
使我们能够访问这些对象。它的 documentElement
属性引用表示 <html>
标签的对象。由于每个 HTML 文档都有一个头部和一个主体,它也具有指向这些元素的 head
和 body
属性。
树
回想一下来自 第 12 章 的语法树。它们的结构与浏览器的文档结构惊人地相似。每个节点可以引用其他节点,即子节点,而子节点反过来可能有自己的子节点。这种形状是嵌套结构的典型特征,其中元素可以包含类似于自身本身的子元素。
当数据结构具有分支结构、没有循环(节点可能不包含自身,直接或间接)并且具有一个明确定义的根时,我们称其为树。在 DOM 的情况下,document.
充当根。
树在计算机科学中经常出现。除了表示 HTML 文档或程序之类的递归结构外,它们还经常用于维护排序的数据集,因为与平面数组相比,元素通常可以在树中更有效地找到或插入。
典型的树具有不同类型的节点。用于 Egg 语言 的语法树有标识符、值和应用节点。应用节点可能具有子节点,而标识符和值是叶子,或没有子节点的节点。
DOM 也是如此。表示 HTML 标签的元素节点决定了文档的结构。这些可能具有子节点。此类节点的一个示例是 document.body
。这些子节点中的一些可以是叶子节点,例如文本片段或注释节点。
每个 DOM 节点对象都有一个 nodeType
属性,它包含一个代码(数字),用于标识节点的类型。元素具有代码 1,该代码也定义为常量属性 Node.
。表示文档中一段文本的文本节点获得代码 3 (Node.TEXT_NODE
)。注释具有代码 8 (Node.
)。
标准
使用神秘的数字代码来表示节点类型并不是 JavaScript 喜欢的做法。在本章的后面,我们将看到 DOM 接口的其他部分也让人感觉笨拙和陌生。这是因为 DOM 接口并非专为 JavaScript 设计。相反,它试图成为一个语言中立的接口,也可以在其他系统中使用——不仅适用于 HTML,也适用于 XML,XML 是一种具有 HTML 类语法的通用数据格式。
这是不幸的。标准通常很有用。但在这种情况下,优势(跨语言一致性)并没有那么引人注目。拥有一个与您使用的语言完全集成的接口将比拥有一个跨语言的熟悉接口为您节省更多时间。
作为这种糟糕集成的示例,请考虑 DOM 中元素节点具有的 childNodes
属性。此属性保存一个类似数组的对象,该对象具有 length
属性以及由数字标记的属性来访问子节点。但它是 NodeList
类型的实例,而不是真正的数组,因此它没有 slice
和 map
这样的方法。
然后是仅仅由设计不良引起的错误。例如,没有办法创建一个新节点并立即向其添加子节点或属性。相反,您必须先创建它,然后使用副作用逐个添加子节点和属性。大量与 DOM 交互的代码往往会变得冗长、重复且难看。
但是这些缺陷并非致命。由于 JavaScript 允许我们创建自己的抽象,因此可以设计出表达我们正在执行的操作的改进方法。许多旨在用于浏览器编程的库都附带了这些工具。
遍历树
尽管图中仅显示了一种类型的链接,但每个节点都有一个 parentNode
属性,它指向节点所在的节点(如果有)。同样,每个元素节点(节点类型 1)都有一个 childNodes
属性,它指向一个包含其子节点的类似数组的对象。
理论上,您可以仅使用这些父级和子级链接在树中移动到任何地方。但 JavaScript 也让您可以访问许多其他便利链接。firstChild
和 lastChild
属性分别指向第一个和最后一个子元素,或者对于没有子元素的节点,其值为 null
。类似地,previousSibling
和 nextSibling
指向相邻节点,这些节点与同一父级节点具有相同的父级节点,并出现在节点本身的紧前方或紧后方。对于第一个子节点,previousSibling
将为 null,而对于最后一个子节点,nextSibling
将为 null。
还有 children
属性,它类似于 childNodes
,但仅包含元素(类型 1)子节点,而不是其他类型的子节点。当您对文本节点不感兴趣时,这很有用。
处理这种嵌套数据结构时,递归函数通常很有用。以下函数扫描文档以查找包含给定字符串的文本节点,并在找到一个节点时返回 true
function talksAbout(node, string) { if (node.nodeType == Node.ELEMENT_NODE) { for (let child of node.childNodes) { if (talksAbout(child, string)) { return true; } } return false; } else if (node.nodeType == Node.TEXT_NODE) { return node.nodeValue.indexOf(string) > -1; } } console.log(talksAbout(document.body, "book")); // → true
文本节点的 nodeValue
属性保存它表示的文本字符串。
查找元素
导航这些父级、子级和兄弟姐妹之间的链接通常很有用。但是,如果我们想在文档中找到一个特定的节点,从 document.body
开始并遵循一组固定的属性路径来访问它,这是一个糟糕的主意。这样做会将有关文档精确结构的假设烘焙到我们的程序中——这是一种您可能希望稍后更改的结构。另一个复杂因素是,即使对于节点之间的空格,也会创建文本节点。示例文档的 <body>
标签不仅有三个子节点(<h1>
和两个 <p>
元素),而是有七个:这三个,加上它们之前的空格、之后的空格和它们之间的空格。
如果我们想获取该文档中链接的 href
属性,我们不想说诸如“获取文档主体第六个子节点的第二个子节点”之类的话。如果我们能说“获取文档中的第一个链接”,那就更好了。我们可以做到。
let link = document.body.getElementsByTagName("a")[0]; console.log(link.href);
所有元素节点都有一个 getElementsByTagName
方法,该方法会收集作为该节点的后代(直接或间接子节点)的所有具有给定标签名称的元素,并将它们作为类似数组的对象返回。
若要查找特定的单个节点,您可以为其提供一个 id
属性,并使用 document.
。
<p>My ostrich Gertrude:</p> <p><img id="gertrude" src="img/ostrich.png"></p> <script> let ostrich = document.getElementById("gertrude"); console.log(ostrich.src); </script>
第三种类似的方法是 getElementsByClassName
,它类似于 getElementsByTagName
,它会搜索元素节点的内容,并检索其 class
属性中包含给定字符串的所有元素。
更改文档
几乎有关 DOM 数据结构的所有内容都可以更改。可以通过更改父子关系来修改文档树的形状。节点具有一个 remove
方法,用于将其从当前父节点中移除。若要将子节点添加到元素节点,我们可以使用 appendChild
,该方法将其放在子节点列表的末尾,或者使用 insertBefore
,该方法将第一个参数给出的节点插入第二个参数给出的节点之前。
<p>One</p> <p>Two</p> <p>Three</p> <script> let paragraphs = document.body.getElementsByTagName("p"); document.body.insertBefore(paragraphs[2], paragraphs[0]); </script>
一个节点只能在一个位置存在于文档中。因此,将段落Three插入到段落One之前,首先会将其从文档末尾移除,然后将其插入到开头,结果为Three/One/Two。所有在某个地方插入节点的操作都会作为副作用,导致它从当前位置(如果有)移除。
replaceChild
方法用于用另一个节点替换子节点。它以两个节点作为参数:一个新节点和要替换的节点。被替换的节点必须是调用该方法的元素的子节点。请注意,replaceChild
和 insertBefore
都将新节点作为第一个参数。
创建节点
假设我们要编写一个脚本,将文档中的所有图像(<img>
标签)替换为其 alt
属性中保存的文本,该属性指定了图像的替代文本表示。这不仅涉及移除图像,还涉及添加新的文本节点来替换它们。
<p>The <img src="img/cat.png" alt="Cat"> in the <img src="img/hat.png" alt="Hat">.</p> <p><button onclick="replaceImages()">Replace</button></p> <script> function replaceImages() { let images = document.body.getElementsByTagName("img"); for (let i = images.length - 1; i >= 0; i--) { let image = images[i]; if (image.alt) { let text = document.createTextNode(image.alt); image.parentNode.replaceChild(text, image); } } } </script>
给定一个字符串,createTextNode
将提供一个文本节点,我们可以将其插入文档中,使其显示在屏幕上。
遍历图像的循环从列表的末尾开始。这是必要的,因为像 getElementsByTagName
(或像 childNodes
这样的属性)这样的方法返回的节点列表是实时的。也就是说,它会在文档更改时更新。如果我们从开头开始,删除第一个图像会导致列表失去第一个元素,因此循环第二次重复时,i
为 1,它将停止,因为集合的长度现在也是 1。
如果想要一个固定的节点集合,而不是实时的,则可以通过调用 Array.from
将集合转换为真正的数组。
let arrayish = {0: "one", 1: "two", length: 2}; let array = Array.from(arrayish); console.log(array.map(s => s.toUpperCase())); // → ["ONE", "TWO"]
要创建元素节点,可以使用 document.
方法。此方法接受一个标签名,并返回一个新的指定类型的空节点。
以下示例定义了一个实用程序 elt
,它创建一个元素节点并将其余参数视为该节点的子节点。然后,此函数用于向引号添加属性。
<blockquote id="quote"> No book can ever be finished. While working on it we learn just enough to find it immature the moment we turn away from it. </blockquote> <script> function elt(type, ...children) { let node = document.createElement(type); for (let child of children) { if (typeof child != "string") node.appendChild(child); else node.appendChild(document.createTextNode(child)); } return node; } document.getElementById("quote").appendChild( elt("footer", "—", elt("strong", "Karl Popper"), ", preface to the second edition of ", elt("em", "The Open Society and Its Enemies"), ", 1950")); </script>
属性
某些元素属性,例如链接的 href
,可以通过元素的 DOM 对象上的同名属性访问。对于大多数常用的标准属性来说,情况都是如此。
HTML 允许您在节点上设置任何您想要的属性。这很有用,因为它允许您在文档中存储额外的信息。要读取或更改自定义属性(不可作为常规对象属性使用),必须使用 getAttribute
和 setAttribute
方法。
<p data-classified="secret">The launch code is 00000000.</p> <p data-classified="unclassified">I have two feet.</p> <script> let paras = document.body.getElementsByTagName("p"); for (let para of Array.from(paras)) { if (para.getAttribute("data-classified") == "secret") { para.remove(); } } </script>
建议在这些自定义属性的名称前加上 data-
前缀,以确保它们不会与任何其他属性冲突。
有一个常用的属性 class
,它是 JavaScript 语言中的关键字。出于历史原因——一些旧的 JavaScript 实现无法处理与关键字匹配的属性名——用于访问此属性的属性称为 className
。您还可以使用 getAttribute
和 setAttribute
方法以其真实名称 "class"
访问它。
布局
您可能已经注意到,不同类型的元素的布局方式不同。有些元素,例如段落(<p>
)或标题(<h1>
),占据整个文档宽度,并在单独的行上呈现。这些称为块级元素。其他元素,例如链接(<a>
)或 <strong>
元素,与其周围的文本在同一行上呈现。这样的元素被称为内联元素。
对于任何给定的文档,浏览器都能够计算出布局,它根据元素的类型和内容为每个元素提供大小和位置。然后使用此布局来实际绘制文档。
可以从 JavaScript 中访问元素的大小和位置。offsetWidth
和 offsetHeight
属性以像素为单位提供元素所占用的空间。像素是浏览器中的基本度量单位。它传统上对应于屏幕可以绘制的最小的点,但在现代显示器上,可以绘制非常小的点,这可能不再是这种情况,并且浏览器像素可能跨越多个显示点。
类似地,clientWidth
和 clientHeight
提供元素内部的空间大小,不包括边框宽度。
<p style="border: 3px solid red"> I'm boxed in </p> <script> let para = document.body.getElementsByTagName("p")[0]; console.log("clientHeight:", para.clientHeight); // → 19 console.log("offsetHeight:", para.offsetHeight); // → 25 </script>
查找元素在屏幕上精确位置的最有效方法是 getBoundingClientRect
方法。它返回一个带有 top
、bottom
、left
和 right
属性的对象,这些属性指示元素相对于屏幕左上角的边的像素位置。如果想要相对于整个文档的像素位置,则必须添加当前滚动位置,您可以在 pageXOffset
和 pageYOffset
绑定中找到它。
布局文档可能需要相当多的工作。为了提高速度,浏览器引擎不会在每次更改文档时立即重新布局文档,而是尽可能长时间地等待,然后再进行布局。当更改了文档的 JavaScript 程序完成运行时,浏览器将不得不计算新的布局,以便将更改后的文档绘制到屏幕上。当程序请求某些内容的位置或大小时,通过读取 offsetHeight
等属性或调用 getBoundingClientRect
来提供该信息,也需要计算布局。
一个反复在读取 DOM 布局信息和更改 DOM 之间切换的程序会导致大量的布局计算发生,因此运行速度会非常慢。以下代码是一个示例。它包含两个不同的程序,它们构建了一行 2,000 像素宽的X 字符,并测量每个程序所需的时间。
<p><span id="one"></span></p> <p><span id="two"></span></p> <script> function time(name, action) { let start = Date.now(); // Current time in milliseconds action(); console.log(name, "took", Date.now() - start, "ms"); } time("naive", () => { let target = document.getElementById("one"); while (target.offsetWidth < 2000) { target.appendChild(document.createTextNode("X")); } }); // → naive took 32 ms time("clever", function() { let target = document.getElementById("two"); target.appendChild(document.createTextNode("XXXXX")); let total = Math.ceil(2000 / (target.offsetWidth / 5)); target.firstChild.nodeValue = "X".repeat(total); }); // → clever took 1 ms </script>
样式
我们已经看到,不同的 HTML 元素的绘制方式不同。有些显示为块级,有些显示为内联。有些添加了样式——<strong>
使其内容变为粗体,<a>
使其变为蓝色并带有下划线。
<img>
标签显示图像的方式或 <a>
标签在单击时导致链接被跟随的方式与元素类型密切相关。但是,我们可以更改与元素关联的样式,例如文本颜色或下划线。以下示例使用 style
属性
<p><a href=".">Normal link</a></p> <p><a href="." style="color: green">Green link</a></p>
样式属性可以包含一个或多个声明,它们是属性(例如 color
)后跟冒号和值(例如 green
)。当有多个声明时,它们必须用分号隔开,如 "color: red; border: none"
。
许多文档方面都可以通过样式来影响。例如,display
属性控制元素是显示为块级还是内联元素。
This text is displayed <strong>inline</strong>, <strong style="display: block">as a block</strong>, and <strong style="display: none">not at all</strong>.
block
标签将出现在它自己的行上,因为块级元素不会与周围的文本内联显示。最后一个标签根本不会显示——display: none
会阻止元素出现在屏幕上。这是一种隐藏元素的方式。与将它们从文档中完全移除相比,它通常更可取,因为它可以轻松地稍后重新显示它们。
JavaScript 代码可以通过元素的 style
属性直接操作元素的样式。此属性保存一个对象,该对象具有所有可能的样式属性的属性。这些属性的值是字符串,我们可以写入它们以更改元素样式的特定方面。
<p id="para" style="color: purple"> Nice text </p> <script> let para = document.getElementById("para"); console.log(para.style.color); para.style.color = "magenta"; </script>
一些样式属性名称包含连字符,例如 font-family
。因为这些属性名在 JavaScript 中很麻烦(您必须说 style["font-family"]
),所以 style
对象中这些属性的属性名将删除连字符并将连字符后的字母大写(style.fontFamily
)。
层叠样式
HTML 的样式系统称为CSS,代表层叠样式表。样式表是一组规则,用于描述如何设置文档中元素的样式。它可以在 <style>
标签中给出。
<style> strong { font-style: italic; color: gray; } </style> <p>Now <strong>strong text</strong> is italic and gray.</p>
名称中的层叠是指多个这样的规则被组合起来生成元素的最终样式。在示例中,<strong>
标签的默认样式,它为它们提供 font-weight: bold
,被 <style>
标签中的规则覆盖,该规则添加了 font-style
和 color
。
当多个规则为同一个属性定义值时,最后读取的规则具有更高的优先级,并且获胜。例如,如果 <style>
标签中的规则包含 font-weight: normal
,与默认的 font-weight
规则相矛盾,则文本将为正常,而不是粗体。直接应用于节点的 style
属性中的样式具有最高的优先级,并且始终获胜。
在 CSS 规则中,可以定位除了标签名之外的其他内容。.abc
的规则适用于所有在 class
属性中包含 "abc"
的元素。#xyz
的规则适用于 id
属性为 "xyz"
的元素(它应该在文档中是唯一的)。
.subtle { color: gray; font-size: 80%; } #header { background: blue; color: white; } /* p elements with id main and with classes a and b */ p#main.a.b { margin-bottom: 20px; }
有利于最后定义的规则的优先级规则仅适用于规则具有相同特异性的情况。规则的特异性是衡量它描述匹配元素的精确度的指标,由它要求的元素方面的数量和种类(标签、类或 ID)决定。例如,定位 p.a
的规则比定位 p
或仅 .a
的规则更具体,因此将优先于它们。
p > a {…}
表示法将给定的样式应用于所有<p>
标签的直接子级 <a>
标签。类似地,p a {…}
应用于<p>
标签内所有 <a>
标签,无论它们是直接子级还是间接子级。
查询选择器
在本书中,我们不会过多地使用样式表。理解它们在浏览器编程中很有帮助,但它们足够复杂,需要单独一本书来介绍。我介绍选择器语法(样式表中用于确定一组样式应用于哪些元素的表示法)的主要原因是,我们可以使用这种简短的语言作为查找 DOM 元素的有效方法。
querySelectorAll
方法(在 document
对象和元素节点上均定义)接受一个选择器字符串,并返回一个包含所有匹配元素的 NodeList
。
<p>And if you go chasing <span class="animal">rabbits</span></p> <p>And you know you're going to fall</p> <p>Tell 'em a <span class="character">hookah smoking <span class="animal">caterpillar</span></span></p> <p>Has given you the call</p> <script> function count(selector) { return document.querySelectorAll(selector).length; } console.log(count("p")); // All <p> elements // → 4 console.log(count(".animal")); // Class animal // → 2 console.log(count("p .animal")); // Animal inside of <p> // → 2 console.log(count("p > .animal")); // Direct child of <p> // → 1 </script>
与 getElementsByTagName
等方法不同,querySelectorAll
返回的对象不是动态的。当您更改文档时,它不会改变。不过它仍然不是真正的数组,因此如果您想将其视为数组,需要调用 Array.from
。
querySelector
方法(没有 All
部分)的工作方式类似。当您想要一个特定的单一元素时,此方法很有用。它只返回第一个匹配元素,如果没有任何元素匹配,则返回 null
。
定位和动画
position
样式属性以强大的方式影响布局。它的默认值为 static
,这意味着元素位于文档中的正常位置。当它设置为 relative
时,元素仍然在文档中占用空间,但现在可以使用 top
和 left
样式属性相对于该正常位置移动它。当 position
设置为 absolute
时,元素将从文档的正常流中移除——也就是说,它不再占用空间,并且可能与其他元素重叠。可以使用 top
和 left
属性相对于最近的 position
属性不是 static
的封闭元素的左上角进行绝对定位,如果不存在这样的封闭元素,则相对于文档进行定位。
我们可以使用它来创建动画。以下文档显示了一张在椭圆中移动的猫的图片
<p style="text-align: center"> <img src="img/cat.png" style="position: relative"> </p> <script> let cat = document.querySelector("img"); let angle = Math.PI / 2; function animate(time, lastTime) { if (lastTime != null) { angle += (time - lastTime) * 0.001; } cat.style.top = (Math.sin(angle) * 20) + "px"; cat.style.left = (Math.cos(angle) * 200) + "px"; requestAnimationFrame(newTime => animate(newTime, time)); } requestAnimationFrame(animate); </script>
我们的图片位于页面中心,并被赋予 position
为 relative
。我们将反复更新该图片的 top
和 left
样式以移动它。
该脚本使用 requestAnimationFrame
来安排 animate
函数在浏览器准备重新绘制屏幕时运行。animate
函数本身再次调用 requestAnimationFrame
来安排下次更新。当浏览器窗口(或选项卡)处于活动状态时,这将导致更新以大约每秒 60 次的速率发生,这往往会产生外观良好的动画。
如果我们只是在循环中更新 DOM,页面会冻结,屏幕上不会出现任何内容。浏览器在 JavaScript 程序运行时不会更新其显示,也不允许任何与页面的交互。这就是我们需要 requestAnimationFrame
的原因——它让浏览器知道我们现在完成了,并且可以继续执行浏览器所做的事情,例如更新屏幕并响应用户操作。
动画函数以当前时间作为参数传递给它。为了确保每毫秒猫的运动稳定,它根据函数上次运行时间与当前时间之间的差值来确定角度变化的速度。如果它只是每步移动固定数量的角度,当例如同一台计算机上运行的其他繁重任务阻止函数运行几分之一秒时,运动会卡顿。
圆形移动是使用三角函数 Math.cos
和 Math.sin
完成的。对于那些不熟悉这些函数的人,我将简要介绍它们,因为我们偶尔会在本书中使用它们。
Math.cos
和 Math.sin
用于查找位于以 (0, 0) 为中心、半径为 1 的圆上的点。这两个函数都将它们的实参解释为圆上的位置,其中 0 表示圆最右边的点,顺时针移动直到 2π(约 6.28)将我们带到整个圆的周围。Math.cos
会告诉你对应于给定位置的点的 x 坐标,Math.sin
会给出 y 坐标。大于 2π 或小于 0 的位置(或角度)是有效的——旋转重复,因此a+2π 指的是与a相同的角度。
这种用于测量角度的单位称为弧度——一个完整的圆周为 2π 弧度,类似于用度数测量时为 360 度。常数 π 在 JavaScript 中可用作 Math.PI
。
猫动画代码保存一个计数器 angle
,用于表示动画的当前角度,并在每次调用 animate
函数时递增它。然后它可以使用此角度来计算图像元素的当前位置。top
样式使用 Math.sin
计算,并乘以 20,这是我们椭圆的垂直半径。left
样式基于 Math.cos
并乘以 200,因此椭圆的宽度远大于高度。
注意,样式通常需要单位。在本例中,我们必须将 "px"
附加到数字,以告知浏览器我们正在使用像素进行计数(而不是厘米、“em”或其他单位)。这一点很容易忘记。使用没有单位的数字会导致你的样式被忽略——除非该数字是 0,无论其单位如何,0 始终表示相同的意思。
总结
JavaScript 程序可以通过称为 DOM 的数据结构来检查和干扰浏览器正在显示的文档。该数据结构表示浏览器对文档的模型,JavaScript 程序可以修改它以更改可见的文档。
DOM 就像一棵树一样组织,其中元素根据文档的结构按层次结构排列。表示元素的对象具有诸如 parentNode
和 childNodes
之类的属性,这些属性可用于浏览这棵树。
文档的显示方式可以通过样式来影响,既可以通过将样式直接附加到节点,也可以通过定义匹配某些节点的规则。存在许多不同的样式属性,例如 color
或 display
。JavaScript 代码可以通过元素的 style
属性直接操作元素的样式。
练习
构建表格
<table> <tr> <th>name</th> <th>height</th> <th>place</th> </tr> <tr> <td>Kilimanjaro</td> <td>5895</td> <td>Tanzania</td> </tr> </table>
对于每个行,<table>
标签包含一个 <tr>
标签。在这些 <tr>
标签内,我们可以放置单元格元素:标题单元格 (<th>
) 或普通单元格 (<td>
)。
给定一个山脉数据集,一个包含 name
、height
和 place
属性的对象数组,生成一个枚举这些对象的表格的 DOM 结构。它每列对应一个键,每行对应一个对象,再加上一个标题行,在顶部使用 <th>
元素列出列名。
编写代码,使列自动从对象中推导出来,方法是获取数据集中第一个对象的属性名称。
通过将其附加到 id
属性为 "mountains"
的元素,在文档中显示生成的表格。
完成此操作后,通过将包含数字值的单元格的 style.textAlign
属性设置为 "right"
,将这些单元格右对齐。
<h1>Mountains</h1> <div id="mountains"></div> <script> const MOUNTAINS = [ {name: "Kilimanjaro", height: 5895, place: "Tanzania"}, {name: "Everest", height: 8848, place: "Nepal"}, {name: "Mount Fuji", height: 3776, place: "Japan"}, {name: "Vaalserberg", height: 323, place: "Netherlands"}, {name: "Denali", height: 6168, place: "United States"}, {name: "Popocatepetl", height: 5465, place: "Mexico"}, {name: "Mont Blanc", height: 4808, place: "Italy/France"} ]; // Your code here </script>
显示提示...
按标签名查找元素
document.
方法返回所有具有给定标签名的子元素。实现您自己的版本,该版本接受节点和字符串(标签名)作为参数,并返回一个包含所有具有给定标签名的后代元素节点的数组。您的函数应该遍历文档本身。它可能不会使用 querySelectorAll
等方法来完成工作。
要查找元素的标签名,请使用其 nodeName
属性。但请注意,这将以全大写形式返回标签名。使用 toLowerCase
或 toUpperCase
字符串方法来弥补这一点。
<h1>Heading with a <span>span</span> element.</h1> <p>A paragraph with <span>one</span>, <span>two</span> spans.</p> <script> function byTagName(node, tagName) { // Your code here. } console.log(byTagName(document.body, "h1").length); // → 1 console.log(byTagName(document.body, "span").length); // → 3 let para = document.querySelector("p"); console.log(byTagName(para, "span").length); // → 2 </script>
显示提示...
该解决方案最容易用递归函数表示,类似于本章前面定义的talksAbout
函数。
您可以递归地调用 byTagname
本身,将结果数组连接起来以产生输出。或者,您可以创建一个内部函数,该函数递归地调用自身,并且可以访问外部函数中定义的数组绑定,它可以在其中添加找到的匹配元素。不要忘记从外部函数中调用一次内部函数来启动该过程。
递归函数必须检查节点类型。在这里,我们只对节点类型 1 (Node.
) 感兴趣。对于此类节点,我们必须循环遍历它们的子节点,并针对每个子节点,查看子节点是否与查询匹配,同时对其进行递归调用以检查其自己的子节点。
猫的帽子
扩展之前定义的猫动画动画,使猫和它的帽子 (<img src="img/
) 在椭圆的相对两侧绕轨道运行。
为了使定位多个对象更容易,您可能需要切换到绝对定位。这意味着 top
和 left
是相对于文档的左上角计算的。为了避免使用负坐标(这会导致图像移出可见页面),您可以向位置值添加固定数量的像素。
<style>body { min-height: 200px }</style> <img src="img/cat.png" id="cat" style="position: absolute"> <img src="img/hat.png" id="hat" style="position: absolute"> <script> let cat = document.querySelector("#cat"); let hat = document.querySelector("#hat"); let angle = 0; let lastTime = null; function animate(time) { if (lastTime != null) angle += (time - lastTime) * 0.001; lastTime = time; cat.style.top = (Math.sin(angle) * 40 + 40) + "px"; cat.style.left = (Math.cos(angle) * 200 + 230) + "px"; // Your extensions here. requestAnimationFrame(animate); } requestAnimationFrame(animate); </script>