DOM操作


目录:

浏览器环境

JavaScript 语言最初是为 Web 浏览器创建的。

下面是 JavaScript 在浏览器中运行时的鸟瞰示意图:

浏览器环境,规格 - 图1

有一个叫做 window 的“根”对象。它有两个角色:

  1. 首先,它是 JavaScript 代码的全局对象,如 全局对象 一章所述。
  2. 其次,它代表“浏览器窗口”,并提供了控制它的方法。

例如,在这里我们将它用作全局对象:

function sayHi() {  alert("Hello");}// 全局函数是全局对象的方法:window.sayHi();

在这里,我们将它用作浏览器窗口,以查看窗口高度:

alert(window.innerHeight); // 内部窗口高度

还有更多窗口特定的方法和属性,我们稍后会介绍它们。

文档对象模型(DOM)

文档对象模型(Document Object Model),简称 DOM,将所有页面内容表示为可以修改的对象。

document 对象是页面的主要“入口点”。我们可以使用它来更改或创建页面上的任何内容。

例如:

// 将背景颜色修改为红色
document.body.style.background = "red";
// 在 1 秒后将其修改回来
setTimeout(() => document.body.style.background = "", 1000);

浏览器对象模型(BOM)

浏览器对象模型(Browser Object Model),简称 BOM,表示由浏览器(主机环境)提供的用于处理文档(document)之外的所有内容的其他对象。

例如:

alert(location.href); 
// 显示当前 URL
if (confirm("Go to Wikipedia?")) {  
    location.href = "https://wikipedia.org"; // 将浏览器重定向到另一个 URL
}

函数 alert/confirm/prompt 也是 BOM 的一部分:它们与文档(document)没有直接关系,但它代表了与用户通信的纯浏览器方法。

在最顶层:documentElement 和 body

最顶层的树节点可以直接作为 document 的属性来使用:

<html> = document.documentElement

最顶层的 document 节点是 document.documentElement。这是对应 <html> 标签的 DOM 节点。

<body>= document.body

另一个被广泛使用的 DOM 节点是 <body> 元素 — document.body

<head> = document.head

<head> 标签可以通过 document.head 访问。

这里有个问题:document.body 的值可能是 null

脚本无法访问在运行时不存在的元素。

尤其是,如果一个脚本是在 <head> 中,那么脚本是访问不到 document.body 元素的,因为浏览器还没有读到它。

所以,下面例子中的第一个 alert 显示 null

<html>
<head>
  <script>
    alert( "From HEAD: " + document.body ); // null,这里目前还没有 <body>
  </script>
</head>
<body>
  <script>
    alert( "From BODY: " + document.body ); // HTMLBodyElement,现在存在了
  </script>
</body>
</html>

在 DOM 的世界中,null 就意味着“不存在”

在 DOM 中,null 值就意味着“不存在”或者“没有这个节点”。

子节点:childNodes,firstChild,lastChild

从现在开始,我们将使用下面这两个术语:

  • 子节点(或者叫作子) — 对应的是直系的子元素。换句话说,它们被完全嵌套在给定的元素中。例如,<head><body> 就是 <html> 元素的子元素。
  • 子孙元素 — 嵌套在给定元素中的所有元素,包括子元素,以及子元素的子元素等。

例如,这里 <body> 有子元素 <div><ul>(以及一些空白的文本节点):

<html>
<body>
  <div>Begin</div>
  <ul>
    <li>
      <b>Information</b>
    </li>
  </ul>
</body>
</html>

……<body> 元素的子孙元素不仅包含直接的子元素 <div><ul>,还包含像 <li><ul> 的子元素)和 <b><li> 的子元素)这样的元素 — 整个子树。

childNodes 集合列出了所有子节点,包括文本节点。

下面这个例子显示了 document.body 的子元素:

<html>
<body>
  <div>Begin</div>
  <ul>
    <li>Information</li>
  </ul>
  <div>End</div>
  <script>
    for (let i = 0; i < document.body.childNodes.length; i++) {
      alert( document.body.childNodes[i] ); // Text, DIV, Text, UL, ..., SCRIPT
    }
  </script>
  ...more stuff...
</body>
</html>

document.getElementById 或者只使用 id

如果一个元素有 id 特性(attribute),那我们就可以使用 document.getElementById(id) 方法获取该元素,无论它在哪里。

<div id="elem">
  <div id="elem-content">Element</div>
</div>
<script>
  // 获取该元素
  let elem = document.getElementById('elem');
  // 将该元素背景改为红色
  elem.style.background = 'red';
</script>

此外,还有一个通过 id 命名的全局变量,它引用了元素:

<div id="elem">
  <div id="elem-content">Element</div>
</div>
<script>
  // elem 是对带有 id="elem" 的 DOM 元素的引用
  elem.style.background = 'red';
  // id="elem-content" 内有连字符,所以它不能成为一个变量
  // ...但是我们可以通过使用方括号 window['elem-content'] 来访问它
</script>

……除非我们声明一个具有相同名称的 JavaScript 变量,否则它具有优先权:

<div id="elem"></div>
<script>
  let elem = 5; // 现在 elem 是 5,而不是对 <div id="elem"> 的引用
  alert(elem); // 5
