【JS 口袋書】第 9 章:使用 JS 操做 HTML 元素

做者:valentinogagliardihtml

譯者:前端小智前端

來源:githubnode


阿(a)裏(li)雲(yun)服務器很便宜火爆,今年比去年便宜,10.24~11.11購買是1年86元,3年229元,能夠點我時行參與git


文檔對象模型(DOM)

JS 有不少地方讓我們吐槽,但沒那麼糟糕。做爲一種在瀏覽器中運行的腳本語言,它對於處理web頁面很是有用。在本中,咱們將看到咱們有哪些方法來交互和修改HTML文檔及其元素。但首先讓咱們來揭開文檔對象模型的神祕面紗。github

文檔對象模型是一個基本概念,它是我們在瀏覽器中所作的一切工做的基礎。但那究竟是什麼? 當我們訪問一個 web 頁面時,瀏覽器會指出如何解釋每一個 HTML 元素。這樣它就建立了 HTML 文檔的虛擬表示,並保存在內存中。HTML 頁面被轉換成樹狀結構,每一個 HTML 元素都變成一個葉子,鏈接到父分支。考慮這個簡單的HTML 頁面:web

<!DOCTYPE html>
<html lang="en">
<head>
    <title>A super simple title!</title>
</head>
<body>
<h1>A super simple web page!</h1>
</body>
</html
複製代碼

當瀏覽器掃描上面的 HTML 時,它建立了一個文檔對象模型,它是HTML結構的鏡像。在這個結構的頂部有一個 document 也稱爲根元素,它包含另外一個元素:htmlhtml 元素包含一個 headhead 又有一個 title。而後是含有 h1body。每一個 HTML 元素由特定類型(也稱爲接口)表示,而且可能包含文本或其餘嵌套元素面試

document (HTMLDocument)
  |
  | --> html (HTMLHtmlElement)
          |  
          | --> head (HtmlHeadElement)
          |       |
          |       | --> title (HtmlTitleElement)
          |                | --> text: "A super simple title!"
          |
          | --> body (HtmlBodyElement)
          |       |
          |       | --> h1 (HTMLHeadingElement)
          |              | --> text: "A super simple web page!"
複製代碼

每一個 HTML 元素都是從 Element 派生而來的,可是它們中的很大一部分是進一步專門化的。我們能夠檢查原型,以查明元素屬於什麼「種類」。例如,h1 元素是 HTMLHeadingElement數組

document.quertSelector('h1').__proto__
// 輸出: HTMLHeadingElement
複製代碼

HTMLHeadingElement 又是 HTMLElement 的「後代」瀏覽器

document.querySelector('h1').__proto__.__proto__

// Output: HTMLElement
複製代碼

Element 是一個通用性很是強的基類,全部 Document 對象下的對象都繼承自它。這個接口描述了全部相同種類的元素所廣泛具備的方法和屬性。一些接口繼承自 Element 而且增長了一些額外功能的接口描述了具體的行爲。例如, HTMLElement 接口是全部 HTML 元素的基本接口,而 SVGElement 接口是全部 SVG 元素的基礎。大多數功能是在這個類的更深層級(hierarchy)的接口中被進一步制定的。服務器

在這一點上(特別是對於初學者),documentwindow 之間可能有些混淆。window 指的是瀏覽器,而 document 指的是當前的 HTML 頁面。window 是一個全局對象,能夠從瀏覽器中運行的任何 JS 代碼直接訪問它。它不是 JS 的「原生」對象,而是由瀏覽器自己公開的。window 有不少屬性和方法,以下所示:

window.alert('Hello world'); // Shows an alert
window.setTimeout(callback, 3000); // Delays execution
window.fetch(someUrl); // makes XHR requests
window.open(); // Opens a new tab
window.location; // Browser location
window.history; // Browser history
window.navigator; // The actual device
window.document; // The current page
複製代碼

因爲這些屬性是全局屬性,所以也能夠省略 window

alert('Hello world'); // Shows an alert
setTimeout(callback, 3000); // Delays execution
fetch(someUrl); // makes XHR requests
open(); // Opens a new tab
location; // Browser location
history; // Browser history
navigator; // The actual device
document; // The current page
複製代碼

