什么是DOM和BOM

JavaScript的三大组成部分

image-20251116144529390

上面这张图,我们可以看到有四个元素:JavaScript,ECMAScript,DOM和BOM,那么它们四个之间有什么联系呢?

1
JavaScript = ECMAscript + BOM + DOM

ECMAScript 是一种由 ECMA国际(前身为欧洲计算机制造商协会)通过 ECMA-262 标准化的脚本程序设计语言,它是JavaScript(简称JS)的标准,浏览器就是去执行这个标准。

ECMAscript更像一个规定,规定了各个浏览器怎么样去执行JavaScript的语法,因为我们知道JavaScript是运行在浏览器上的脚本语言!有了规定,但是我们还缺少与页面中各个元素交互的方式,此时下面的DOM诞生了!

DOM

Document Object Model,文档对象模型

简单说,DOM 是浏览器将 HTML 文档解析后生成的树状结构对象,它将 HTML 中的每个标签、文本、属性等都转换成可被 JS 操作的 “对象”,让 JS 能动态修改页面内容、结构和样式。

它也可以理解成一种独立于语言,用于操作xml,html文档的应用编程接口,对于JavaScript,为了能够使JavaScript操作Html,JavaScript就有了一套自己的DOM编程接口。

树状结构:HTML 文档的每个部分(如 <html><div>、文本、属性)都是树中的一个 “节点(Node)”,根节点是 document(代表整个文档)。

例:一个简单的 HTML 结构对应的 DOM 树:

1
2
3
4
5
6
<html>
<body>
<h1>Hello</h1>
<p>World</p>
</body>
</html>

对应的 DOM 树结构:

documenthtml 节点 → body 节点 → 包含 h1 节点(文本 “Hello”)和 p 节点(文本 “World”)。、

很明显,BOM树中,每个节点可以有两个身份:可以是父节点的子节点,也可以是其他子节点的父节点

BOM

Browser Object Model,浏览器对象模型

BOM 是浏览器提供的与浏览器窗口交互的对象模型,它不针对页面内容,而是控制浏览器本身的行为(如窗口大小、地址栏、历史记录等)。

BOM 是为了控制浏览器的行为而出现的接口。对于JavaScript,为了能够让JavaScript能控制浏览器的行为,JavaScript就有了一套自己的BOM接口。

核心特点:

  1. window 为核心window 是 BOM 的顶层对象,代表浏览器窗口,所有 BOM 对象都是 window 的属性。
  2. 主要子对象:
    • window.document:指向 DOM 的根节点(因此 DOM 可以看作是 BOM 的一部分)。
    • window.location:控制浏览器地址栏(如 location.href = 'https://www.baidu.com' 可跳转页面)。
    • window.history:操作浏览器历史记录(如 history.back() 后退一页)。
    • window.navigator:获取浏览器信息(如浏览器版本、设备类型)。
    • window.screen:获取屏幕信息(如屏幕分辨率)。
    • 弹窗相关:alert()confirm()prompt() 等。

DOM 与 BOM 的关系

  • 包含关系:DOM 是 BOM 的一部分(window.document 属于 BOM)。
  • 作用范围:
    • DOM 专注于页面内容(HTML 结构、样式、文本)。
    • BOM 专注于浏览器本身(窗口、地址、历史等)。

JavaScript操作DOM

DOM 树中的每个元素都是 “节点”,常见节点类型包括:

  1. 元素节点(Element):HTML 标签(如 <div><p>),是最常用的节点类型。
  2. 文本节点(Text):标签内的文本内容(如 <p>文本</p> 中的 “文本”)。
  3. 属性节点(Attribute):标签的属性(如 <img src="pic.jpg"> 中的 src)。
  4. 文档节点(Document):整个 HTML 文档的根节点(document 对象)。

可以通过节点的 nodeType 属性判断类型(元素节点为 1,文本节点为 3,文档节点为 9)。

获取节点的DOM方法

要操作节点,首先需要 “找到” 它。DOM 提供了多种查找方法:

按照标签名查找

1
2
//1.通过元素的id属性值来获取元素,返回的是一个元素对象,唯一
var element = document.getElementById(id_content)

按照 name 属性查找

1
2
//2.通过元素的name属性值来获取元素,返回的是一个元素对象的数组
var element_list = document.getElementsByName(name_content)

按照类名查找

1
2
3
4
5
// 通过元素的class属性值来获取元素,返回的是一个元素对象的数组
var element_list = document.getElementsByClassName(class_content)

// 返回所有含 class="active" 的元素(HTMLCollection 集合)
const actives = document.getElementsByClassName('active');

按照标签名查找

1
2
3
4
5
6
// 通过标签名获取元素,返回的是一个元素对象数组
var element_list = document.getElementsByTagName(tagName)

// 在指定父元素内查找(更高效)
const parent = document.getElementById('parent');
const childSpans = parent.getElementsByTagName('span');

按 CSS 选择器查找

