2022 年 8 月 5 日

节点属性:类型、标签和内容

让我们更深入地了解 DOM 节点。

在本章中,我们将进一步了解它们是什么,并学习它们最常用的属性。

DOM 节点类

不同的 DOM 节点可能具有不同的属性。例如,对应于标签 <a> 的元素节点具有链接相关属性,而对应于 <input> 的元素节点具有输入相关属性,依此类推。文本节点与元素节点不同。但它们之间也有公共属性和方法,因为所有类别的 DOM 节点都形成一个单一的层次结构。

每个 DOM 节点都属于相应的内置类。

层次结构的根是 EventTarget,它被 Node 继承,而其他 DOM 节点又继承自它。

这是图片,后面有解释

这些类是

  • EventTarget – 是所有内容的根“抽象”类。

    该类的对象永远不会被创建。它用作一个基础,以便所有 DOM 节点都支持所谓的“事件”,我们稍后会学习它们。

  • Node – 也是一个“抽象”类,用作 DOM 节点的基础。

    它提供了核心树功能:parentNodenextSiblingchildNodes 等等(它们是 getter)。Node 类的对象永远不会被创建。但还有其他类继承自它(因此继承了 Node 功能)。

  • Document,出于历史原因,通常由 HTMLDocument 继承(尽管最新规范没有规定)——是一个整体文档。

    document 全局对象恰好属于此类。它用作 DOM 的入口点。

  • CharacterData – 一个“抽象”类,由以下类继承

    • Text – 对应于元素内文本的类,例如 <p>Hello</p> 中的 Hello
    • Comment – 注释的类。它们不会显示,但每个注释都会成为 DOM 的成员。
  • Element – 是 DOM 元素的基本类。

    它提供了元素级别的导航,如 nextElementSiblingchildren 和搜索方法,如 getElementsByTagNamequerySelector

    浏览器不仅支持 HTML,还支持 XML 和 SVG。因此,Element 类用作更具体类的基础:SVGElementXMLElement(我们这里不需要它们)和 HTMLElement

  • 最后,HTMLElement 是所有 HTML 元素的基本类。我们大部分时间都会使用它。

    它由具体 HTML 元素继承

还有许多其他带有自己类的标签,它们可能具有特定的属性和方法,而某些元素(如 <span><section><article>)没有任何特定属性,因此它们是 HTMLElement 类的实例。

因此,给定节点的完整属性和方法集是继承链的结果。

例如,让我们考虑 <input> 元素的 DOM 对象。它属于 HTMLInputElement 类。

它获取属性和方法作为(按继承顺序列出)的叠加

  • HTMLInputElement – 此类提供特定于输入的属性,
  • HTMLElement – 它提供常见的 HTML 元素方法(以及 getter/setter),
  • Element – 提供通用元素方法,
  • Node – 提供常见的 DOM 节点属性,
  • EventTarget – 提供对事件的支持(待介绍),
  • …最后它从 Object 继承,因此“普通对象”方法(如 hasOwnProperty)也可用。

要查看 DOM 节点类名,我们可以回想一下一个对象通常具有 constructor 属性。它引用类构造函数,而 constructor.name 是其名称

alert( document.body.constructor.name ); // HTMLBodyElement

…或者我们也可以对其进行 toString

alert( document.body ); // [object HTMLBodyElement]

我们还可以使用 instanceof 来检查继承

alert( document.body instanceof HTMLBodyElement ); // true
alert( document.body instanceof HTMLElement ); // true
alert( document.body instanceof Element ); // true
alert( document.body instanceof Node ); // true
alert( document.body instanceof EventTarget ); // true

正如我们所见,DOM 节点是常规 JavaScript 对象。它们使用基于原型的类进行继承。

通过在浏览器中使用 console.dir(elem) 输出元素也很容易看到这一点。在控制台中,你可以看到 HTMLElement.prototypeElement.prototype 等。

console.dir(elem)console.log(elem)

大多数浏览器在其开发者工具中支持两个命令:console.logconsole.dir。它们将参数输出到控制台。对于 JavaScript 对象,这些命令通常执行相同操作。

