目录

🍥 JavaScript DOM 操作

# 操作 DOM

每一个 DOM 元素节点都属于一个特定的类。这些类形成层次结构。

例如,标签 <a> 相对应的元素节点具有链接相关的(link-related)属性,标签 <input> 相对应的元素节点具有与输入相关的属性;

文本节点与元素节点不同。但是所有这些标签节点之间也存在共有的属性和方法,因为所有类型的 DOM 节点都形成了一个单一层次的结构。

最终 层次结构 的根节点是 EventTarget 类, Node 节点类继承自它,其他 DOM 节点继承自 Node 类。

点击查看
  • EventTarget :根「抽象」类。它作为一个基础,让所有的节点都支持 事件行为。(没有实现对象)
  • Node :抽象类。是 DOM 节点的基础,提供了 DOM 树的核心功能( parentNodenextSiblingchildNodesgetter )(没有实现对象)但是有继承自它的具体节点类, Text 文本节点、 Element 元素节点、注释节点等。
  • Element :DOM 元素节点的基础类。提供了 元素级导航的 getter 。特殊的元素基础节点类继承自它, SVGElementXMLElementHTMLElement 等。
  • HTMLElement :是所有的 HTML 元素基础类。各类的 HTML 元素继承自它:
    • HTMLInputElement<input> 元素的类;
    • HTMLBodyElementHTMLAnchorElement … 等。

一些元素没有特定的属性,例如 <span><section><article> ,都是 HTMLElement 的实例。

从一个 DOM 元素角度出发,例如 <input> 的全部属性和方法的来源(都是来自继承下列的类):

  • HTMLInputElement :该类提供特定于输入的属性;
  • HTMLElement :提供了通用的 HTML 元素方法(以及 getter 和 setter);
  • Element :提供通用泛型元素方法;
  • Node :提供通用 DOM 节点属性;
  • EventTarget : 为事件(包括事件本身)提供支持;
  • 最后包括 Object 普通对象的方法。

对于 对象 是适用的属性和方法,DOM 节点也适用:

  • 获取名称:

    以获取引用类的名称为例,通过 constructor 获取:

    console.log(document.body.constructor.name) // HTMLBodyElement
    
    1

    toString 方法:

    console.log(document.body) // [object HTMLBodyElement]
    
    1
  • 检查继承 instanceof :

    console.log(document.body instanceof HTMLBodyElement) // true
    console.log(document.body instanceof HTMLElement) // true
    console.log(document.body instanceof Element) // true
    console.log(document.body instanceof Node) // true
    console.log(document.body instanceof EventTarget) // true
    
    1
    2
    3
    4
    5

console... 开发者命令行输出 DOM 元素:

  • console.log :显示元素的 DOM 树。
  • console.dir :显示为 DOM 对象,展示属性和方法。

# 读取 DOM 元素属性

  • 获取 DOM 节点的节点 / 标签名称:
    • tagName :仅适用于 Element 元素节点。
    • nodeName :适用于任意的 Node 节点。

标签名称的大小写问题:

  • 在浏览器 HTML 模式文档中, tagName/nodeName 始终是大写的:它是 BODY ,而不是 <body><BoDy>
  • 在 XML 模式中,大小写保持为原样。
  • 获取元素名称: ele.name
  • 获取元素 idele.id
  • 获取元素值 valueele.value
  • 获取元素的类名: ele.className

# 写入 HTML

# innerHTML

innerHTML 允许将元素中的 HTML 获取为字符串形式,并且可以修改它。是更改页面最有效的方法之一。

🌰 例子 / 获取 body 的内容,完全替换:

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

  <script>
    consoel.log(document.body.innerHTML); // 读取当前内容
    document.body.innerHTML = 'The new BODY!'; // 替换它
  </script>

</body>
1
2
3
4
5
6
7
8
9
10

🌰 例子 / 插入无效的 HTML 会被修复(未闭合的标签):

<body>

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

</body>
1
2
3
4
5
6
7
8