1
2
3
4
5
// 返回匹配选择器的第一个元素
const firstItem = document.querySelector('.list .item');

// 返回匹配选择器的所有元素(NodeList 集合)
const allItems = document.querySelectorAll('.list .item');

注意

  • getElementsByXXX 返回的是动态集合(页面元素变化时会自动更新)。
  • querySelectorAll 返回的是静态集合(不会随页面变化)

可交互示例:尝试不同的获取节点方法

下面是一个可交互的示例,你可以尝试不同的获取节点方法:

<!DOCTYPE html>
<h3>获取节点方法演示</h3>
<div id="test-area">
  <p id="para1" class="item active">段落1 (id: para1, class: item active)</p>
  <p id="para2" class="item">段落2 (id: para2, class: item)</p>
  <span name="test" class="item">Span元素 (name: test)</span>
  <p class="item active">段落3 (class: item active)</p>
</div>
<div style="margin: 15px 0;">
  <button onclick="testGetById()">getElementById('para1')</button>
  <button onclick="testGetByClass()">getElementsByClassName('active')</button>
  <button onclick="testGetByTag()">getElementsByTagName('p')</button>
  <button onclick="testGetByName()">getElementsByName('test')</button>
  <button onclick="testQuerySelector()">querySelector('.active')</button>
  <button onclick="testQuerySelectorAll()">querySelectorAll('.item')</button>
  <button onclick="clearHighlight()">清除高亮</button>
</div>
<div class="result-box" id="result">点击上方按钮查看结果...</div>

操作 DOM 节点(增删改查)

创建节点

1
2
3
4
5
6
7
8
9
10
11
// 创建元素节点(<p>)
const p = document.createElement('p');

// 创建文本节点("Hello DOM")
const textNode = document.createTextNode('Hello DOM');

// 创建一个属性节点,传参是对应的属性名
var attr_node = document.createAttribute(attribute_name)