</script>

在实际开发中,document.getElementById 是首选方法。

id 必须是唯一的

id 必须是唯一的。在文档中,只能有一个元素带有给定的 id

如果有多个元素都带有同一个 id,那么使用它的方法的行为是不可预测的,例如 document.getElementById 可能会随机返回其中一个元素。因此,请遵守规则,保持 id 的唯一性。

只有 document.getElementById,没有 anyElem.getElementById

getElementById 方法只能被在 document 对象上调用。它会在整个文档中查找给定的 id

querySelectorAll

到目前为止,最通用的方法是 elem.querySelectorAll(css),它返回 elem 中与给定 CSS 选择器匹配的所有元素。

在这里,我们查找所有为最后一个子元素的 <li> 元素:

<ul>
  <li>The</li>
  <li>test</li>
</ul>
<ul>
  <li>has</li>
  <li>passed</li>
</ul>
<script>
  let elements = document.querySelectorAll('ul > li:last-child');
  for (let elem of elements) {
    alert(elem.innerHTML); // "test", "passed"
  }
</script>

这个方法确实功能强大,因为可以使用任何 CSS 选择器。

也可以使用伪类

CSS 选择器的伪类,例如 :hover:active 也都是被支持的。例如,document.querySelectorAll(':hover') 将会返回鼠标指针现在已经结束的元素的集合(按嵌套顺序:从最外层 <html> 到嵌套最多的元素)。

querySelector

elem.querySelector(css) 调用会返回给定 CSS 选择器的第一个元素。

换句话说,结果与 elem.querySelectorAll(css)[0] 相同,但是后者会查找 所有 元素,并从中选取一个,而 elem.querySelector 只会查找一个。因此它在速度上更快,并且写起来更短。

matches

之前的方法是搜索 DOM。

elem.matches(css) 不会查找任何内容,它只会检查 elem 是否与给定的 CSS 选择器匹配。它返回 truefalse

当我们遍历元素(例如数组或其他内容)并试图过滤那些我们感兴趣的元素时,这个方法会很有用。

例如:

<a href="http://example.com/file.zip">...</a>
<a href="http://ya.ru">...</a>
<script>
  // 不一定是 document.body.children,还可以是任何集合
  for (let elem of document.body.children) {
    if (elem.matches('a[href$="zip"]')) {
      alert("The archive reference: " + elem.href );
    }
  }
</script>

closest

元素的祖先(ancestor)是:父级,父级的父级,它的父级等。祖先们一起组成了从元素到顶端的父级链。

elem.closest(css) 方法会查找与 CSS 选择器匹配的最近的祖先。elem 自己也会被搜索。

换句话说,方法 closest 在元素中得到了提升,并检查每个父级。如果它与选择器匹配,则停止搜索并返回该祖先。

例如:

<h1>Contents</h1>
<div class="contents">
  <ul class="book">
    <li class="chapter">Chapter 1</li>
    <li class="chapter">Chapter 1</li>
  </ul>
</div>
<script>
  let chapter = document.querySelector('.chapter'); // LI
  alert(chapter.closest('.book')); // UL
  alert(chapter.closest('.contents')); // DIV
  alert(chapter.closest('h1')); // null(因为 h1 不是祖先)
</script>

getElementsBy*

还有其他通过标签,类等查找节点的方法。

如今,它们大多已经成为了历史,因为 querySelector 功能更强大,写起来更短。

因此,这里我们介绍它们只是为了完整起见,而你仍然可以在就脚本中找到这些方法。

  • elem.getElementsByTagName(tag) 查找具有给定标签的元素,并返回它们的集合。tag 参数也可以是对于“任何标签”的星号 "*"
  • elem.getElementsByClassName(className) 返回具有给定CSS类的元素。
  • document.getElementsByName(name) 返回在文档范围内具有给定 name 特性的元素。很少使用。

例如:

// 获取文档中的所有 div
let divs = document.getElementsByTagName('div');

让我们查找 table 中的所有 input 标签:

<table id="table">
  <tr>
    <td>Your age:</td>
    <td>
      <label>
        <input type="radio" name="age" value="young" checked> less than 18
      </label>
      <label>
        <input type="radio" name="age" value="mature"> from 18 to 50
      </label>
      <label>
        <input type="radio" name="age" value="senior"> more than 60
      </label>
    </td>
  </tr>
</table>
<script>
  let inputs = table.getElementsByTagName('input');
  for (let input of inputs) {
    alert( input.value + ': ' + input.checked );
  }
</script>

不要忘记字母 "s"

新手开发者有时会忘记字符 "s"。也就是说,他们会调用 getElementByTagName 而不是 getElementsByTagName

getElementById 中没有字母 "s",是因为它只返回单个元素。但是 getElementsByTagName 返回的是元素的集合,所以里面有 "s"

它返回的是一个集合,不是一个元素!

新手的另一个普遍的错误是写:

// 行不通
document.getElementsByTagName('input').value = 5;

这是行不通的,因为它需要的是一个 input 的 集合,并将值赋(assign)给它,而不是赋值给其中的一个元素。

我们应该遍历集合或通过对应的索引来获取元素,然后赋值,如下所示:

// 应该可以运行(如果有 input)
document.getElementsByTagName('input')[0].value = 5;

查找 .article 元素:

<form name="my-form">
  <div class="article">Article</div>
  <div class="long article">Long article</div>
</form>
<script>
  // 按 name 特性查找
  let form = document.getElementsByName('my-form')[0];
  // 在 form 中按 class 查找
  let articles = form.getElementsByClassName('article');
  alert(articles.length); // 2, found two elements with class "article"
</script>

实时的集合

所有的 "getElementsBy*" 方法都会返回一个 实时的(live) 集合。这样的集合始终反映的是文档的当前状态,并且在文档发生更改时会“自动更新”。

在下面的例子中,有两个脚本。

  1. 第一个创建了对 <div> 的集合的引用。截至目前,它的长度是 1
  2. 第二个脚本在浏览器再遇到一个 <div> 时运行,所以它的长度是 2
<div>First div</div>
<script>
  let divs = document.getElementsByTagName('div');
  alert(divs.length); // 1
</script>
<div>Second div</div>
<script>
  alert(divs.length); // 2
</script>

相反,querySelectorAll 返回的是一个 静态的 集合。就像元素的固定数组。

如果我们使用它,那么两个脚本都会输出 1

<div>First div</div>
<script>
  let divs = document.querySelectorAll('div');
  alert(divs.length); // 1
</script>
<div>Second div</div>
<script>
  alert(divs.length); // 1
</script>

现在我们可以很容易地看到不同之处。在文档中出现新的 div 后,静态集合并没有增加。

总结

有 6 种主要的方法,可以在 DOM 中搜素节点:

Method Searches by… Can call on an element? Live?
querySelector CSS-selector -
querySelectorAll CSS-selector -
getElementById id - -
getElementsByName name -
getElementsByTagName tag or ‘*’
getElementsByClassName class

目前为止,最常用的是 querySelectorquerySelectorAll,但是 getElementBy* 可能会偶尔有用,或者可以在就脚本中找到。

此外:

  • elem.matches(css) 用于检查 elem 与给定的 CSS 选择器是否匹配。
  • elem.closest(css) 用于查找与给定 CSS 选择器相匹配的最近的祖先。elem 本身也会被检查。

让我们在这里提一下另一种用来检查子级与父级之间关系的方法,因为它有时很有用:

  • 如果 elemBelemA 内(elemA 的后代)或者 elemA==elemBelemA.contains(elemB) 将返回 true。

任务

搜索元素

重要程度: 4

这是带有表格(table)和表单(form)的文档。

如何查找?……

  1. 带有 id="age-table" 的表格。
  2. 表格内的所有 label 元素(应该有三个)。
  3. 表格中的第一个 td(带有 “Age” 字段)。
  4. 带有 name="search"form
  5. 表单中的第一个 input
  6. 表单中的最后一个 input

在一个单独的窗口中打开 table.html 页面,并对此页面使用浏览器开发者工具。

解决方案

实现的方式有很多种。

以下列举的是其中一些方法:

// 1. 带有 id="age-table" 的表格。
let table = document.getElementById('age-table')
// 2. 表格内的所有 label 元素
table.getElementsByTagName('label')
// 或
document.querySelectorAll('#age-table label')
// 3. 表格中的第一个 td(带有 "Age" 字段)
table.rows[0].cells[0]
// 或
table.getElementsByTagName('td')[0]
// 或
table.querySelector('td')
// 4. 带有 name="search" 的 form。
// 假设文档中只有一个 name="search" 的元素
let form = document.getElementsByName('search')[0]
// 或者,专门对于 form
document.querySelector('form[name="search"]')
// 5. 表单中的第一个 input
form.getElementsByTagName('input')[0]
// 或
form.querySelector('input')
// 6. 表单中的最后一个 input
let inputs = form.querySelectorAll('input') // 查找所有 input
inputs[inputs.length-1] // 取出最后一个

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,我们将其“作为文本”插入,所有符号(symbol)均按字面意义处理。

比较两者:

<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> 获取 name “作为 HTML”:所有标签都变成标签,所以我们可以看到粗体的 name。
  2. 第二个 <div> 获取 name “作为文本”,因此我们可以从字面上看到 <b>Winnie-the-pooh!</b>

在大多数情况下,我们期望来自用户的文本,并希望将其视为文本对待。我们不希望在我们的网站中出现意料不到的 HTML。对 textContent 的赋值正好可以做到这一点。

“hidden” 属性

“hidden” 特性(attribute)和 DOM 属性(property)指定元素是否可见。

我们可以在 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" 做的是相同的事。但 hidden 写法更简洁。

这里有一个 blinking 元素:

<div id="elem">A blinking element</div>
<script>
  setInterval(() => elem.hidden = !elem.hidden, 1000);
</script>

更多属性

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

  • value<input><select><textarea>HTMLInputElementHTMLSelectElement……)的 value。
  • href<a href="...">HTMLAnchorElement)的 href。
  • id — 所有元素(HTMLElement)的 “id” 特性(attribute)的值。
  • ……以及更多其他内容……

例如:

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

总结

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

