<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                ## 十四、文檔對象模型 > 原文:[The Document Object Model](https://eloquentjavascript.net/14_dom.html) > > 譯者:[飛龍](https://github.com/wizardforcel) > > 協議:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) > > 自豪地采用[谷歌翻譯](https://translate.google.cn/) > > 部分參考了[《JavaScript 編程精解(第 2 版)》](https://book.douban.com/subject/26707144/) > 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://img.kancloud.cn/10/6a/106ac576c96cf5457f6f7dc2dea69c1c_490x310.jpg) 當你在瀏覽器中打開網頁時,瀏覽器會接收網頁的 HTML 文本并進行解析,其解析方式與第 11 章中介紹的解析器非常相似。瀏覽器構建文檔結構的模型,并使用該模型在屏幕上繪制頁面。 JavaScript 在其沙箱中提供了將文本轉換成文檔對象模型的功能。它是你可以讀取或者修改的數據結構。模型是一個所見即所得的數據結構,改變模型會使得屏幕上的頁面產生相應變化。 ## 文檔結構 你可以將 HTML 文件想象成一系列嵌套的箱子。諸如`<body>`和`</body>`之類的標簽會將其他標簽包圍起來,而包含在內部的標簽也可以包含其他的標簽和文本。這里給出上一章中已經介紹過的示例文件。 ```html <!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> ``` 該頁面結構如下所示。 ![](https://img.kancloud.cn/b3/04/b304119aa7e5401f4134a72f57072fb8.svg) 瀏覽器使用與該形狀對應的數據結構來表示文檔。每個盒子都是一個對象,我們可以和這些對象交互,找出其中包含的盒子與文本。我們將這種表示方式稱為文檔對象模型(Document Object Model),或簡稱 DOM。 我們可以通過全局綁定`document`來訪問這些對象。該對象的`documentElement`屬性引用了`<html>`標簽對象。由于每個 HTML 文檔都有一個頭部和一個主體,它還具有`head`和`body`屬性,指向這些元素。 ## 樹 回想一下第 12 章中提到的語法樹。其結構與瀏覽器文檔的結構極為相似。每個節點使用`children`引用其他節點,而每個子節點又有各自的`children`。其形狀是一種典型的嵌套結構,每個元素可以包含與其自身相似的子元素。 如果一個數據結構有分支結構,而且沒有任何環路(一個節點不能直接或間接包含自身),并且有一個單一、定義明確的“根節點”,那么我們將這種數據結構稱之為樹。就 DOM 來講,`document.documentElement`就是其根節點。 在計算機科學中,樹的應用極為廣泛。除了表現諸如 HTML 文檔或程序之類的遞歸結構,樹還可以用于維持數據的有序集合,因為在樹中尋找或插入一個節點往往比在數組中更高效。 一棵典型的樹有不同類型的節點。Egg 語言的語法樹有標識符、值和應用節點。應用節點常常包含子節點,而標識符、值則是葉子節點,也就是沒有子節點的節點。 DOM中也是一樣。元素(表示 HTML 標簽)的節點用于確定文檔結構。這些節點可以包含子節點。這類節點中的一個例子是`document.body`。其中一些子節點可以是葉子節點,比如文本片段或注釋。 每個 DOM 節點對象都包含`nodeType`屬性,該屬性包含一個標識節點類型的代碼(數字)。元素的值為 1,DOM 也將該值定義成一個常量屬性`document.ELEMENT_NODE`。文本節點(表示文檔中的一段文本)代碼為 3(`document.TEXT_NODE`)。注釋的代碼為 8(`document.COMMENT_NODE`)。 因此我們可以使用另一種方法來表示文檔樹: ![](https://img.kancloud.cn/b9/d2/b9d2d4d93b9f6431af0201c9b9e77b2f.svg) 葉子節點是文本節點,而箭頭則指出了節點之間的父子關系。 ## 標準 并非只有 JavaScript 會使用數字代碼來表示節點類型。本章隨后將會展示其他的 DOM 接口,你可能會覺得這些接口有些奇怪。這是因為 DOM 并不是為 JavaScript 而設計的,它嘗試成為一組語言中立的接口,確保也可用于其他系統中,不只是 HTML,還有 XML。XML 是一種通用數據格式,語法與 HTML 相近。 這就比較糟糕了。一般情況下標準都是非常易于使用的。但在這里其優勢(跨語言的一致性)并不明顯。相較于為不同語言提供類似的接口,如果能夠將接口與開發者使用的語言進行適當集成,可以為開發者節省大量時間。 我們舉例來說明一下集成問題。比如 DOM 中每個元素都有`childNodes`屬性。該屬性是一個類數組對象,有`length`屬性,也可以使用數字標簽訪問對應的子節點。但該屬性是`NodeList`類型的實例,而不是真正的數組,因此該類型沒有諸如`slice`和`map`之類的方法。 有些問題是由不好的設計導致的。例如,我們無法在創建新的節點的同時立即為其添加子節點和屬性。相反,你首先需要創建節點,然后使用副作用,將子節點和屬性逐個添加到節點中。大量使用 DOM 的代碼通常較長、重復和丑陋。 但這些問題并非無法改善。因為 JavaScript 允許我們構建自己的抽象,可以設計改進方式來表達你正在執行的操作。 許多用于瀏覽器編程的庫都附帶這些工具。 ## 沿著樹移動 DOM 節點包含了許多指向相鄰節點的鏈接。下面的圖表展示了這一點。 ![](https://img.kancloud.cn/22/73/22731c6defda15257ad6ea4b2e339646.svg) 盡管圖表中每種類型的節點只顯示出一條鏈接,但每個節點都有`parentNode`屬性,指向一個節點,它是這個節點的一部分。類似的,每個元素節點(節點類型為 1)均包含`childNodes`屬性,該屬性指向一個類數組對象,用于保存其子節點。 理論上,你可以通過父子之間的鏈接移動到樹中的任何地方。但 JavaScript 也提供了一些更加方便的額外鏈接。`firstChild`屬性和`lastChild`屬性分別指向第一個子節點和最后一個子節點,若沒有子節點則值為`null`。類似的,`previousSibling`和`nextSibling`指向相鄰節點,分別指向擁有相同父親的前一個節點和后一個節點。對于第一個子節點,`previousSibling`是`null`,而最后一個子節點的`nextSibling`則是`null`。 也存在`children`屬性,它就像`childNodes`,但只包含元素(類型為 1)子節點,而不包含其他類型的子節點。 當你對文本節點不感興趣時,這可能很有用。 處理像這樣的嵌套數據結構時,遞歸函數通常很有用。 以下函數在文檔中掃描包含給定字符串的文本節點,并在找到一個時返回`true`: ```html 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`屬性保存它所表示的文本字符串。 ## 查找元素 使用父節點、子節點和兄弟節點之間的連接遍歷節點確實非常實用。但是如果我們只想查找文檔中的特定節點,那么從`document.body`開始盲目沿著硬編碼的鏈接路徑查找節點并非良策。如果程序通過樹結構定位節點,就需要依賴于文檔的具體結構,而文檔結構隨后可能發生變化。另一個復雜的因素是 DOM 會為不同節點之間的空白字符創建對應的文本節點。例如示例文檔中的`body`標簽不止包含 3 個子節點(`<h1>`和兩個`<p>`元素),其實包含 7 個子節點:這三個節點、三個節點前后的空格、以及元素之間的空格。 因此,如果你想獲取文檔中某個鏈接的`href`屬性,最好不要去獲取文檔`body`元素中第六個子節點的第二個子節點,而最好直接獲取文檔中的第一個鏈接,而且這樣的操作確實可以實現。 ```html let link = document.body.getElementsByTagName("a")[0]; console.log(link.href); ``` 所有元素節點都包含`getElementsByTagName`方法,用于從所有后代節點中(直接或間接子節點)搜索包含給定標簽名的節點,并返回一個類數組的對象。 你也可以使用`document.getElementById`來尋找包含特定`id`屬性的某個節點。 ```html <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`則將第一個參數表示的節點插入到第二個參數表示的節點前面。 ```html <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`屬性中的文本,該文本指定了圖像的文字替代表示。 這不僅涉及刪除圖像,還涉及添加新的文本節點,并替換原有圖像節點。為此我們使用`document.createTextNode`方法。 ```html <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`之類的方法返回的節點列表是動態變化的。該列表會隨著文檔改變還改變。若我們從列表頭開始遍歷,移除掉第一個圖像會導致列表丟失其第一個元素,第二次循環時,因為集合的長度此時為 1,而`i`也為 1,所以循環會停止。 如果你想要獲得一個固定的節點集合,可以使用數組的`Array.from`方法將其轉換成實際數組。 ```html 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`工具,用于創建一個新的元素節點,并將其剩余參數當作該節點的子節點。接著使用該函數為引用添加來源信息。 ```html <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> ``` ## 屬性 我們可以通過元素的 DOM 對象的同名屬性去訪問元素的某些屬性,比如鏈接的`href`屬性。這僅限于最常用的標準屬性。 HTML 允許你在節點上設定任何屬性。這一特性非常有用,因為這樣你就可以在文檔中存儲額外信息。你自己創建的屬性不會出現在元素節點的屬性中。你必須使用`getAttribute`和`setAttribute`方法來訪問這些屬性。 ```html <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`來訪問該屬性。 ## 布局 你可能已經注意到不同類型的元素有不同的布局。某些元素,比如段落(`<p>`)和標題(`<h1>`)會占據整個文檔的寬度,并且在獨立的一行中渲染。這些元素被稱為塊(Block)元素。其他的元素,比如鏈接(`<a>`或`<strong>`元素則與周圍文本在同一行中渲染。這類元素我們稱之為內聯(Inline)元素。 對于任意特定文檔,瀏覽器可以根據每個元素的類型和內容計算其尺寸與位置等布局信息。接著使用布局來繪制文檔。 JavaScript 中可以訪問元素的尺寸與位置。 屬性`offsetWidth`和`offsetHeight`給出元素的起始位置(單位是像素)。像素是瀏覽器中的基本測量單元。它通常對應于屏幕可以繪制的最小的點,但是在現代顯示器上,可以繪制非常小的點,這可能不再適用了,并且瀏覽器像素可能跨越多個顯示點。 同樣,`clientWidth`和`clientHeight`向你提供元素內的空間大小,忽略邊框寬度。 ```html <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 像素,并計算每個任務的時間。 ```html <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`屬性的示例。 ```html <p><a href=".">Normal link</a></p> <p><a href="." style="color: green">Green link</a></p> ``` 樣式屬性可以包含一個或多個聲明,格式為屬性(比如`color`)后跟著一個冒號和一個值(比如`green`)。當包含更多聲明時,不同屬性之間必須使用分號分隔,比如`color:red;border:none`。 文檔的很多方面會受到樣式的影響。例如,`display`屬性控制一個元素是否顯示為塊元素或內聯元素。 ```html 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`屬性操作元素的樣式。該屬性保存了一個對象,對象中存儲了所有可能的樣式屬性,這些屬性的值是字符串,我們可以把字符串寫入屬性,修改某些方面的元素樣式。 ```html <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"]`),因此在 JavaScript 中,樣式對象中的屬性名都移除了破折號,并將破折號之后的字母大寫(`style.fontFamily`)。 ## 層疊樣式 我們把 HTML 的樣式化系統稱為 CSS,即層疊樣式表(Cascading Style Sheets)。樣式表是一系列規則,指出如何為文檔中元素添加樣式。可以在`<style>`標簽中寫入 CSS。 ```html <style> strong { font-style: italic; color: gray; } </style> <p>Now <strong>strong text</strong> is italic and gray.</p> ``` 所謂層疊指的是將多條規則組合起來產生元素的最終樣式。在示例中,`<strong>`標簽的默認樣式`font-weight:bold`,會被`<style>`標簽中的規則覆蓋,并為`<strong>`標簽樣式添加`font-style`和`color`屬性。 當多條規則重復定義同一屬性時,最近的規則會擁有最高的優先級。因此如果`<style>`標簽中的規則包含`font-weight:normal`,違背了默認的`font-weight`規則,那么文本將會顯示為普通樣式,而非粗體。屬性`style`中的樣式會直接作用于節點,而且往往擁有最高優先級。 我們可以在 CSS 規則中使用標簽名來定位標簽。規則`.abc`指的是所有`class`屬性中包含`abc`的元素。規則`#xyz`作用于`id`屬性為`xyz`(應當在文檔中唯一存在)的元素。 ```css .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; } ``` 優先級規則偏向于最近定義的規則,僅在規則特殊性相同時適用。規則的特殊性用于衡量該規則描述匹配元素時的準確性。特殊性取決于規則中的元素數量和類型(`tag`、`class`或`id`)。例如,目標規則`p.a`比目標規則`p`或`.a`更具體,因此有更高優先級。 `p>a`這種寫法將樣式作用于`<p>`標簽的直系子節點。類似的,`p a`應用于所有的`<p>`標簽中的`<a>`標簽,無論是否是直系子節點。 ## 查詢選擇器 本書不會使用太多樣式表。盡管理解樣式表對瀏覽器程序設計至關重要,想要正確解釋所有瀏覽器支持的屬性及其使用方式,可能需要兩到三本書才行。 我介紹選擇器語法(用在樣式表中,確定樣式作用的元素)的主要原因是這種微型語言同時也是一種高效的 DOM 元素查找方式。 `document`對象和元素節點中都定義了`querySelectorAll`方法,該方法接受一個選擇器字符串并返回類數組對象,返回的對象中包含所有匹配的元素。 ```html <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`)與`querySelectorAll`作用相似。如果只想尋找某一個特殊元素,該方法非常有用。該方法只返回第一個匹配的元素,如果沒有匹配的元素則返回`null`。 ## 位置與動畫 `position`樣式屬性是一種強大的布局方法。默認情況下,該屬性值為`static`,表示元素處于文檔中的默認位置。若該屬性設置為`relative`,該元素在文檔中依然占據空間,但此時其`top`和`left`樣式屬性則是相對于常規位置的偏移。若`position`設置為`absolute`,會將元素從默認文檔流中移除,該元素將不再占據空間,而會與其他元素重疊。其`top`和`left`屬性則是相對其最近的閉合元素的偏移,其中`position`屬性的值不是`static`。如果沒有任何閉合元素存在,則是相對于整個文檔的偏移。 我們可以使用該屬性創建一個動畫。下面的文檔用于顯示一幅貓的圖片,該圖片會沿著橢圓軌跡移動。 ```html <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; } 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> ``` 我們的圖像在頁面中央,`position`為`relative`。為了移動這只貓,我們需要不斷更新圖像的`top`和`left`樣式。 腳本使用`requestAnimationFrame`在每次瀏覽器準備重繪屏幕時調用`animate`函數。`animate`函數再次調用`requestAnimationFrame`以準備下一次更新。當瀏覽器窗口(或標簽)激活時,更新頻率大概為 60 次每秒,這種頻率可以生成美觀的動畫。 若我們只是在循環中更新 DOM,頁面會靜止不動,頁面上也不會顯示任何東西。瀏覽器不會在執行 JavaScript 程序時刷新顯示內容,也不允許頁面上的任何交互。這就是我們需要`requestAnimationFrame`的原因,該函數用于告知瀏覽器 JavaScript 程序目前已經完成工作,因此瀏覽器可以繼續執行其他任務,比如刷新屏幕,響應用戶動作。 我們將動畫生成函數作為參數傳遞給`requestAnimationFrame`。為了確保每一毫秒貓的移動是穩定的,而且動畫是圓滑的,它基于一個速度,角度以這個速度改變這一次與上一次函數運行的差。如果僅僅每次走幾步,貓的動作可能略顯遲鈍,例如,另一個在相同電腦上的繁重任務可能使得該函數零點幾秒之后才會運行一次。 我們使用三角函數`Math.cos`和`Math.sin`來使貓沿著圓弧移動。你可能不太熟悉這些計算,我在這里簡要介紹它們,因為你會在這本書中偶爾遇到。 `Math.cos`和`Math.sin`非常實用,我們可以利用一個 1 個弧度,計算出以點`(0,0`為圓心的圓上特定點的位置。兩個函數都將參數解釋為圓上的一個位置,0 表示圓上最右側那個點,一直逆時針遞增到`2π`(大概是 6.28),正好走過整個圓。`Math.cos`可以計算出圓上某一點對應的`x`坐標,而`Math.sin`則計算出`y`坐標。超過`2π`或小于 0 的位置(或角度)都是合法的。因為弧度是循環重復的,`a+2π`與`a`的角度相同。 用于測量角度的單位稱為弧度 - 一個完整的圓弧是`2π`個弧度,類似于以角度度量時的 360 度。 常量`π`在 JavaScript 中為`Math.PI`。 ![](https://img.kancloud.cn/d1/08/d108782b62118fa1aac4be725570c1e2.svg) 貓的動畫代碼保存了一個名為`angle`的計數器,該綁定記錄貓在圓上的角度,而且每當調用`animate`函數時,增加該計數器的值。我們接著使用這個角度來計算圖像元素的當前位置。`top`樣式是`Math.sin`的結果乘以 20,表示圓中的垂直弧度。`left`樣式是 Math.cos 的結果乘以`200`,因此圓的寬度大于其高度,導致最后貓會沿著橢圓軌跡移動。 這里需要注意的是樣式的值一般需要指定單位。本例中,我們在數字后添加`px`來告知瀏覽器以像素為計算單位(而非厘米,`ems`,或其他單位)。我們很容易遺漏這個單位。如果我們沒有為樣式中的數字加上單位,瀏覽器最后會忽略掉該樣式,除非數字是 0,在這種情況下使用什么單位,其結果都是一樣的。 ## 本章小結 JavaScript 程序可以通過名為 DOM 的數據結構,查看并修改瀏覽器中顯示的文檔。該數據結構描述了瀏覽器文檔模型,而 JavaScript 程序可以通過修改該數據結構來修改瀏覽器展示的文檔。 DOM 的組織就像樹一樣,DOM 根據文檔結構來層次化地排布元素。描述元素的對象包含很多屬性,比如`parentNode`和`childNodes`這兩個屬性可以用來遍歷 DOM 樹。 我們可以通過樣式來改變文檔的顯示方式,可以直接在節點上附上樣式,也可以編寫匹配節點的規則。樣式包含許多不同的屬性,比如`color`和`display`。JavaScript 代碼可以直接通過節點的`style`屬性操作元素的樣式。 ## 習題 ### 創建一張表 HTML 表格使用以下標簽結構構建: ```html <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`,將包含數值的單元格右對齊。 ```html <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.getElementsByTagName`方法返回帶有特定標簽名稱的所有子元素。實現該函數,這里注意是函數不是方法。該函數的參數是一個節點和字符串(標簽名稱),并返回一個數組,該數組包含所有帶有特定標簽名稱的所有后代元素節點。 你可以使用`nodeName`屬性從 DOM 元素中獲取標簽名稱。但這里需要注意,使用`tagName`獲取的標簽名稱是全大寫形式。可以使用字符串的`toLowerCase`或`toUpperCase`來解決這個問題。 ```html <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> ``` ### 貓的帽子 擴展一下之前定義的用來繪制貓的動畫函數,讓貓和它的帽子沿著橢圓形軌道邊(帽子永遠在貓的對面)移動。 你也可以嘗試讓帽子環繞著貓移動,或修改成其他有趣的動畫。 為了便于定位多個對象,一個比較好的方法是使用絕對(`absolute`)定位。這就意味著`top`和`left`屬性是相對于文檔左上角的坐標。你可以簡單地在坐標上加上一個固定數字,以避免出現負的坐標,它會使圖像移出可見頁面。 ```html <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> ```
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看