JavaScript 編程精解 中文第三版 十四、文檔對(duì)象模型

十四、文檔對(duì)象模型

原文:The Document Object Model

譯者:飛龍

協(xié)議:CC BY-NC-SA 4.0

自豪地采用谷歌翻譯

部分參考了《JavaScript 編程精解(第 2 版)》

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》

https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/img/14-0.jpg

當(dāng)你在瀏覽器中打開(kāi)網(wǎng)頁(yè)時(shí),瀏覽器會(huì)接收網(wǎng)頁(yè)的 HTML 文本并進(jìn)行解析,其解析方式與第 11 章中介紹的解析器非常相似。瀏覽器構(gòu)建文檔結(jié)構(gòu)的模型,并使用該模型在屏幕上繪制頁(yè)面。

JavaScript 在其沙箱中提供了將文本轉(zhuǎn)換成文檔對(duì)象模型的功能。它是你可以讀取或者修改的數(shù)據(jù)結(jié)構(gòu)。模型是一個(gè)所見(jiàn)即所得的數(shù)據(jù)結(jié)構(gòu),改變模型會(huì)使得屏幕上的頁(yè)面產(chǎn)生相應(yīng)變化。

文檔結(jié)構(gòu)

你可以將 HTML 文件想象成一系列嵌套的箱子。諸如<body></body>之類的標(biāo)簽會(huì)將其他標(biāo)簽包圍起來(lái),而包含在內(nèi)部的標(biāo)簽也可以包含其他的標(biāo)簽和文本。這里給出上一章中已經(jīng)介紹過(guò)的示例文件。

<!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 >here</a>.</p>
  </body>
</html>

該頁(yè)面結(jié)構(gòu)如下所示。

https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/img/14-1.svg

瀏覽器使用與該形狀對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)來(lái)表示文檔。每個(gè)盒子都是一個(gè)對(duì)象,我們可以和這些對(duì)象交互,找出其中包含的盒子與文本。我們將這種表示方式稱為文檔對(duì)象模型(Document Object Model),或簡(jiǎn)稱 DOM。

我們可以通過(guò)全局綁定document來(lái)訪問(wèn)這些對(duì)象。該對(duì)象的documentElement屬性引用了<html>標(biāo)簽對(duì)象。由于每個(gè) HTML 文檔都有一個(gè)頭部和一個(gè)主體,它還具有headbody屬性,指向這些元素。

樹(shù)

回想一下第 12 章中提到的語(yǔ)法樹(shù)。其結(jié)構(gòu)與瀏覽器文檔的結(jié)構(gòu)極為相似。每個(gè)節(jié)點(diǎn)使用children引用其他節(jié)點(diǎn),而每個(gè)子節(jié)點(diǎn)又有各自的children。其形狀是一種典型的嵌套結(jié)構(gòu),每個(gè)元素可以包含與其自身相似的子元素。

如果一個(gè)數(shù)據(jù)結(jié)構(gòu)有分支結(jié)構(gòu),而且沒(méi)有任何環(huán)路(一個(gè)節(jié)點(diǎn)不能直接或間接包含自身),并且有一個(gè)單一、定義明確的“根節(jié)點(diǎn)”,那么我們將這種數(shù)據(jù)結(jié)構(gòu)稱之為樹(shù)。就 DOM 來(lái)講,document.documentElement就是其根節(jié)點(diǎn)。

在計(jì)算機(jī)科學(xué)中,樹(shù)的應(yīng)用極為廣泛。除了表現(xiàn)諸如 HTML 文檔或程序之類的遞歸結(jié)構(gòu),樹(shù)還可以用于維持?jǐn)?shù)據(jù)的有序集合,因?yàn)樵跇?shù)中尋找或插入一個(gè)節(jié)點(diǎn)往往比在數(shù)組中更高效。

一棵典型的樹(shù)有不同類型的節(jié)點(diǎn)。Egg 語(yǔ)言的語(yǔ)法樹(shù)有標(biāo)識(shí)符、值和應(yīng)用節(jié)點(diǎn)。應(yīng)用節(jié)點(diǎn)常常包含子節(jié)點(diǎn),而標(biāo)識(shí)符、值則是葉子節(jié)點(diǎn),也就是沒(méi)有子節(jié)點(diǎn)的節(jié)點(diǎn)。

DOM中也是一樣。元素(表示 HTML 標(biāo)簽)的節(jié)點(diǎn)用于確定文檔結(jié)構(gòu)。這些節(jié)點(diǎn)可以包含子節(jié)點(diǎn)。這類節(jié)點(diǎn)中的一個(gè)例子是document.body。其中一些子節(jié)點(diǎn)可以是葉子節(jié)點(diǎn),比如文本片段或注釋。

每個(gè) DOM 節(jié)點(diǎn)對(duì)象都包含nodeType屬性,該屬性包含一個(gè)標(biāo)識(shí)節(jié)點(diǎn)類型的代碼(數(shù)字)。元素的值為 1,DOM 也將該值定義成一個(gè)常量屬性document.ELEMENT_NODE。文本節(jié)點(diǎn)(表示文檔中的一段文本)代碼為 3(document.TEXT_NODE)。注釋的代碼為 8(document.COMMENT_NODE)。

因此我們可以使用另一種方法來(lái)表示文檔樹(shù):

https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/img/14-2.svg

葉子節(jié)點(diǎn)是文本節(jié)點(diǎn),而箭頭則指出了節(jié)點(diǎn)之間的父子關(guān)系。

標(biāo)準(zhǔn)

并非只有 JavaScript 會(huì)使用數(shù)字代碼來(lái)表示節(jié)點(diǎn)類型。本章隨后將會(huì)展示其他的 DOM 接口,你可能會(huì)覺(jué)得這些接口有些奇怪。這是因?yàn)?DOM 并不是為 JavaScript 而設(shè)計(jì)的,它嘗試成為一組語(yǔ)言中立的接口,確保也可用于其他系統(tǒng)中,不只是 HTML,還有 XML。XML 是一種通用數(shù)據(jù)格式,語(yǔ)法與 HTML 相近。

這就比較糟糕了。一般情況下標(biāo)準(zhǔn)都是非常易于使用的。但在這里其優(yōu)勢(shì)(跨語(yǔ)言的一致性)并不明顯。相較于為不同語(yǔ)言提供類似的接口,如果能夠?qū)⒔涌谂c開(kāi)發(fā)者使用的語(yǔ)言進(jìn)行適當(dāng)集成,可以為開(kāi)發(fā)者節(jié)省大量時(shí)間。

我們舉例來(lái)說(shuō)明一下集成問(wèn)題。比如 DOM 中每個(gè)元素都有childNodes屬性。該屬性是一個(gè)類數(shù)組對(duì)象,有length屬性,也可以使用數(shù)字標(biāo)簽訪問(wèn)對(duì)應(yīng)的子節(jié)點(diǎn)。但該屬性是NodeList類型的實(shí)例,而不是真正的數(shù)組,因此該類型沒(méi)有諸如slicemap之類的方法。

有些問(wèn)題是由不好的設(shè)計(jì)導(dǎo)致的。例如,我們無(wú)法在創(chuàng)建新的節(jié)點(diǎn)的同時(shí)立即為其添加子節(jié)點(diǎn)和屬性。相反,你首先需要?jiǎng)?chuàng)建節(jié)點(diǎn),然后使用副作用,將子節(jié)點(diǎn)和屬性逐個(gè)添加到節(jié)點(diǎn)中。大量使用 DOM 的代碼通常較長(zhǎng)、重復(fù)和丑陋。

但這些問(wèn)題并非無(wú)法改善。因?yàn)?JavaScript 允許我們構(gòu)建自己的抽象,可以設(shè)計(jì)改進(jìn)方式來(lái)表達(dá)你正在執(zhí)行的操作。 許多用于瀏覽器編程的庫(kù)都附帶這些工具。

沿著樹(shù)移動(dòng)

DOM 節(jié)點(diǎn)包含了許多指向相鄰節(jié)點(diǎn)的鏈接。下面的圖表展示了這一點(diǎn)。

https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/img/14-3.svg

盡管圖表中每種類型的節(jié)點(diǎn)只顯示出一條鏈接,但每個(gè)節(jié)點(diǎn)都有parentNode屬性,指向一個(gè)節(jié)點(diǎn),它是這個(gè)節(jié)點(diǎn)的一部分。類似的,每個(gè)元素節(jié)點(diǎn)(節(jié)點(diǎn)類型為 1)均包含childNodes屬性,該屬性指向一個(gè)類數(shù)組對(duì)象,用于保存其子節(jié)點(diǎn)。

理論上,你可以通過(guò)父子之間的鏈接移動(dòng)到樹(shù)中的任何地方。但 JavaScript 也提供了一些更加方便的額外鏈接。firstChild屬性和lastChild屬性分別指向第一個(gè)子節(jié)點(diǎn)和最后一個(gè)子節(jié)點(diǎn),若沒(méi)有子節(jié)點(diǎn)則值為null。類似的,previousSiblingnextSibling指向相鄰節(jié)點(diǎn),分別指向擁有相同父親的前一個(gè)節(jié)點(diǎn)和后一個(gè)節(jié)點(diǎn)。對(duì)于第一個(gè)子節(jié)點(diǎn),previousSiblingnull,而最后一個(gè)子節(jié)點(diǎn)的nextSibling則是null。

也存在children屬性,它就像childNodes,但只包含元素(類型為 1)子節(jié)點(diǎn),而不包含其他類型的子節(jié)點(diǎn)。 當(dāng)你對(duì)文本節(jié)點(diǎn)不感興趣時(shí),這可能很有用。

處理像這樣的嵌套數(shù)據(jù)結(jié)構(gòu)時(shí),遞歸函數(shù)通常很有用。 以下函數(shù)在文檔中掃描包含給定字符串的文本節(jié)點(diǎn),并在找到一個(gè)時(shí)返回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

因?yàn)?code>childNodes不是真正的數(shù)組,所以我們不能用for/of來(lái)遍歷它,并且必須使用普通的for循環(huán)遍歷索引范圍。

文本節(jié)點(diǎn)的nodeValue屬性保存它所表示的文本字符串。

查找元素

使用父節(jié)點(diǎn)、子節(jié)點(diǎn)和兄弟節(jié)點(diǎn)之間的連接遍歷節(jié)點(diǎn)確實(shí)非常實(shí)用。但是如果我們只想查找文檔中的特定節(jié)點(diǎn),那么從document.body開(kāi)始盲目沿著硬編碼的鏈接路徑查找節(jié)點(diǎn)并非良策。如果程序通過(guò)樹(shù)結(jié)構(gòu)定位節(jié)點(diǎn),就需要依賴于文檔的具體結(jié)構(gòu),而文檔結(jié)構(gòu)隨后可能發(fā)生變化。另一個(gè)復(fù)雜的因素是 DOM 會(huì)為不同節(jié)點(diǎn)之間的空白字符創(chuàng)建對(duì)應(yīng)的文本節(jié)點(diǎn)。例如示例文檔中的body標(biāo)簽不止包含 3 個(gè)子節(jié)點(diǎn)(<h1>和兩個(gè)<p>元素),其實(shí)包含 7 個(gè)子節(jié)點(diǎn):這三個(gè)節(jié)點(diǎn)、三個(gè)節(jié)點(diǎn)前后的空格、以及元素之間的空格。

因此,如果你想獲取文檔中某個(gè)鏈接的href屬性,最好不要去獲取文檔body元素中第六個(gè)子節(jié)點(diǎn)的第二個(gè)子節(jié)點(diǎn),而最好直接獲取文檔中的第一個(gè)鏈接,而且這樣的操作確實(shí)可以實(shí)現(xiàn)。

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

所有元素節(jié)點(diǎn)都包含getElementsByTagName方法,用于從所有后代節(jié)點(diǎn)中(直接或間接子節(jié)點(diǎn))搜索包含給定標(biāo)簽名的節(jié)點(diǎn),并返回一個(gè)類數(shù)組的對(duì)象。

你也可以使用document.getElementById來(lái)尋找包含特定id屬性的某個(gè)節(jié)點(diǎn)。

<p>My ostrich Gertrude:</p>
<p><img id="gertrude" src="https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/img/ostrich.png"></p>

<script>
  let ostrich = document.getElementById("gertrude");
  console.log(ostrich.src);
</script>

第三個(gè)類似的方法是getElementsByClassName,它與getElementsByTagName類似,會(huì)搜索元素節(jié)點(diǎn)的內(nèi)容并獲取所有包含特定class屬性的元素。

修改文檔

幾乎所有 DOM 數(shù)據(jù)結(jié)構(gòu)中的元素都可以被修改。文檔樹(shù)的形狀可以通過(guò)改變父子關(guān)系來(lái)修改。 節(jié)點(diǎn)的remove方法將它們從當(dāng)前父節(jié)點(diǎn)中移除。appendChild方法可以添加子節(jié)點(diǎn),并將其放置在子節(jié)點(diǎn)列表末尾,而insertBefore則將第一個(gè)參數(shù)表示的節(jié)點(diǎn)插入到第二個(gè)參數(shù)表示的節(jié)點(diǎn)前面。

<p>One</p>
<p>Two</p>
<p>Three</p>

<script>
  let paragraphs = document.body.getElementsByTagName("p");
  document.body.insertBefore(paragraphs[2], paragraphs[0]);
</script>

每個(gè)節(jié)點(diǎn)只能存在于文檔中的某一個(gè)位置。因此,如果將段落Three插入到段落One前,會(huì)將該節(jié)點(diǎn)從文檔末尾移除并插入到文檔前面,最后結(jié)果為Three/One/Two。所有將節(jié)點(diǎn)插入到某處的方法都有這種副作用——會(huì)將其從當(dāng)前位置移除(如果存在的話)。