主要的 DOM 节点属性有:

nodeType

我们可以使用它来查看节点是文本节点还是元素节点。它具有一个数值型值(numeric value):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 特性(attribute)都具有相应的 DOM 属性。

导航:表单和元素

文档中的表单是特殊集合 document.forms 的成员。

这就是所谓的“命名的集合”:既是被命名了的,也是有序的。我们既可以使用名字,也可以使用在文档中的编号来获取表单。

document.forms.my  // name="my" 的表单
document.forms[0]    //  文档中的第一个表单
<form name="my">
  <input name="one" value="1">
  <input name="two" value="2">
</form>
<script>
  // 获取表单
  let form = document.forms.my; // <form name="my"> 元素
  // 获取表单中的元素
  let elem = form.elements.one; // <input name="one"> 元素
  alert(elem.value); // 1
</script>
<form>
  <input type="radio" name="age" value="10">
  <input type="radio" name="age" value="20">
</form>
<script>
let form = document.forms[0];
let ageElems = form.elements.age;
alert(ageElems[0]); // [object HTMLInputElement]
</script>

DOM 属性

我们已经见过了内建 DOM 属性。它们数量庞大。但是从技术上讲,没有人会限制我们,如果我们觉得这些 DOM 还不够,我们可以添加我们自己的。

DOM 节点是常规的 JavaScript 对象。我们可以 alert 它们。

例如,让我们在 document.body 中创建一个新的属性:

document.body.myData = {  name: 'Caesar',  title: 'Imperator'};
alert(document.body.myData.title); // Imperator

我们也可以像下面这样添加一个方法:

document.body.sayTagName = function() {  alert(this.tagName);};
document.body.sayTagName(); // BODY(这个方法中的 "this" 的值是 document.body)

我们还可以修改内建属性的原型,例如修改 Element.prototype 为所有元素添加一个新方法:

Element.prototype.sayHi = function() {  alert(`Hello, I'm ${this.tagName}`);};

document.documentElement.sayHi(); // Hello, I'm HTML

document.body.sayHi(); // Hello, I'm BODY

所以,DOM 属性和方法的行为就像常规的 Javascript 对象一样:

  • 它们可以有很多值。
  • 它们是大小写敏感的(要写成 elem.nodeType,而不是 elem.NoDeTyPe)。

HTML 特性

在 HTML 中,标签可能拥有特性(attributes)。当浏览器解析 HTML 文本,并根据标签创建 DOM 对象时,浏览器会辨别 标准的 特性并以此创建 DOM 属性。

所以,当一个元素有 id 或其他 标准的 特性,那么就会生成对应的 DOM 属性。但是非 标准的 特性则不会。

例如:

<body id="test" something="non-standard">  
<script>    
    alert(document.body.id); // test    // 非标准的特性没有获得对应的属性   
    alert(document.body.something); // undefined  
</script>
</body>

请注意,一个元素的标准的特性对于另一个元素可能是未知的。例如 "type"<input> 的一个标准的特性(HTMLInputElement),但对于 <body>HTMLBodyElement)来说则不是。规范中对相应元素类的标准的属性进行了详细的描述。

这里我们可以看到:

<body id="body" type="...">  
<input id="input" type="text">  
    <script>    
        alert(input.type); // text    
        alert(body.type); // undefined:DOM 属性没有被创建,因为它不是一个标准的特性  
    </script>
</body>

所以,如果一个特性不是标准的,那么就没有相对应的 DOM 属性。那我们有什么方法来访问这些特性吗?

当然。所有特性都可以通过使用以下方法进行访问:

  • elem.hasAttribute(name) — 检查特性是否存在。
  • elem.getAttribute(name) — 获取这个特性值。
  • elem.setAttribute(name, value) — 设置这个特性值。
  • elem.removeAttribute(name) — 移除这个特性。

这些方法操作的实际上是 HTML 中的内容。

我们也可以使用 elem.attributes 读取所有特性:属于内建 Attr 类的对象的集合,具有 namevalue 属性。

下面是一个读取非标准的特性的示例:

<body something="non-standard">  
    <script>    
        alert(document.body.getAttribute('something')); // 非标准的  
    </script>
</body>

HTML 特性有以下几个特征:

  • 它们的名字是大小写不敏感的(idID 相同)。
  • 它们的值总是字符串类型的。

下面是一个使用特性的扩展示例:

<body>  
    <div id="elem" about="Elephant">
    </div>  
    <script>    
        alert( elem.getAttribute('About') ); // (1) 'Elephant',读取    
        elem.setAttribute('Test', 123); // (2) 写入    
        alert( elem.outerHTML ); // (3) 查看特性是否在 HTML 中(在)    
        for (let attr of elem.attributes) { // (4) 列出所有      
            alert( `${attr.name} = ${attr.value}` );    
        }  
    </script>
</body>

请注意:

  1. getAttribute('About') — 这里的第一个字母是大写的,但是在 HTML 中,它们都是小写的。但这没有影响:特性的名称是大小写不敏感的。
  2. 我们可以将任何东西赋值给特性,但是这些东西会变成字符串类型的。所以这里我们的值为 "123"
  3. 所有特性,包括我们设置的那个特性,在 outerHTML 中都是可见的。
  4. attributes 集合是可迭代对象,该对象将所有元素的特性(标准和非标准的)作为 namevalue 属性存储在对象中。