但对于 DOM 元素,它们是不同的

  • console.log(elem) 显示元素 DOM 树。
  • console.dir(elem) 将元素显示为 DOM 对象,便于探索其属性。

document.body 上试一试。

规范中的 IDL

在规范中,DOM 类不是使用 JavaScript 描述的,而是使用一种特殊的 接口描述语言 (IDL),它通常很容易理解。

在 IDL 中,所有属性都以其类型作为前缀。例如,DOMStringboolean 等。

以下是其中的一段摘录,带注释

// Define HTMLInputElement
// The colon ":" means that HTMLInputElement inherits from HTMLElement
interface HTMLInputElement: HTMLElement {
  // here go properties and methods of <input> elements

  // "DOMString" means that the value of a property is a string
  attribute DOMString accept;
  attribute DOMString alt;
  attribute DOMString autocomplete;
  attribute DOMString value;

  // boolean value property (true/false)
  attribute boolean autofocus;
  ...
  // now the method: "void" means that the method returns no value
  void select();
  ...
}

“nodeType”属性

nodeType 属性提供了另一种“老式”方法来获取 DOM 节点的“类型”。

它具有一个数字值

  • 对于元素节点,elem.nodeType == 1
  • 对于文本节点,elem.nodeType == 3
  • 对于文档对象,elem.nodeType == 9
  • 规范 中还有其他一些值。

例如

<body>
  <script>
  let elem = document.body;

  // let's examine: what type of node is in elem?
  alert(elem.nodeType); // 1 => element

  // and its first child is...
  alert(elem.firstChild.nodeType); // 3 => text

  // for the document object, the type is 9
  alert( document.nodeType ); // 9
  </script>
</body>

在现代脚本中,我们可以使用 instanceof 和其他基于类的测试来查看节点类型,但有时 nodeType 可能更简单。我们只能读取 nodeType,不能更改它。

标记:nodeName 和 tagName

给定一个 DOM 节点,我们可以从 nodeNametagName 属性读取其标记名称

例如

alert( document.body.nodeName ); // BODY
alert( document.body.tagName ); // BODY

tagNamenodeName 之间有什么区别?

当然,区别反映在它们的名称中,但确实有点微妙。

  • tagName 属性仅存在于 Element 节点中。
  • nodeName 为任何 Node 定义
    • 对于元素,它与 tagName 的含义相同。
    • 对于其他节点类型(文本、注释等),它有一个带有节点类型的字符串。

换句话说,tagName 仅受元素节点支持(因为它源自 Element 类),而 nodeName 可以说明其他节点类型。

例如,让我们比较 document 和注释节点的 tagNamenodeName

<body><!-- comment -->

  <script>
    // for comment
    alert( document.body.firstChild.tagName ); // undefined (not an element)
    alert( document.body.firstChild.nodeName ); // #comment

    // for document
    alert( document.tagName ); // undefined (not an element)
    alert( document.nodeName ); // #document
  </script>
</body>

如果我们只处理元素,那么我们可以同时使用 tagNamenodeName - 没有区别。

标记名称始终大写,除非在 XML 模式下

浏览器有两种处理文档的模式:HTML 和 XML。通常 HTML 模式用于网页。当浏览器接收到带有头部的 XML 文档时,将启用 XML 模式:Content-Type: application/xml+xhtml

在 HTML 模式下,tagName/nodeName 始终大写:对于 <body><BoDy>,它都是 BODY

在 XML 模式下,大小写保持“原样”。如今,XML 模式很少使用。

innerHTML:内容

innerHTML 属性允许将元素内的 HTML 作为字符串获取。

我们也可以修改它。因此,它是更改页面最强大的方法之一。

该示例显示了 document.body 的内容,然后将其完全替换

<body>
  <p>A paragraph</p>
  <div>A div</div>

  <script>
    alert( document.body.innerHTML ); // read the current contents
    document.body.innerHTML = 'The new BODY!'; // replace it
  </script>

</body>

我们可以尝试插入无效的 HTML,浏览器将修复我们的错误

<body>

  <script>
    document.body.innerHTML = '<b>test'; // forgot to close the tag
    alert( document.body.innerHTML ); // <b>test</b> (fixed)
  </script>

</body>
脚本不执行

