2022年7月26日

节点属性:type,tag 和 content

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

在本章中,我们将更深入地了解它们是什么,并学习它们最常用的属性。

DOM 节点类

不同的 DOM 节点可能有不同的属性。例如,标签 <a> 相对应的元素节点具有链接相关的(link-related)属性,标签 <input> 相对应的元素节点具有与输入相关的属性,等。文本节点与元素节点不同。但是所有这些标签对应的 DOM 节点之间也存在共有的属性和方法,因为所有类型的 DOM 节点都形成了一个单一层次的结构(single hierarchy)。

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

层次结构(hierarchy)的根节点是 EventTargetNode 继承自它,其他 DOM 节点继承自 Node。

下图做了进一步说明:

类如下所示:

  • EventTarget —— 是一切的根“抽象(abstract)”类。

    该类的对象从未被创建。它作为一个基础,以便让所有 DOM 节点都支持所谓的“事件(event)”,我们会在之后学习它。

  • Node —— 也是一个“抽象”类,充当 DOM 节点的基础。

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

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

    全局变量 document 就是属于这个类。它作为 DOM 的入口。

  • CharacterData —— 一个“抽象”类,被下述类继承:

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

    它提供了元素级导航(navigation),如 nextElementSiblingchildren,以及搜索方法,如 getElementsByTagNamequerySelector

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

  • 最后,HTMLElement —— 是所有 HTML 元素的基础类。我们大部分时候都会用到它。

    它会被更具体的 HTML 元素继承:

还有很多其他标签具有自己的类,可能还具有特定的属性和方法,而一些元素,如 <span><section><article> 等,没有任何特定的属性,所以它们是 HTMLElement 类的实例。

因此,给定节点的全部属性和方法都是继承链的结果。

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

它获取属性和方法,并将其作为下列类(按继承顺序列出)的叠加:

  • HTMLInputElement —— 该类提供特定于输入的属性,
  • HTMLElement —— 它提供了通用(common)的 HTML 元素方法(以及 getter 和 setter)
  • Element —— 提供通用(generic)元素方法,
  • Node —— 提供通用 DOM 节点属性,
  • EventTarget —— 为事件(包括事件本身)提供支持,
  • ……最后,它继承自 Object,因为像 hasOwnProperty 这样的“普通对象”方法也是可用的。

我们可以通过回调来查看 DOM 节点类名,因为对象通常都具有 constructor 属性。它引用类的 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 来描述的,而是一种特殊的 接口描述语言(Interface description language),简写为 IDL,它通常很容易理解。

在 IDL 中,所有属性以其类型开头。例如,DOMStringboolean 等。

以下是摘录(excerpt),并附有注释:

// 定义 HTMLInputElement
// 冒号 ":" 表示 HTMLInputElement 继承自 HTMLElement
interface HTMLInputElement: HTMLElement {
  // 接下来是 <input> 元素的属性和方法

  // "DOMString" 表示属性的值是字符串
  attribute DOMString accept;
  attribute DOMString alt;
  attribute DOMString autocomplete;
  attribute DOMString value;

  // 布尔值属性(true/false)
  attribute boolean autofocus;
  ...
  // 现在方法:"void" 表示方法没有返回值
  void select();
  ...
}

“nodeType” 属性

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

它有一个数值型值(numeric value):

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

例如:

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

  // 让我们检查一下:elem 中的节点类型是什么?
  alert(elem.nodeType); // 1 => element

  // 它的第一个子节点的类型是……
  alert(elem.firstChild.nodeType); // 3 => text

  // 对于 document 对象,类型是 9
  alert( document.nodeType ); // 9
  </script>
</body>

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

标签:nodeName 和 tagName

给定一个 DOM 节点,我们可以从 nodeName 或者 tagName 属性中读取它的标签名:

例如:

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

tagName 和 nodeName 之间有什么不同吗?

当然,差异就体现在它们的名字上,但确实有些微妙。

  • tagName 属性仅适用于 Element 节点。
  • nodeName 是为任意 Node 定义的:
    • 对于元素,它的意义与 tagName 相同。
    • 对于其他节点类型(text,comment 等),它拥有一个对应节点类型的字符串。

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

例如,我们比较一下 documenttagNamenodeName,以及一个注释节点:

<body><!-- comment -->

  <script>
    // for comment
    alert( document.body.firstChild.tagName ); // undefined(不是一个元素)
    alert( document.body.firstChild.nodeName ); // #comment

    // for document
    alert( document.tagName ); // undefined(不是一个元素)
    alert( document.nodeName ); // #document
  </script>
</body>

如果我们只处理元素,那么 tagNamenodeName 这两种方法,我们都可以使用,没有区别。

标签名称始终是大写的,除非是在 XML 模式下

浏览器有两种处理文档(document)的模式:HTML 和 XML。通常,HTML 模式用于网页。只有在浏览器接收到带有 Content-Type: application/xml+xhtml header 的 XML-document 时,XML 模式才会被启用。

在 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 ); // 读取当前内容
    document.body.innerHTML = 'The new BODY!'; // 替换它
  </script>

</body>

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

<body>

  <script>
    document.body.innerHTML = '<b>test'; // 忘记闭合标签
    alert( document.body.innerHTML ); // <b>test</b>(被修复了)
  </script>

</body>
脚本不会执行

如果 innerHTML 将一个 <script> 标签插入到 document 中 —— 它会成为 HTML 的一部分,但是不会执行。

小心:“innerHTML+=” 会进行完全重写

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

就像这样: