【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次左右,挺苦的,还望支持,给点鼓励

相关文章
相关标签/搜索