// 将文本节点添加到元素节点中(<p>Hello DOM</p>)
p.appendChild(textNode);
  • 每个属性节点都有一个 name(属性名,如 hrefclass)和 value(属性值,如 https://example.com)。

  • 属性节点不能独立存在,必须通过 setAttributeNode 绑定到某个元素节点上才有效。

  • 所以创建属性节点这个方法,要搭配具体的元素,也就是你要先获取某个具体元素,然后对这个元素创建一个属性节点,最后对这个元素添加这个属性节点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <!-- 初始元素 -->
    <div id="box"></div>

    <script>
    // 1. 获取元素节点
    const box = document.getElementById('box');

    // 2. 创建属性节点(指定属性名:"class")
    const classAttr = document.createAttribute('class');

    // 3. 给属性节点设置属性值("active")
    classAttr.value = 'active';

    // 4. 将属性节点绑定到元素上
    box.setAttributeNode(classAttr);

    // 此时元素变为:<div id="box" class="active"></div>
    </script>
  • 但是几乎没有像上面这么写的,上面的操作可以用 setAttribute 直接完成(无需显式创建属性节点)

    1
    box.setAttribute('class', 'active'); // 直接设置属性名和值

添加节点

1
2
3
4
5
6
7
// 向父节点末尾添加子节点
const parent = document.getElementById('parent');
parent.appendChild(p); // 将上面创建的 <p> 添加到 parent 中

// 在指定子节点前插入新节点
const referenceNode = document.getElementById('ref');
parent.insertBefore(p, referenceNode); // 在 ref 前插入 p

注意,添加节点之前,你要先创建好节点,同时要选好父节点element,在指定子节点前插入新节点,用这个方法你还要找好插入位置后面的兄弟节点。

可交互示例:动态创建和添加节点

下面是一个可交互的示例,你可以动态创建节点并添加到页面中:

<!DOCTYPE html>
<h3>创建和添加节点演示</h3>
<div>
  <label>节点内容:</label>
  <input type="text" id="nodeContent" value="新节点" placeholder="输入节点内容">
</div>
<div>
  <label>插入位置:</label>
  <select id="insertPosition">
    <option value="append">末尾添加 (appendChild)</option>
    <option value="before">在第一个节点前 (insertBefore)</option>
  </select>
</div>
<div>
  <button onclick="createAndAdd()">创建并添加节点</button>
  <button onclick="clearAll()">清空所有节点</button>
</div>
<div id="parent-container">
  <p class="new-item">这是初始节点1</p>
  <p class="new-item">这是初始节点2</p>
</div>
<div class="info" id="info">节点数量:2</div>

删除节点

删除节点的核心是将某个节点从 DOM 树中移除,使其不再参与页面渲染。DOM 提供了两种常用方法:

1
2
3
4
5
6
7
// 父节点删除子节点
const child = document.getElementById('child');
// 删除 parent 内的某个节点 child
parent.removeChild(child);

// 节点自身删除(更简单)
child.remove();

parent.removeChild(child):通过父节点删除子节点

父节点.removeChild(要删除的子节点)

从父节点的子节点列表中移除指定的子节点,并返回被删除的节点(可后续复用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="parent">
<p id="child1">子节点1</p>
<p id="child2">子节点2</p>
</div>

<script>
const parent = document.getElementById('parent');
const child1 = document.getElementById('child1');

// 用父节点删除子节点
const deletedNode = parent.removeChild(child1);
// 此时页面中 parent 只剩 child2,deletedNode 保存被删除的 child1
</script>

必须明确指定 “父节点” 和 “要删除的子节点”,且子节点必须是父节点的直接子节点

注意,被删除的节点并未被销毁,只是从 DOM 树中移除,可通过变量保存后重新添加到其他位置。

child.remove():节点自身删除(更简洁)

要删除的节点.remove(),直接删除当前节点,无需显式指定父节点(内部会自动找到其父节点并执行删除)。

1
2
const child2 = document.getElementById('child2');
child2.remove(); // 直接删除自身,无需找父节点

IE 不支持

可交互示例:删除节点演示

下面是一个可交互的示例,你可以尝试删除节点:

<!DOCTYPE html>
<h3>删除节点演示</h3>
<div id="list-container">
  <div class="list-item" id="item1">
    <span>项目 1</span>
    <button class="delete-btn" onclick="removeByParent(this)">使用 parent.removeChild() 删除</button>
  </div>
  <div class="list-item" id="item2">
    <span>项目 2</span>
    <button class="delete-btn" onclick="removeBySelf(this)">使用 element.remove() 删除</button>
  </div>
  <div class="list-item" id="item3">
    <span>项目 3</span>
    <button class="delete-btn" onclick="removeByParent(this)">使用 parent.removeChild() 删除</button>
  </div>
  <div class="list-item" id="item4">
    <span>项目 4</span>
    <button class="delete-btn" onclick="removeBySelf(this)">使用 element.remove() 删除</button>
  </div>
</div> 
<div class="info" id="info">当前节点数量:4</div>

替换节点(parent.replaceChild

替换节点是用一个新节点替换父节点中的某个旧节点,本质是 “先删除旧节点,再添加新节点” 的合并操作。

父节点.replaceChild(新节点, 旧节点)

将父节点中的 “旧节点” 替换为 “新节点”,并返回被替换的旧节点,旧节点并非被销毁,只是移出DOM树,可后续复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="parent">
<p id="old">旧节点</p>
</div>

<script>
const parent = document.getElementById('parent');
const oldNode = document.getElementById('old');

// 1. 创建新节点
const newNode = document.createElement('span');
newNode.textContent = '新节点';

// 2. 替换节点
const replacedNode = parent.replaceChild(newNode, oldNode);
// 此时 parent 中旧节点被替换为新节点,replacedNode 保存旧节点
</script>
  1. 新节点可以是:

    • 新创建的节点(如 createElement 创建的元素);
    • 从其他位置 “移动” 过来的节点(此时会从原位置移除,添加到新位置)。
    1
    2
    3
    // 从 otherParent 中移动 node 到 parent 中,替换 oldNode
    const node = document.getElementById('node');
    parent.replaceChild(node, oldNode);
  2. 旧节点必须是父节点的直接子节点,否则会报错(同 removeChild)。

复制节点(cloneNode

复制节点用于创建一个节点的副本(克隆),便于快速生成结构相同的节点。

被复制的节点.cloneNode(deep)

  • 参数 deep:布尔值,true 表示 “深复制”(复制节点本身及所有子节点),false 表示 “浅复制”(只复制节点本身,不包含子节点)。
  • 返回值:复制出的新节点(未添加到 DOM 树中,需手动添加)。

浅复制(deep: false

只复制节点本身,不包含子节点(文本、子元素等都不会复制)。

1
2
3
4
5
6
7
8
9
10
11
12
<p id="original">
原始文本 <span>子元素</span>
</p>

<script>
const original = document.getElementById('original');
// 浅复制:只复制 <p> 标签本身,不包含文本和 <span>
const shallowClone = original.cloneNode(false);

document.body.appendChild(shallowClone);
// 页面中新增一个空的 <p> 标签(无内容和子元素)
</script>

深复制(deep: true

复制节点本身及所有子节点(包括文本、嵌套元素等,递归复制整个子树)。

1
2
3
4
// 深复制:复制 <p> 及所有子节点(文本和 <span>)
const deepClone = original.cloneNode(true);
document.body.appendChild(deepClone);
// 页面中新增一个与 original 完全相同的节点:<p>原始文本 <span>子元素</span></p>

注意事项

  1. 复制节点不会复制事件监听器:

    无论 addEventListener 绑定的事件,还是 onclick 等属性绑定的事件,都不会被复制到新节点。

  2. 复制节点的 id 属性会重复:

    若原节点有 id(唯一标识),复制后新节点会保留相同 id,导致页面中 id 重复(不符合 HTML 规范)。需手动修改新节点的 id

    1
    2
    const clone = original.cloneNode(true);
    clone.id = 'clone-' + original.id; // 避免 id 重复
  3. 表单元素的状态不会被复制:

    如输入框的 value、复选框的 checked 等用户操作后的状态,深复制时会保留默认值(而非当前值)。

    1
    2
    3
    4
    5
    6
    7
    <input type="text" id="input" value="默认值">
    <script>
    const input = document.getElementById('input');
    input.value = '用户输入'; // 手动修改值
    const clone = input.cloneNode(true);
    console.log(clone.value); // 输出 "默认值"(而非 "用户输入")
    </script>

修改节点内容与样式

内容修改

  • textContent:纯文本内容(推荐,安全,不会解析 HTML)。
  • innerHTML:HTML 内容(会解析 HTML,有 XSS 风险,谨慎使用)。
1
2
3
const p = document.querySelector('p');
p.textContent = '新文本'; // <p>新文本</p>
p.innerHTML = '<strong>加粗文本</strong>'; // <p><strong>加粗文本</strong></p>

样式修改

  • 通过 style 属性直接修改行内样式(CSS 属性名用驼峰式,如 fontSize)。
  • 通过 classList 操作 CSS 类(推荐,更符合分离思想)。
1
2
3
4
5
6
7
8
9
10
11
12
const div = document.querySelector('div');

// 方法1:修改行内样式
div.style.color = 'red';
div.style.fontSize = '16px';
div.style.backgroundColor = '#f0f0f0';

// 方法2:操作 CSS 类
div.classList.add('active'); // 添加类
div.classList.remove('old'); // 移除类
div.classList.toggle('hidden'); // 切换类(有则删,无则加)
div.classList.contains('active'); // 判断是否有类(返回 true/false)

可交互示例:修改节点内容和样式

下面是一个可交互的示例,你可以尝试修改节点的内容和样式: <!DOCTYPE html>
<h3>修改节点内容和样式演示</h3>
<div id="target-element">这是目标元素,尝试修改它的内容和样式!</div>
<div class="control-group">
  <h4>修改内容:</h4>
  <input type="text" id="textContent" placeholder="使用 textContent" value="纯文本内容">
  <button onclick="setTextContent()">设置 textContent</button>
  <br>
  <input type="text" id="innerHTML" placeholder="使用 innerHTML" value="<strong>HTML</strong>内容">
  <button onclick="setInnerHTML()">设置 innerHTML</button>
</div>
<div class="control-group">
  <h4>修改样式(style 属性):</h4>
  <input type="color" id="colorPicker" value="#000000">
  <button onclick="setColor()">设置文字颜色</button>
  <br>
  <input type="text" id="fontSize" placeholder="字体大小,如: 20px" value="16px">
  <button onclick="setFontSize()">设置字体大小</button>
</div>
<div class="control-group">
  <h4>操作 CSS 类(classList):</h4>
  <button onclick="addClass('style-red')">添加红色样式</button>
  <button onclick="addClass('style-blue')">添加蓝色样式</button>
  <button onclick="addClass('style-green')">添加绿色样式</button>
  <button onclick="addClass('style-large')">添加大字体</button>
  <button onclick="addClass('style-border')">添加边框</button>
  <br>
  <button onclick="removeClass('style-red')">移除红色</button>
  <button onclick="removeClass('style-blue')">移除蓝色</button>
  <button onclick="removeClass('style-green')">移除绿色</button>
  <button onclick="removeClass('style-large')">移除大字体</button>
  <button onclick="removeClass('style-border')">移除边框</button>
  <br>
  <button onclick="toggleClass('style-large')">切换大字体</button>
  <button onclick="checkClass('style-red')">检查红色类</button>
  <button onclick="clearAllClasses()">清除所有类</button>
</div>

操作节点属性

HTML 标签的属性(如 srchrefid)可以通过 DOM 方法操作:

获取/设置元素的属性值的DOM方法

他们是通用方法(setAttribute/getAttribute),适用于所有属性(包括自定义属性)

1
2
3
4
5
//1.获取元素的属性值,传参是属性名,例如class、id、href
var attr = element.getAttribute(attribute_name)

//2.设置元素的属性值,传参自然地是元素的属性名及要设置的对应的属性值
element.setAttribute(attribute_name,attribute_value)

属性节点是一个 “容器”,里面存储着属性名和属性值(attrNode.name 是属性名,attrNode.value 是属性值)。

属性值是属性节点中存储的具体内容,即属性名对应的 “值”。例如:

  • <img src="pic.jpg" alt="图片"> 中,src 的属性值是 pic.jpgalt 的属性值是 图片
  • 属性值可以是字符串、布尔值(如 disabled 的值可以是 truefalse)等。

原生属性(如 idsrc

1
2
3
4
5
6
7
8
9
const img = document.querySelector('img');

// 获取属性值
console.log(img.src); // 获取 src 属性
console.log(img.id); // 获取 id 属性

// 设置属性值
img.src = 'new-pic.jpg'; // 修改 src
img.id = 'new-id'; // 修改 id

自定义属性(data-*

HTML5 允许通过 data-* 定义自定义属性,通过 dataset 访问:

1
<div data-user-id="123" data-user-name="张三"></div>
1
2
3
4
5
const div = document.querySelector('div');
console.log(div.dataset.userId); // "123"(注意,这里的属性要写成驼峰式)
console.log(div.dataset.userName); // "张三"

div.dataset.userAge = '20'; // 添加 data-user-age="20"

DOM 遍历(节点关系)

每个节点都有属性表示与其他节点的关系,可用于遍历 DOM 树:

DOM 树中,节点之间的关系类似 “家族关系”,可分为三大类:父节点与子节点兄弟节点祖先与后代节点

每个节点都有一系列的属性用于描述这些关系,那么遍历就可以通过这些属性来进行

父节点相关属性(向上遍历)

属性 含义
node.parentNode 返回当前节点的直接父节点(如果没有父节点,返回 null)。
node.parentElement 返回当前节点的直接父元素节点(仅返回元素节点,非元素节点返回 null)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="parent">
<!-- 注释节点 -->
<p id="child">文本</p>
</div>

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

console.log(child.parentNode); // <div id="parent">(父节点,元素节点)
console.log(child.parentElement); // <div id="parent">(父元素节点,与上面相同)

// 注释节点的父节点
const comment = document.body.childNodes[1]; // 获取注释节点
console.log(comment.parentNode); // <body>(父节点)
console.log(comment.parentElement); // <body>(父元素节点,注释节点的父节点是元素时有效)

// document 节点没有父节点
console.log(document.parentNode); // null
</script>
  • parentNode 可以返回任何类型的父节点(元素、文档、文档片段等)。
  • 那我这里为什么要提这么个注释节点呢?因为非元素节点(这里是注释节点)” 的父节点属性用法,会在parentNodeparentElement 有细微差异
    • HTML 中 <!-- 注释内容 --> 会被浏览器解析为 注释节点(DOM 节点类型 nodeType = 8),它不属于元素节点(元素节点是 <div><p> 这类标签),但也是 DOM 树的一部分,有自己的父节点。
    • 首先,document.body<body> 元素节点,childNodes<body> 下的所有子节点(包括元素、文本、注释等)。document.body.childNodes[1];<div id="parent">下的<!-- 注释节点 -->
    • 而parentNode 会返回它的 “直接父节点”,不管父节点是什么类型。而parentElement 只返回 “直接父元素节点”,也就是父节点必须是元素节点,比如 <div><body> 这类标签,不能是文本、文档等节点。

子节点相关属性(向下遍历)

属性 含义
node.childNodes 返回当前节点的所有子节点集合(NodeList 类型,包括元素、文本、注释等)。
node.children 返回当前节点的所有子元素节点集合(HTMLCollection 类型,仅包含元素节点)。
node.firstChild 返回当前节点的第一个子节点(任意类型)。
node.lastChild 返回当前节点的最后一个子节点(任意类型)。
node.firstElementChild 返回当前节点的第一个子元素节点(仅元素节点)。
node.lastElementChild 返回当前节点的最后一个子元素节点(仅元素节点)。
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
<div id="parent">
文本节点
<p>子元素1</p>
<!-- 注释 -->
<span>子元素2</span>
文本节点
</div>

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

// childNodes:所有子节点(包括文本、元素、注释)
console.log(parent.childNodes.length); // 5(文本、p、文本、注释、文本、span?注意:换行/空格会被解析为文本节点)
// 注:实际输出可能因HTML格式(换行/空格)不同而变化,需注意文本节点的影响

// children:仅子元素节点
console.log(parent.children); // HTMLCollection(2) [p, span]
console.log(parent.children.length); // 2(只包含 <p> 和 <span>)

// firstChild vs firstElementChild
console.log(parent.firstChild); // #text(第一个子节点是文本节点,因<div>后有换行)
console.log(parent.firstElementChild); // <p>(第一个子元素节点)

// lastChild vs lastElementChild
console.log(parent.lastChild); // #text(最后一个子节点是文本节点)
console.log(parent.lastElementChild); // <span>(最后一个子元素节点)
</script>
  • childNodes 会包含空白文本节点(由 HTML 中的换行、空格产生),遍历时常需过滤(如 node.nodeType === 1 只保留元素节点)。
  • children 只包含元素节点,无需处理文本 / 注释节点,更适合大多数场景,一般都用这个

兄弟节点相关属性(水平遍历)

属性 含义
node.nextSibling 返回当前节点的下一个兄弟节点(任意类型,包括文本、注释等)。
node.previousSibling 返回当前节点的上一个兄弟节点(任意类型)。
node.nextElementSibling 返回当前节点的下一个兄弟元素节点(仅元素节点)。
node.previousElementSibling 返回当前节点的上一个兄弟元素节点(仅元素节点)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="parent">
<p id="p1">段落1</p>
<!-- 注释 -->
<span id="span1">span1</span>
<p id="p2">段落2</p>
</div>

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

// 下一个兄弟节点
console.log(span1.nextSibling); // #text(换行产生的文本节点)
console.log(span1.nextElementSibling); // <p id="p2">(下一个元素节点)

// 上一个兄弟节点
console.log(span1.previousSibling); // #text(注释后的换行文本节点)
console.log(span1.previousElementSibling); // <p id="p1">(上一个元素节点)
</script>
  • Element 的属性(如 nextElementSibling)会自动跳过文本、注释等非元素节点,直接定位到元素节点,更实用。
  • 不带 Element 的属性(如 nextSibling)需手动处理空白文本节点,否则可能拿到意料之外的节点。

一些注意

  1. 空白文本节点的干扰

    HTML 中的换行、空格会被解析为文本节点(nodeType = 3),使用 childNodesnextSibling 等属性时需注意过滤(推荐优先用带 Element 的属性)。

  2. 动态集合的特性

    children(HTMLCollection)和 childNodes(NodeList)是动态集合,当 DOM 结构变化时会自动更新。遍历过程中若修改 DOM(如删除节点),可能导致遍历异常(如跳过或重复处理节点)。

    解决方法:将动态集合转为静态数组(如 Array.from(children))再遍历。

JS 的 DOM 事件处理

事件相关内容

DOM 事件处理是 JavaScript 与用户交互的核心机制,它让页面能够响应各种操作

  • 事件(Event):指用户在页面上的操作(如点击按钮、输入文本)或浏览器自身的状态变化(如页面加载完成、窗口大小改变)。
  • 事件源(Event Target):触发事件的 DOM 节点(如被点击的按钮、输入的文本框)。
  • 事件处理程序(Event Handler):当事件触发时执行的函数(也叫 “事件监听器”)。

例如,点击按钮弹出提示框,其中 “点击” 是事件,“按钮” 是事件源,“弹出提示框的函数” 是事件处理程序

事件绑定的三种方式

将事件处理程序与事件源关联的过程,称为 “事件绑定”。

HTML 内联绑定

不推荐

直接在 HTML 标签中通过 onXXX 属性绑定事件处理函数(XXX 为事件类型,如 onclick)。

1
2
3
4
5
6
7
8
9
10
<!-- 直接写函数调用 -->
<button onclick="alert('点击了按钮')">点击我</button>

<!-- 调用外部函数 -->
<button onclick="handleClick()">点击我</button>
<script>
function handleClick() {
console.log('按钮被点击');
}
</script>

不推荐,为什么?

  • HTML 与 JavaScript 代码混合,而且极易因为函数名重复导致冲突(会覆盖之前的定义)
  • 所以只能绑定一个处理函数(重复绑定会覆盖)。

DOM 属性绑定(onXXX 属性)

通过 JS 获取 DOM 节点后,直接给节点的 onXXX 属性赋值事件处理函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<button id="btn">点击我</button>
<script>
const btn = document.getElementById('btn');

// 绑定事件处理函数
btn.onclick = function() {
console.log('按钮被点击');
};

// 若重复绑定,会覆盖之前的函数
btn.onclick = function() {
console.log('新的点击事件'); // 只有这个会执行
};
</script>
  • 逻辑清晰(JS 代码集中管理),比内联绑定更推荐。
  • 同样只能绑定一个处理函数(重复绑定会覆盖)。
  • 兼容性好(支持所有浏览器,包括 IE)。

可交互示例:三种事件绑定方式对比

下面是一个可交互的示例,对比三种事件绑定方式:

<!DOCTYPE html>
<h3>三种事件绑定方式对比</h3>
<div class="button-group">
  <h4>1. HTML 内联绑定(不推荐)</h4>
  <button class="inline-btn" onclick="handleInlineClick()">内联绑定按钮</button>
  <button class="inline-btn" onclick="handleInlineClick()">重复绑定(会覆盖)</button>
  <p style="color: #666; font-size: 12px;">注意:两个按钮都调用同一个函数,但只能绑定一个处理函数</p>
</div>
<div class="button-group">
  <h4>2. DOM 属性绑定(onXXX)</h4>
  <button class="property-btn" id="propertyBtn1">属性绑定按钮1</button>
  <button class="property-btn" id="propertyBtn2">属性绑定按钮2(重复绑定会覆盖)</button>
</div>
<div class="button-group">
  <h4>3. addEventListener(推荐)</h4>
  <button class="listener-btn" id="listenerBtn">addEventListener 按钮</button>
  <button class="listener-btn" id="removeListenerBtn">移除事件监听</button>
  <p style="color: #666; font-size: 12px;">可以绑定多个处理函数,且可以移除</p>
</div>
<div class="log-area" id="logArea">
  <div class="log-item">事件日志将显示在这里...</div>
</div>
<button onclick="clearLog()" style="background: #757575; color: white;">清空日志</button>

addEventListener 方法(推荐)

通过 DOM 节点的 addEventListener 方法绑定事件,支持绑定多个处理函数,是大伙开发的首选方式。

语法

1
element.addEventListener(eventType, handler, useCapture);
  • eventType:事件类型字符串(如 'click''input',不带 on 前缀)。
  • handler:事件触发时执行的函数。
  • useCapture:布尔值(可选,默认 false),表示是否在 “捕获阶段” 触发事件(详见 “事件流” 部分)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<button id="btn">点击我</button>
<script>
const btn = document.getElementById('btn');

// 绑定第一个处理函数
btn.addEventListener('click', function() {
console.log('点击事件1');
});

// 绑定第二个处理函数(不会覆盖,会依次执行)
btn.addEventListener('click', function() {
console.log('点击事件2');
});
</script>

点击按钮后,控制台会依次输出 点击事件1点击事件2

Java 中的 swing 也是这样为按钮绑定事件的

他也能移除事件,通过 removeEventListener 移除已绑定的事件(需传入与绑定相同的事件类型和函数):

1
2
3
4
5
6
7
8
9
function handleClick() {
console.log('可移除的事件');
}

// 绑定
btn.addEventListener('click', handleClick);

// 移除
btn.removeEventListener('click', handleClick);

事件对象

当事件触发时,浏览器会自动创建一个事件对象(Event Object),包含事件的详细信息(如触发位置、按键信息等),它会作为参数传递给事件处理函数。

常用属性和方法:

属性 / 方法 说明
target 事件的实际触发节点(事件源)。
currentTarget 绑定事件处理函数的节点(当前处理事件的节点,通常与 this 一致)。
type 事件类型(如 'click''input')。
clientX/clientY 鼠标触发事件时,相对于浏览器可视区域的坐标。
key 键盘事件中,按下的按键值(如 'Enter''a')。
preventDefault() 阻止事件的默认行为(如阻止链接跳转、表单提交)。
stopPropagation() 阻止事件冒泡(详见 “事件流”)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<a href="https://example.com" id="link">链接</a>
<script>
const link = document.getElementById('link');

link.addEventListener('click', function(event) {
// event 是事件对象
console.log('事件类型:', event.type); // 'click'
console.log('触发节点:', event.target); // <a> 元素
console.log('当前处理节点:', event.currentTarget); // <a> 元素(与 this 一致)

// 阻止链接默认跳转行为
event.preventDefault();
});
</script>

事件流

事件流就是事件传播机制

当一个事件触发时,会在 DOM 树中按照特定顺序传播,这个过程称为事件流。DOM 事件流分为 3 个阶段:

  1. 捕获阶段(Capture Phase)

    事件从最顶层的节点(document)向下传播到事件源的父节点(不包括事件源本身)。

  2. 目标阶段(Target Phase)

    事件到达实际触发事件的节点(事件源)。

  3. 冒泡阶段(Bubbling Phase)

    事件从事件源向上传播回最顶层的节点(document)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="grandparent" style="padding: 50px; background: red;">
<div id="parent" style="padding: 50px; background: green;">
<div id="child" style="width: 50px; height: 50px; background: blue;"></div>
</div>
</div>

<script>
const grandparent = document.getElementById('grandparent');
const parent = document.getElementById('parent');
const child = document.getElementById('child');

// 捕获阶段触发(useCapture: true)
grandparent.addEventListener('click', () => console.log('爷爷捕获'), true);
parent.addEventListener('click', () => console.log('爸爸捕获'), true);
child.addEventListener('click', () => console.log('儿子捕获'), true);

// 冒泡阶段触发(useCapture: false,默认)
grandparent.addEventListener('click', () => console.log('爷爷冒泡'), false);
parent.addEventListener('click', () => console.log('爸爸冒泡'), false);
child.addEventListener('click', () => console.log('儿子冒泡'), false);
</script>

当我们点击蓝色的 child 节点,输出顺序为:

1
爷爷捕获 → 爸爸捕获 → 儿子捕获 → 儿子冒泡 → 爸爸冒泡 → 爷爷冒泡

可通过 event.stopPropagation() 阻止事件冒泡(避免父节点处理事件)。

可交互示例:事件流(捕获和冒泡)可视化

下面是一个可交互的示例,直观展示事件流的三个阶段:

<!DOCTYPE html>
<h3>事件流(捕获和冒泡)可视化演示</h3>
<div class="info-box">
  <strong>说明:</strong>点击最内层的蓝色方块,观察事件如何在 DOM 树中传播。
  事件会经历三个阶段:<strong>捕获阶段</strong>(向下)→ <strong>目标阶段</strong>(到达目标)→ <strong>冒泡阶段</strong>(向上)
</div>
<div id="grandparent">
  <div id="parent">
    <div id="child"></div>
  </div>
</div>
<div class="controls">
  <button onclick="clearLog()">清空日志</button>
  <button onclick="toggleStopPropagation()" id="stopBtn">启用 stopPropagation()</button>
  <span id="stopStatus" style="margin-left: 10px; color: #666;"></span>
</div>
<div class="log-area" id="logArea">
  <div class="log-item log-target">点击上面的蓝色方块开始...</div>
</div>

常见事件类型

按场景分类,常用事件类型如下:

鼠标事件

事件名 触发时机
click 鼠标左键单击
dblclick 鼠标左键双击
mouseover 鼠标移入节点
mouseout 鼠标移出节点
mousemove 鼠标在节点上移动
mousedown 鼠标按下(任意键)
mouseup 鼠标松开(任意键)

键盘事件

事件名 触发时机
keydown 按键按下(持续按住会重复触发)
keyup 按键松开
keypress 按下字符键(已逐步被弃用)

表单事件

事件名 触发时机
input 表单元素值变化(实时触发)
change 表单元素值变化且失去焦点(如下拉框选择后)
submit 表单提交(点击提交按钮或按 Enter)
focus 元素获得焦点(如输入框被点击)
blur 元素失去焦点

页面事件

事件名 触发时机
load 页面或资源(如图片)加载完成
resize 浏览器窗口大小改变
scroll 页面或元素滚动
DOMContentLoaded DOM 加载完成(无需等待图片等资源)

事件委托(Event Delegation)

利用事件冒泡让父节点帮子节点干活

把多个子节点的事件处理逻辑集中到父节点

当需要给多个子节点绑定相同事件时(如列表项点击),直接遍历绑定会导致性能问题(尤其是动态生成的节点)。

事件委托利用事件冒泡机制,将事件处理函数绑定到父节点,通过 event.target 判断实际触发事件的子节点,从而统一处理。

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
<ul id="list">
<li>项1</li>
<li>项2</li>
<li>项3</li>
</ul>
<button id="add">添加项</button>

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

// 只给父节点 ul 绑定一次点击事件
list.addEventListener('click', function(event) {
// 判断触发事件的是否是 li 节点
if (event.target.tagName === 'LI') {
console.log('点击了:', event.target.textContent);
}
});

// 动态添加新项(无需重新绑定事件,事件委托自动生效)
document.getElementById('add').addEventListener('click', function() {
const newLi = document.createElement('li');
newLi.textContent = `新项${list.children.length + 1}`;
list.appendChild(newLi);
});
</script>

我这个太保守了,假设我们上面的这一个列表,里面有 100 个列表项(<li>),一个个绑有点费人

既然所有<li>都有同一个父节点(比如<ul>),而且事件会冒泡(点击子节点时,事件会向上传播到父节点),那我们可以只给父节点绑定一次事件,让父节点 “代理” 所有子节点的事件。

给父节点绑定事件

1
2
3
4
5
const list = document.getElementById('list');
// 只给父节点 ul 绑定一次点击事件
list.addEventListener('click', function(event) {
// 事件处理逻辑
});

点击任何子节点(比如<li>)时,事件会通过 “冒泡” 机制传到父节点<ul>,所以父节点的事件处理函数会被触发。

判断实际点击的子节点

当父节点的事件处理函数被触发时,我们需要知道 “到底是哪个子节点被点击了”,你不可能都让所有子节点全绑上

这时可以通过事件对象(event)的target属性获取实际触发事件的节点(即被点击的子节点)。

1
2
3
4
5
6
7
list.addEventListener('click', function(event) {
// 判断实际点击的是不是 li 节点
if (event.target.tagName === 'LI') {
// 只处理 li 的点击
console.log('点击了:', event.target.textContent);
}
});
  • tagName返回节点的标签名(大写,如LISPAN),通过它可以筛选出我们需要的节点。

当点击 “添加项” 按钮时,新的<li>会被添加到<ul>中:

1
2
3
4
5
document.getElementById('add').addEventListener('click', function() {
const newLi = document.createElement('li');
newLi.textContent = `新项${list.children.length + 1}`;
list.appendChild(newLi); // 新的 li 被添加到 ul 中
});

由于新的<li>也是<ul>的子节点,点击它时,事件同样会冒泡到<ul>,父节点的事件处理函数会自动处理这个新节点的点击 ——不需要重新绑定事件

可交互示例:事件委托实战演示

下面是一个完整的事件委托示例,展示如何高效处理动态列表:

<!DOCTYPE html>
<h3>事件委托实战演示</h3>
<div class="info-box">
  <strong>优势:</strong>只给父节点 `<ul>` 绑定一次事件,所有子节点(包括动态添加的)都能自动响应点击事件,无需为每个子节点单独绑定。
</div>
<div class="controls">
  <input type="text" id="newItemText" placeholder="输入新项目内容" value="新项目">
  <button onclick="addItem()">添加项目</button>
  <button onclick="addMultipleItems()">批量添加 5 个项目</button>
  <button onclick="clearList()">清空列表</button>
</div>
<ul id="list">
  <li>项目 1 - 点击我试试</li>
  <li>项目 2 - 点击我试试</li>
  <li>项目 3 - 点击我试试</li>
</ul>
<div class="log-area" id="logArea">
  <div class="log-item">点击列表项查看事件委托效果...</div>
</div>