属性—特性同步

当一个标准的特性被改变,对应的属性也会自动更新,(除了几个特例)反之亦然。

在下面这个示例中,id 被修改为特性,我们可以看到对应的属性也发生了变化。然后反过来也是同样的效果:

<input>
    <script>  
        let input = document.querySelector('input');  // 特性 => 属性  
        input.setAttribute('id', 'id');  
        alert(input.id); // id(被更新了)  
        // 属性 => 特性  
        input.id = 'newId';  
        alert(input.getAttribute('id')); // newId(被更新了)
    </script>

但这里也有些例外,例如 input.value 只能从特性同步到属性,反过来则不行:

<input>
    <script>  
        let input = document.querySelector('input');  // 特性 => 属性  
        input.setAttribute('value', 'text');  
        alert(input.value); // text  
        // 这个操作无效,属性 => 特性 
                input.value = 'newValue'; 
                alert(input.getAttribute('value')); // text(没有被更新!)
          </script>

在上面这个例子中:

  • 改变特性值 value 会更新属性。
  • 但是属性的更改不会影响特性。

这个“功能”在实际中会派上用场,因为用户行为可能会导致 value 的更改,然后在这些操作之后,如果我们想从 HTML 中恢复“原始”值,那么该值就在特性中。

DOM 属性是多类型的

DOM 属性不总是字符串类型的。例如,input.checked 属性(对于 checkbox 的)是布尔型的。

<input id="input" type="checkbox" checked>
    checkbox
    <script> 
        alert(input.getAttribute('checked')); // 特性值是:空字符串  
        alert(input.checked); // 属性值是:true
         </script>

还有其他的例子。style 特性是字符串类型的,但 style 属性是一个对象:

<div id="div" style="color:red;font-size:120%">
    Hello
</div>
<script>  // 字符串  
alert(div.getAttribute('style')); // color:red;font-size:120%  

// 对象 
alert(div.style); // [object CSSStyleDeclaration]  

alert(div.style.color); // red

</script>

尽管大多数 DOM 属性都是字符串类型的。

有一种非常少见的情况,即使一个 DOM 属性是字符串类型的,但它可能和 HTML 特性也是不同的。例如,href DOM 属性一直是一个 完整的 URL,即使该特性包含一个相对路径或者包含一个 #hash

这里有一个例子:

<a id="a" href="#hello">link</a>
<script>  
    // 特性  
    alert(a.getAttribute('href')); // #hello  
    // 属性  
    alert(a.href ); // http://site.com/page#hello 形式的完整 URL
</script>

如果我们需要 href 特性的值,或者其他与 HTML 中所写的完全相同的特性,则可以使用 getAttribute

非标准的特性,dataset

当编写 HTML 时,我们会用到很多标准的特性。但是非标准的,自定义的呢?首先,让我们看看它们是否有用?用来做什么?

有时,非标准的特性常常用于将自定义的数据从 HTML 传递到 JavaScript,或者用于为 JavaScript “标记” HTML 元素。

像这样:

<!-- 标记这个 div 以在这显示 "name" -->
<div show-info="name"></div>
<!-- 标记这个 div 以在这显示 "age" -->
<div show-info="age"></div>
<script>
  // 这段代码找到带有标记的元素,并显示需要的内容
  let user = {
    name: "Pete",
    age: 25
  };
  for(let div of document.querySelectorAll('[show-info]')) {
    // 在字段中插入相应的信息
    let field = div.getAttribute('show-info');
    div.innerHTML = user[field]; // 首先 "name" 变为 Pete,然后 "age" 变为 25
  }
</script>

它们还可以用来设置元素的样式。

例如,这里使用 order-state 特性来设置订单状态:

<style>
  /* 样式依赖于自定义特性 "order-state" */
  .order[order-state="new"] {
    color: green;
  }
  .order[order-state="pending"] {
    color: blue;
  }
  .order[order-state="canceled"] {
    color: red;
  }
</style>
<div class="order" order-state="new">
  A new order.
</div>
<div class="order" order-state="pending">
  A pending order.
</div>
<div class="order" order-state="canceled">
  A canceled order.
</div>

为什么使用特性比使用 .order-state-new.order-state-pendingorder-state-canceled 这些样式类要好?

因为特性值更容易管理。我们可以轻松地更改状态:

// 比删除旧的或者添加一个新的类要简单一些
div.setAttribute('order-state', 'canceled');

但是自定义的特性也存在问题。如果我们出于我们的目的使用了非标准的特性,之后它被引入到了标准中并有了其自己的用途,该怎么办?HTML 语言是在不断发展的,并且更多的特性出现在了标准中,以满足开发者的需求。在这种情况下,自定义的属性可能会产生意料不到的影响。

为了避免冲突,存在 data-* 特性。

所有以 “data-” 开头的特性均被保留供程序员使用。它们可在 dataset 属性中使用。

例如,如果一个 elem 有一个名为 "data-about" 的特性,那么可以通过 elem.dataset.about 取到它。