你應該已經熟悉其中的一些方法,例如 setTimeout()window.navigator,它能夠獲取當前瀏覽器使用的語言:

if (window.navigator) {
  var lang = window.navigator.language;
  if (lang === "en-US") {
    // show something
  }

  if (lang === "it-IT") {
    // show something else
  }
}
複製代碼

要了解更多 window 上的方法,請查看MDN文檔。在下一節中,我們深刻地研究一下 DOM

節點、元素 和DOM 操做

document 接口有許多實用的方法,好比 querySelector(),它是用於選擇當前 HTML 頁面內的任何 HTML 元素:

document.querySelector('h1');
複製代碼

window 表示當前窗口的瀏覽器,下面的指令與上面的相同:

window.document.querySelector('h1');
複製代碼

無論怎樣,下面的語法更常見,在下一節中我們將大量使用這種形式:

document.methodName();
複製代碼

除了 querySelector() 用於選擇 HTML 元素以外,還有不少更有用的方法

// 返回單個元素
document.getElementById('testimonials'); 

// 返回一個 HTMLCollection
document.getElementsByTagName('p'); 

// 返回一個節點列表
document.querySelectorAll('p');
複製代碼

我們不只能夠選 擇HTML 元素,還能夠交互和修改它們的內部狀態。例如,但願讀取或更改給定元素的內部內容:

// Read or write
document.querySelector('h1').innerHtml; // Read
document.querySelector('h1').innerHtml = ''; // Write! Ouch!
複製代碼

DOM 中的每一個 HTML 元素也是一個**「節點」**,實際上我們能夠像這樣檢查節點類型:

document.querySelector('h1').nodeType;
複製代碼

上述結果返回 1,表示是 Element 類型的節點的標識符。我們還能夠檢查節點名:

document.querySelector('h1').nodeName;

"H1"
複製代碼

這裏,節點名以大寫形式返回。一般咱們處理 DOM 中的四種類型的節點

  • document: 根節點(nodeType 9)

  • 類型爲Element的節點:實際的HTML標籤(nodeType 1),例如 <p><div>

  • 類型屬性的節點:每一個HTML元素的屬性(屬性)

  • Text 類型的節點:元素的實際文本內容(nodeType 3)

因爲元素是節點,節點能夠有屬性(properties )(也稱爲attributes),我們能夠檢查和操做這些屬性:

// 返回 true 或者 false
document.querySelector('a').hasAttribute('href');

// 返回屬性文本內容,或 null
document.querySelector('a').getAttribute('href');

// 設置給定的屬性
document.querySelector('a').setAttribute('href', 'someLink');
複製代碼

前面咱們說過 DOM 是一個相似於樹的結構。這種特性也反映在 HTML 元素上。每一個元素均可能有父元素和子元素,咱們能夠經過檢查元素的某些屬性來查看它們:

// 返回一個 HTMLCollection
document.children;

// 返回一個節點列表
document.childNodes;

// 返回一個節點
document.querySelector('a').parentNode;

// 返回HTML元素
document.querySelector('a').parentElement;
複製代碼

瞭解瞭如何選擇和查詢 HTML 元素。那建立元素又是怎麼樣?爲了建立 Element 類型的新節點,原生 DOM API 提供了 createElement 方法:

var heading = document.createElement('h1');
複製代碼

使用 createTextNode 建立文本節點:

var text = document.createTextNode('Hello world');
複製代碼

經過將 text 附加到新的 HTML 元素中,能夠將這兩個節點組合在一塊兒。最後,還能夠將heading元素附加到根文檔中:

var heading = document.createElement('h1');
var text = document.createTextNode('Hello world');
heading.appendChild(text);
document.body.appendChild(heading);
複製代碼

還可使用 remove() 方法從 DOM 中刪除節點。 在元素上調用方法,該節點將從頁面中消失:

document.querySelector('h1').remove();
複製代碼

這些是我們開始在瀏覽器中使用 JS 操做 DOM 所須要知道的所有內容。在下一節中,我們將靈活地使用 DOM,但首先要繞個彎,由於我們還須要討論**「DOM事件」**。