replaceChild方法用于將一個(gè)子節(jié)點(diǎn)替換為另一個(gè)子節(jié)點(diǎn)。該方法接受兩個(gè)參數(shù),第一個(gè)參數(shù)是新節(jié)點(diǎn),第二個(gè)參數(shù)是待替換的節(jié)點(diǎn)。待替換的節(jié)點(diǎn)必須是該方法調(diào)用者的子節(jié)點(diǎn)。這里需要注意,replaceChildinsertBefore都將新節(jié)點(diǎn)作為第一個(gè)參數(shù)。

創(chuàng)建節(jié)點(diǎn)

假設(shè)我們要編寫一個(gè)腳本,將文檔中的所有圖像(<img>標(biāo)簽)替換為其alt屬性中的文本,該文本指定了圖像的文字替代表示。

這不僅涉及刪除圖像,還涉及添加新的文本節(jié)點(diǎn),并替換原有圖像節(jié)點(diǎn)。為此我們使用document.createTextNode方法。

<p>The <img src="https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/img/cat.png" alt="Cat"> in the
  <img src="https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/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>

給定一個(gè)字符串,createTextNode為我們提供了一個(gè)文本節(jié)點(diǎn),我們可以將它插入到文檔中,來(lái)使其顯示在屏幕上。

該循環(huán)從列表末尾開(kāi)始遍歷圖像。我們必須這樣反向遍歷列表,因?yàn)?code>getElementsByTagName之類的方法返回的節(jié)點(diǎn)列表是動(dòng)態(tài)變化的。該列表會(huì)隨著文檔改變還改變。若我們從列表頭開(kāi)始遍歷,移除掉第一個(gè)圖像會(huì)導(dǎo)致列表丟失其第一個(gè)元素,第二次循環(huán)時(shí),因?yàn)榧系拈L(zhǎng)度此時(shí)為 1,而i也為 1,所以循環(huán)會(huì)停止。

如果你想要獲得一個(gè)固定的節(jié)點(diǎn)集合,可以使用數(shù)組的Array.from方法將其轉(zhuǎn)換成實(shí)際數(shù)組。

let arrayish = {0: "one", 1: "two", length: 2};
let array = Array.from(arrayish);
console.log(array.map(s => s.toUpperCase()));
// → ["ONE", "TWO"]

你可以使用document.createElement方法創(chuàng)建一個(gè)元素節(jié)點(diǎn)。該方法接受一個(gè)標(biāo)簽名,返回一個(gè)新的空節(jié)點(diǎn),節(jié)點(diǎn)類型由標(biāo)簽名指定。

下面的示例定義了一個(gè)elt工具,用于創(chuàng)建一個(gè)新的元素節(jié)點(diǎn),并將其剩余參數(shù)當(dāng)作該節(jié)點(diǎn)的子節(jié)點(diǎn)。接著使用該函數(shù)為引用添加來(lái)源信息。

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

屬性

我們可以通過(guò)元素的 DOM 對(duì)象的同名屬性去訪問(wèn)元素的某些屬性,比如鏈接的href屬性。這僅限于最常用的標(biāo)準(zhǔn)屬性。

HTML 允許你在節(jié)點(diǎn)上設(shè)定任何屬性。這一特性非常有用,因?yàn)檫@樣你就可以在文檔中存儲(chǔ)額外信息。你自己創(chuàng)建的屬性不會(huì)出現(xiàn)在元素節(jié)點(diǎn)的屬性中。你必須使用getAttributesetAttribute方法來(lái)訪問(wèn)這些屬性。

<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-前綴,來(lái)確保它們不與任何其他屬性發(fā)生沖突。

這里有一個(gè)常用的屬性:class。該屬性是 JavaScript 中的保留字。因?yàn)槟承v史原因(某些舊版本的 JavaScript 實(shí)現(xiàn)無(wú)法處理和關(guān)鍵字或保留字同名的屬性),訪問(wèn)class的屬性名為className。你也可以使用getAttributesetAttribute方法,使用其實(shí)際名稱class來(lái)訪問(wèn)該屬性。

布局

你可能已經(jīng)注意到不同類型的元素有不同的布局。某些元素,比如段落(<p>)和標(biāo)題(<h1>)會(huì)占據(jù)整個(gè)文檔的寬度,并且在獨(dú)立的一行中渲染。這些元素被稱為塊(Block)元素。其他的元素,比如鏈接(<a><strong>元素則與周圍文本在同一行中渲染。這類元素我們稱之為內(nèi)聯(lián)(Inline)元素。

對(duì)于任意特定文檔,瀏覽器可以根據(jù)每個(gè)元素的類型和內(nèi)容計(jì)算其尺寸與位置等布局信息。接著使用布局來(lái)繪制文檔。

JavaScript 中可以訪問(wèn)元素的尺寸與位置。

屬性offsetWidthoffsetHeight給出元素的起始位置(單位是像素)。像素是瀏覽器中的基本測(cè)量單元。它通常對(duì)應(yīng)于屏幕可以繪制的最小的點(diǎn),但是在現(xiàn)代顯示器上,可以繪制非常小的點(diǎn),這可能不再適用了,并且瀏覽器像素可能跨越多個(gè)顯示點(diǎn)。

同樣,clientWidthclientHeight向你提供元素內(nèi)的空間大小,忽略邊框?qū)挾取?/p>

