1
0
mirror of https://github.com/apachecn/eloquent-js-3e-zh.git synced 2025-05-23 20:02:20 +00:00
wizardforcel 4862c9cb9c 14.
2018-05-11 18:02:00 +08:00

31 KiB
Raw Blame History

十四、文档对象模型

Too bad! Same old story! Once you've finished building your house you notice you've accidentally learned something that you really should have known—before you started.

Friedrich Nietzsche《Beyond Good and Evil》

当你在浏览器中打开网页时浏览器会接收网页的HTML文本并进行解析其解析方式与第11章中介绍的解析器非常相似。浏览器构建文档结构的模型并使用该模型在屏幕上绘制页面。

JavaScript在其沙箱中提供了将文本转换成文档对象模型的功能。它是你可以读取或者修改的数据结构。模型是一个所见即所得的数据结构改变模型会使得屏幕上的页面产生相应变化。

13.1 文档结构

你可以将HTML文件想象成一系列嵌套的箱子。诸如<body>和</body>之类的标签会将其他标签包围起来,而包含在内部的标签也可以包含其他的标签和文本。这里给出上一章中已经介绍过的示例文件。

<!doctype html>
<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="http://eloquentjavascript.net">here</a>.</p>
  </body>
</html>

该页面结构如下所示。

浏览器使用与该形状对应的数据结构来表示文档。每个盒子都是一个对象我们可以和这些对象交互找出其中包含的盒子与文本。我们将这种表示方式称为文档对象模型Document Object Model或简称DOM。

我们可以通过全局绑定document来访问这些对象。该对象的documentElement属性引用了表示<html>标签的对象。由于每个 HTML 文档都有一个头部和一个主体,它还具有headbody属性,指向这些元素。

13.2 树

回想一下第12章中提到的语法树。其结构与浏览器文档的结构极为相似。每个节点可以使用children引用其他节点而每个子节点又有各自的children。其形状是一种典型的嵌套结构每个元素可以包含与其自身相似的子元素。

如果一个数据结构有分支结构而且没有任何回环一个节点不能直接或间接包含自身并且有一个单一、定义明确的“根节点”那么我们将这种数据结构称之为树。就DOM来讲document.documentElement就是其根节点。

在计算机科学中树的应用极为广泛。除了表现诸如HTML文档或程序之类的递归结构树还可以用于维持数据的有序集合因为在树中寻找或插入一个节点往往比在数组中更高效。

一棵典型的树有不同类型的节点。Egg 语言的语法树有标识符、值和应用节点。应用节点常常包含子节点,而标识符、值则是叶子节点,也就是没有孩子的节点。

DOM中也是一样。元素表示 HTML 标签的节点用于确定文档结构。这些节点可以包含子节点。这类节点中的一个例子是document.body。其中一些子节点可以是叶子节点比如文本片段或注释。

每个DOM节点对象都包含nodeType属性该属性包含一个标识节点类型的代码数字。元素的值为1DOM也将该值定义成一个常量属性document.ELEMENT_NODE。文本节点表示文档中的一段文本代码为3document.TEXT_NODE。注释的代码为8document.COMMENT_NODE

因此我们可以使用另一种方法来表示文档树:

叶子节点是文本节点,而箭头则指出了节点之间的父子关系。

13.3 标准

并非只有JavaScript会使用数字代码来表示节点类型。本章随后将会展示其他的DOM接口你可能会觉得这些接口有些奇怪。这是因为DOM并不是为JavaScript而设计的它尝试成为一组语言中立的接口确保也可用于其他系统中不只是HTML还有XML。XML是一种通用数据格式语法与HTML相近。

这就比较糟糕了。一般情况下标准都是非常易于使用的。但在这里其优势(跨语言的一致性)并不明显。相较于为不同语言提供类似的接口,如果能够将接口与开发者使用的语言进行适当集成,可以为开发者节省大量时间。

我们举例来说明一下集成问题。比如DOM中每个元素都有childNodes属性。该属性是一个类似于数组的对象有length属性也可以使用数字标签访问对应的子节点。但该属性是NodeList类型的实例而不是真正的数组因此该类型没有诸如slice和map之类的方法。

有些问题是由不好的设计导致的。例如我们无法在创建新的节点的同时立即为其添加孩子和属性。相反你首先需要创建节点然后使用副作用将子节点和属性逐个添加到节点中。大量使用DOM的代码通常较长、重复和丑陋。

