跟着 MDN 学 HTML day_48:深入理解 Node 接口
📑 目录
| 左列章节 | 右列章节 |
|---|---|
| [一、了解节点的类型与名称](#左列章节 右列章节 一、了解节点的类型与名称 二、父子关系的导航属性 三、兄弟关系的导航属性 四、textContent 与 nodeValue 五、节点的增删改操作 六、节点的克隆与比较 七、遍历技巧与 normalize) | [二、父子关系的导航属性](#左列章节 右列章节 一、了解节点的类型与名称 二、父子关系的导航属性 三、兄弟关系的导航属性 四、textContent 与 nodeValue 五、节点的增删改操作 六、节点的克隆与比较 七、遍历技巧与 normalize) |
| [三、兄弟关系的导航属性](#左列章节 右列章节 一、了解节点的类型与名称 二、父子关系的导航属性 三、兄弟关系的导航属性 四、textContent 与 nodeValue 五、节点的增删改操作 六、节点的克隆与比较 七、遍历技巧与 normalize) | [四、textContent 与 nodeValue](#左列章节 右列章节 一、了解节点的类型与名称 二、父子关系的导航属性 三、兄弟关系的导航属性 四、textContent 与 nodeValue 五、节点的增删改操作 六、节点的克隆与比较 七、遍历技巧与 normalize) |
| [五、节点的增删改操作](#左列章节 右列章节 一、了解节点的类型与名称 二、父子关系的导航属性 三、兄弟关系的导航属性 四、textContent 与 nodeValue 五、节点的增删改操作 六、节点的克隆与比较 七、遍历技巧与 normalize) | [六、节点的克隆与比较](#左列章节 右列章节 一、了解节点的类型与名称 二、父子关系的导航属性 三、兄弟关系的导航属性 四、textContent 与 nodeValue 五、节点的增删改操作 六、节点的克隆与比较 七、遍历技巧与 normalize) |
| [七、遍历技巧与 normalize](#左列章节 右列章节 一、了解节点的类型与名称 二、父子关系的导航属性 三、兄弟关系的导航属性 四、textContent 与 nodeValue 五、节点的增删改操作 六、节点的克隆与比较 七、遍历技巧与 normalize) |
一、了解节点的类型与名称
DOM 树中有多种不同类型的节点,区分它们的最直接方式是使用 nodeType 和 nodeName 属性。nodeType 返回一个整数,对应不同的节点类型常量;nodeName 返回一个字符串,其具体含义取决于节点类型。
代码示例:检测节点类型
javascript
const container = document.getElementById('container');
// 检查容器本身
console.log('容器 nodeType:', container.nodeType); // 1 表示 ELEMENT_NODE
console.log('容器 nodeName:', container.nodeName); // DIV
// 遍历容器的所有子节点
const childNodes = container.childNodes;
console.log('子节点数量:', childNodes.length);
for (let i = 0; i < childNodes.length; i++) {
const node = childNodes[i];
console.log('节点', i, ':');
console.log(' nodeType:', node.nodeType);
console.log(' nodeName:', node.nodeName);
// 根据 nodeType 判断节点类型
if (node.nodeType === Node.ELEMENT_NODE) {
console.log(' 这是一个元素节点');
} else if (node.nodeType === Node.TEXT_NODE) {
console.log(' 这是一个文本节点');
} else if (node.nodeType === Node.COMMENT_NODE) {
console.log(' 这是一个注释节点');
}
}
核心结论 :
nodeType的返回值是一个无符号短整数,常见值包括:1 代表元素节点,3 代表文本节点,8 代表注释节点,9 代表文档节点。在代码中使用Node接口提供的常量(如Node.ELEMENT_NODE)可以让代码更具可读性。nodeName对于元素节点返回的是标签名的大写形式 ,对于文本节点固定返回#text,对于文档节点返回#document。
⚠️ 【重点 / 面试考点 / 易错点】
| 常量 | 数值 | 说明 |
|---|---|---|
Node.ELEMENT_NODE |
1 | 元素节点(如 <div>) |
Node.TEXT_NODE |
3 | 文本节点 |
Node.COMMENT_NODE |
8 | 注释节点(<!-- -->) |
Node.DOCUMENT_NODE |
9 | 文档节点(document) |
Node.DOCUMENT_TYPE_NODE |
10 | 文档类型节点(<!DOCTYPE>) |
Node.DOCUMENT_FRAGMENT_NODE |
11 | 文档片段节点 |
nodeName对元素节点返回大写标签名 (如DIV),不是小写nodeName对文本节点返回#text,不是内容本身- 判断节点类型应优先使用
nodeType而非instanceof,因为跨窗口/iframe 时构造函数不同
二、父子关系的导航属性
Node 接口提供了一组属性用于在 DOM 树中上下导航 ,包括 parentNode、childNodes、firstChild 和 lastChild。这些属性是遍历和操作 DOM 树的基础工具。
代码示例:父子关系导航
javascript
const menu = document.getElementById('menu');
// 获取父节点
console.log('menu 的父节点:', menu.parentNode.nodeName);
// 获取所有子节点(包括文本节点)
const childNodes = menu.childNodes;
console.log('childNodes 数量:', childNodes.length);
// 获取第一个和最后一个子节点
console.log('第一个子节点:', menu.firstChild.nodeName);
console.log('最后一个子节点:', menu.lastChild.nodeName);
childNodes 返回的是一个实时的 NodeList ,这意味着如果 DOM 树的结构发生变化,这个列表会自动更新。需要特别注意的是,childNodes 包含所有类型的子节点,包括元素之间的空白文本节点。
代码示例:parentNode 与 parentElement 的区别
javascript
// parentNode 返回父节点(可能是 Document)
// parentElement 只返回父元素节点,如果父节点不是元素则返回 null
const textNode = menu.firstChild;
console.log('文本节点的 parentNode:', textNode.parentNode); // 元素节点
console.log('文本节点的 parentElement:', textNode.parentElement); // 元素节点
// 检查节点是否已连接到 DOM 树
console.log('menu 是否已连接:', menu.isConnected); // true
// 创建一个未连接的节点
const detachedElement = document.createElement('div');
console.log('新创建的元素是否已连接:', detachedElement.isConnected); // false
三、兄弟关系的导航属性
在 DOM 树中,同一层级之间的节点可以相互访问,通过 nextSibling 和 previousSibling 属性可以在同级节点之间自由移动。
代码示例:兄弟节点导航
javascript
const secondParagraph = document.getElementById('second');
// 获取上一个同级节点(可能是文本节点)
const prevNode = secondParagraph.previousSibling;
console.log('上一个节点类型:', prevNode.nodeName);
// 获取下一个同级节点(可能是文本节点)
const nextNode = secondParagraph.nextSibling;
console.log('下一个节点类型:', nextNode.nodeName);
nextSibling 和 previousSibling 返回的可能是任意类型的节点(包括空白文本节点)。在实际开发中,我们通常更关心元素节点,因此需要编写辅助函数来跳过空白文本节点。
代码示例:跳过文本节点获取元素兄弟
javascript
function getNextElementSibling(node) {
let next = node.nextSibling;
while (next && next.nodeType !== Node.ELEMENT_NODE) {
next = next.nextSibling;
}
return next;
}
function getPreviousElementSibling(node) {
let prev = node.previousSibling;
while (prev && prev.nodeType !== Node.ELEMENT_NODE) {
prev = prev.previousSibling;
}
return prev;
}
// 使用辅助函数获取相邻的元素节点
const prevElement = getPreviousElementSibling(secondParagraph);
if (prevElement) {
console.log('上一个元素节点 ID:', prevElement.id);
}
const nextElement = getNextElementSibling(secondParagraph);
if (nextElement) {
console.log('下一个元素节点 ID:', nextElement.id);
}
核心结论:这种节点间的横向导航能力在构建 DOM 树遍历算法时非常重要,它允许我们从任意节点出发,在树的同一层级上自由移动,而不需要先回到父节点再向下查找。
四、使用 textContent 和 nodeValue 操作内容
Node 接口提供了两个与内容相关的属性:textContent 和 nodeValue。它们在功能上有所重叠,但使用场景和效果有重要区别。
代码示例:textContent 的使用
javascript
const article = document.getElementById('article');
// textContent 获取所有文本内容(包括隐藏元素的文本)
console.log('article 的 textContent:', article.textContent);
// 获取第一个段落的 textContent(忽略 HTML 标签)
const firstParagraph = article.querySelector('p');
console.log('第一个段落的 textContent:', firstParagraph.textContent);
// 设置 textContent 会替换所有子节点为单个文本节点
const testDiv = document.createElement('div');
testDiv.innerHTML = '<p>原始内容</p><span>更多内容</span>';
console.log('设置前的子节点数量:', testDiv.childNodes.length); // 2
testDiv.textContent = '全新的纯文本内容';
console.log('设置后的子节点数量:', testDiv.childNodes.length); // 1
代码示例:nodeValue 的使用
javascript
// nodeValue 主要用于文本节点
const textNode = firstParagraph.firstChild;
console.log('文本节点的 nodeValue:', textNode.nodeValue);
// 对于元素节点,nodeValue 返回 null
console.log('元素节点的 nodeValue:', firstParagraph.nodeValue); // null
// 使用 nodeValue 修改文本内容
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
textNode.nodeValue = '这是修改后的文本内容';
console.log('修改后的段落:', firstParagraph.textContent);
}
⚠️ 【重点 / 面试考点】
| 属性 | 适用节点 | 返回值 | 设置时的行为 |
|---|---|---|---|
textContent |
所有节点 | 节点及所有后代的纯文本 | 移除所有子节点,替换为单个文本节点 |
nodeValue |
文本节点、注释节点 | 文本内容字符串 | 修改当前节点的文本内容 |
innerText |
元素节点 | 渲染后的可见文本 | 触发重排,性能较差 |
innerHTML |
元素节点 | HTML 字符串 | 解析 HTML,有 XSS 风险 |
textContent无视 CSS 样式 (包括display:none的隐藏元素),innerText会受样式影响textContent不会触发重排,innerText会触发重排,性能差距明显nodeValue对元素节点返回null,这是常见易错点- 设置
textContent是快速清空元素内容的安全方法(无 XSS 风险)
五、节点的增删改操作
Node 接口提供了完整的节点操作方法,包括 appendChild、insertBefore、removeChild 和 replaceChild,这些方法构成了 DOM 操作的基础。
代码示例:appendChild 添加节点
javascript
const taskList = document.getElementById('taskList');
// 在末尾添加子节点
const newTask = document.createElement('p');
newTask.textContent = '任务四(新增)';
newTask.style.color = 'blue';
taskList.appendChild(newTask);
console.log('添加后子节点数量:', taskList.children.length);
代码示例:insertBefore 在指定位置插入
javascript
const referenceNode = document.getElementById('reference');
const priorityTask = document.createElement('p');
priorityTask.textContent = '紧急任务(插入到参考节点之前)';
priorityTask.style.color = 'red';
taskList.insertBefore(priorityTask, referenceNode);
代码示例:replaceChild 替换节点
javascript
const replacementTask = document.createElement('p');
replacementTask.textContent = '任务三(已替换)';
replacementTask.style.textDecoration = 'line-through';
taskList.replaceChild(replacementTask, referenceNode);
代码示例:removeChild 移除节点
javascript
function removeNode(nodeToRemove) {
if (nodeToRemove && nodeToRemove.parentNode) {
nodeToRemove.parentNode.removeChild(nodeToRemove);
console.log('节点已移除');
}
}
const tempNode = document.createElement('p');
tempNode.textContent = '临时任务';
taskList.appendChild(tempNode);
console.log('移除前子节点数量:', taskList.children.length);
removeNode(tempNode);
console.log('移除后子节点数量:', taskList.children.length);
// 被移除的节点仍存在于内存中,可重新插入
console.log('被移除节点的父节点:', tempNode.parentNode); // null
console.log('被移除节点是否连接到 DOM:', tempNode.isConnected); // false
核心结论 :如果使用
appendChild或insertBefore移动一个已存在于文档中的节点 ,该节点会从其当前位置被移除,然后添加到新位置,而不是创建副本。removeChild要求传入的节点必须是调用节点的直接子节点,否则会抛出异常。被移除的节点仍然存在于内存中,可以在之后将它插入到文档的其他位置。
六、节点的克隆与比较
Node 接口提供了 cloneNode 方法用于复制节点,以及 compareDocumentPosition、isEqualNode 和 contains 等方法用于比较和判断节点之间的关系。
代码示例:cloneNode 的浅克隆与深克隆
javascript
const originalDiv = document.getElementById('original');
// 浅克隆 - 只克隆节点本身,不包含子节点
const shallowClone = originalDiv.cloneNode(false);
console.log('浅克隆的节点名:', shallowClone.nodeName); // DIV
console.log('浅克隆的子节点数量:', shallowClone.childNodes.length); // 0
console.log('浅克隆的 ID:', shallowClone.id); // original
// 深克隆 - 克隆节点及其所有后代节点
const deepClone = originalDiv.cloneNode(true);
console.log('深克隆的子节点数量:', deepClone.childNodes.length); // 包含所有子节点
重要注意 :克隆的节点不会自动拥有原始节点的事件监听器,事件监听器需要重新绑定。
代码示例:contains 检查后代关系
javascript
const body = document.body;
const h3Element = originalDiv.querySelector('h3');
// contains - 检查节点是否为后代节点
console.log('body 是否包含 h3:', body.contains(h3Element)); // true
console.log('body 是否包含自身:', body.contains(body)); // true(节点包含自身)
代码示例:compareDocumentPosition 比较位置
javascript
const firstParagraph = originalDiv.querySelector('p');
const relationship = h3Element.compareDocumentPosition(firstParagraph);
console.log('位置关系值:', relationship);
// 解读 compareDocumentPosition 的返回值(位掩码)
if (relationship & Node.DOCUMENT_POSITION_FOLLOWING) {
console.log('h3 在段落之前(段落在其后)');
}
if (relationship & Node.DOCUMENT_POSITION_CONTAINS) {
console.log('h3 包含段落');
}
代码示例:isEqualNode 比较节点结构
javascript
const div1 = document.createElement('div');
div1.innerHTML = '<p>测试</p>';
const div2 = document.createElement('div');
div2.innerHTML = '<p>测试</p>';
// 比较两个节点结构是否相等(不比较引用)
console.log('两个结构相同的 div 是否相等:', div1.isEqualNode(div2)); // true
console.log('同一节点引用比较:', h3Element === h3Element); // true
⚠️ 【重点 / 面试考点】
cloneNode(true)深克隆不复制事件监听器,必须重新手动绑定cloneNode(true)会复制 ID 属性,插入文档前需修改 ID 避免重复compareDocumentPosition返回位掩码 ,必须用位运算(&)解读contains对自身调用返回true,这是规范定义的行为isEqualNode比较结构和内容 ,isSameNode已废弃,直接用===
| 方法 | 功能 | 返回值 |
|---|---|---|
cloneNode(deep) |
复制节点 | 新的节点副本 |
contains(node) |
检查是否包含某节点 | boolean |
compareDocumentPosition(node) |
比较文档位置 | 位掩码数字 |
isEqualNode(node) |
结构是否相等 | boolean |
isSameNode(node) |
是否为同一节点 | boolean(已废弃) |
七、实用遍历技巧与 normalize 方法
在实际开发中,我们经常需要遍历 DOM 树来执行各种操作。同时,normalize 方法可以帮助我们清理文档中碎片化的文本节点。
代码示例:递归遍历 DOM 树
javascript
// 递归遍历所有后代节点的函数
function traverseNodes(node, callback) {
// 对当前节点执行回调
const shouldContinue = callback(node);
if (shouldContinue === false) return false;
// 递归遍历子节点
if (node.hasChildNodes()) {
const children = node.childNodes;
for (let i = 0; i < children.length; i++) {
if (traverseNodes(children[i], callback) === false) {
return false;
}
}
}
}
// 搜索包含指定文本的文本节点
function searchText(rootNode, searchTerm) {
const matches = [];
traverseNodes(rootNode, function(node) {
if (node.nodeType === Node.TEXT_NODE) {
if (node.textContent.indexOf(searchTerm) !== -1) {
matches.push(node);
}
}
});
return matches;
}
// 执行搜索
const matches = searchText(searchArea, '搜索');
console.log('找到的匹配节点数量:', matches.length);
代码示例:normalize 合并文本节点
javascript
// normalize 方法演示 - 合并相邻文本节点
const testContainer = document.createElement('div');
testContainer.appendChild(document.createTextNode('第一段文本'));
testContainer.appendChild(document.createTextNode('第二段文本'));
testContainer.appendChild(document.createTextNode('第三段文本'));
console.log('normalize 前子节点数量:', testContainer.childNodes.length); // 3
testContainer.normalize();
console.log('normalize 后子节点数量:', testContainer.childNodes.length); // 1
console.log('合并后的文本:', testContainer.textContent);
代码示例:获取根节点
javascript
// getRootNode 在 Shadow DOM 场景中特别有用
console.log('searchArea 的根节点:', searchArea.getRootNode().nodeName); // HTML
// hasChildNodes 快速检查
console.log('searchArea 是否有子节点:', searchArea.hasChildNodes()); // true
核心结论 :
normalize是一个经常被忽视但非常实用的工具,它会将相邻的文本节点合并为一个 ,并移除空的文本节点。在通过脚本频繁操作文本内容后,调用normalize可以保持 DOM 树的整洁。getRootNode在使用了 Shadow DOM 的场景中特别有用,它可以返回包含影子根的实际根节点。
八、Node 接口核心属性与方法速查
节点类型判断
| 属性 | 说明 |
|---|---|
nodeType |
节点类型数值(1=元素, 3=文本, 8=注释, 9=文档) |
nodeName |
节点名称(元素返回大写标签名) |
父子导航
| 属性 | 说明 |
|---|---|
parentNode |
父节点(可能是 Document) |
parentElement |
父元素节点(父节点不是元素时返回 null) |
childNodes |
所有子节点的实时 NodeList |
firstChild |
第一个子节点 |
lastChild |
最后一个子节点 |
hasChildNodes() |
是否有子节点 |
isConnected |
是否已连接到文档树 |
兄弟导航
| 属性 | 说明 |
|---|---|
nextSibling |
下一个同级节点 |
previousSibling |
上一个同级节点 |
内容操作
| 属性 | 说明 |
|---|---|
textContent |
获取/设置纯文本内容 |
nodeValue |
文本/注释节点的内容(元素节点返回 null) |
节点操作
| 方法 | 说明 |
|---|---|
appendChild(node) |
在末尾添加子节点 |
insertBefore(new, ref) |
在参考节点前插入 |
removeChild(node) |
移除指定的子节点 |
replaceChild(new, old) |
替换指定的子节点 |
cloneNode(deep) |
克隆节点 |
normalize() |
合并相邻文本节点 |
getRootNode() |
获取根节点 |
节点比较
| 方法 | 说明 |
|---|---|
contains(node) |
是否包含某节点 |
compareDocumentPosition(node) |
比较文档位置 |
isEqualNode(node) |
结构是否相等 |
Node 在 DOM 继承链中的位置
EventTarget
Node
Element
Text
Comment
Document
DocumentFragment
HTMLElement
HTMLDivElement
HTMLAnchorElement
✅ 文档总结
Node是 DOM 树中所有节点类型的共同祖先 ,Element、Text、Comment、Document等都继承自它nodeType(数值)和nodeName(字符串)是判断节点类型的核心属性,优先使用Node.ELEMENT_NODE等常量childNodes返回实时的 NodeList ,包含空白文本节点;children只返回元素子节点parentNode返回父节点,parentElement只返回父元素节点(父节点为 Document 时返回 null)textContent获取/设置纯文本(无视 CSS、无 XSS 风险),nodeValue仅对文本/注释节点有效appendChild/insertBefore移动已存在节点时会先从原位置移除 ;removeChild移除的节点仍存在于内存中cloneNode(true)深克隆不复制事件监听器 ,且会复制 ID 属性,插入前需修改 IDnormalize()合并相邻文本节点,getRootNode()在 Shadow DOM 场景中获取实际根节点compareDocumentPosition返回位掩码,需用位运算解读节点间的位置关系
完整实践示例
javascript
// 综合应用:安全的 DOM 树遍历与操作工具函数
/**
* 安全地移除所有子节点
* @param {Node} node - 目标节点
*/
function removeAllChildren(node) {
while (node.firstChild) {
node.removeChild(node.firstChild);
}
}
/**
* 获取所有元素类型的子节点(跳过文本节点)
* @param {Node} node - 目标节点
* @returns {Element[]}
*/
function getElementChildren(node) {
const elements = [];
let child = node.firstChild;
while (child) {
if (child.nodeType === Node.ELEMENT_NODE) {
elements.push(child);
}
child = child.nextSibling;
}
return elements;
}
/**
* 深度克隆节点并重新绑定事件
* @param {Node} node - 要克隆的节点
* @param {Function} eventBinder - 事件绑定回调
* @returns {Node}
*/
function deepCloneWithEvents(node, eventBinder) {
const clone = node.cloneNode(true);
// 修改克隆节点的 ID 避免重复
if (clone.id) {
clone.id = clone.id + '-clone-' + Date.now();
}
// 重新绑定事件
if (eventBinder) {
eventBinder(clone);
}
return clone;
}
/**
* 查找包含指定文本的最近祖先元素
* @param {Node} textNode - 文本节点
* @param {string} tagName - 目标标签名
* @returns {Element|null}
*/
function findAncestorElement(textNode, tagName) {
let current = textNode.parentNode;
while (current && current.nodeType === Node.ELEMENT_NODE) {
if (current.nodeName.toLowerCase() === tagName.toLowerCase()) {
return current;
}
current = current.parentNode;
}
return null;
}
// 使用示例
document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('container');
// 1. 安全清空容器
removeAllChildren(container);
// 2. 批量创建并插入内容
const fragment = document.createDocumentFragment();
for (let i = 0; i < 5; i++) {
const p = document.createElement('p');
p.textContent = '段落 ' + (i + 1);
p.dataset.index = i;
fragment.appendChild(p);
}
container.appendChild(fragment);
// 3. 获取所有段落元素(跳过空白文本节点)
const paragraphs = getElementChildren(container);
console.log('段落数量:', paragraphs.length);
// 4. 深克隆第一个段落
const clonedPara = deepCloneWithEvents(paragraphs[0], function(clone) {
clone.addEventListener('click', function() {
console.log('克隆段落被点击:', this.textContent);
});
});
container.appendChild(clonedPara);
// 5. 合并可能的碎片化文本节点
container.normalize();
// 6. 验证节点连接状态
console.log('容器是否连接:', container.isConnected);
console.log('容器子节点数:', container.childNodes.length);
});
结语
ShadowRoot 接口是 Shadow DOM 技术的核心,它为 Web 组件提供了真正的封装能力。通过 mode 属性,你可以控制 Shadow DOM 内部实现的可访问性;通过 host 属性,内部逻辑可以与外部宿主元素建立联系;innerHTML 让你方便地构建和修改 Shadow DOM 的结构;activeElement 和 getSelection 使得焦点和选区追踪能够穿透封装的边界;elementFromPoint 和 elementsFromPoint 则让坐标定位能力延伸到 Shadow DOM 的内部。而 delegatesFocus 属性更是为组件在复杂页面中的焦点管理提供了优雅的解决方案。掌握 ShadowRoot 接口的这些核心特性,将帮助你构建出封装良好、可维护性强且用户体验出色的 Web 组件。建议你在实际项目中多加练习,将这些概念应用到自定义元素的开发中,逐步建立起对 Shadow DOM 体系的深入理解。
想要解锁更多HTML 核心标签实战、前端零基础入门干货、开发避坑全指南吗?
持续关注,后续将更新CSS 布局实战、JavaScript 交互基础、全站导航开发等硬核内容,带你从新手快速进阶,轻松搞定前端开发!