# JavaScript教程 - 16 DOM

继续讲解 DOM。

# 16.5 文本节点

在前面也说了文本也是节点。

下面再举个例子:

<body>
  <div id="my-div">For技术栈</div>

  <script>
    const element = document.getElementById('my-div');  // 获取到div元素
    console.log(element.firstChild);  // 获取div下的第一个节点,也就是文本节点:For技术栈
  </script>
</body>
1
2
3
4
5
6
7
8
  • 上面就获取到了文本节点。

但是通过获取文本节点再修改文本,太麻烦了,我们一般不这样做,我们只需要获取到文本节点的父节点进行操作就可以了。

所以在上面的代码中,获取 div 元素,就可以修改 div 中的文本了,有三种方式可以获取和修改元素中的文本内容:

  • element.textContent
  • element.innerText
  • element.innerHTML

举个栗子:

<body>
  <div id="box1">For技术栈</div>
  <div id="box2">For技术栈</div>
  <div id="box3">For技术栈</div>

  <script>
    const box1 = document.getElementById('box1');
    const box2 = document.getElementById('box2');
    const box3 = document.getElementById('box3');

    console.log(box1.textContent);  // For技术栈
    console.log(box2.innerText);  // For技术栈
    console.log(box3.innerHTML);  // For技术栈

    box1.textContent = 'For技术栈1';
    box2.innerText = 'For技术栈2';
    box3.innerHTML = 'For技术栈3';
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 在上面的代码中,可以通过上面三种方式来获取和修改 div 中的文本内容。

问题来了,有什么区别?

在看一个例子:

<body>
  <div id="box1">
    <span>For技术栈</span>
  </div>
  <div id="box2">
    <span>For技术栈</span>
  </div>
  <div id="box3">
    <span>For技术栈</span>
  </div>

  <script>
    const box1 = document.getElementById('box1');
    const box2 = document.getElementById('box2');
    const box3 = document.getElementById('box3');

    console.log(box1.textContent);  // 
    console.log(box2.innerText);  // 
    console.log(box3.innerHTML);  // <span>For技术栈</span>
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • textContentinnerText 获取不到 html 标签,innerText 就是纯文本,textContent 会包括换行符。
  • innerHTML 可以获取到 html 标签内容。

执行结果如下:


再看一下修改文本内容的区别:

<body>
  <div id="box1"></div>
  <div id="box2"></div>
  <div id="box3"></div>

  <script>
    const box1 = document.getElementById('box1');
    const box2 = document.getElementById('box2');
    const box3 = document.getElementById('box3');

    box1.textContent = '<b>For技术栈1</b>';
    box2.innerText = '<b>For技术栈2</b>';
    box3.innerHTML = '<b>For技术栈3</b>';
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

显示如下:

可以看到 textContentinnerText 会对文本内容进行转义(例如,大于号转换为 &gt; ),将 html 内容按照文本内容来显示,而 innerHTML 会将 html 内容渲染后显示,但是 innerHTML 这种方式容易存在 xss 注入风险,因为如果文本内容是用户自定义的,用户可以在文本内容中添加 html 内容(插入不和谐的内容,你的网站吃不了兜着走),而且可以添加 <script> 标签引入第三方的 js 脚本去执行,这就很危险了,所以使用 innerHTML 要确保内容是自己的。


textContentinnerText 还有一个区别就是 textContent 获取标签中的内容不会考虑 CSS 样式,而 innerText 会考虑 CSS 样式。

举个栗子:

<body>
  <div id="box1">
    <span style="display: none;">For技术栈</span>
  </div>
  <div id="box2">
    <span style="display: none;">For技术栈</span>
  </div>
  <div id="box3">
    <span style="display: none;">For技术栈</span>
  </div>

  <script>
    const box1 = document.getElementById('box1');
    const box2 = document.getElementById('box2');
    const box3 = document.getElementById('box3');

    console.log(box1.textContent);  // 'For技术栈'
    console.log(box2.innerText);  // ''
    console.log(box3.innerHTML);  // <span style="display: none;">For技术栈</span>
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • 将内容隐藏后,innerText 是获取不到内容的;通过 innerText 获取内容的时候,会触发网页重排(Reflow),也就是重新结算 CSS 样式,所以 textContent 的性能是要好一点的。

# 16.6 属性节点

下面来讲解 DOM 中的属性节点。标签中各个属性也是一个个的节点对象,但是和文本节点对象类似,我们没必要在操作属性的时候获取到属性节点再进行属性的操作,只需要获取到标签元素,就可以操作属性了。

属性的操作有两种方式:

  • 元素对象.属性
  • 元素对象.

# 1 元素对象.属性

举个栗子:

<body>
  <input id="username" name="username" value="foooor" class="input-class" />

  <script>
    const usernameEle = document.getElementById('username');

    // 获取属性
    console.log(usernameEle.type);  // text
    console.log(usernameEle.id);  // username
    console.log(usernameEle.name);  // username
    console.log(usernameEle.value);  // foooor
    console.log(usernameEle.className);  // 获取class,输出:input-class

    // 设置属性
    usernameEle.value = 'www.foooor.com'
    usernameEle.className = 'new-class';  // 设置class
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 可以通过 元素对象.属性 来获取和设置属性。

  • 上面修改了 value 的值,那么文本框的值就会发生变化了。

  • 但是有一个属性比较例外,需要注意一下,就是 class ,获取和设置 class 需要使用 className


在 HTML 中,有一些属性是布尔属性,例如 disabledcheckedreadonly 等。这些属性存在即生效,只要属性出现在标签中,无论值是什么,都表示"真"(true)。这种属性在获取和设置的时候,值为 boolean 类型。

举个栗子:

<body>
  <input disabled id="username" name="username" value="foooor" class="input-class" />

  <script>
    const usernameEle = document.getElementById('username');

    // 获取属性
    console.log(usernameEle.disabled);  // true,表示不可用

    // 设置属性
    usernameEle.disabled = false;  // 设置为可用
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2 attribute

获取和设置属性还可以通过 getAttribute('属性名')setAttribute('属性名', 值)

举个栗子:

<body>
  <input id="username" name="username" value="foooor" class="input-class" />

  <script>
    const usernameEle = document.getElementById("username");

    // 获取属性
    console.log(usernameEle.getAttribute('id')); // username
    console.log(usernameEle.getAttribute('name')); // username
    console.log(usernameEle.getAttribute('value')); // foooor
    console.log(usernameEle.getAttribute('class')); // 获取class,输出:input-class

    // 设置属性
    usernameEle.setAttribute('value', 'www.foooor.com');
    usernameEle.setAttribute ('class', 'new-class');
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 通过 attribute 的方式,class 属性不用特殊对待。

但是对于布尔属性需要注意一下,按照下面的方式来设置:

// 禁用,值为任何值都为true
usernameEle.setAttribute('disabled', 'disable');
// 启用,移除属性
usernameEle.removeAttribute('disabled');
1
2
3
4
  • 如果通过 setAttribute('disabled', false); 来设置,会被转换为 disable="false"disable="xxxx"结果都是 true 的,元素依旧被禁用

# 16.7 事件处理

什么是事件?

DOM 中的事件是网页与用户互动的核心机制之一。它允许我们监听用户的操作(如点击、键盘输入、鼠标移动等),并在事件发生时作出响应。

事件处理有三种方式:

方式一:HTML 属性方式(不推荐)

举个栗子:

<body>
  <button onclick="alert('你最帅!')">点击我</button>
</body>
1
2
3
  • 在上面的代码中,当点击按钮后,会触发 onclick,执行 onclick 的代码。

方式二:DOM 属性方式

<body>
  <button id="my-button">点击我</button>

  <script>
    const btn = document.getElementById("my-button");
    btn.onclick = function () {
      alert('你最帅!');
    };
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
  • 上面先获取到按钮元素,然后给按钮的 onclick 属性设置事件处理的方法,当点击按钮后,就会回调该方法。
  • 这种方式,同一个事件只能绑定一个事件处理函数,多次绑定会覆盖前面的。

方式三:标准事件监听

<body>
  <button id="my-button">点击我</button>

  <script>
    const btn = document.getElementById("my-button");
    // 绑定一个点击事件
    btn.addEventListener("click", function () {
      alert("你最帅!");
    });
    // 再绑定一个点击事件
    btn.addEventListener("click", function () {
      alert("你最牛逼!");
    });
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 首先获取到按钮元素,然后通过 addEventListener 给按钮添加事件监听器,第一个参数是事件的名称 click 表示单击,不同的事件有不同的名称,第二个是事件触发后的回调方法。
  • 这种方式一个事件可以添加多次回调,可以触发多个回调方法。

事件有很多种,后面再讲,现在先记住点击事件,以及事件的处理。

# 16.8 文档的加载

在前学习 DOM 的时候,编写 JavaScript 代码的时候,都是将 <script> 标签写在 HTML 元素的后面,这是为什么呢?

这是因为网页在加载的时候,是从上到下加载的,如果我们将 <script> 标签写在前面,那么在执行 JS 脚本的时候,DOM 元素都还没有被加载,那么通过 document.getElementById("my-button") 来获取元素是获取不到的。所以写在后面,等加载 JS 并执行的时候,前面的内容已经被加载了,就能获取到了。

问题来了, <script> 标签能否写在上面,位置随便呢?

可以,只需要让页面加载完成,JS 代码再执行就可以了。

# 1 window.load

可以将代码写到 window.load 的回调方法中,window.onload 事件会等到页面所有资源加载完成后才执行,例如会等到图片加载完成,页面中还有其他的 iframe 的话,也会等到 iframe 的内容加载完成。

举个栗子:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>For技术栈</title>

    <script>
      window.onload = function() {
        console.log("页面所有资源加载完成");
        const btn = document.getElementById("my-button");
        btn.onclick = () => alert("按钮被点击了");
      };
    </script>
  </head>
  <body>
    <button id="my-button">点击我</button>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 在上面的代码中,在 window.onload 事件回调方法中,就可以获取到 DOM 元素了。

还可以使用 addEventListener 来绑定:

window.addEventListener("load", function () {
  console.log("页面所有资源加载完成");
  const btn = document.getElementById("my-button");
  btn.onclick = () => alert("按钮被点击了");
});
1
2
3
4
5

# 2 DOMContentLoaded

DOMContentLoaded 事件在文档结构(DOM)加载完成后触发,不必等图片、CSS 加载完成。

document.addEventListener("DOMContentLoaded", function () {
  const btn = document.getElementById("my-button");
  btn.onclick = () => alert("按钮被点击了");
});
1
2
3
4

# 3 defer

我们可以将 JS 代码放在单独的 JS文件中,然后使用 <script> 引入,并添加 defer 属性(推荐现代做法之一)。defer 会让脚本延迟到整个 HTML 解析完成后再执行,等价于 DOMContentLoaded 前自动执行

举个栗子:

首先在 JS 文件中编写代码,例如 index.js

const btn = document.getElementById("my-button");
btn.onclick = () => alert("按钮被点击了");
1
2

然后在 HTML 页面引入 JS 文件:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>For技术栈</title>

    <!-- 引入js,并添加defer -->
    <script defer src="./script/index.js"></script>
  </head>
  <body>
    <button id="my-button">点击我</button>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 通过 <script> 标签引入,并添加 defer 属性。

# 4 模块化脚本

使用模块化 <script type="module">(现代推荐),模块化脚本会自动延迟执行,相当于默认带 defer 行为,适用于使用 ES6 模块语法的现代开发。

举个栗子:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>For技术栈</title>

    <script type="module">
      const btn = document.getElementById("my-button");
      btn.onclick = () => alert("模块化脚本按钮");
    </script>
  </head>
  <body>
    <button id="my-button">点击我</button>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • <script> 标签添加 type="module"

# 16.9 练习

上面讲到了很多 DOM 的操作,下面做一个联系。

点击上一张和下一张,实现多张图片的切换。

如下图:

实现起来也是比较简单的,首先创建一个数组,在数组中存储所有图片的 url,然后监听两个按钮的点击事件,在点击按钮的时候,从数组中获取 url,修改图片标签的 src 属性即可。

代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>For技术栈</title>

    <style>
      #gallery {
        text-align: center;
        margin-top: 50px;
      }
      img {
        width: 400px;
        height: 240px;
      }
      button {
        margin: 20px;
      }
    </style>

    <script>
      window.onload = function () {
        // 获取各个元素
        const imgElement = document.getElementById("image");  // 图片
        const prevButton = document.getElementById("prev");  // 上一张
        const nextButton = document.getElementById("next");  // 下一张

        // 图片 URL 数组
        const imageUrls = [
          './image/html5.webp',
          './image/css3.webp',
          './image/javascript.webp',
          './image/git.webp',
          './image/nginx.webp'
        ];
        let currentIndex = 0; // 当前显示图片的索引

        // 上一张按钮
        prevButton.addEventListener("click", function () {
          currentIndex--;
          // 防止越界
          if (currentIndex < 0) {
            currentIndex = 0;
          }

          imgElement.src = imageUrls[currentIndex];  // 修改图片
        });

        // 下一张按钮
        nextButton.addEventListener("click", function () {
          currentIndex++;
          // 防止越界
          if (currentIndex >= imageUrls.length) {
            currentIndex = imageUrls.length - 1;
          }
          
          imgElement.src = imageUrls[currentIndex];  // 修改图片
        });
      };
    </script>
  </head>
  <body>
    <div id="gallery">
      <img id="image" src="./image/html5.webp" alt="图片" />
      <br />
      <button id="prev">上一张</button>
      <button id="next">下一张</button>
    </div>
  </body>
</html>
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
  • 在切换图片的时候,切换图片的 url 即可。