DOM 和事件

DOM 元素是很智能的。它們不只能夠包含文本和其餘 HTML 元素,還能夠「發出」和響應「事件」。瀏覽任何網站並打開瀏覽器控制檯。使用如下命令選擇一個元素:

document.querySelector('p')
複製代碼

看看這個屬性

document.querySelector('p').onclick
複製代碼

它是什麼類型:

typeof document.querySelector('p').onclick // "object"
複製代碼

"object"! 爲何它被稱爲「onclick」? 憑一點直覺咱們能夠想象它是元素上的某種神奇屬性,可以對點擊作出反應? 徹底正確。

若是你感興趣,能夠查看任何 HTML 元素的原型鏈。會發現每一個元素也是一個 Element,而元素又是一個節點,而節點又是一個EventTarget。可使用 instanceof 來驗證這一點。

document.querySelector('p') instanceof EventTarget // true
複製代碼

我很樂意稱 EventTarget 爲全部 HTML 元素之父,但在JS中沒有真正的繼承,它更像是任何 HTML 元素均可以看到另外一個鏈接對象的屬性。所以,任何 HTML 元素都具備與 EventTarget 相同的特性:發佈事件的能力

但事件究竟是什麼呢?以 HTML 按鈕爲例。若是你點擊它,那就是一個事件。有了這個.onclick對象,我們能夠註冊事件,只要元素被點擊,它就會運行。傳遞給事件的函數稱爲**「事件監聽器」「事件句柄」**。

事件和監聽

在 DOM 中註冊事件監聽器有三種方法。第一種形式比較陳舊,應該避免,由於它耦合了邏輯操做和標籤

<!-- 很差的方式 -->
<button onclick="console.log('clicked')">喜歡,就點點我</button>
複製代碼

第二個選項依賴於以事件命名的對象。例如,我們能夠經過在對象.onclick上註冊一個函數來監聽click事件:

document.querySelector("button").onclick = handleClick;

function handleClick() {
  console.log("Clicked!");
}
複製代碼

此語法更加簡潔,是內聯處理程序的不錯替代方案。 還有另外一種基於addEventListener的現代形式:

document.querySelector("button").addEventListener("click", handleClick);

function handleClick() {
  console.log("Clicked!");
}
複製代碼

就我我的而言,我更喜歡這種形式,但若是想爭取最大限度的瀏覽器兼容性,請使用 .on 方式。如今我們已經有了一 個 HTML 元素和一個事件監聽器,接着進一步研究一下 DOM 事件。

事件對象、事件默認值和事件冒泡

做爲事件處理程序傳遞的每一個函數默認接收一個名爲「event」的對象

var button = document.querySelector("button");
button.addEventListener("click", handleClick);

function handleClick() {
  console.log(event);
}
複製代碼

它能夠直接在函數體中使用,可是在個人代碼中,我更喜歡將它顯式地聲明爲參數:

function handleClick(event) {
  console.log(event);
}
複製代碼

事件對象是**「必需要有的」,由於我們能夠經過調用事件上的一些方法來控制事件的行爲。事件實際上有特定的特徵,尤爲是「默認」「冒泡」**。考慮一 個HTML 連接。使用如下標籤建立一個名爲click-event.html的新HTML文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Click event</title>
</head>
<body>
<div>
    <a href="/404.html">click me!</a>
</div>
</body>
<script src="click-event.js"></script>
</html>
複製代碼

在瀏覽器中運行該文件並嘗試單擊連接。它將跳轉到一個404的界面。連接上單擊事件的默認行爲是轉到href屬性中指定的實際頁面。但若是我告訴你有辦法阻止默認值呢?輸入preventDefault(),該方法可用於事件對象。使用如下代碼建立一個名爲click-event.js的新文件:

var button = document.querySelector("a");
button.addEventListener("click", handleClick);

function handleClick(event) {
  event.preventDefault();
}
複製代碼