像这样:

<body data-about="Elephants">
<script>
  alert(document.body.dataset.about); // Elephants
</script>

data-order-state 这样的多词特性可以以驼峰式进行调用:dataset.orderState

这里是 “order state” 那个示例的重构版:

<style>
  .order[data-order-state="new"] {
    color: green;
  }
  .order[data-order-state="pending"] {
    color: blue;
  }
  .order[data-order-state="canceled"] {
    color: red;
  }
</style>
<div id="order" class="order" data-order-state="new">
  A new order.
</div>
<script>
  // 读取
  alert(order.dataset.orderState); // new
  // 修改
  order.dataset.orderState = "pending"; // (*)
</script>

使用 data-* 特性是一种合法且安全的传递自定义数据的方式。

请注意,我们不仅可以读取数据,还可以修改数据属性(data-attributes)。然后 CSS 会更新相应的视图:在上面这个例子中的最后一行 (*) 将颜色更改为了蓝色。

总结

  • 特性(attribute)— 写在 HTML 中的内容。
  • 属性(property)— DOM 对象中的内容。

简略的对比:

属性 特性
类型 任何值,标准的属性具有规范中描述的类型 字符串
名字 名字(name)是大小写敏感的 名字(name)是大小写不敏感的

操作特性的方法:

  • elem.hasAttribute(name) — 检查是否存在这个特性。
  • elem.getAttribute(name) — 获取这个特性值。
  • elem.setAttribute(name, value) — 设置这个特性值。
  • elem.removeAttribute(name) — 移除这个特性。
  • elem.attributes — 所有特性的集合。

在大多数情况下,最好使用 DOM 属性。仅当 DOM 属性无法满足开发需求,并且我们真的需要特性时,才使用特性,例如:

  • 我们需要一个非标准的特性。但是如果它以 data- 开头,那么我们应该使用 dataset
  • 我们想要读取 HTML 中“所写的”值。对应的 DOM 属性可能不同,例如 href 属性一直是一个 完整的 URL,但是我们想要的是“原始的”值。

修改文档(document)

DOM 修改是创建“实时”页面的关键。

在这里,我们将会看到如何“即时”创建新元素并修改现有页面内容。

例子:展示一条消息

让我们使用一个示例进行演示。我们将在页面上添加一条比 alert 更好看的消息。

它的外观如下:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>
<div class="alert">
  <strong>Hi there!</strong> You've read an important message.
</div>

这是一个 HTML 示例。现在,让我们使用 JavaScript 创建一个相同的 div(假设样式在 HTML 或外部 CSS 文件中)。

创建一个元素

要创建 DOM 节点,这里有两种方法:

document.createElement(tag)

用给定的标签创建一个新 元素节点(element node)

let div = document.createElement('div');
document.createTextNode(text)

用给定的文本创建一个 文本节点

let textNode = document.createTextNode('Here I am');

创建一条消息

在我们的例子中,消息是一个带有 alert 类和 HTML 的 div

let div = document.createElement('div');
div.className = "alert";
div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

我们创建了元素,但到目前为止,它还只是在变量中。我们无法在页面上看到该元素,因为它还不是文档的一部分。

插入方法

为了让 div 显示出来,我们需要将其插入到 document 中的某处。例如,在 document.body 中。

对此有一个特殊的方法 appenddocument.body.append(div)

这是完整代码:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>
<script>
  let div = document.createElement('div');
  div.className = "alert";
  div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";
  document.body.append(div);
</script>

下面这些方法提供了更多的插入方式:

  • node.append(...nodes or strings) — 在 node 末尾插入节点或字符串,
  • node.prepend(...nodes or strings) — 在 node 开头插入节点或字符串,
  • node.before(...nodes or strings) — 在 node 前面插入节点或字符串,
  • node.after(...nodes or strings) — 在 node 后面插入节点或字符串,
  • node.replaceWith(...nodes or strings) — 将 node 替换为给定的节点或字符串。

下面是使用这些方法将列表项添加到列表中,以及将文本添加到列表前面和后面的示例:

<ol id="ol">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>
<script>
  ol.before('before'); // 将字符串 "before" 插入到 <ol> 前面
  ol.after('after'); // 将字符串 "after" 插入到 <ol> 后面
  let liFirst = document.createElement('li');
  liFirst.innerHTML = 'prepend';
  ol.prepend(liFirst); // 将 liFirst 插入到 <ol> 的最开始
  let liLast = document.createElement('li');
  liLast.innerHTML = 'append';
  ol.append(liLast); // 将 liLast 插入到 <ol> 的最末尾
</script>

这张图片直观地显示了这些方法所做的工作:

修改文档(document) - 图1

因此,最终列表将为:

before
<ol id="ol">
  <li>prepend</li>
  <li>0</li>
  <li>1</li>
  <li>2</li>
  <li>append</li>
</ol>
after

这些方法可以在单个调用中插入多个节点列表和文本片段。

例如,在这里插入了一个字符串和一个元素:

<div id="div"></div>
<script>
  div.before('<p>Hello</p>', document.createElement('hr'));
</script>

所有内容都被“作为文本”插入。

所以,最终的 HTML 为:

<p>Hello</p>
<hr>
<div id="div"></div>

换句话说,字符串被以一种安全的方式插入到页面中,就像 elem.textContent 所做的一样。

所以,这些方法只能用来插入 DOM 节点或文本片段。

但是,如果我们想在所有标签和内容正常工作的情况下,将这些内容“作为 HTML” 插入到 HTML 中,就像 elem.innerHTML 方法一样,那有什么方法可以实现吗?

insertAdjacentHTML/Text/Element

为此,我们可以使用另一个非常通用的方法:elem.insertAdjacentHTML(where, html)

该方法的第一个参数是代码字(code word),指定相对于 elem 的插入位置。必须为以下之一:

  • "beforebegin" — 将 html 插入到 elem 前插入,
  • "afterbegin" — 将 html 插入到 elem 开头,
  • "beforeend" — 将 html 插入到 elem 末尾,
  • "afterend" — 将 html 插入到 elem 后。

第二个参数是 HTML 字符串,该字符串会被“作为 HTML” 插入。

例如:

<div id="div"></div>
<script>
  div.insertAdjacentHTML('beforebegin', '<p>Hello</p>');
  div.insertAdjacentHTML('afterend', '<p>Bye</p>');
</script>

……将导致:

<p>Hello</p><div id="div"></div><p>Bye</p>

这就是我们可以在页面上附加任意 HTML 的方式。

这是插入变体的示意图:

修改文档(document) - 图2

我们很容易就会注意到这张图片和上一张图片的相似之处。插入点实际上是相同的,但此方法插入的是 HTML。

这个方法有两个兄弟:

  • elem.insertAdjacentText(where, text) — 语法一样,但是将 text 字符串“作为文本”插入而不是作为 HTML,
  • elem.insertAdjacentElement(where, elem) — 语法一样,但是插入的是一个元素。

它们的存在主要是为了使语法“统一”。实际上,大多数时候只使用 insertAdjacentHTML。因为对于元素和文本,我们有 append/prepend/before/after 方法 — 它们也可以用于插入节点/文本片段,但写起来更短。

所以,下面是显示一条消息的另一种变体:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>
<script>
  document.body.insertAdjacentHTML("afterbegin", `<div class="alert">
    <strong>Hi there!</strong> You've read an important message.
  </div>`);
</script>

节点移除

想要移除一个节点,可以使用 node.remove()

让我们的消息在一秒后消失:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>
<script>
  let div = document.createElement('div');
  div.className = "alert";
  div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";
  document.body.append(div);
  setTimeout(() => div.remove(), 1000);
</script>

请注意:如果我们要将一个元素 移动 到另一个地方,则无需将其从原来的位置中删除。

所有插入方法都会自动从旧位置删除该节点。

例如,让我们进行元素交换:

<div id="first">First</div>
<div id="second">Second</div>
<script>
  // 无需调用 remove
  second.after(first); // 获取 #second,并在其后面插入 #first
</script>

克隆节点:cloneNode

如何再插入一条类似的消息?

我们可以创建一个函数,并将代码放在其中。但是另一种方法是 克隆 现有的 div,并修改其中的文本(如果需要)。

当我们有一个很大的元素时,克隆的方式可能更快更简单。

调用 elem.cloneNode(true) 来创建元素的一个“深”克隆 — 具有所有特性(attribute)和子元素。如果我们调用 elem.cloneNode(false),那克隆就不包括子元素。

一个拷贝消息的示例:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>
<div class="alert" id="div">
  <strong>Hi there!</strong> You've read an important message.
</div>
<script>
  let div2 = div.cloneNode(true); // 克隆消息
  div2.querySelector('strong').innerHTML = 'Bye there!'; // 修改克隆
  div.after(div2); // 在已有的 div 后显示克隆
</script>

DocumentFragment

DocumentFragment 是一个特殊的 DOM 节点,用作来传递节点列表的包装器(wrapper)。

我们可以向其附加其他节点,但是当我们将其插入某个位置时,则会插入其内容。

例如,下面这段代码中的 getListContent 会生成带有 <li> 列表项的片段,然后将其插入到 <ul> 中:

<ul id="ul"></ul>
<script>
function getListContent() {
  let fragment = new DocumentFragment();
  for(let i=1; i<=3; i++) {
    let li = document.createElement('li');
    li.append(i);
    fragment.append(li);
  }
  return fragment;
}
ul.append(getListContent()); // (*)
</script>

请注意,在最后一行 (*) 我们附加了 DocumentFragment,但是它和 ul “融为一体(blends in)”了,所以最终的文档结构应该是:

<ul>  <li>1</li>  <li>2</li>  <li>3</li></ul>

DocumentFragment 很少被显式使用。如果可以改为返回一个节点数组,那为什么还要附加到特殊类型的节点上呢?重写示例:

<ul id="ul"></ul>
<script>
function getListContent() {
  let result = [];
  for(let i=1; i<=3; i++) {
    let li = document.createElement('li');
    li.append(i);
    result.push(li);
  }
  return result;
}
ul.append(...getListContent()); // append + "..." operator = friends!
</script>

我们之所以提到 DocumentFragment,主要是因为它上面有一些概念,例如 template 元素,我们将在以后讨论。