<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方法是獲取屏幕中某個(gè)元素精確位置的最有效方法。該方法返回一個(gè)對(duì)象,包含top、bottomleftright四個(gè)屬性,表示元素相對(duì)于屏幕左上角的位置(單位是像素)。若你想要知道其相對(duì)于整個(gè)文檔的位置,必須加上其滾動(dòng)位置,你可以在pageXOffsetpageYOffset綁定中找到。

我們還需要花些力氣才能完成文檔的排版工作。為了加快速度,每次你改變它時(shí),瀏覽器引擎不會(huì)立即重新繪制整個(gè)文檔,而是盡可能等待并推遲重繪操作。當(dāng)一個(gè)修改文檔的 JavaScript 程序結(jié)束時(shí),瀏覽器會(huì)計(jì)算新的布局,并在屏幕上顯示修改過(guò)的文檔。若程序通過(guò)讀取offsetHeightgetBoundingClientRect這類屬性獲取某些元素的位置或尺寸時(shí),為了提供正確的信息,瀏覽器也需要計(jì)算布局。

如果程序反復(fù)讀取 DOM 布局信息或修改 DOM,會(huì)強(qiáng)制引發(fā)大量布局計(jì)算,導(dǎo)致運(yùn)行非常緩慢。下面的代碼展示了一個(gè)示例。該示例包含兩個(gè)不同的程序,使用X字符構(gòu)建一條線,其長(zhǎng)度是 2000 像素,并計(jì)算每個(gè)任務(wù)的時(shí)間。

<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 元素的繪制是不同的。一些元素顯示為塊,一些則是以內(nèi)聯(lián)方式顯示。我們還可以添加一些樣式,比如使用<strong>加粗內(nèi)容,或使用<a>使內(nèi)容變成藍(lán)色,并添加下劃線。

<img>標(biāo)簽顯示圖片的方式或點(diǎn)擊標(biāo)簽<a>時(shí)跳轉(zhuǎn)的鏈接都和元素類型緊密相關(guān)。但元素的默認(rèn)樣式,比如文本的顏色、是否有下劃線,都是可以改變的。這里給出使用style屬性的示例。

<p><a href=".">Normal link</a></p>
<p><a href="." style="color: green">Green link</a></p>

樣式屬性可以包含一個(gè)或多個(gè)聲明,格式為屬性(比如color)后跟著一個(gè)冒號(hào)和一個(gè)值(比如green)。當(dāng)包含更多聲明時(shí),不同屬性之間必須使用分號(hào)分隔,比如color:red;border:none。

文檔的很多方面會(huì)受到樣式的影響。例如,display屬性控制一個(gè)元素是否顯示為塊元素或內(nèi)聯(lián)元素。

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標(biāo)簽會(huì)結(jié)束其所在的那一行,因?yàn)閴K元素是不會(huì)和周圍文本內(nèi)聯(lián)顯示的。最后一個(gè)標(biāo)簽完全不會(huì)顯示出來(lái),因?yàn)?code>display:none會(huì)阻止一個(gè)元素呈現(xiàn)在屏幕上。這是隱藏元素的一種方式。更好的方式是將其從文檔中完全移除,因?yàn)樯院髮⑵浞呕厝ナ且患芎?jiǎn)單的事情。

JavaScript 代碼可以通過(guò)元素的style屬性操作元素的樣式。該屬性保存了一個(gè)對(duì)象,對(duì)象中存儲(chǔ)了所有可能的樣式屬性,這些屬性的值是字符串,我們可以把字符串寫入屬性,修改某些方面的元素樣式。

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

一些樣式屬性名包含破折號(hào),比如font-family。由于這些屬性的命名不適合在 JavaScript 中使用(你必須寫成style["font-family"]),因此在 JavaScript 中,樣式對(duì)象中的屬性名都移除了破折號(hào),并將破折號(hào)之后的字母大寫(style.fontFamily)。

層疊樣式

我們把 HTML 的樣式化系統(tǒng)稱為 CSS,即層疊樣式表(Cascading Style Sheets)。樣式表是一系列規(guī)則,指出如何為文檔中元素添加樣式??梢栽?code><style>標(biāo)簽中寫入 CSS。

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

所謂層疊指的是將多條規(guī)則組合起來(lái)產(chǎn)生元素的最終樣式。在示例中,<strong>標(biāo)簽的默認(rèn)樣式font-weight:bold,會(huì)被<style>標(biāo)簽中的規(guī)則覆蓋,并為<strong>標(biāo)簽樣式添加font-stylecolor屬性。