如果 innerHTML<script> 标记插入文档中,它将成为 HTML 的一部分,但不会执行。

注意:“innerHTML+=” 执行完全覆盖

我们可以通过使用 elem.innerHTML+="more html" 将 HTML 附加到元素。

像这样

chatDiv.innerHTML += "<div>Hello<img src='smile.gif'/> !</div>";
chatDiv.innerHTML += "How goes?";

但我们应该非常小心地这样做,因为正在进行的不是添加,而是完全覆盖。

从技术上讲,这两行代码执行相同的功能

elem.innerHTML += "...";
// is a shorter way to write:
elem.innerHTML = elem.innerHTML + "..."

换句话说,innerHTML+= 执行此操作

  1. 删除旧内容。
  2. innerHTML 被写入了(旧内容和新内容的连接)。

由于内容被“清零”并从头开始重写,所有图像和其他资源都将重新加载.

在上面的 chatDiv 示例中,行 chatDiv.innerHTML+="How goes?" 重新创建 HTML 内容并重新加载 smile.gif(希望它已缓存)。如果 chatDiv 有很多其他文本和图像,则重新加载将变得清晰可见。

还有其他副作用。例如,如果现有文本已用鼠标选中,则大多数浏览器将在重写 innerHTML 时删除选择。如果存在一个访客输入文本的 <input>,则该文本将被删除。等等。

幸运的是,除了 innerHTML 之外,还有其他方法可以添加 HTML,我们很快就会学习它们。

outerHTML:元素的完整 HTML

outerHTML 属性包含元素的完整 HTML。这就像 innerHTML 加上元素本身。

这是一个例子

<div id="elem">Hello <b>World</b></div>

<script>
  alert(elem.outerHTML); // <div id="elem">Hello <b>World</b></div>
</script>

注意:与 innerHTML 不同,写入 outerHTML 不会更改元素。相反,它在 DOM 中替换元素。

是的,听起来很奇怪,而且很奇怪,这就是我们在这里单独做笔记的原因。看一看。

考虑示例

<div>Hello, world!</div>

<script>
  let div = document.querySelector('div');

  // replace div.outerHTML with <p>...</p>
  div.outerHTML = '<p>A new element</p>'; // (*)

  // Wow! 'div' is still the same!
  alert(div.outerHTML); // <div>Hello, world!</div> (**)
</script>

看起来真的很奇怪,对吗?

在行 (*) 中,我们将 div 替换为 <p>A new element</p>。在外层文档(DOM)中,我们可以看到新内容而不是 <div>。但是,正如我们在行 (**) 中看到的那样,旧 div 变量的值没有改变!

outerHTML 赋值不会修改 DOM 元素(在这种情况下,变量“div”引用的对象),而是将其从 DOM 中删除,并将其插入新的 HTML。

因此,在 div.outerHTML=... 中发生的事情是

  • div 已从文档中删除。
  • 另一段 HTML <p>A new element</p> 已插入其中。
  • div 仍然具有其旧值。新 HTML 未保存到任何变量中。

在这里很容易出错:修改 div.outerHTML,然后继续使用 div,就好像其中包含新内容一样。但事实并非如此。对于 innerHTML 来说,这样的事情是正确的,但对于 outerHTML 来说却不是。

我们可以写入 elem.outerHTML,但应该记住它不会改变我们正在写入的元素(“elem”)。相反,它将新的 HTML 放置在其位置。我们可以通过查询 DOM 来获取对新元素的引用。

nodeValue/data:文本节点内容

innerHTML 属性仅对元素节点有效。

其他节点类型(如文本节点)有其对应项:nodeValuedata 属性。这两个属性在实际使用中几乎相同,只有细微的规范差异。因此,我们将使用 data,因为它更短。

读取文本节点和注释内容的一个示例

<body>
  Hello
  <!-- Comment -->
  <script>
    let text = document.body.firstChild;
    alert(text.data); // Hello

    let comment = text.nextSibling;
    alert(comment.data); // Comment
  </script>
</body>

对于文本节点,我们可以想象出读取或修改它们的理由,但为什么是注释呢?

有时,开发人员会将信息或模板指令嵌入到 HTML 中,如下所示

<!-- if isAdmin -->
  <div>Welcome, Admin!</div>