老式的 insert/remove 方法

Old school

This information helps to understand old scripts, but not needed for new development.

由于历史原因,还存在“老式”的 DOM 操作方法。

这些方法来自真正的远古时代。如今,没有理由再使用它们了,因为诸如 appendprependbeforeafterremovereplaceWith 这些现代方法更加灵活。

我们在这儿列出这些方法的唯一原因是,你可能会在许多就脚本中遇到它们。

parentElem.appendChild(node)

node 附加为 parentElem 的最后一个子元素。

下面这个示例在 <ol> 的末尾添加了一个新的 <li>

<ol id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>
<script>
  let newLi = document.createElement('li');
  newLi.innerHTML = 'Hello, world!';
  list.appendChild(newLi);
</script>

parentElemnextSibling 前插入 node

下面这段代码在第二个 <li> 前插入了一个新的列表项:

<ol id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>
<script>
  let newLi = document.createElement('li');
  newLi.innerHTML = 'Hello, world!';
  list.insertBefore(newLi, list.children[1]);
</script>

如果要将 newLi 插入为第一个元素,我们可以这样做:

list.insertBefore(newLi, list.firstChild);
parentElem.replaceChild(node, oldChild)

parentElem 的后代中的 oldChild 替换为 node

parentElem.removeChild(node)

parentElem 中删除 node(假设 nodeparentElem 的后代)。

下面这个示例从 <ol> 中删除了 <li>

<ol id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>
<script>
  let li = list.firstElementChild;
  list.removeChild(li);
</script>

所有这些方法都会返回插入/删除的节点。换句话说,parentElem.appendChild(node) 返回 node。但是通常我们不会使用返沪值,我们只是使用对应的方法。

聊一聊 “document.write”

还有一个非常古老的向网页添加内容的方法:document.write

语法如下:

<p>Somewhere in the page...</p>
<script>
  document.write('<b>Hello from JS</b>');
</script>
<p>The end</p>

调用 document.write(html) 意味着将 html “就地马上”写入页面。html 字符串可以是动态生成的,所以它很灵活。我们可以使用 JavaScript 创建一个完整的页面并对其进行写入。

这个方法来自于没有 DOM,没有标准的上古时期……。但这个方法依被保留了下来,因为还有脚本在使用它。

由于以下重要的限制,在现代脚本中我们很少看到它:

document.write 调用只在页面加载时工作。

如果我们稍后调用它,则现有文档内容将被擦除。

例如:

<p>After one second the contents of this page will be replaced...</p>
<script>
  // 1 秒后调用 document.write
  // 这时页面已经加载完成,所以它会擦除现有内容
  setTimeout(() => document.write('<b>...By this.</b>'), 1000);
</script>

因此,在某种程度上讲,它在“加载完成”阶段是不可用的,这与我们上面介绍的其他 DOM 方法不同。

这是它的缺陷。

还有一个好处。从技术上讲,当在浏览器正在读取(“解析”)传入的 HTML 时调用 document.write 方法来写入一些东西,浏览器会像它本来就在 HTML 文本中那样使用它。

所以它运行起来出奇的快,因为它 不涉及 DOM 修改。它直接写入到页面文本中,而此时 DOM 尚未构建。

因此,如果我们需要向 HTML 动态地添加大量文本,并且我们正处于页面加载阶段,并且速度很重要,那么它可能会有帮助。但实际上,这些要求很少同时出现。我们可以在脚本中看到此方法,通常是因为这些脚本很旧。

总结

  • 创建新节点的方法:

    • document.createElement(tag) — 用给定的标签创建一个元素节点,
    • document.createTextNode(value) — 创建一个文本节点(很少使用),
    • elem.cloneNode(deep) — 克隆元素,如果 deep==true 则与其后代一起克隆。
  • 插入和移除节点的方法:

    • node.append(...nodes or strings) — 在 node 末尾插入,
    • node.prepend(...nodes or strings) — 在 node 开头插入,
    • node.before(...nodes or strings) — 在 node 之前插入,
    • node.after(...nodes or strings) — 在 node 之后插入,
    • node.replaceWith(...nodes or strings) — 替换 node
    • node.remove() — 移除 node

    文本字符串被“作为文本”插入。

  • 这里还有“旧式”的方法:

    • parent.appendChild(node)
    • parent.insertBefore(node, nextSibling)
    • parent.removeChild(node)
    • parent.replaceChild(newElem, node)

    这些方法都返回 node

  • html 中给定一些 HTML,elem.insertAdjacentHTML(where, html) 会根据 where 的值来插入它:

    • "beforebegin" — 将 html 插入到 elem 前面,
    • "afterbegin" — 将 html 插入到 elem 的开头,
    • "beforeend" — 将 html 插入到 elem 的末尾,
    • "afterend" — 将 html 插入到 elem 后面。

另外,还有类似的方法,elem.insertAdjacentTextelem.insertAdjacentElement,它们会插入文本字符串和元素,但很少使用。

  • 要在页面加载完成之前将 HTML 附加到页面:

    • document.write(html)

    页面加载完成后,这样的调用将会擦除文档。多见于旧脚本。