但这些问题并非无法改善。因为JavaScript允许我们构建自己的抽象可以设计改进方式来表达您正在执行的操作。 许多用于浏览器编程的库都附带这些工具。

13.4 通过树结构访问节点

DOM节点包含了许多指向相邻节点的链接。下面的图表展示了这一点。

尽管图表中每种类型的节点只显示出一条链接但每个节点都有parentNode属性指向一个节点它是这个节点的一部分。类似的每个元素节点节点类型为1均包含childNodes属性该属性指向一个类似于数组的对象用于保存其子节点。

理论上你可以通过父子之间的链接移动到树中的任何地方。但JavaScript也提供了一些更加方便的额外链接。firstChild属性和lastChild属性分别指向第一个孩子和最后一个孩子若没有孩子则值为null。类似的previousSibling和nextSibling指向相邻节点分别指向拥有相同父亲的前一个节点和后一个节点。对于第一个孩子previousSibling是null而最后一个孩子的nextSibling则是null。

也存在children属性,它就像childNodes,但只包含元素(类型为 1的子节点而不包含其他类型的子节点。 当您对文本节点不感兴趣时,这可能很有用。

处理像这样的嵌套数据结构时,递归函数通常很有用。 以下函数在文档中扫描包含给定字符串的文本节点,并在找到一个时返回true

function talksAbout(node, string) {
  if (node.nodeType == document.ELEMENT_NODE) {
    for (let i = 0; i < node.childNodes.length; i++) {
      if (talksAbout(node.childNodes[i], string)) {
        return true;
      }
    }
    return false;
  } else if (node.nodeType == document.TEXT_NODE) {
    return node.nodeValue.indexOf(string) > -1;
  }
}

console.log(talksAbout(document.body, "book"));
// → true

因为childNodes不是真正的数组,所以我们不能用for/of来遍历它,并且必须使用普通的for循环遍历索引范围。

文本节点的nodeValue属性保存它所表示的文本字符串。

13.5 查找元素

使用父节点、子节点和兄弟节点之间的连接遍历节点确实非常实用。但是如果我们只想查找文档中的特定节点那么从document.body开始盲目沿着硬编码的链接路径查找节点并非良策。如果程序通过树结构定位节点就需要依赖于文档的具体结构而文档结构随后可能发生变化。另一个复杂的因素是DOM会为不同节点之间的空白字符创建对应的文本节点。例如示例文档中的body标签不止包含3个孩子<h1>和两个<p>元素其实包含7个孩子这三个节点、三个节点前后的空格、以及元素之间的空格。

因此如果你想获取文档中某个链接的href属性最好不要去获取文档body元素中第六个孩子的第二个孩子而最好直接获取文档中的第一个链接而且这样的操作确实可以实现。

let link = document.body.getElementsByTagName("a")[0];
console.log(link.href);

所有元素节点都包含getElementsByTagName方法用于从所有后代节点中直接或间接子节点搜索包含给定标签名的节点并返回一个类数组的对象。

你也可以使用document.getElementById来寻找包含特定id属性的某个节点。

<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属性的元素。

13.6 修改文档

几乎所有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都将新节点作为第一个参数。

13.7 创建节点

假设我们要编写一个脚本,将文档中的所有图像(<img>标签)替换为其alt属性中的文本,该文本指定了图像的文字替代表示。

这不仅涉及删除图像还涉及添加新的文本节点并替换原有图像节点。为此我们使用document.createTextNode方法。

<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];
      var image = images[i];
      if (image.alt) {
        let text = document.createTextNode(image.alt);
        image.parentNode.replaceChild(text, image);
      }
    }
  }
</script>

给定一个字符串,createTextNode为我们提供了一个文本节点,我们可以将它插入到文档中,来使其显示在屏幕上。

该循环从列表末尾开始遍历图像。我们必须这样反向遍历列表因为getElementsByTagName之类的方法返回的节点列表是动态变化的。该列表会随着文档改变还改变。若我们从列表头开始遍历移除掉第一个图像会导致列表丢失其第一个元素第二次循环时因为集合的长度此时为1而i也为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.createElement方法创建一个元素节点。该方法接受一个标签名返回一个新的空节点节点类型由标签名指定。

下面的示例定义了一个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 editon of ",
        elt("em", "The Open Society and Its Enemies"),
        ", 1950"));
</script>

13.8 属性

我们可以通过元素的DOM对象的同名属性去访问元素的某些属性比如链接的href属性。这仅限于最常用的标准属性。

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实现无法处理和关键字或保留字同名的属性访问class的属性名为className。你也可以使用getAttribute和setAttribute方法使用其实际名称“class”来访问该属性。

13.9 布局

你可能已经注意到不同类型的元素有不同的布局。某些元素,比如段落(<p>)和标题(<h1>会占据整个文档的宽度并且在独立的一行中渲染。这些元素被称为块Block元素。其他的元素比如链接<a>或<strong>元素则与周围文本在同一行中渲染。这类元素我们称之为内联Inline元素。

对于任意特定文档,浏览器可以根据每个元素的类型和内容计算其尺寸与位置等布局信息。接着使用布局来绘制文档。

JavaScript中可以访问元素的尺寸与位置。

属性offsetWidth和offsetHeight给出元素的起始位置单位是像素。像素是浏览器中的基本测量单元。它通常对应于屏幕可以绘制的最小的点但是在现代显示器上可以绘制非常小的点这可能不再适用了并且浏览器像素可能跨越多个显示点。

同样,clientWidthclientHeight向你提供元素内的空间大小,忽略边框宽度。

<p style="border: 3px solid red">
  I'm boxed in
</p>

<script>
  let para = document.body.getElementsByTagName("p")[0];
  console.log("clientHeight:", para.clientHeight);
  console.log("offsetHeight:", para.offsetHeight);
</script>

getBoundingClientRect方法是获取屏幕中某个元素精确位置的最有效方法。该方法返回一个对象包含top、bottom、left和right四个属性表示元素相对于屏幕左上角的位置单位是像素。若你想要知道其相对于整个文档的位置必须加上其滚动位置你可以在pageXOffset和pageYOffset绑定中找到。

我们还需要花些力气才能完成文档的排版工作。为了加快速度每次你改变它时浏览器引擎不会立即重新绘制整个文档而是尽可能等待并推迟重绘操作。当一个修改文档的JavaScript程序结束时浏览器会计算新的布局并在屏幕上显示修改过的文档。若程序通过读取offsetHeight和getBoundingClientRect这类属性获取某些元素的位置或尺寸时为了提供正确的信息浏览器也需要计算布局。

如果程序反复读取DOM布局信息或修改DOM会强制引发大量布局计算导致运行非常缓慢。下面的代码展示了一个示例。该示例包含两个不同的程序使用X字符构建一条线其长度是2000像素并计算每个任务的时间。

<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>

13.10 样式

我们看到了不同的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。当包含更多声明时不同属性之间必须使用分号分隔比如“colorredbordernone”。

样式会受到很多因素的影响。例如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会结束其所在的那一行因为块元素是不会和周围文本内联显示的。最后一个标签完全不会显示出来因为displaynone会阻止一个元素呈现在屏幕上。这是隐藏元素的一种方式。更好的方式是将其从文档中完全移除因为随后将其放回去是一件很简单的事情。

JavaScript代码可以通过节点的style属性操作元素的样式。该属性保存了一个对象对象中存储了所有可能的样式属性这些属性的值是字符串我们可以把字符串写入属性修改某些方面的元素样式。

<p id="para" style="color: purple">
  Pretty text
</p>

<script>
  var para = document.getElementById("para");
  console.log(para.style.color);
  para.style.color = "magenta";
</script>

一些样式属性名包含破折号比如font-family。由于这些属性的命名不适合在JavaScript中使用你必须写成style[“font-family”]因此在JavaScript中样式对象中的属性名都移除了破折号并将破折号之后的字母大写style.fontFamily

13.11 层叠样式

我们把HTML的样式化系统称为CSS即层叠样式表Cascading Style Sheets。样式表是一系列规则指出如何为文档中元素添加样式。可以在<style>标签中写入CSS。

<style>
  strong {
    font-style: italic;
    color: gray;
  }
</style>
<p>Now <strong>strong text</strong> is italic and gray.</p>

所谓层叠指的是将多条规则组合起来产生元素的最终样式。在上面的示例中,<strong>标签的默认样式font-weightbold会被<style>标签中的规则覆盖,并为<strong>标签样式添加font-style和color属性。

当多条规则重复定义同一属性时,最近的规则会拥有最高的优先级。因此如果<style>标签中的规则包含font-weightnormal与默认的font-weight规则冲突那么文本将会显示为普通样式而非粗体。属性style中的样式会直接作用于节点而且往往拥有最高优先级。

我们可以在CSS规则中使用标签名来定位标签。规则.abc指的是所有class属性中包含“abc”的元素。规则#xyz作用于id属性为“xyz”应当在文档中唯一存在的元素。

.subtle {
  color: gray;
  font-size: 80%;
}
#header {
  background: blue;
  color: white;
}
/* p elements, with classes a and b, and id main */
p.a.b#main {
  margin-bottom: 20px;
}

优先级规则偏向于最近定义的规则只有在规则特殊性相同的情况下有效。规则的特殊性用于衡量该规则描述匹配元素时的准确性。特殊性取决于规则中的元素数量和类型tag、class或id。例如目标规则p.a比目标规则p或.a更具体因此有更高优先级。

p>a{….}这种写法将样式作用于<p>标签的直系孩子。类似的p a{…}应用于所有的<p>标签中的<a>标签,无论是否是直系孩子。

13.12 查询选择器

本书不会使用太多样式表。尽管理解样式表对浏览器程序设计至关重要,想要正确解释所有浏览器支持的属性及其使用方式,可能需要两到三本书才行。

我介绍选择器语法用在样式表中确定样式作用的元素的主要原因是这种微型语言同时也是一种高效的DOM元素查找方式。

document对象和元素节点中都定义了querySelectorAll方法该方法接受一个选择器字符串并返回类似于数组的对象返回的对象中包含所有匹配的元素。

<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返回的对象不是动态变更的。修改文档时其内容不会被修改。

querySelector方法没有All与querySelectorAll作用相似。如果只想寻找某一个特殊元素该方法非常有用。该方法只返回第一个匹配元素如果不存在则返回null。

13.13 位置与动画

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>
  var cat = document.querySelector("img");
  var angle = 0, lastTime = null;
  function animate(time) {
    if (lastTime != null)
      angle += (time - lastTime) * 0.001;
    lastTime = time;
    cat.style.top = (Math.sin(angle) * 20) + "px";
    cat.style.left = (Math.cos(angle) * 200) + "px";
    requestAnimationFrame(animate);
  }
  requestAnimationFrame(animate);
</script>

该图像在页面中央position为relative。为了移动这只猫我们需要不断更新图像的top和left样式。

脚本使用request-AnimationFrame在每次浏览器准备重绘屏幕时调用animate函数。animate函数再次调用requestAnimationFrame以准备下一次更新。当浏览器窗口或标签激活时更新频率大概为60次每秒这种频率可以生成美观的动画。

若我们只是在循环中更新DOM页面会静止不动页面上也不会显示任何东西。浏览器不会在执行JavaScript程序时刷新显示内容也不允许页面上的任何交互。这就是我们需要requestAnimationFrame的原因该函数用于告知浏览器JavaScript程序目前已经完成工作因此浏览器可以继续执行其他任务比如刷新屏幕响应用户动作。

我们将动画生成函数作为参数传递给requestAnimationFrame。该函数比较现在的时间和上一次时间lastTime绑定确保每一毫秒猫的移动是稳定的而且动画是圆滑的。如果仅仅每次走几步猫的动作可能略显迟钝例如另一个在相同电脑上的繁重任务可能使得该函数零点几秒之后才会运行一次。

我们使用三角函数Math.cos和Math.sin来使猫沿着圆弧移动。你可能不太熟悉这些计算这些计算是本书第一次提及因此我在这里对这几个计算进行一个大致的介绍。

Math.cos和Math.sin非常实用我们可以利用一个1单元的弧度计算出以点00为圆心的圆上特定点的位置。两个函数都将参数解释为圆上的一个位置0表示圆上最右侧那个点一直逆时针递增到2π大概是6.28刚刚走过整个圆。Math.cos可以计算出圆上某一点对应的x坐标而Math.sin则计算出y坐标。超过2π或小于0的位置或角度都是合法的。因为弧度是循环重复的a+2π与a的角度相同。