<!-- /if -->

…然后 JavaScript 可以从 data 属性中读取它并处理嵌入的指令。

textContent:纯文本

textContent 提供对元素内部文本的访问:仅文本,减去所有 <tags>

例如

<div id="news">
  <h1>Headline!</h1>
  <p>Martians attack people!</p>
</div>

<script>
  // Headline! Martians attack people!
  alert(news.textContent);
</script>

正如我们所看到的,只返回文本,就像剪掉了所有 <tags>,但其中的文本仍然保留。

在实践中,很少需要读取这样的文本。

写入 textContent 更有用,因为它允许以“安全的方式”编写文本。

假设我们有一个任意字符串,例如由用户输入,并希望显示它。

  • 使用 innerHTML,我们会以“作为 HTML”的方式插入它,包括所有 HTML 标记。
  • 使用 textContent,我们会以“作为文本”的方式插入它,所有符号都按字面意思处理。

比较两者

<div id="elem1"></div>
<div id="elem2"></div>

<script>
  let name = prompt("What's your name?", "<b>Winnie-the-Pooh!</b>");

  elem1.innerHTML = name;
  elem2.textContent = name;
</script>
  1. 第一个 <div> 获取名称“作为 HTML”:所有标记都变为标记,因此我们看到加粗的名称。
  2. 第二个 <div> 获取名称“作为文本”,因此我们从字面上看到 <b>Winnie-the-Pooh!</b>

在大多数情况下,我们期望用户提供文本,并希望将其视为文本。我们不希望在我们的网站中出现意外的 HTML。对 textContent 的赋值正是这样做的。

“hidden”属性

“hidden”属性和 DOM 属性指定元素是否可见。

我们可以在 HTML 中使用它,或使用 JavaScript 对其进行赋值,如下所示

<div>Both divs below are hidden</div>

<div hidden>With the attribute "hidden"</div>

<div id="elem">JavaScript assigned the property "hidden"</div>

<script>
  elem.hidden = true;
</script>

从技术上讲,hiddenstyle="display:none" 的工作方式相同。但它更简洁。

这是一个闪烁的元素

<div id="elem">A blinking element</div>

<script>
  setInterval(() => elem.hidden = !elem.hidden, 1000);
</script>

更多属性

DOM 元素还具有其他属性,特别是那些依赖于类的属性

  • value - <input><select><textarea> 的值(HTMLInputElementHTMLSelectElement 等)。
  • href - <a href="..."> 的“href”(HTMLAnchorElement)。
  • id - 所有元素的“id”属性的值(HTMLElement)。
  • …以及更多…

例如

<input type="text" id="elem" value="value">

<script>
  alert(elem.type); // "text"
  alert(elem.id); // "elem"
  alert(elem.value); // value
</script>

大多数标准 HTML 属性都有相应的 DOM 属性,我们可以像这样访问它。

如果我们想了解给定类支持的属性的完整列表,我们可以在规范中找到它们。例如,HTMLInputElementhttps://html.whatwg.com.cn/#htmlinputelement 中记录。

或者,如果我们想快速获取它们或对具体的浏览器规范感兴趣,我们始终可以使用 console.dir(elem) 输出元素并读取属性。或者在浏览器开发者工具的“元素”选项卡中浏览“DOM 属性”。

总结

每个 DOM 节点都属于某个类。这些类形成一个层次结构。属性和方法的完整集合是继承的结果。

主要的 DOM 节点属性是

nodeType
我们可以用它来查看一个节点是文本节点还是元素节点。它有一个数值:元素为 1,文本节点为 3,其他节点类型还有其他一些数值。只读。
nodeName/tagName
对于元素,标签名称(除非处于 XML 模式,否则大写)。对于非元素节点,nodeName 描述了它是什么。只读。
innerHTML
元素的 HTML 内容。可以修改。
outerHTML
元素的完整 HTML。对 elem.outerHTML 的写操作不会触及 elem 本身。相反,它会在外部上下文中被新的 HTML 替换。
nodeValue/data
非元素节点(文本、注释)的内容。这两个几乎相同,我们通常使用 data。可以修改。
textContent
元素内的文本:HTML 减去所有 <tags>。写入它会将文本放入元素内,所有特殊字符和标签都将被视为文本。可以安全地插入用户生成文本,并防止不需要的 HTML 插入。
hidden
当设置为 true 时,与 CSS display:none 相同。