在瀏覽器中刷新頁面並嘗試如今單擊連接:它不會跳轉了。由於我們阻止了瀏覽器的「事件默認」 連接不是默認操做的唯一HTML 元素,表單具備相同的特性。

當 HTML 元素嵌套在另外一個元素中時,還會出現另外一個有趣的特性。考慮如下 HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Nested events</title>
</head>
<body>
<div id="outer">
    I am the outer div
    <div id="inner">
        I am the inner div
    </div>
</div>
</body>
<script src="nested-events.js"></script>
</html>
複製代碼

和下面的 JS 代碼:

// nested-events.js

var outer = document.getElementById('inner');
var inner = document.getElementById('outer');

function handleClick(event){
    console.log(event);
}

inner.addEventListener('click', handleClick);
outer.addEventListener('click', handleClick);
複製代碼

有兩個事件監聽器,一個用於外部 div,一個用於內部 div。準確地點擊內部div,你會看到:

兩個事件對象被打印。這就是事件冒泡在起做用。它看起來像是瀏覽器行爲中的一個 bug,使用 stopPropagation() 方法能夠禁用,這也是在事件對象上調用的

//
function handleClick(event) {
  event.stopPropagation();
  console.log(event);
}
///
複製代碼

儘管看起來實現效果不好,但在註冊過多事件監聽器確實對性能不利的狀況下,冒泡仍是會讓人眼前一亮。 考慮如下示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Event bubbling</title>
</head>
<body>
<ul>
    <li>one</li>
    <li>two</li>
    <li>three</li>
    <li>four</li>
    <li>five</li>
</ul>
</body>
<script src="event-bubbling.js"></script>
</html>
複製代碼

若是要兼聽列表的點擊事件,須要在列表中註冊多少事件監聽器?答案是:一個。只須要一個在ul上註冊的偵聽器就能夠截獲任何li上的全部單擊:

// event-bubbling.js

var ul = document.getElementsByTagName("ul")[0];

function handleClick(event) {
  console.log(event);
}

ul.addEventListener("click", handleClick);
複製代碼

能夠看到,事件冒泡是提升性能的一種實用方法。實際上,對瀏覽器來講,註冊事件監聽器是一項昂貴的操做,並且在出現大量元素列表的狀況下,可能會致使性能損失。

用 JS 生成表格

如今我們開始編碼。給定一個對象數組,但願動態生成一個HTML 表格。HTML 表格由 <table> 元素表示。每一個表也能夠有一個頭部,由 <thead> 元素表示。頭部能夠有一個或多個行 <tr>,每一個行都有一個單元格,由一個 <th>元 素表示。以下所示:

<table>
    <thead>
    <tr>
        <th>name</th>
        <th>height</th>
        <th>place</th>
    </tr>
    </thead>
    <!-- more stuff here! -->
</table>
複製代碼

不止這樣,大多數狀況下,每一個表都有一個主體,由 <tbody> 定義,而 <tbody> 又包含一組行<tr>。每一行均可以有包含實際數據的單元格。表單元格由<td>定義。完整以下所示:

<table>
    <thead>
    <tr>
        <th>name</th>
        <th>height</th>
        <th>place</th>
    </tr>
    </thead>
    <tbody>
    <tr>
        <td>Monte Falco</td>
        <td>1658</td>
        <td>Parco Foreste Casentinesi</td>
    </tr>
    <tr>
        <td>Monte Falterona</td>
        <td>1654</td>
        <td>Parco Foreste Casentinesi</td>
    </tr>
    </tbody>
</table>
複製代碼

如今的任務是從 JS 對象數組開始生成表格。首先,建立一個名爲build-table.html的新文件,內容以下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Build a table</title>
</head>
<body>
<table>
<!-- here goes our data! -->
</table>
</body>
<script src="build-table.js"></script>
</html>
複製代碼

在相同的文件夾中建立另外一個名爲build-table.js的文件,並使用如下數組開始:

"use strict";

var mountains = [
  { name: "Monte Falco", height: 1658, place: "Parco Foreste Casentinesi" },
  { name: "Monte Falterona", height: 1654, place: "Parco Foreste Casentinesi" },
  { name: "Poggio Scali", height: 1520, place: "Parco Foreste Casentinesi" },
  { name: "Pratomagno", height: 1592, place: "Parco Foreste Casentinesi" },
  { name: "Monte Amiata", height: 1738, place: "Siena" }
];
複製代碼

考慮這個表格。首先,我們須要一個 <thead>

document.createElement('thead')
複製代碼

這沒有錯,可是仔細查看MDN的表格文檔會發現一個有趣的細節。<table> 是一個 HTMLTableElement,它還包含有趣方法。其中最有用的是HTMLTableElement.createTHead(),它能夠幫助建立我們須要的 <thead>

首先,編寫一個生成 thead 標籤的函數 generateTableHead

function generateTableHead(table) {
  var thead = table.createTHead();
}
複製代碼

該函數接受一個選擇器並在給定的表上建立一個 <thead>:

function generateTableHead(table) {
  var thead = table.createTHead();
}

var table = document.querySelector("table");

generateTableHead(table);
複製代碼

在瀏覽器中打開 build-table.html:什麼都沒有.可是,若是打開瀏覽器控制檯,能夠看到一個新的 <thead> 附加到表。

接着填充 header 內容。首先要在裏面建立一行。還有另外一個方法能夠提供幫助:HTMLTableElement.insertRow()。有了這個,我們就能夠擴展方法了:

function generateTableHead (table) {
  var thead = table,createThead();
  var row = thead.insertRow();
}
複製代碼

此時,咱們能夠生成咱們的行。經過查看源數組,能夠看到其中的任何對象都有我們須要信息:

var mountains = [
  { name: "Monte Falco", height: 1658, place: "Parco Foreste Casentinesi" },
  { name: "Monte Falterona", height: 1654, place: "Parco Foreste Casentinesi" },
  { name: "Poggio Scali", height: 1520, place: "Parco Foreste Casentinesi" },
  { name: "Pratomagno", height: 1592, place: "Parco Foreste Casentinesi" },
  { name: "Monte Amiata", height: 1738, place: "Siena" }
];
複製代碼

這意味着我們能夠將另外一個參數傳遞給咱們的函數:一個遍歷以生成標題單元格的數組:

function generateTableHead(table, data) {
  var thead = table.createTHead();
  var row = thead.insertRow();
  for (var i = 0; i < data.length; i++) {
    var th = document.createElement("th");
    var text = document.createTextNode(data[i]);
    th.appendChild(text);
    row.appendChild(th);
  }
}
複製代碼

不幸的是,沒有建立單元格的原生方法,所以求助於document.createElement("th")。一樣值得注意的是,document.createTextNode(data[i])用於建立文本節點,appendChild()用於向每一個標記添加新元素。

當以這種方式建立和操做元素時,咱們稱之爲**「命令式」** DOM 操做。現代前端庫經過支持**「聲明式」**方法來解決這個問題。咱們能夠聲明須要哪些 HTML 元素,而不是一步一步地命令瀏覽器,其他的由庫處理。

回到咱們的代碼,能夠像下面這樣使用第一個函數

var table = document.querySelector("table");
var data = Object.keys(mountains[0]);
generateTableHead(table, data);
複製代碼

如今咱們能夠進一步生成實際表的數據。下一個函數將實現一個相似於generateTableHead的邏輯,但這一次我們須要兩個嵌套的for循環。在最內層的循環中,使用另外一種原生方法來建立一系列td。方法是HTMLTableRowElement.insertCell()。在前面建立的文件中添加另外一個名爲generateTable的函數

function generateTable(table, data) {
  for (var i = 0; i < data.length; i++) {
    var row = table.insertRow();
    for (var key in data[i]) {
      var cell = row.insertCell();
      var text = document.createTextNode(data[i][key]);
      cell.appendChild(text);
    }
  }
}
複製代碼

調用上面的函數,將 HTML表 和對象數組做爲參數傳遞:

generateTable(table, mountains);
複製代碼

我們深刻研究一下 generateTable 的邏輯。參數 data 是一個與 mountains 相對應的數組。最外層的 for 循環遍歷數組併爲每一個元素建立一行:

function generateTable(table, data) {
  for (var i = 0; i < data.length; i++) {
    var row = table.insertRow();
    // omitted for brevity
  }
}
複製代碼

最內層的循環遍歷任何給定對象的每一個鍵,併爲每一個對象建立一個包含鍵值的文本節點

function generateTable(table, data) {
  for (var i = 0; i < data.length; i++) {
    var row = table.insertRow();
    for (var key in data[i]) {
      // inner loop
      var cell = row.insertCell();
      var text = document.createTextNode(data[i][key]);
      cell.appendChild(text);
    }
  }
}
複製代碼

最終代碼:

var mountains = [
  { name: "Monte Falco", height: 1658, place: "Parco Foreste Casentinesi" },
  { name: "Monte Falterona", height: 1654, place: "Parco Foreste Casentinesi" },
  { name: "Poggio Scali", height: 1520, place: "Parco Foreste Casentinesi" },
  { name: "Pratomagno", height: 1592, place: "Parco Foreste Casentinesi" },
  { name: "Monte Amiata", height: 1738, place: "Siena" }
];

function generateTableHead(table, data) {
  var thead = table.createTHead();
  var row = thead.insertRow();
  for (var i = 0; i < data.length; i++) {
    var th = document.createElement("th");
    var text = document.createTextNode(data[i]);
    th.appendChild(text);
    row.appendChild(th);
  }
}

function generateTable(table, data) {
  for (var i = 0; i < data.length; i++) {
    var row = table.insertRow();
    for (var key in data[i]) {
      var cell = row.insertCell();
      var text = document.createTextNode(data[i][key]);
      cell.appendChild(text);
    }
  }
}
複製代碼

其中調用:

var table = document.querySelector("table");
var data = Object.keys(mountains[0]);
generateTable(table, mountains);
generateTableHead(table, data);
複製代碼

執行結果:

固然,我們的方法還能夠該進,下個章節將介紹。

總結

DOM 是 web 瀏覽器保存在內存中的 web 頁面的虛擬副本。DOM 操做是指從 DOM 中建立、修改和刪除 HTML 元素的操做。在過去,我們經常依賴 jQuery 來完成更簡單的任務,但如今原生 API 已經足夠成熟,可讓 jQuery 過期了。另外一方面,jQuery 不會很快消失,可是每一個 JS 開發人員都必須知道如何使用原生 API 操做 DOM。

這樣作的理由有不少,額外的庫增長了加載時間和 JS 應用程序的大小。更不用說 DOM 操做在面試中常常出現。

DOM 中每一個可用的 HTML 元素都有一個接口,該接口公開必定數量的屬性和方法。當你對使用何種方法有疑問時,參考MDN文檔。操做 DOM 最經常使用的方法是 document.createElement() 用於建立新的 HTML 元素,document.createTextNode() 用於在 DOM 中建立文本節點。最後但一樣重要的是 .appendchild(),用於將新的 HTML 元素或文本節點附加到現有元素。

HTML 元素還可以發出事件,也稱爲DOM事件。值得注意的事件爲「click」「submit」「drag」「drop」等等。DOM 事件有一些特殊的行爲,好比「默認」和冒泡。

JS 開發人員能夠利用這些屬性,特別是對於事件冒泡,這些屬性對於加速 DOM 中的事件處理很是有用。雖然對原生 API 有很好的瞭解是件好事,可是現代前端庫提供了無可置疑的好處。用 AngularReactVue 來構建一個大型的JS應用程序確實是可行的。

代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

**原文:**github.com/valentinoga…

交流

阿里雲最近在作活動,低至2折,有興趣能夠看看:promotion.aliyun.com/ntms/yunpar…

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

github.com/qq449245884…

由於篇幅的限制,今天的分享只到這裏。若是你們想了解更多的內容的話,能夠去掃一掃每篇文章最下面的二維碼,而後關注我們的微信公衆號,瞭解更多的資訊和有價值的內容。

clipboard.png

每次整理文章,通常都到2點才睡覺,一週4次左右,挺苦的,還望支持,給點鼓勵

相關文章
相關標籤/搜索