猫的动画代码保存了一个名为angle的计数器该绑定记录猫在圆上的角度而且每当调用animate函数时该计数器的值与流逝的时间成比例递增。我们接着使用这个角度来计算图像元素的当前位置。top样式是Math.sin的结果乘以20表示圆中的垂直弧度。left样式是Math.cos的结果与200的乘积因此圆的宽度大于其高度导致最后猫会沿着椭圆轨迹移动。

这里需要注意的是样式的值一般需要指定单位。本例中我们在数字后添加“px”来告知浏览器以像素为计算单位而非厘米“ems”或其他单位。我们很容易遗漏这个单位。如果我们没有为样式中的数字加上单位浏览器最后会忽略掉该样式除非数字是0在这种情况下使用什么单位其结果都是一样的。

13.14 本章小结

JavaScript程序可以通过名为DOM的数据结构查看并修改浏览器中显示的文档。该数据结构描述了浏览器文档模型而JavaScript程序可以通过修改该数据结构来修改浏览器展示的文档。

DOM的组织就像树一样DOM根据文档结构来层次化地排布元素。描述元素的对象包含很多属性比如parentNode和childNodes这两个属性可以用来遍历DOM树。

我们可以通过样式来改变文档的显示方式可以直接在节点上附上样式也可以编写匹配节点的规则。样式包含许多不同的属性比如color和display。JavaScript可以直接通过节点的style属性操作元素的样式。

13.15 习题

13.15.1 创建一张表

我们在第6章中使用纯文本来构建表格。HTML使得表格排版更加容易。我们可以使用下面的标签结构来构建HTML表格。

<table>
  <tr>
    <th>name</th>
    <th>height</th>
    <th>country</th>
  </tr>
  <tr>
    <td>Kilimanjaro</td>
    <td>5895</td>
    <td>Tanzania</td>
  </tr>
</table>

<table>标签中,每一行包含一个<tr>标签。<tr>标签内部则是单元格元素,分为表头(<th>)和常规单元格(<td>)。

我们这里使用第6章中已经使用过的源数据源数据存储在沙箱的MOUNTAINS绑定中。你也可以从网站上下载http://eloquentjavascript.net/code/)数据。

编写一个函数buildTable调用者指定一个对象数组数组中每个对象都包含相同的一组属性该函数根据数组构建出表示表格的DOM结构。表格应该以属性名称作为表头表头使用<th>元素包围,每一行代表数组中的一个对象,其属性值存放在<td>元素中。

Object.keys函数返回某个对象所有属性名称的数组读者可能会在程序中用到。

当你完成基本功能后将元素的style.textAlign属性设置为right将单元格中的数字右对齐。

<style>
  /* Defines a cleaner look for tables */
  table  { border-collapse: collapse; }
  td, th { border: 1px solid black; padding: 3px 8px; }
  th     { text-align: left; }
</style>

<script>
  function buildTable(data) {
    // Your code here.
  }

  document.body.appendChild(buildTable(MOUNTAINS));
</script>

13.15.2 通过标签名获取元素

方法getElementsByTagName返回带有特定标签名称的所有子元素。实现该函数这里注意是函数不是方法。该函数的参数是一个节点和字符串标签名称并返回一个数组该数组包含所有带有特定标签名称的所有后代元素节点。

你可以使用tagName属性从DOM元素中获取标签名称。但这里需要注意使用tagName获取的标签名称是全大写形式。可以使用字符串的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
  var para = document.querySelector("p");
  console.log(byTagName(para, "span").length);
  // → 2
</script>

13.15.3 猫的帽子

扩展一下之前定义的用来绘制猫的动画函数,让猫和它的帽子沿着椭圆形轨道边(帽子永远在猫的对面)移动。

你也可以尝试让帽子环绕着猫移动,或修改成其他有趣的动画。

为了便于定位多个对象一个比较好的方法是使用绝对absolute定位。这就意味着top和left属性是相对于文档左上角的坐标。你可以简单地在坐标上加上一个固定数字以避免出现负的坐标。

<img src="img/cat.png" id="cat" style="position: absolute">
<img src="img/hat.png" id="hat" style="position: absolute">

<script>
  var cat = document.querySelector("#cat");
  var hat = document.querySelector("#hat");
  // Your code here.
</script>