DOM 节点还具有其他属性,具体取决于它们的类。例如,<input> 元素(HTMLInputElement)支持 valuetype,而 <a> 元素(HTMLAnchorElement)支持 href 等。大多数标准 HTML 属性都有相应的 DOM 属性。

但是,正如我们将在下一章中看到的那样,HTML 属性和 DOM 属性并不总是相同的。

任务

重要性:5

有一个树形结构,嵌套了 ul/li

编写代码,为每个 <li> 显示

  1. 它内部的文本(不包括子树)
  2. 嵌套 <li> 的数量 - 所有后代,包括深度嵌套的后代。

在新窗口中演示

为任务打开沙盒。

让我们对 <li> 进行循环。

for (let li of document.querySelectorAll('li')) {
  ...
}

在循环中,我们需要获取每个 li 中的文本。

我们可以从 li 的第一个子节点(即文本节点)中读取文本

for (let li of document.querySelectorAll('li')) {
  let title = li.firstChild.data;

  // title is the text in <li> before any other nodes
}

然后,我们可以将后代的数量获取为 li.getElementsByTagName('li').length

在沙盒中打开解决方案。

重要性:5

脚本显示了什么?

<html>

<body>
  <script>
    alert(document.body.lastChild.nodeType);
  </script>
</body>

</html>

这里有一个陷阱。

<script> 执行时,最后一个 DOM 节点恰好是 <script>,因为浏览器尚未处理页面的其余部分。

因此,结果为 1(元素节点)。

<html>

<body>
  <script>
    alert(document.body.lastChild.nodeType);
  </script>
</body>

</html>
重要性:3

这段代码显示了什么?

<script>
  let body = document.body;

  body.innerHTML = "<!--" + body.tagName + "-->";

  alert( body.firstChild.data ); // what's here?
</script>

答案:BODY

<script>
  let body = document.body;

  body.innerHTML = "<!--" + body.tagName + "-->";

  alert( body.firstChild.data ); // BODY
</script>

逐步了解发生了什么

  1. <body> 的内容被注释替换。注释是 <!--BODY-->,因为 body.tagName == "BODY"。正如我们所记得的,tagName 在 HTML 中始终是大写的。
  2. 注释现在是唯一的子节点,因此我们在 body.firstChild 中获取它。
  3. 注释的 data 属性是其内容(在 <!--...--> 内):"BODY"
重要性:4

document 属于哪个类?

它在 DOM 层次结构中的位置是什么?

它是否继承自 NodeElement,或者可能是 HTMLElement

我们可以通过输出它来查看它属于哪个类,例如

alert(document); // [object HTMLDocument]

alert(document.constructor.name); // HTMLDocument

因此,documentHTMLDocument 类的实例。

它在层次结构中的位置是什么?

是的,我们可以浏览规范,但手动找出会更快。

让我们通过 __proto__ 遍历原型链。

如我们所知,类的函数位于构造函数的 prototype 中。例如,HTMLDocument.prototype 具有用于文档的函数。

此外,prototype 中还有对构造函数的引用

alert(HTMLDocument.prototype.constructor === HTMLDocument); // true

要将类的名称作为字符串获取,我们可以使用 constructor.name。让我们对整个 document 原型链执行此操作,直到类 Node

alert(HTMLDocument.prototype.constructor.name); // HTMLDocument
alert(HTMLDocument.prototype.__proto__.constructor.name); // Document
alert(HTMLDocument.prototype.__proto__.__proto__.constructor.name); // Node

这就是层次结构。

我们还可以使用 console.dir(document) 检查对象,并通过打开 __proto__ 来查看这些名称。控制台在内部从 constructor 获取这些名称。

教程地图

评论

在评论之前阅读此内容...
  • 如果您有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 - 请详细说明。
  • 要插入一些代码,请使用 <code> 标记,对于多行 - 将它们包装在 <pre> 标记中,对于超过 10 行 - 使用沙盒 (plnkrjsbincodepen...)。