对于脚本标签 <script> 无效,可以插入但是不会执行。

注意

使用 innerHTML+= 会完全重写掉原来的内容并且附加的内容。

例如:

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

技术上, innerHTML 完成了两项工作:移除原来的内容;写入新的内容。

因为内容被重写,所以所有的图片和资源都会重新加载;这对于一些输入、选中等类型的元素是致命的,这会丢失原来的数据。

# outerHTML

outerHTML 属性包含了元素的 完整 HTML。 innerHTML 的内容加上元素标签。

🌰 例子:

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

<script>
  console.log(elem.outerHTML); // <div id="elem">Hello <b>World</b></div>
</script>
1
2
3
4
5

注意:与 innerHTML 不同,写入 outerHTML 不会改变元素。而是在 DOM 中替换它。

🌰 例子 / 选中一个 DOM 元素,使用 outerHTML 替换:

<div>
  hello world!
</div>
1
2
3
let div = querySelector("div")
div.outerHTML = '<p>new world</p>'
console.log(div.outerHTML) // <div> hello world! </div>
1
2
3

尽管第二行中,使用了 outerHTML 替换掉原来的内容,在外部 HTML 文档可以看到原来的内容被替换。但是原来的 outerHTML 内容不会改变。

说明, outerHTML 赋值不会修改 DOM 元素, 而是将其从 DOM 中删除并在其位置插入新的 HTML。

所以,上面的例子 outerHTML 完成了:将 div 从文档中移除;另一个 HTML 文档片段被插入到其位置; div 仍然拥有旧的值,新的 HTML 没有被赋值给任何变量。

仍然可以通过 选择器 获取新的元素的引用;并且原来的获取的元素仍然保留。

注意

innerHTMLouterHTML 只对 元素节点 有效。

# nodeValue / data 文本节点

对于其他节点类型,例如文本节点类型,具有它们的对应项: nodeValuedata 属性。两种属性只有细微的差异。

🌰 例子 / 使用 data 获取节点的内容:

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

    let comment = text.nextSibling;
    console.log(comment.data); // Comment
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11

# textContent 纯文本

textContent 提供了对元素内的 文本 的访问权限:仅文本,去掉所有 标签。

🌰 例子 / 读取纯文本内容:

<div id="news">
  <h1>Headline!</h1>
  <p>Martians attack people!</p>
</div>
1
2
3
4
console.log(news.textContent); // Headline! Martians attack people!
1
  • 使用 innerHTML ,将其 作为 HTML 插入,带有所有 HTML 标签会被作为 HTML 处理。
  • 使用 textContent ,将其 作为文本 插入,所有符号均按字面意义处理。

🌰 例子 / 安全方式写入文本:

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

  elem1.innerHTML = name;
  elem2.textContent = name;
</script>
1
2
3
4
5
6

# hidden 属性

用于 指定 DOM 元素是否可见。

🌰 例子:

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

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

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

<script>
  let elem = document.querySelector('#elem')
  elem.hidden = true;
</script>
1
2
3
4
5
6
7
8
9
10

实现的效果与 style="display: none" 相同。

# 区分 Atrributesproperties

在浏览器页面加载时,会解析 HTML 从中生成 DOM 对象。对于元素节点 的大多数 HTML Attributes 特性 会自动变成 DOM 对象节点的 properties 属性。但是特性和属性映射不是一一对应的。

🌰 例如: <body id="page">id 特性会对应属性 body.id=“page”

# DOM 属性 Property

由于 DOM 元素对象也是 JavaScript 常规对象,所以可以更改它们。

  • 可以有很多值。
  • 键大小写敏感。

🌰 例子:

document.body.myData = {
  name: "name",
  title: "title"
}

console.log(document.body.myData.name) // "name"
1
2
3
4
5
6

添加一个方法:

document.body.sayTagName = function () {
  console.log(this.tagName)
}

document.body.sayTagName() // BODY
1
2
3
4
5

修改原型:

Element.prototype.sayHi = funciton () {
  console.log(`HI, IAM ${this.tagName}`)
}

document.documentElement.sayHi() // HI, IAM HTML
document.body.sayHi() // HI, IAM BODY
1
2
3
4
5
6

# HTML 特性 Attribute

指的是 HTML 标签中的 标准特性 并且以此创建 DOM 属性。

HTML 特性有以下几个特征:

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

🌰 例子:

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

可以看出:对应的 标准特性 id 才会创建 DOM 属性。非标准的特性不会。

但是不同的标签,标准的特性可能会不同。例如 type 特性是 <input> 的一个标准的特性(HTMLInputElement (opens new window)),但对于 <body>HTMLBodyElement (opens new window))来说则不是。规范中对相应元素类的标准的属性进行了详细的描述。

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

所有特性(包含非标准特性)都可以通过使用以下方法进行访问:

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

🌰 例子 / 使用特性:

<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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 特性的名称是大小写不敏感的。
  • 可以将任何东西赋值给特性,但是这些东西会变成 字符串类型。所以这里的值为 "123"
  • 所有特性,包括自己设置的那个特性,在 outerHTML 中都是可见的。
  • attributes 集合是可迭代对象,该对象将所有元素的特性(标准和非标准的)作为 namevalue 属性存储在对象中。

# 属性与特性同步

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

🌰 例子:

<input>

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

  // 特性 => 属性
  input.setAttribute('id', 'id');
  alert(input.id); // id(被更新了)

  // 属性 => 特性
  input.id = 'newId';
  alert(input.getAttribute('id')); // newId(被更新了)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

id 被修改为特性,对应的属性也会发生变化。反之亦然。

🌰 例子 / 例外:

<input>

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

  // 特性 => 属性
  input.setAttribute('value', 'text');
  alert(input.value); // text

  // 这个操作无效,属性 => 特性
  input.value = 'newValue';
  alert(input.getAttribute('value')); // text(没有被更新!)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

这个例子中:

  • 改变特性值 value 会更新属性。
  • 但是属性的更改不会影响特性。意味着 特性的 value 会被保留在特性中。(之后想要原始值可用)

# DOM 属性的类型

DOM 属性不总是 字符串类型,但是大多是都是字符串。

🌰 例子 / checked 属性是布尔型:

<input id="input" type="checkbox" checked> checkbox

<script>
  console.log(input.getAttribute('checked')); // 特性值是:空字符串
  console.log(input.checked); // 属性值是:true
</script>
1
2
3
4
5
6

🌰 例子 / 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>
1
2
3
4
5
6
7
8
9
10

🌰 例子 / 即使一个 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>
1
2
3
4
5
6
7
8

所以要使用完整的路径 href 或者其他与 HTML 中所写的完全相同的特性,则可以使用 getAttribute

# 非标准 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]; 
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

🌰 例子 / 设置元素的样式:

<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

使用特性值比使用特定的类名更好管理,可以轻松改变状态。

div.setAttribute('order-state', 'canceled');
1

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

为了避免冲突,保留以 data-* 开头命名的特性,供程序员使用。它们可在 dataset 属性中使用。

使用 data-* 特性是一种合法且安全的传递自定义数据的方式。不仅可以直接读取数据,还可以修改数据属性。

🌰 例子:

<body data-about="Elephants">
<script>
  console.log(document.body.dataset.about); // Elephants
</script>
1
2
3
4

🌰 例子:

<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>
  // 读取
  console.log(order.dataset.orderState); // new

  // 修改
  order.dataset.orderState = "pending"; // (*)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 总结

  • 大多数情况下,最好使用 DOM 属性。仅当 DOM 属性无法满足开发需求,并且真的需要特性时,才使用特性。
    • 需要一个非标准的特性。它应该以 data- 开头,使用 dataset 获取、修改。

# 修改 DOM 文档

动态创建新元素并修改现有页面内容。

🌰 例子 / 向用户展示新的信息:

<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>
1
2
3
4
5
6
7
8
9
10
11
12
13

要使用 JavaScript 创建这个相同的 div 元素内容(CSS 样式已存在)。

# 创建元素

创建 DOM 元素节点的两种方法:

  • document.createElement(tag) :用给定的标签创建一个新 元素节点

    let div = document.createElement('div');
    
    1
  • document.createTextNode(text) :用给定的文本创建一个文本节点:

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

# 完善元素内容

  • 创建元素节点 ✅
  • 设置类名
  • 填充内容
let div = document.createElement('div');

div.className = "alert";

div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";
1
2
3
4
5

# 插入元素

为了让 div 显示出来,需要将其插入到 document 文档中的某处。

使用特殊的方法 append :可以在其他任何元素上调用 append 方法,以将另外一个元素放入到里面。

document.body.append(div)
div.append(anotherElement)
1
2

🌰 例子 / 完整的代码:

<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

document.body 调用了 append 方法。

更多的插入元素的方法,对应不同的插入位置:

  • node.append()node 末尾 插入节点或字符串,
  • node.prepend()node 开头 插入节点或字符串,
  • node.before() :在 node 前面 插入节点或字符串,
  • node.after() —— 在 node 后面 插入节点或字符串,
  • node.replaceWith()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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

最终列表为:

before
<ol id="ol">
  <li>prepend</li>
  <li>0</li>
  <li>1</li>
  <li>2</li>
  <li>append</li>
</ol>
after
1
2
3
4
5
6
7
8
9

以上的插入元素的方法只能插入 带有内容节点 或者字符串内容。对于 HTML 代码的字符串,并不能被 浏览器解析,而是作为文本片段插入到页面。

通用的插入元素方法: elem.insertAdjacentHTML(where, html)

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

第二个参数 html ,是 HTML 字符串,该字符串会被「作为 HTML」 插入。

🌰 例子:

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

结果:

<p>Hello</p>
<div id="div"></div>
<p>Bye</p>
1
2
3

衍生的兄弟方法:

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

(对于这些 元素和文本,一般使用 append/prepend/before/after 更加方便)

# 移除节点

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

🌰 例子 / 将消息在一秒后移除:

<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>
1
2
3
4
5
6
7
8

# 移动节点

利用 插入元素的方法 after() 或者其他方法。

如果要将一个元素 移动 到另一个地方,则无需将其从原来的位置中删除。所有插入方法都会自动从旧位置删除该节点。

🌰 例子 / 交换元素的位置:

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

# 克隆节点

调用 elem.cloneNode(true) 来创建元素的一个深克隆:具有所有特性和子元素。

调用 elem.cloneNode(false) ,那克隆就不包括子元素。

🌰 例子:

<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>
1
2
3
4
5
6
7
8
9
10

# 节点片段 DocumentFragment

这是一个特殊的 DOM 节点,用来传递节点列表的 包装器。可以向其 附加其他节点;当将其插入某个位置时,则会插入其内容。

🌰 例子 / 生成带有 <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(getListCount())
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

结果:

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

一般 DocumentFragment 很少被显式使用。直接写成返回一个节点数组即可:

function getListContent() {
  let result = [];

  for(let i=1; i<=3; i++) {
    let li = document.createElement('li');
    li.append(i);
    result.push(li);
  }

  return result;
}
1
2
3
4
5
6
7
8
9
10
11

# 总结

  • 创建新节点的方法:

    • document.createElement(tag) :用给定的 tag 标签创建新的元素节点。
    • document.createTextNode(value) :创建文本节点。
    • element.cloneNode(deep) :克隆 element 节点元素。
  • 插入和移除节点的方法:

    • node.append() — 在 node 末尾插入,
    • node.prepend() — 在 node 开头插入,
    • node.before() — 在 node 之前插入,
    • node.after() — 在 node 之后插入,
    • node.replaceWith() — 替换 node
    • node.remove() — 移除 node
📢 上次更新: 2022/09/02, 10:18:16