當(dāng)多條規(guī)則重復(fù)定義同一屬性時(shí),最近的規(guī)則會(huì)擁有最高的優(yōu)先級(jí)。因此如果<style>標(biāo)簽中的規(guī)則包含font-weight:normal,違背了默認(rèn)的font-weight規(guī)則,那么文本將會(huì)顯示為普通樣式,而非粗體。屬性style中的樣式會(huì)直接作用于節(jié)點(diǎn),而且往往擁有最高優(yōu)先級(jí)。

我們可以在 CSS 規(guī)則中使用標(biāo)簽名來(lái)定位標(biāo)簽。規(guī)則.abc指的是所有class屬性中包含abc的元素。規(guī)則#xyz作用于id屬性為xyz(應(yīng)當(dāng)在文檔中唯一存在)的元素。

.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;
}

優(yōu)先級(jí)規(guī)則偏向于最近定義的規(guī)則,僅在規(guī)則特殊性相同時(shí)適用。規(guī)則的特殊性用于衡量該規(guī)則描述匹配元素時(shí)的準(zhǔn)確性。特殊性取決于規(guī)則中的元素?cái)?shù)量和類型(tag、classid)。例如,目標(biāo)規(guī)則p.a比目標(biāo)規(guī)則p.a更具體,因此有更高優(yōu)先級(jí)。

p>a這種寫法將樣式作用于<p>標(biāo)簽的直系子節(jié)點(diǎn)。類似的,p a應(yīng)用于所有的<p>標(biāo)簽中的<a>標(biāo)簽,無(wú)論是否是直系子節(jié)點(diǎn)。

查詢選擇器

本書不會(huì)使用太多樣式表。盡管理解樣式表對(duì)瀏覽器程序設(shè)計(jì)至關(guān)重要,想要正確解釋所有瀏覽器支持的屬性及其使用方式,可能需要兩到三本書才行。

我介紹選擇器語(yǔ)法(用在樣式表中,確定樣式作用的元素)的主要原因是這種微型語(yǔ)言同時(shí)也是一種高效的 DOM 元素查找方式。

document對(duì)象和元素節(jié)點(diǎn)中都定義了querySelectorAll方法,該方法接受一個(gè)選擇器字符串并返回類數(shù)組對(duì)象,返回的對(duì)象中包含所有匹配的元素。

<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返回的對(duì)象不是動(dòng)態(tài)變更的。修改文檔時(shí)其內(nèi)容不會(huì)被修改。但它仍然不是一個(gè)真正的數(shù)組,所以如果你打算將其看做真的數(shù)組,你仍然需要調(diào)用Array.from。

querySelector方法(沒(méi)有All)與querySelectorAll作用相似。如果只想尋找某一個(gè)特殊元素,該方法非常有用。該方法只返回第一個(gè)匹配的元素,如果沒(méi)有匹配的元素則返回null。

位置與動(dòng)畫

position樣式屬性是一種強(qiáng)大的布局方法。默認(rèn)情況下,該屬性值為static,表示元素處于文檔中的默認(rèn)位置。若該屬性設(shè)置為relative,該元素在文檔中依然占據(jù)空間,但此時(shí)其topleft樣式屬性則是相對(duì)于常規(guī)位置的偏移。若position設(shè)置為absolute,會(huì)將元素從默認(rèn)文檔流中移除,該元素將不再占據(jù)空間,而會(huì)與其他元素重疊。其topleft屬性則是相對(duì)其最近的閉合元素的偏移,其中position屬性的值不是static。如果沒(méi)有任何閉合元素存在,則是相對(duì)于整個(gè)文檔的偏移。

我們可以使用該屬性創(chuàng)建一個(gè)動(dòng)畫。下面的文檔用于顯示一幅貓的圖片,該圖片會(huì)沿著橢圓軌跡移動(dòng)。

<p style="text-align: center">
  <img src="https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/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;
    }
    lastTime = time;
    cat.style.top = (Math.sin(angle) * 20) + "px";
    cat.style.left = (Math.cos(angle) * 200) + "px";
    requestAnimationFrame(newTime => animate(newTime, time));
  }
  requestAnimationFrame(animate);
</script>

我們的圖像在頁(yè)面中央,positionrelative。為了移動(dòng)這只貓,我們需要不斷更新圖像的topleft樣式。

腳本使用requestAnimationFrame在每次瀏覽器準(zhǔn)備重繪屏幕時(shí)調(diào)用animate函數(shù)。animate函數(shù)再次調(diào)用requestAnimationFrame以準(zhǔn)備下一次更新。當(dāng)瀏覽器窗口(或標(biāo)簽)激活時(shí),更新頻率大概為 60 次每秒,這種頻率可以生成美觀的動(dòng)畫。

若我們只是在循環(huán)中更新 DOM,頁(yè)面會(huì)靜止不動(dòng),頁(yè)面上也不會(huì)顯示任何東西。瀏覽器不會(huì)在執(zhí)行 JavaScript 程序時(shí)刷新顯示內(nèi)容,也不允許頁(yè)面上的任何交互。這就是我們需要requestAnimationFrame的原因,該函數(shù)用于告知瀏覽器 JavaScript 程序目前已經(jīng)完成工作,因此瀏覽器可以繼續(xù)執(zhí)行其他任務(wù),比如刷新屏幕,響應(yīng)用戶動(dòng)作。

我們將動(dòng)畫生成函數(shù)作為參數(shù)傳遞給requestAnimationFrame。為了確保每一毫秒貓的移動(dòng)是穩(wěn)定的,而且動(dòng)畫是圓滑的,它基于一個(gè)速度,角度以這個(gè)速度改變這一次與上一次函數(shù)運(yùn)行的差。如果僅僅每次走幾步,貓的動(dòng)作可能略顯遲鈍,例如,另一個(gè)在相同電腦上的繁重任務(wù)可能使得該函數(shù)零點(diǎn)幾秒之后才會(huì)運(yùn)行一次。

我們使用三角函數(shù)Math.cosMath.sin來(lái)使貓沿著圓弧移動(dòng)。你可能不太熟悉這些計(jì)算,我在這里簡(jiǎn)要介紹它們,因?yàn)槟銜?huì)在這本書中偶爾遇到。

Math.cosMath.sin非常實(shí)用,我們可以利用一個(gè) 1 個(gè)弧度,計(jì)算出以點(diǎn)(0,0為圓心的圓上特定點(diǎn)的位置。兩個(gè)函數(shù)都將參數(shù)解釋為圓上的一個(gè)位置,0 表示圓上最右側(cè)那個(gè)點(diǎn),一直逆時(shí)針遞增到(大概是 6.28),正好走過(guò)整個(gè)圓。Math.cos可以計(jì)算出圓上某一點(diǎn)對(duì)應(yīng)的x坐標(biāo),而Math.sin則計(jì)算出y坐標(biāo)。超過(guò)或小于 0 的位置(或角度)都是合法的。因?yàn)榛《仁茄h(huán)重復(fù)的,a+2πa的角度相同。

用于測(cè)量角度的單位稱為弧度 - 一個(gè)完整的圓弧是個(gè)弧度,類似于以角度度量時(shí)的 360 度。 常量π在 JavaScript 中為Math.PI。

https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/img/14-4.svg

貓的動(dòng)畫代碼保存了一個(gè)名為angle的計(jì)數(shù)器,該綁定記錄貓?jiān)趫A上的角度,而且每當(dāng)調(diào)用animate函數(shù)時(shí),增加該計(jì)數(shù)器的值。我們接著使用這個(gè)角度來(lái)計(jì)算圖像元素的當(dāng)前位置。top樣式是Math.sin的結(jié)果乘以 20,表示圓中的垂直弧度。left樣式是 Math.cos 的結(jié)果乘以200,因此圓的寬度大于其高度,導(dǎo)致最后貓會(huì)沿著橢圓軌跡移動(dòng)。

這里需要注意的是樣式的值一般需要指定單位。本例中,我們?cè)跀?shù)字后添加px來(lái)告知瀏覽器以像素為計(jì)算單位(而非厘米,ems,或其他單位)。我們很容易遺漏這個(gè)單位。如果我們沒(méi)有為樣式中的數(shù)字加上單位,瀏覽器最后會(huì)忽略掉該樣式,除非數(shù)字是 0,在這種情況下使用什么單位,其結(jié)果都是一樣的。

本章小結(jié)

JavaScript 程序可以通過(guò)名為 DOM 的數(shù)據(jù)結(jié)構(gòu),查看并修改瀏覽器中顯示的文檔。該數(shù)據(jù)結(jié)構(gòu)描述了瀏覽器文檔模型,而 JavaScript 程序可以通過(guò)修改該數(shù)據(jù)結(jié)構(gòu)來(lái)修改瀏覽器展示的文檔。

DOM 的組織就像樹(shù)一樣,DOM 根據(jù)文檔結(jié)構(gòu)來(lái)層次化地排布元素。描述元素的對(duì)象包含很多屬性,比如parentNodechildNodes這兩個(gè)屬性可以用來(lái)遍歷 DOM 樹(shù)。

我們可以通過(guò)樣式來(lái)改變文檔的顯示方式,可以直接在節(jié)點(diǎn)上附上樣式,也可以編寫匹配節(jié)點(diǎn)的規(guī)則。樣式包含許多不同的屬性,比如colordisplay。JavaScript 代碼可以直接通過(guò)節(jié)點(diǎn)的style屬性操作元素的樣式。

習(xí)題

創(chuàng)建一張表

HTML 表格使用以下標(biāo)簽結(jié)構(gòu)構(gòu)建:

<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>標(biāo)簽中,每一行包含一個(gè)<tr>標(biāo)簽。<tr>標(biāo)簽內(nèi)部則是單元格元素,分為表頭(<th>)和常規(guī)單元格(<td>)。

給定一個(gè)山的數(shù)據(jù)集,一個(gè)包含name,heightplace屬性的對(duì)象數(shù)組,為枚舉對(duì)象的表格生成 DOM 結(jié)構(gòu)。 每個(gè)鍵應(yīng)該有一列,每個(gè)對(duì)象有一行,外加一個(gè)頂部帶有<th>元素的標(biāo)題行,列出列名。

編寫這個(gè)程序,以便通過(guò)獲取數(shù)據(jù)中第一個(gè)對(duì)象的屬性名稱,從對(duì)象自動(dòng)產(chǎn)生列。

將所得表格添加到id屬性為"mountains"的元素,以便它在文檔中可見(jiàn)。

當(dāng)你完成后,將元素的style.textAlign屬性設(shè)置為right,將包含數(shù)值的單元格右對(duì)齊。

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

通過(guò)標(biāo)簽名獲取元素

document.getElementsByTagName方法返回帶有特定標(biāo)簽名稱的所有子元素。實(shí)現(xiàn)該函數(shù),這里注意是函數(shù)不是方法。該函數(shù)的參數(shù)是一個(gè)節(jié)點(diǎn)和字符串(標(biāo)簽名稱),并返回一個(gè)數(shù)組,該數(shù)組包含所有帶有特定標(biāo)簽名稱的所有后代元素節(jié)點(diǎn)。

你可以使用nodeName屬性從 DOM 元素中獲取標(biāo)簽名稱。但這里需要注意,使用tagName獲取的標(biāo)簽名稱是全大寫形式??梢允褂米址?code>toLowerCase或toUpperCase來(lái)解決這個(gè)問(wèn)題。

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

貓的帽子

擴(kuò)展一下之前定義的用來(lái)繪制貓的動(dòng)畫函數(shù),讓貓和它的帽子沿著橢圓形軌道邊(帽子永遠(yuǎn)在貓的對(duì)面)移動(dòng)。

你也可以嘗試讓帽子環(huán)繞著貓移動(dòng),或修改成其他有趣的動(dòng)畫。

為了便于定位多個(gè)對(duì)象,一個(gè)比較好的方法是使用絕對(duì)(absolute)定位。這就意味著topleft屬性是相對(duì)于文檔左上角的坐標(biāo)。你可以簡(jiǎn)單地在坐標(biāo)上加上一個(gè)固定數(shù)字,以避免出現(xiàn)負(fù)的坐標(biāo),它會(huì)使圖像移出可見(jiàn)頁(yè)面。

<style>body { min-height: 200px }</style>
<img src="https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/img/cat.png" id="cat" style="position: absolute">
<img src="https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/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>
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • ??DOM 1 級(jí)主要定義的是 HTML 和 XML 文檔的底層結(jié)構(gòu)。 ??DOM2 和 DOM3 級(jí)則在這個(gè)結(jié)構(gòu)...
    霜天曉閱讀 1,594評(píng)論 1 3
  • 問(wèn)答題47 /72 常見(jiàn)瀏覽器兼容性問(wèn)題與解決方案? 參考答案 (1)瀏覽器兼容問(wèn)題一:不同瀏覽器的標(biāo)簽?zāi)J(rèn)的外補(bǔ)...
    _Yfling閱讀 14,113評(píng)論 1 92
  • ??DOM(文檔對(duì)象模型)是針對(duì) HTML 和 XML 文檔的一個(gè) API(應(yīng)用程序編程接口)。 ??DOM 描繪...
    霜天曉閱讀 3,863評(píng)論 0 7
  • 第3章 基本概念 3.1 語(yǔ)法 3.2 關(guān)鍵字和保留字 3.3 變量 3.4 數(shù)據(jù)類型 5種簡(jiǎn)單數(shù)據(jù)類型:Unde...
    RickCole閱讀 5,502評(píng)論 0 21
  • 節(jié)點(diǎn)層次 DOM 可以將任何 HTML 和 XML 文檔描繪成一個(gè)由多層節(jié)點(diǎn)構(gòu)成的結(jié)構(gòu)。節(jié)點(diǎn)分為幾種不同的類型,每...
    云之外閱讀 579評(píng)論 0 1

友情鏈接更多精彩內(nèi)容