XML DOM 教程

XML DOM 教程

XML DOM(XML Document Object Model)是 W3C 定义的标准编程接口,用于访问和操作 XML 文档。通过 DOM,程序可以读取、修改、添加和删除 XML 中的节点,把 XML 当作一棵可编程的节点树来处理。

本教程内容

本教程从 XML DOM 的基本概念讲起,逐步覆盖:节点与节点树、解析 XML、DOM 方法、节点访问与导航、节点的增删改查、XMLHttpRequest,以及 Node、Element、Attribute、Text 等核心对象的详细说明。

为什么要学 XML DOM

  • XML 仍是配置文件与数据交换的常用格式
  • DOM 是理解所有文档型接口(包括 HTML DOM)的基础
  • 掌握节点树模型,前端开发更扎实
  • 很多后端与桌面技术依赖 XML 处理

适用语言

DOM 是语言无关的标准,本教程以 JavaScript 为例(浏览器原生支持),概念同样适用于 Python、Java、PHP 等语言。

学习前提

建议具备基础的 HTML/JavaScript 知识,了解 XML 基本语法即可开始。

DOM 简介

DOM 简介

文档对象模型(Document Object Model,DOM)是 W3C 的官方标准,定义了访问和操作 XML/HTML 文档的方法与属性。

DOM 是什么

DOM 把文档表示为一棵节点树:文档中的每个元素、属性、文本都是一个节点。通过 DOM 接口,程序可以:

  • 遍历文档中的所有节点
  • 读取节点内容(元素名、文本、属性)
  • 修改节点(改文本、改属性)
  • 创建、删除、替换、克隆节点

DOM 的层级

DOM 标准分层:
- 核心 DOM: 操作 XML/HTML 的基础接口
- XML DOM: 针对 XML 的接口(本教程)
- HTML DOM: 针对 HTML 的接口(更上层,包含
  document.forms、document.images 等便捷属性)

节点树示例

<?xml version="1.0"?>
<bookstore>
  <book>
    <title>Ruby 入门</title>
    <price>59.0</price>
  </book>
</bookstore>

对应的节点树:
bookstore (根元素)
 └── book (元素节点)
      ├── title (元素节点)
      │    └── "Ruby 入门" (文本节点)
      └── price (元素节点)
           └── "59.0" (文本节点)

DOM 与编程语言

DOM 是接口标准,各语言提供实现:浏览器 JavaScript、Python 的 xml.dom/minidom、Java 的 javax.xml.parsers 等。接口概念一致,语法不同。

版本

DOM 1 级(1998):核心接口;DOM 2 级(2000):事件、遍历等;DOM 3 级(2004):加载保存、XPath 等。现代浏览器与解析器基本都支持。

DOM 节点

DOM 节点

节点(Node)是 DOM 的基本组成单元。理解节点的类型与关系,是操作 XML 的前提。

什么是节点

文档中的一切内容都是节点。XML 文档是节点的集合,节点按层次组织成树状结构。

节点类型

常见节点类型:
- 元素节点(element): <book>、<title>
- 属性节点(attribute): <book id="1"> 中的 id
- 文本节点(text): 元素中的文本内容
- 注释节点(comment): <!-- 注释 -->
- 处理指令(processing instruction): <?xml?> 等
- CDATA 节点: <![CDATA[...]]>
- 文档节点(document): 整个文档的根

每种节点对应一个 nodeType 常量:
- 元素: 1
- 属性: 2
- 文本: 3
- CDATA: 4
- 注释: 8
- 文档: 9

节点关系

<bookstore>                  (根节点)
 ├── <book id="1">          (bookstore 的子节点)
 │    ├── <title>Ruby</title>  (book 的子节点)
 │    └── <price>59</price>
 └── <book id="2">          (book 的兄弟节点)

- 父节点(parentNode)
- 子节点(childNodes)
- 兄弟节点(sibling)
- 后代节点(descendant)
- 祖先节点(ancestor)

节点的属性

每个节点都有:
- nodeName: 节点名称(元素=标签名,文本=#text)
- nodeValue: 节点值(元素=null,文本=文本内容,属性=属性值)
- nodeType: 节点类型(数字常量)
- parentNode / childNodes / firstChild / lastChild
- nextSibling / previousSibling
- attributes(NamedNodeMap,仅元素节点)

JavaScript 判断节点类型

if (node.nodeType === 1) {
    console.log("元素节点: " + node.nodeName);
} else if (node.nodeType === 3) {
    console.log("文本节点: " + node.nodeValue);
}

掌握节点类型与关系,后面章节的遍历、导航、增删改查都是在此基础上展开的。

DOM 节点树

DOM 节点树

DOM 把 XML 文档组织成一棵节点树,树是理解 DOM 全部操作的"地图"。

树的结构

<?xml version="1.0"?>
<company>
  <employee id="101">
    <name>张三</name>
    <age>28</age>
  </employee>
  <employee id="102">
    <name>李四</name>
    <age>32</age>
  </employee>
</company>
Document (文档根)
 └── company (根元素)
      ├── employee (id="101")
      │    ├── name → "张三"
      │    └── age → "28"
      └── employee (id="102")
           ├── name → "李四"
           └── age → "32"

树的基本术语

- 根节点: 树的最顶层(文档节点)
- 父/子: 直接上下级关系
- 兄弟: 同一父节点的子节点
- 叶节点: 没有子节点的节点
- 深度: 从根到该节点的层数

JavaScript 访问节点树

// 通过 DOM 解析后:
var doc = parser.parseFromString(xmlText, "text/xml");
var root = doc.documentElement;          // company
var employees = root.childNodes;         // 子节点集合

// 遍历
for (var i = 0; i < root.childNodes.length; i++) {
    var node = root.childNodes[i];
    if (node.nodeType === 1) {           // 元素
        console.log(node.nodeName + " " +
                    node.getAttribute("id"));
    }
}
// 输出: employee 101 / employee 102

为什么用树模型

树模型让文档的访问"结构化":层次关系天然清晰,遍历、搜索、修改都有统一规则。这也是 DOM 成为标准的原因。

DOM 解析

DOM 解析

解析(Parsing)是把 XML 文本转换为 DOM 节点树的过程。解析成功后才能用 DOM API 操作文档。

解析方式

# 1. 从字符串解析(JavaScript 浏览器)
var parser = new DOMParser();
var doc = parser.parseFromString(xmlText, "text/xml");

# 2. 从文件加载(通过 XMLHttpRequest)
var xhr = new XMLHttpRequest();
xhr.open("GET", "data.xml", false);   // 同步
xhr.send();
var doc = xhr.responseXML;            // 已解析的 DOM

# 3. Python 解析(示例)
from xml.dom import minidom
doc = minidom.parse("data.xml")       # 文件
doc = minidom.parseString(xml_text)   # 字符串

解析错误处理

// JavaScript:检查解析是否成功
var doc = parser.parseFromString(badXml, "text/xml");
var error = doc.getElementsByTagName("parsererror");
if (error.length > 0) {
    console.log("XML 解析失败: " + error[0].textContent);
} else {
    console.log("解析成功");
}

XML 语法要求

解析器严格要求 XML 语法,常见错误:
- 标签未闭合: <book>...</book> 必须配对
- 大小写敏感: <Book> 与 <book> 不同
- 属性必须加引号: id="1" 不能写成 id=1
- 正确嵌套: 不能交错 <a><b></a></b>
- 特殊字符转义: < 用 &lt; , & 用 &amp;

验证解析结果

// 解析后检查根元素
console.log(doc.documentElement.nodeName);
// 应输出 XML 文档的根元素名

// 遍历验证
var titles = doc.getElementsByTagName("title");
console.log("title 数量: " + titles.length);

解析是 DOM 操作的第一步:字符串/文件 → 节点树。解析成功与否,决定了后续所有操作能否进行。

DOM 加载函数

DOM 加载函数

把 XML 文档加载进 DOM 是解析的关键环节。本节介绍常用语言中的 XML 加载函数。

JavaScript(浏览器)

// 方式1: DOMParser 解析字符串
var parser = new DOMParser();
var doc = parser.parseFromString(xmlString, "text/xml");

// 方式2: XMLHttpRequest 加载文件
function loadXMLDoc(filename) {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", filename, false);
    xhr.send();
    return xhr.responseXML;
}
var doc = loadXMLDoc("books.xml");

XMLHttpRequest 细节

var xhr = new XMLHttpRequest();
xhr.open("GET", "books.xml", false);  // false=同步

// 同步模式下,send 后直接可用
xhr.send();
var doc = xhr.responseXML;

// 异步模式(推荐,不阻塞)
xhr.open("GET", "books.xml", true);
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        var doc = xhr.responseXML;
        // 处理文档
    }
};
xhr.send();

Python 加载

from xml.dom import minidom

# 从文件
doc = minidom.parse("books.xml")

# 从字符串
doc = minidom.parseString(
    "<book><title>Ruby</title></book>"
)

Node.js 加载

// 使用 xmldom 库
const { DOMParser } = require("xmldom");
const fs = require("fs");

const xml = fs.readFileSync("books.xml", "utf-8");
const doc = new DOMParser().parseFromString(xml, "text/xml");

加载后的操作入口

// 加载完成后,统一从 documentElement 开始
var root = doc.documentElement;

// 常用入口方法
doc.getElementsByTagName("book")   // 按标签名取集合
doc.getElementsByTagNameNS(ns, "book")  // 按命名空间
doc.getElementById(id)             // 按 ID(XML 不常用)

加载函数是把外部 XML 变成 DOM 的"入口"。浏览器用 XHR/DOMParser,后端用各自的 XML 库,概念相同。

DOM 方法

DOM 方法

DOM 提供一套标准方法用于操作节点与文档。本节汇总最常用的 DOM 方法。

节点操作方法

// 获取节点
getElementsByTagName("title")   // 按标签名返回集合
getElementsByTagNameNS(ns, "t") // 按命名空间
getAttribute("id")              // 获取属性值
getAttributeNode("id")          // 获取属性节点

// 修改节点
setAttribute("id", "100")       // 设置属性
removeAttribute("id")           // 删除属性
nodeValue / textContent         // 修改文本

// 创建节点
createElement("book")           // 创建元素
createTextNode("内容")          // 创建文本
createAttribute("id")           // 创建属性
createComment("注释")           // 创建注释
createCDATASection("...")       // 创建 CDATA

// 增删改节点
appendChild(newNode)            // 添加子节点(末尾)
insertBefore(newNode, refNode)  // 在指定节点前插入
removeChild(oldNode)            // 删除子节点
replaceChild(newNode, oldNode)  // 替换子节点
cloneNode(deep)                 // 克隆(deep=true 深克隆)

节点关系方法

// 判断与查找
hasChildNodes()          // 是否有子节点
hasAttribute("id")       // 是否有某属性
contains(node)           // 是否包含某节点(HTML5)

// 遍历
node.childNodes          // 子节点集合
node.parentNode          // 父节点
node.firstChild          // 第一个子节点
node.lastChild           // 最后一个子节点
node.nextSibling         // 下一个兄弟
node.previousSibling     // 上一个兄弟

文档级方法

// Document 对象的方法
doc.documentElement      // 根元素
doc.createElement(...)   // 创建元素
doc.importNode(node, true)  // 从其他文档导入节点
doc.getElementsByTagName(...)
doc.getElementById(...)

完整示例

// 创建一个新 book 节点并添加到文档
var doc = loadXMLDoc("books.xml");
var root = doc.documentElement;

var newBook = doc.createElement("book");
newBook.setAttribute("id", "3");

var title = doc.createElement("title");
title.appendChild(doc.createTextNode("Ruby 进阶"));
newBook.appendChild(title);

root.appendChild(newBook);

DOM 方法覆盖"查、改、建、删"四大类操作。把常用方法记熟,后续章节的节点操作就是组合运用。

DOM 访问

DOM 访问

访问(DOM Access)是读取文档内容的操作:查找元素、读取属性与文本。本节介绍常用的访问方式。

按标签名访问

var doc = loadXMLDoc("books.xml");

// 获取所有 title 元素
var titles = doc.getElementsByTagName("title");

// 遍历读取文本
for (var i = 0; i < titles.length; i++) {
    console.log(titles[i].childNodes[0].nodeValue);
}
// 或简洁写法
console.log(titles[i].textContent);

读取属性

<book id="1" category="web">...</book>

var book = doc.getElementsByTagName("book")[0];

// 读取属性值
var id = book.getAttribute("id");         // "1"
var cat = book.getAttribute("category");  // "web"

// 判断是否有属性
if (book.hasAttribute("category")) { ... }

// 遍历所有属性
var attrs = book.attributes;   // NamedNodeMap
for (var i = 0; i < attrs.length; i++) {
    console.log(attrs[i].nodeName + "=" +
                attrs[i].nodeValue);
}

读取文本内容

<title>Ruby 入门</title>

// 方法1: 通过子文本节点
var title = doc.getElementsByTagName("title")[0];
var text = title.childNodes[0].nodeValue;  // Ruby 入门

// 方法2: textContent(简洁,推荐)
var text = title.textContent;

// 注意: childNodes[0] 假设第一个子节点是文本
// 若元素内嵌套子元素,用 textContent 更安全

访问节点信息

var node = doc.getElementsByTagName("book")[0];

node.nodeName      // "book"(元素名)
node.nodeType      // 1(元素)
node.nodeValue     // null(元素节点的值为 null)
node.tagName       // "book"(元素专用,等同 nodeName)

// 文本节点
var textNode = node.childNodes[0];
textNode.nodeName  // "#text"
textNode.nodeValue // 文本内容

遍历访问

// 递归遍历所有元素
function walk(node) {
    if (node.nodeType === 1) {
        console.log(node.nodeName);
    }
    var kids = node.childNodes;
    for (var i = 0; i < kids.length; i++) {
        walk(kids[i]);
    }
}
walk(doc.documentElement);

访问是 DOM 最频繁的操作。核心三问:找什么(标签/属性/文本)、怎么找(getElementsByTagName 等)、怎么读(nodeValue/textContent)。

DOM 节点信息

DOM 节点信息

每个 DOM 节点都携带描述自身的信息:名称、值、类型。本节详解节点的三大基础属性。

nodeName(节点名称)

节点类型     nodeName 取值
元素节点     标签名(如 "book")
属性节点     属性名(如 "id")
文本节点     "#text"
注释节点     "#comment"
CDATA 节点   "#cdata-section"
文档节点     "#document"

var node = doc.documentElement;   // company 元素
node.nodeName;   // "company"

nodeValue(节点值)

节点类型     nodeValue 取值
元素节点     null
属性节点     属性值(如 "101")
文本节点     文本内容
注释节点     注释内容

// 示例:读取文本
var title = doc.getElementsByTagName("title")[0];
title.childNodes[0].nodeValue;   // "Ruby 入门"

// 示例:读取属性值
var book = doc.getElementsByTagName("book")[0];
book.getAttributeNode("id").nodeValue;  // "1"

nodeType(节点类型)

常见 nodeType 常量:
1   元素节点
2   属性节点
3   文本节点
4   CDATA 节点
5   实体引用
6   实体
7   处理指令
8   注释节点
9   文档节点
10  文档类型
11  文档片段
12  记号

// 判断示例
if (node.nodeType === 1) {
    // 元素节点处理
} else if (node.nodeType === 3) {
    // 文本节点处理
}

综合示例

// 遍历并打印所有节点信息
function showNodeInfo(node) {
    console.log("名称: " + node.nodeName);
    console.log("类型: " + node.nodeType);
    console.log("值: " + node.nodeValue);
    console.log("---");
}

// 遍历文档
var nodes = doc.getElementsByTagName("*");
for (var i = 0; i < nodes.length; i++) {
    showNodeInfo(nodes[i]);
}

其他常用信息

// 元素特有
element.tagName       // 标签名(等同 nodeName)
element.attributes    // 属性集合(NamedNodeMap)
element.childNodes    // 子节点集合

// 文档
doc.documentElement   // 根元素
doc.nodeName          // "#document"

节点信息三件套(nodeName/nodeValue/nodeType)是识别与处理节点的钥匙。写遍历代码时,先判断类型再取数据,最稳妥。

DOM 节点列表

DOM 节点列表

NodeList 是节点集合对象,由 getElementsByTagName 等方法返回。理解 NodeList 的特性,才能正确遍历。

NodeList 是什么

var titles = doc.getElementsByTagName("title");

// titles 是一个 NodeList(节点列表)
// 类似数组,但不是真正的 Array

titles.length      // 节点数量
titles[0]          // 第一个节点(索引访问)
titles.item(0)     // 等价写法

遍历 NodeList

// 方式1: for 循环(经典)
for (var i = 0; i < titles.length; i++) {
    console.log(titles[i].textContent);
}

// 方式2: for...of(现代浏览器)
for (var t of titles) {
    console.log(t.textContent);
}

// 方式3: 转为数组(可用数组方法)
var arr = Array.prototype.slice.call(titles);
arr.forEach(function(t) {
    console.log(t.textContent);
});

NodeList 的特点

// 1. 可能是"活的"(live)
// getElementsByTagName 返回的是活集合:
// 文档变化时,集合自动更新

var books = doc.getElementsByTagName("book");
console.log(books.length);   // 2
doc.documentElement.appendChild(newBook);
console.log(books.length);   // 3 (自动更新)

// 2. 索引与长度
books[0] / books.item(0)     // 第一个
books[books.length - 1]      // 最后一个

过滤非元素节点

// getElementsByTagName 只返回元素节点
// 但 childNodes 会包含文本节点等
var children = element.childNodes;
for (var i = 0; i < children.length; i++) {
    if (children[i].nodeType === 1) {   // 只处理元素
        console.log(children[i].nodeName);
    }
}

// 更简便:children 属性(仅元素子节点)
var kids = element.children;   // 只含元素

NodeList 与 NamedNodeMap 的区别

// NodeList: 节点集合(元素、文本等)
getElementsByTagName("title")   → NodeList

// NamedNodeMap: 属性节点集合
element.attributes              → NamedNodeMap
// 有 getNamedItem/getAttributeNode 等方法

NodeList 是 DOM 中最常见的集合类型。记住三点:索引访问、length、可能是活集合。

DOM 遍历

DOM 遍历

遍历(Traversal)是按特定顺序访问节点树的过程:从前到后、从上到下,访问每个需要处理的节点。

遍历方式

# 1. 递归遍历(深度优先)
function walkTree(node, depth) {
    // 处理当前节点
    var indent = "  ".repeat(depth);
    if (node.nodeType === 1) {
        console.log(indent + node.nodeName);
    }

    // 递归子节点
    var kids = node.childNodes;
    for (var i = 0; i < kids.length; i++) {
        walkTree(kids[i], depth + 1);
    }
}
walkTree(doc.documentElement, 0);

只遍历元素节点

function walkElements(node, depth) {
    if (node.nodeType === 1) {
        console.log("  ".repeat(depth) + node.nodeName);
        var kids = node.childNodes;
        for (var i = 0; i < kids.length; i++) {
            walkElements(kids[i], depth + 1);
        }
    }
}

// 或使用元素专属集合
function walkByChildren(node, depth) {
    console.log("  ".repeat(depth) + node.nodeName);
    var kids = node.children;    // 仅元素
    for (var i = 0; i < kids.length; i++) {
        walkByChildren(kids[i], depth + 1);
    }
}

按条件收集节点

// 收集所有含文本的元素
function collectTextElements(node, result) {
    if (node.nodeType === 1 && node.childNodes.length === 1
        && node.firstChild.nodeType === 3) {
        result.push(node);
    }
    for (var i = 0; i < node.childNodes.length; i++) {
        collectTextElements(node.childNodes[i], result);
    }
    return result;
}
var textElems = collectTextElements(
    doc.documentElement, []);

XPath 遍历(更强大)

// 部分实现支持 XPath 查询
// Python 示例
from xml.dom import minidom
from xml.etree import ElementTree

tree = ElementTree.parse("books.xml")
for book in tree.iter("book"):      # 遍历所有 book
    print(book.get("id"))

# XPath
for title in tree.findall(".//title"):
    print(title.text)

遍历注意事项

# 1. 修改时遍历: 删除节点时反向遍历
# 从后往前删,避免索引错乱
for (var i = nodes.length - 1; i >= 0; i--) {
    nodes[i].parentNode.removeChild(nodes[i]);
}

# 2. 活集合遍历: 注意 length 会变
# 先复制快照再遍历更安全
var snapshot = Array.prototype.slice.call(nodes);

遍历是 DOM 编程的基础功。递归 + 类型判断是最通用组合;需要复杂查询时,考虑 XPath 或 ElementTree 等高级接口。

DOM 导航

DOM 导航

导航(Navigation)是通过节点之间的关系(父、子、兄弟)在树中移动,而非按标签名搜索。掌握导航,可以精确定位相邻节点。

导航属性

// 父节点
node.parentNode            // 父节点(null 表示是根)

// 子节点
node.childNodes            // 所有子节点(NodeList)
node.firstChild            // 第一个子节点
node.lastChild             // 最后一个子节点

// 兄弟节点
node.nextSibling           // 下一个兄弟
node.previousSibling       // 上一个兄弟

// 文档根
doc.documentElement        // 根元素

示例 XML

<bookstore>
  <book id="1">
    <title>Ruby 入门</title>
    <price>59</price>
  </book>
  <book id="2">
    <title>Python 基础</title>
    <price>49</price>
  </book>
</bookstore>

导航示例

var root = doc.documentElement;    // bookstore
root.childNodes                    // 所有子节点(含空白文本)

// 找到第一个 book 元素
var books = root.getElementsByTagName("book");
var book1 = books[0];

book1.parentNode.nodeName          // "bookstore"
book1.nextSibling                  // 可能是空白文本节点
book1.nextSibling.nodeName         // "#text"(如果有空白)

// 直接取元素兄弟
book1.nextElementSibling           // book2(仅元素)
book1.previousElementSibling       // null
book1.children[0].nodeName         // "title"

空白节点问题

// 格式化 XML 中的缩进会产生文本节点!
// 解决方案:
// 1. 使用 children / nextElementSibling(仅元素)
// 2. 遍历时过滤 nodeType === 1
// 3. 压缩 XML(无空白)

function firstElementChild(node) {
    var kids = node.childNodes;
    for (var i = 0; i < kids.length; i++) {
        if (kids[i].nodeType === 1) return kids[i];
    }
    return null;
}

导航到指定位置

// 找到 title 后导航到兄弟 price
var title = doc.getElementsByTagName("title")[0];
var parent = title.parentNode;       // book
var price = parent.children[1];      // price 元素
console.log(price.textContent);      // "59"

// 或通过 nextElementSibling
var price2 = title.nextElementSibling;  // price

注意事项

// childNodes 包含文本、注释等所有节点
// children 只含元素子节点
// 导航时优先用元素相关的属性,减少类型判断

// parentNode 永远存在(除 document)
// 移动节点: 用 appendChild 到新父节点即可
// (节点会自动从原位置移除)

导航与"按标签搜索"互补:搜索适合"按名字找",导航适合"沿关系走"。实际开发常两者混用。

DOM 获取节点

DOM 获取节点

获取节点是 DOM 操作的基础:按标签、按属性、按关系找到目标节点。本节系统介绍获取节点的方法。

按标签名获取

var doc = loadXMLDoc("books.xml");

// 获取所有指定标签元素
var books = doc.getElementsByTagName("book");

// 从指定元素下获取(局部搜索)
var book1 = books[0];
var titles = book1.getElementsByTagName("title");

// 通配符获取所有元素
var all = doc.getElementsByTagName("*");

按属性获取

// 注意: getElementById 对 XML 不保证可用
// (XML 没有 ID 类型属性概念)
// 通常需要自己遍历查找属性

function getByAttr(root, tag, attrName, attrValue) {
    var nodes = root.getElementsByTagName(tag);
    for (var i = 0; i < nodes.length; i++) {
        if (nodes[i].getAttribute(attrName) === attrValue) {
            return nodes[i];
        }
    }
    return null;
}

var book = getByAttr(doc.documentElement,
                     "book", "id", "2");

通过导航获取

var root = doc.documentElement;
root.firstElementChild     // 第一个子元素
root.lastElementChild      // 最后一个子元素
root.children[1]           // 第二个子元素

// 兄弟导航
var node = someNode;
node.nextElementSibling
node.previousElementSibling

获取文本与属性

// 文本
var title = doc.getElementsByTagName("title")[0];
title.textContent              // 文本(推荐)
title.childNodes[0].nodeValue  // 第一个文本子节点

// 属性
var book = doc.getElementsByTagName("book")[0];
book.getAttribute("id")        // "1"
book.getAttributeNode("id")    // 属性节点对象
book.hasAttribute("id")        // true

高级: XPath(部分环境)

// 浏览器可用 evaluate 做 XPath 查询
var xpathResult = doc.evaluate(
    "//book[@id='2']/title",
    doc, null,
    XPathResult.FIRST_ORDERED_NODE_TYPE, null);
var title = xpathResult.singleNodeValue;
console.log(title.textContent);   // Python 基础

选择方法的取舍

| 需求               | 推荐方法                    |
|--------------------|-----------------------------|
| 按标签取集合       | getElementsByTagName        |
| 精确到单个节点     | 遍历 + 属性判断             |
| 相邻节点           | next/previousElementSibling |
| 复杂条件查询       | XPath(如支持)              |
| 只取元素           | children                    |

获取节点的方法组合:标签搜索 + 属性判断 + 关系导航。按场景选最直接的方式,代码更清晰。

DOM 改变节点

DOM 改变节点

改变节点包括:修改文本内容、修改属性、修改元素名。本节介绍常见的节点修改操作。

改变文本

var title = doc.getElementsByTagName("title")[0];

// 方式1: 修改文本节点的 nodeValue
title.firstChild.nodeValue = "Ruby 高级教程";

// 方式2: textContent(简洁,推荐)
title.textContent = "Ruby 高级教程";

// 方式3: 删除旧节点,插入新节点
title.removeChild(title.firstChild);
title.appendChild(doc.createTextNode("Ruby 高级教程"));

改变属性

var book = doc.getElementsByTagName("book")[0];

// 新增/修改属性
book.setAttribute("category", "web");
book.setAttribute("id", "100");

// 修改属性节点
var attrNode = book.getAttributeNode("id");
attrNode.nodeValue = "200";

// 删除属性
book.removeAttribute("category");

改变元素名

// DOM 没有直接的"改元素名"方法
// 常用做法: 创建新元素,复制内容,替换旧元素

function renameElement(oldElem, newName) {
    var doc = oldElem.ownerDocument;
    var newElem = doc.createElement(newName);

    // 复制属性
    for (var i = 0; i < oldElem.attributes.length; i++) {
        var attr = oldElem.attributes[i];
        newElem.setAttribute(attr.nodeName, attr.nodeValue);
    }

    // 复制子节点
    while (oldElem.firstChild) {
        newElem.appendChild(oldElem.firstChild);
    }

    // 替换
    oldElem.parentNode.replaceChild(newElem, oldElem);
    return newElem;
}

改变节点值(文本节点)

// 直接操作文本节点
var textNode = title.firstChild;
if (textNode.nodeType === 3) {
    textNode.nodeValue = "新内容";
}

// 元素节点的 nodeValue 是 null,不能直接改
// 元素文本必须通过子文本节点或 textContent 修改

修改验证

// 修改后验证
console.log(title.textContent);            // 新内容
console.log(book.getAttribute("id"));      // 新值

// 序列化查看(部分环境)
var xmlString = new XMLSerializer()
    .serializeToString(doc);
console.log(xmlString);

改变节点的核心:文本用 textContent/nodeValue,属性用 setAttribute/removeAttribute,元素名用"创建新元素替换"。

DOM 删除节点

DOM 删除节点

删除节点是从文档树中移除节点的操作。删除前务必先找到父节点,因为 DOM 要求通过父节点删除子节点。

删除元素节点

// 基本流程: 父节点.removeChild(子节点)
var book = doc.getElementsByTagName("book")[0];
var parent = book.parentNode;     // bookstore
parent.removeChild(book);

// 更简洁: 直接通过 parentNode
book.parentNode.removeChild(book);

删除文本节点

var title = doc.getElementsByTagName("title")[0];
var textNode = title.firstChild;

if (textNode && textNode.nodeType === 3) {
    title.removeChild(textNode);   // 删除文本
}
// 此时 title 元素仍在,但内容为空

删除属性

var book = doc.getElementsByTagName("book")[0];

// 方式1: removeAttribute
book.removeAttribute("category");

// 方式2: 通过属性节点
var attrNode = book.getAttributeNode("category");
if (attrNode) {
    book.removeAttributeNode(attrNode);
}

// 删除所有属性
while (book.attributes.length > 0) {
    book.removeAttributeNode(book.attributes[0]);
}

清空元素内容

// 删除所有子节点
function removeAllChildren(elem) {
    while (elem.firstChild) {
        elem.removeChild(elem.firstChild);
    }
}
removeAllChildren(title);

// 或一次赋值
title.textContent = "";   // 清空文本(保留元素)

批量删除与活集合

// 删除所有 book(注意活集合陷阱)
// ❌ 错误: length 会动态变化
for (var i = 0; i < books.length; i++) {
    books[i].parentNode.removeChild(books[i]);
}

// ✅ 正确: 从后往前删,或循环判断
while (books.length > 0) {
    books[0].parentNode.removeChild(books[0]);
}

// 或先复制快照
var snapshot = Array.prototype.slice.call(books);
snapshot.forEach(function(b) {
    b.parentNode.removeChild(b);
});

删除后的状态

// 被删除的节点仍在内存中(可再插入)
var removed = book.parentNode.removeChild(book);
// 重新插入到其他位置
doc.documentElement.appendChild(removed);

删除节点的铁律:通过父节点删、注意活集合、删后节点对象仍可用。掌握这三条,删除操作不会出错。

DOM 替换节点

DOM 替换节点

替换节点是用新节点替代旧节点的操作。DOM 提供 replaceChild 方法完成这一任务。

replaceChild 基本用法

// 语法: parent.replaceChild(新节点, 旧节点)
var oldBook = doc.getElementsByTagName("book")[0];

// 创建新节点
var newBook = doc.createElement("book");
newBook.setAttribute("id", "99");
newBook.textContent = "新书";

// 替换
oldBook.parentNode.replaceChild(newBook, oldBook);

替换文本内容

// 场景: 替换元素内的文本
function replaceText(elem, newText) {
    // 清空
    while (elem.firstChild) {
        elem.removeChild(elem.firstChild);
    }
    // 添加新文本
    elem.appendChild(
        elem.ownerDocument.createTextNode(newText));
}
replaceText(title, "全新标题");

替换属性值

// setAttribute 本身就是"替换或新增"
book.setAttribute("id", "777");   // 旧值被替换

// 通过属性节点替换
var oldAttr = book.getAttributeNode("id");
var newAttr = doc.createAttribute("id");
newAttr.nodeValue = "888";
book.replaceChild(newAttr, oldAttr);
// 或: book.setAttributeNode(newAttr);

复制并替换(内容克隆)

// 用已有节点的副本替换
var source = doc.getElementsByTagName("book")[1];
var clone = source.cloneNode(true);   // 深克隆

var target = doc.getElementsByTagName("book")[0];
target.parentNode.replaceChild(clone, target);

跨文档替换

// 从另一个文档导入节点再替换
var otherDoc = loadXMLDoc("other.xml");
var imported = doc.importNode(
    otherDoc.getElementsByTagName("book")[0], true);

var target = doc.getElementsByTagName("book")[0];
target.parentNode.replaceChild(imported, target);

替换验证

// 验证替换结果
var books = doc.getElementsByTagName("book");
console.log(books.length);            // 数量不变
console.log(books[0].getAttribute("id"));  // 新值

// 旧节点已被移除,但对象仍可访问
console.log(oldBook.nodeName);        // 仍可用

替换的核心:replaceChild(新, 旧)。新节点可以现建、克隆或跨文档导入,替换后旧节点对象仍留在内存。

DOM 创建节点

DOM 创建节点

创建节点是向文档添加内容的前提:创建元素、文本、属性、注释等各类节点。

创建各类节点

var doc = loadXMLDoc("books.xml");

// 创建元素节点
var newBook = doc.createElement("book");

// 创建文本节点
var text = doc.createTextNode("Ruby 高级");

// 创建属性节点
var attr = doc.createAttribute("category");
attr.nodeValue = "programming";

// 创建注释节点
var comment = doc.createComment("这是一本新书");

// 创建 CDATA 节点
var cdata = doc.createCDATASection(
    "含 <特殊> 字符的内容");

// 创建文档片段(批量添加优化)
var frag = doc.createDocumentFragment();

组装节点

// 完整创建一本书
var doc = loadXMLDoc("books.xml");

var book = doc.createElement("book");
book.setAttribute("id", "5");
book.setAttribute("category", "web");

var title = doc.createElement("title");
title.appendChild(
    doc.createTextNode("JavaScript 高级"));
book.appendChild(title);

var price = doc.createElement("price");
price.appendChild(doc.createTextNode("79"));
book.appendChild(price);

// 添加到文档
doc.documentElement.appendChild(book);

使用文档片段批量添加

// 大量添加时用 DocumentFragment 提升性能
var frag = doc.createDocumentFragment();

for (var i = 0; i < 100; i++) {
    var b = doc.createElement("book");
    b.setAttribute("id", i);
    b.textContent = "Book " + i;
    frag.appendChild(b);
}

// 一次添加到文档(只触发一次更新)
doc.documentElement.appendChild(frag);

创建属性并设置

// 方式1: createAttribute + nodeValue
var attr = doc.createAttribute("lang");
attr.nodeValue = "zh";
book.setAttributeNode(attr);

// 方式2: setAttribute 一步到位(推荐)
book.setAttribute("lang", "zh");

创建后的操作

// 新创建的节点默认是"游离"的(不在文档中)
// 必须通过插入操作挂到树上:
// - appendChild: 添加到末尾
// - insertBefore: 插入到指定位置
// - replaceChild: 替换已有节点

// 判断是否在文档中
book.isConnected  // true=已在文档中(HTML5)

创建节点 = 造零件,插入操作 = 组装。先 create 再 append/insert,是 DOM 添加内容的标准流程。

DOM 添加节点

DOM 添加节点

添加节点是把新节点插入文档树的操作。核心方法:appendChild 与 insertBefore。

appendChild(末尾添加)

var root = doc.documentElement;
var newBook = doc.createElement("book");
newBook.textContent = "新书";

// 添加到末尾
root.appendChild(newBook);

// 返回被添加的节点
var added = root.appendChild(newBook);

insertBefore(指定位置插入)

// 语法: parent.insertBefore(新节点, 参考节点)
// 新节点插到参考节点之前

var root = doc.documentElement;
var firstBook = root.getElementsByTagName("book")[0];

var newBook = doc.createElement("book");
newBook.textContent = "插到最前";

root.insertBefore(newBook, firstBook);

插入到末尾/开头工具

// 末尾: appendChild
// 开头: insertBefore(新, firstChild)

function prepend(parent, newNode) {
    parent.insertBefore(newNode, parent.firstChild);
}

function insertAfter(parent, newNode, refNode) {
    // 插到 refNode 之后
    parent.insertBefore(newNode,
        refNode.nextSibling);
}

移动已有节点

// appendChild/insertBefore 会自动"移动"节点
// 原位置自动移除,插入到新位置

var book1 = doc.getElementsByTagName("book")[0];
// 把 book1 移到末尾
doc.documentElement.appendChild(book1);
// book1 自动从原位置移除,添加到末尾

添加文本与属性

// 给元素添加文本
var title = doc.createElement("title");
title.appendChild(doc.createTextNode("内容"));

// 或 textContent(更简洁)
title.textContent = "内容";

// 添加属性
book.setAttribute("id", "101");
// 属性用 setAttribute,不走 appendChild

添加后验证

// 验证位置与数量
var books = doc.getElementsByTagName("book");
console.log(books.length);
console.log(books[0].textContent);   // 检查顺序

// 父节点验证
newBook.parentNode.nodeName;         // "bookstore"

添加节点的要点:appendChild 加末尾、insertBefore 插指定位置、已有节点会被自动移动。属性用 setAttribute 单独设置。

DOM 克隆节点

DOM 克隆节点

克隆节点是复制已有节点创建副本的操作,用于"复制模板"式地快速生成结构相似的节点。

cloneNode 基本用法

// 语法: node.cloneNode(deep)
// deep=true: 深克隆(含子节点)
// deep=false: 浅克隆(仅自身)

var book = doc.getElementsByTagName("book")[0];

// 深克隆(复制 book 及其所有子节点)
var clone = book.cloneNode(true);

// 浅克隆(只有 book 元素本身,无子节点)
var shallow = book.cloneNode(false);

// 克隆节点是"游离"的,需插入
doc.documentElement.appendChild(clone);

深克隆 vs 浅克隆

<book id="1">
  <title>Ruby</title>
</book>

// 浅克隆结果
<book id="1"></book>   // 属性复制,子节点不复制

// 深克隆结果
<book id="1">
  <title>Ruby</title>
</book>               // 全部复制

克隆后修改

// 模板式生成:克隆后改内容
var template = doc.getElementsByTagName("book")[0];

function cloneBook(id, titleText) {
    var clone = template.cloneNode(true);
    clone.setAttribute("id", id);
    clone.getElementsByTagName("title")[0]
        .textContent = titleText;
    return clone;
}

doc.documentElement.appendChild(cloneBook("7", "新书"));

克隆注意事项

// 1. 事件监听不会被克隆(属性 onclick 会)
// 2. 克隆节点的 ownerDocument 保持不变
// 3. 跨文档克隆用 importNode 更规范

// 跨文档克隆
var otherDoc = loadXMLDoc("template.xml");
var tpl = otherDoc.getElementsByTagName("book")[0];
var imported = doc.importNode(tpl, true);
doc.documentElement.appendChild(imported);

克隆节点实例

// 复制整个 book 集合并做差异处理
var books = doc.getElementsByTagName("book");
var frag = doc.createDocumentFragment();

for (var i = 0; i < books.length; i++) {
    var c = books[i].cloneNode(true);
    c.setAttribute("copy", "true");   // 标记副本
    frag.appendChild(c);
}
doc.documentElement.appendChild(frag);

克隆节点的价值:模板化生成。"克隆 + 修改 + 插入"三步组合,是批量创建结构相似节点的标准姿势。

DOM XMLHttpRequest

DOM XMLHttpRequest

XMLHttpRequest(XHR)是浏览器中发起 HTTP 请求、加载 XML/JSON 数据的核心对象,也是 AJAX 技术的基石。

基本用法

var xhr = new XMLHttpRequest();
xhr.open("GET", "books.xml", true);   // 异步

xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {        // 请求完成
        if (xhr.status === 200) {      // 成功
            var doc = xhr.responseXML; // XML DOM
            console.log(
                doc.documentElement.nodeName);
        } else {
            console.log("错误: " + xhr.status);
        }
    }
};
xhr.send();

readyState 状态

0  UNSENT      未初始化(open 未调用)
1  OPENED      已调用 open
2  HEADERS_RECEIVED 已收到响应头
3  LOADING     正在接收响应体
4  DONE        请求完成

加载 XML 的两种方式

// 方式1: 同步加载(简单但阻塞)
xhr.open("GET", "books.xml", false);
xhr.send();
var doc = xhr.responseXML;

// 方式2: 异步加载(推荐)
xhr.open("GET", "books.xml", true);
xhr.onload = function() {
    if (xhr.status === 200) {
        var doc = xhr.responseXML;
        // 处理文档
    }
};
xhr.send();

加载 JSON 与其他数据

// JSON
xhr.open("GET", "data.json", true);
xhr.responseType = "json";
xhr.onload = function() {
    console.log(xhr.response);   // 已解析的对象
};
xhr.send();

// 文本
xhr.open("GET", "readme.txt", true);
xhr.onload = function() {
    console.log(xhr.responseText);
};
xhr.send();

发送数据(POST)

var xhr = new XMLHttpRequest();
xhr.open("POST", "/api/books", true);
xhr.setRequestHeader("Content-Type",
    "application/x-www-form-urlencoded");
xhr.onload = function() {
    console.log(xhr.responseText);
};
xhr.send("id=10&title=新书");

常见问题

// 1. 跨域: 同源策略限制
// 解决: 服务端 CORS 或代理

// 2. 本地文件: file:// 协议下 XHR 受限
// 解决: 本地起服务器(如 python -m http.server)

// 3. 响应不是 XML: 检查 Content-Type
// 解决: xhr.overrideMimeType("text/xml")

现代替代 fetch

// fetch API 更现代简洁
fetch("books.xml")
    .then(r => r.text())
    .then(text => {
        var doc = new DOMParser()
            .parseFromString(text, "text/xml");
        // 处理
    });

XHR 是把远程 XML 加载进 DOM 的标准途径。掌握 readyState 判断与异步模式,配合 responseXML 就能实现"远程数据 → DOM 操作"的完整链路。

DOM 节点类型

DOM 节点类型

DOM 标准定义了 12 种节点类型,每种对应一个常量值。本节完整介绍各类型及其用途。

节点类型常量表

常量名                    值   说明
ELEMENT_NODE               1   元素节点
ATTRIBUTE_NODE             2   属性节点
TEXT_NODE                  3   文本节点
CDATA_SECTION_NODE         4   CDATA 段
ENTITY_REFERENCE_NODE      5   实体引用
ENTITY_NODE                6   实体
PROCESSING_INSTRUCTION_NODE 7  处理指令
COMMENT_NODE               8   注释节点
DOCUMENT_NODE              9   文档节点
DOCUMENT_TYPE_NODE         10  文档类型(DOCTYPE)
DOCUMENT_FRAGMENT_NODE     11  文档片段
NOTATION_NODE              12  记号

// 访问方式
Node.ELEMENT_NODE           // 1
node.nodeType === Node.TEXT_NODE  // 3

各类型说明

// 元素节点(1): 最常见的节点
<book></book>
nodeName="book" nodeValue=null

// 属性节点(2): 元素上的属性
id="1"
nodeName="id" nodeValue="1"

// 文本节点(3): 元素内文本
"Ruby"
nodeName="#text" nodeValue="Ruby"

// CDATA(4): 含特殊字符的文本
<![CDATA[<b>粗体</b>]]>

// 注释(8)
<!-- 说明 -->

// 文档(9): 整棵树
nodeName="#document"

// 文档类型(10)
<!DOCTYPE books>

属性节点的特殊之处

// 属性节点不属于元素的 childNodes!
// 属性通过 attributes(NamedNodeMap)访问

var book = doc.getElementsByTagName("book")[0];
book.attributes                // NamedNodeMap
book.attributes.length         // 属性数量
book.attributes.getNamedItem("id")

// 属性节点有 parentNode 返回 null(部分实现)

判断节点类型的模式

// 常用判断模式
function isElement(node) {
    return node.nodeType === 1;
}

function isText(node) {
    return node.nodeType === 3;
}

// 遍历时过滤
for (var i = 0; i < node.childNodes.length; i++) {
    var child = node.childNodes[i];
    if (child.nodeType === 1) {
        // 只处理元素
    }
}

// 现代浏览器可用
node.nodeType === Node.ELEMENT_NODE

document 与 documentType

doc.nodeType      // 9 (DOCUMENT_NODE)
doc.nodeName      // "#document"
doc.documentElement   // 根元素(1)

// 文档类型声明(如果存在)
var doctype = doc.doctype;   // DOCUMENT_TYPE_NODE
if (doctype) {
    console.log(doctype.name);   // DOCTYPE 名称
}

节点类型是 DOM 世界的地基:所有遍历、过滤、判断都建立在 nodeType 之上。记住最常见的 1/2/3/8/9 五种即可应对 95% 场景。

DOM Node

DOM Node

Node 是所有 DOM 节点类型的基接口,定义了所有节点共有的属性与方法。本节系统介绍 Node 对象。

Node 概览

所有节点类型(元素、文本、属性、注释...)都继承自 Node。
Node 提供:
- 通用属性: nodeName/nodeValue/nodeType
- 关系属性: parentNode/childNodes/nextSibling
- 操作方法: appendChild/removeChild/cloneNode

Node 属性

// 基本信息
node.nodeName       // 名称(标签名/#text 等)
node.nodeValue      // 值(文本内容/属性值/null)
node.nodeType       // 类型常量(1/2/3...)
node.localName      // 本地名(去命名空间前缀)
node.prefix         // 命名空间前缀
node.namespaceURI   // 命名空间 URI

// 关系
node.parentNode        // 父节点
node.childNodes        // 子节点集合
node.firstChild        // 第一个子节点
node.lastChild         // 最后一个子节点
node.nextSibling       // 下一个兄弟
node.previousSibling   // 上一个兄弟
node.ownerDocument     // 所属文档

// 状态
node.hasChildNodes()   // 是否有子节点
node.isConnected       // 是否在文档中

Node 方法

// 插入与删除
parent.appendChild(newChild)      // 末尾添加
parent.insertBefore(new, ref)     // 指定位置前插入
parent.removeChild(child)         // 删除子节点
parent.replaceChild(new, old)     // 替换子节点

// 复制
node.cloneNode(deep)              // 克隆节点

// 比较与规范化
node.isEqualNode(other)           // 是否相等
node.isSameNode(other)            // 是否同一节点
node.normalize()                  // 合并相邻文本节点

// 判断
node.hasChildNodes()
node.contains(otherNode)

文本规范化 normalize

// 多次添加文本会形成多个文本节点
var p = doc.createElement("p");
p.appendChild(doc.createTextNode("Hello"));
p.appendChild(doc.createTextNode(" World"));
p.childNodes.length      // 2

p.normalize();           // 合并相邻文本
p.childNodes.length      // 1
p.textContent            // "Hello World"

isEqualNode 示例

var a = doc.getElementsByTagName("book")[0];
var b = doc.getElementsByTagName("book")[1];

a.isEqualNode(b)     // 内容相同返回 true
a.isSameNode(b)      // false(不同对象)
a.isSameNode(a)      // true(同一对象)

完整遍历示例(基于 Node)

// 用 Node 通用接口遍历整棵树
function walkNode(node, depth) {
    var indent = "  ".repeat(depth);
    console.log(indent + node.nodeName +
                " (" + node.nodeType + ")");

    for (var i = 0; i < node.childNodes.length; i++) {
        walkNode(node.childNodes[i], depth + 1);
    }
}
walkNode(doc.documentElement, 0);

Node 是所有节点的"公共基类"。先把 Node 的通用接口掌握,再学 Element/Text 等子接口,就会非常轻松。

DOM NodeList

DOM NodeList

NodeList 是节点的有序集合,由 childNodes 与 getElementsByTagName 等返回。本节详解 NodeList 的接口与使用。

NodeList 基本接口

// 获取 NodeList
var list = element.childNodes;
var books = doc.getElementsByTagName("book");

// 核心接口
list.length          // 节点数量
list.item(0)         // 第 0 个节点
list[0]              // 等价写法(索引访问)
list.item(list.length - 1)  // 最后一个

获取 NodeList 的常见来源

element.childNodes               // 所有子节点(含文本)
element.getElementsByTagName("x") // 后代元素集合
element.children                 // 仅元素子节点(HTML5)
document.querySelectorAll("x")   // CSS 选择器(HTML DOM)

// 注意: childNodes 含所有类型节点
//        getElementsByTagName 只含元素

活集合(Live)与静态集合

// 活集合: 文档变化时自动更新
var books = doc.getElementsByTagName("book");
books.length;          // 2
doc.documentElement.appendChild(doc.createElement("book"));
books.length;          // 3 (自动更新)

// childNodes 也是活集合
// querySelectorAll 是静态集合(不自动更新)

// 遍历活集合时注意 length 变化:
// 删除元素时从后往前遍历

遍历 NodeList

// 方式1: for 循环
for (var i = 0; i < list.length; i++) {
    console.log(list[i].nodeName);
}

// 方式2: 转数组后使用数组方法
var arr = Array.prototype.slice.call(list);
arr.forEach(function(n) {
    console.log(n.nodeName);
});

// 方式3: for...of(现代浏览器)
for (var n of list) { ... }

NodeList 与 Array 的区别

// NodeList 不是 Array:
// - 没有 push/pop/map/filter 等方法
// - 有 length 和索引访问
// 需要数组方法时先转换:
var arr = Array.from(list);        // 现代
var arr = [].slice.call(list);     // 传统

实践: 过滤元素并操作

// 获取某元素下所有 book,逐个修改
var root = doc.documentElement;
var books = root.getElementsByTagName("book");

// 转数组便于安全操作
Array.from(books).forEach(function(b, idx) {
    b.setAttribute("index", idx);
});

NodeList 使用要点:length + 索引访问、可能是活集合、转数组后才能用数组方法。它是最常用的集合类型。

DOM NamedNodeMap

DOM NamedNodeMap

NamedNodeMap 是属性节点的集合,可通过名称访问。元素的 attributes 属性返回的就是 NamedNodeMap。

NamedNodeMap 概览

<book id="1" category="web" lang="zh">

var attrs = book.attributes;
// attrs 是 NamedNodeMap

attrs.length               // 3
attrs[0]                   // 第一个属性节点
attrs.item(0)              // 等价
attrs.getNamedItem("id")   // 按名称获取属性节点
attrs.getNamedItem("id").nodeValue  // "1"

常用方法

// 获取
attrs.getNamedItem("category")     // 属性节点
attrs.item(1)                      // 按索引
attrs[1]                           // 等价

// 新增/替换
var newAttr = doc.createAttribute("rating");
newAttr.nodeValue = "5";
attrs.setNamedItem(newAttr);       // 添加或替换同名

// 删除
attrs.removeNamedItem("category");

// 遍历
for (var i = 0; i < attrs.length; i++) {
    var attr = attrs[i];
    console.log(attr.nodeName + "=" + attr.nodeValue);
}

NamedNodeMap 与 NodeList 的区别

NamedNodeMap(属性集合)          NodeList(节点集合)
- 可通过名称访问 getNamedItem    - 只能按索引
- 成员通常是属性节点(2)         - 成员是任意节点
- setNamedItem 可增改             - 无对应方法
- 顺序不保证                     - 文档顺序

共同点:
- 都是活集合
- 都有 length/item/索引访问

常用属性操作模式

// 复制属性到新元素
function copyAttributes(from, to) {
    var attrs = from.attributes;
    for (var i = 0; i < attrs.length; i++) {
        var a = attrs[i];
        to.setAttribute(a.nodeName, a.nodeValue);
    }
}

// 清空所有属性
function removeAllAttrs(elem) {
    while (elem.attributes.length > 0) {
        elem.removeAttribute(
            elem.attributes[0].nodeName);
    }
}

// 判断是否有属性
if (elem.hasAttributes()) { ... }

getAttributeNode 关联

// getAttributeNode 返回单一属性节点
var attrNode = book.getAttributeNode("id");
attrNode.nodeValue = "999";   // 直接改值(同步到元素)

// 等价于
book.setAttribute("id", "999");

// 注意: 修改 attrNode.nodeValue 会更新元素属性
// 两者效果一致

NamedNodeMap 是理解"属性"的窗口。日常用 setAttribute/getAttribute 即可,涉及遍历属性或属性节点操作时才直接使用 NamedNodeMap。

DOM Document

DOM Document

Document 对象代表整个 XML 文档,是节点树的根,也是所有节点的"所有者"。本节介绍 Document 的常用接口。

Document 概览

// 获取 Document 对象
var doc = xhr.responseXML;          // XHR 加载
var doc = parser.parseFromString(s, "text/xml");

// 节点身份
doc.nodeType        // 9 (DOCUMENT_NODE)
doc.nodeName        // "#document"
doc.documentElement // 根元素

Document 属性

doc.documentElement      // 根元素节点
doc.childNodes           // 子节点(通常只有根元素)
doc.firstChild           // 可能包含 XML 声明/DOCTYPE
doc.doctype              // DOCTYPE(如有)
doc.implementation       // DOMImplementation 对象
doc.URL                  // 文档 URL(HTML)
doc.title                // 标题(HTML)

Document 创建方法

// 创建各类节点(重要!)
doc.createElement("book")        // 元素
doc.createTextNode("内容")       // 文本
doc.createAttribute("id")        // 属性
doc.createComment("注释")        // 注释
doc.createCDATASection("...")    // CDATA
doc.createDocumentFragment()     // 文档片段
doc.createProcessingInstruction("xml-stylesheet", "...")

// 创建后需插入文档树才生效

Document 查询方法

doc.getElementsByTagName("book")     // 按标签名
doc.getElementsByTagNameNS(ns, "b")  // 按命名空间
doc.getElementById("x")              // 按 ID(HTML 常用)

// 按命名空间示例
var books = doc.getElementsByTagNameNS(
    "http://example.com/ns", "book");

Document 导入与创建文档

// 从其他文档导入节点
var otherDoc = loadXMLDoc("other.xml");
var node = doc.importNode(
    otherDoc.documentElement, true);

// 创建空白 XML 文档(部分实现)
var newDoc = doc.implementation.createDocument(
    null, "root", null);
newDoc.documentElement.appendChild(
    newDoc.createElement("item"));

序列化 Document

// 把 DOM 转回 XML 字符串
var serializer = new XMLSerializer();
var xmlString = serializer.serializeToString(doc);
console.log(xmlString);

// Node.js / 其他环境也有类似 API

Document 使用模式

// 典型流程
// 1. 加载: XHR/DOMParser 得到 doc
// 2. 查询: documentElement / getElementsByTagName
// 3. 创建: doc.createElement / createTextNode
// 4. 修改: appendChild / setAttribute
// 5. 序列化: XMLSerializer 输出

// 所有创建方法必须通过 doc 调用!
// 用错文档创建节点会报错或产生孤儿节点

Document 是 DOM 世界的"总入口":承载根元素、提供创建与查询方法。牢记"所有节点都由所属文档创建"这一规则。

DOM DocumentImpl

DOM DocumentImpl

DocumentImpl 是 DOMImplementation 接口的实现类,提供文档类型检测与文档创建等底层能力。

DOMImplementation 是什么

// DOMImplementation 提供与文档实例无关的操作
// 通过 document.implementation 访问

var impl = doc.implementation;

// 能力检测
impl.hasFeature("XML", "1.0")       // true/false
impl.hasFeature("Core", "2.0")
impl.hasFeature("Events", "2.0")

createDocument 创建文档

// 创建新的 XML 文档
var newDoc = doc.implementation.createDocument(
    namespaceURI,   // 命名空间(无则 null)
    qualifiedName,  // 根元素名(如 "root")
    doctype         // 文档类型(无则 null)
);

// 示例: 创建根元素为 root 的文档
var d = doc.implementation.createDocument(
    null, "root", null);
d.documentElement.nodeName;   // "root"

// 创建带命名空间的文档
var nsDoc = doc.implementation.createDocument(
    "http://example.com/xml", "ns:root", null);

createDocumentType

// 创建 DOCTYPE
var doctype = doc.implementation.createDocumentType(
    "bookstore",          // 名称
    "SYSTEM",             // publicId
    "bookstore.dtd"       // systemId
);

// 与文档关联
var d = doc.implementation.createDocument(
    null, "bookstore", doctype);

// 序列化时 DOCTYPE 会输出:
// <!DOCTYPE bookstore SYSTEM "bookstore.dtd">

hasFeature 实践

// 特性检测(不同环境支持不同)
var features = [
    ["XML", "1.0"],
    ["Core", "2.0"],
    ["HTML", "2.0"]
];

for (var i = 0; i < features.length; i++) {
    console.log(features[i].join(" ") + ": " +
        doc.implementation.hasFeature(
            features[i][0], features[i][1]));
}

浏览器与后端的差异

// 浏览器: document.implementation
var impl = document.implementation;

// Node.js: 通过库提供
const { DOMImplementation } = require("xmldom");
const impl = new DOMImplementation();

// Python: xml.dom.minidom 中
# from xml.dom.minidom import getDOMImplementation
# impl = getDOMImplementation()
# doc = impl.createDocument(None, "root", None)

使用场景

// 1. 动态生成新的 XML 文档(内存中构建)
// 2. 特性检测(判断 API 可用性)
// 3. 创建带 DOCTYPE 的文档(如 RSS/配置)

// 示例: 内存构建 RSS 文档
var d = doc.implementation.createDocument(
    null, "rss", null);
var channel = d.createElement("channel");
d.documentElement.appendChild(channel);

DocumentImpl 是"文档的工厂":创建文档、创建 DOCTYPE、特性检测。日常用得不多,但动态生成 XML 文档时离不开它。

DOM DocumentType

DOM DocumentType

DocumentType 节点代表文档的 DOCTYPE 声明,描述文档遵循的 DTD(文档类型定义)。

什么是 DOCTYPE

<!DOCTYPE bookstore
  SYSTEM "bookstore.dtd">

// 或 HTML 中的
<!DOCTYPE html>

DOCTYPE 声明文档类型,部分解析器用它校验文档。
DocumentType 节点表示这个声明。

DocumentType 接口

// 通过 doc.doctype 访问
var doctype = doc.doctype;

doctype.nodeType     // 10 (DOCUMENT_TYPE_NODE)
doctype.nodeName     // DOCTYPE 名称(如 "bookstore")
doctype.name         // 同上

// DTD 标识
doctype.publicId     // PUBLIC 标识(如 "-//W3C//DTD XHTML")
doctype.systemId     // SYSTEM 标识(如 "bookstore.dtd")

// 内部子集(可选)
doctype.internalSubset   // 内部 DTD 声明字符串

示例 XML 与 DOCTYPE

<?xml version="1.0"?>
<!DOCTYPE bookstore SYSTEM "bookstore.dtd">
<bookstore>
  <book></book>
</bookstore>

// 解析后
var dt = doc.doctype;
dt.name            // "bookstore"
dt.systemId        // "bookstore.dtd"
dt.publicId        // ""(无 PUBLIC)

判断是否有 DOCTYPE

if (doc.doctype) {
    console.log("文档类型: " + doc.doctype.name);
} else {
    console.log("无 DOCTYPE 声明");
}

// 注意: 无 DOCTYPE 时 doc.doctype 为 null

创建 DOCTYPE

// 通过 DOMImplementation 创建
var doctype = doc.implementation.createDocumentType(
    "bookstore",          // qualifiedName
    "",                   // publicId
    "bookstore.dtd"       // systemId
);

// 用于创建新文档
var d = doc.implementation.createDocument(
    null, "bookstore", doctype);

DOCTYPE 与校验

// 有 DOCTYPE 的文档,解析器可做合法性校验
// (取决于解析器是否启用校验)

// 常见 DTD 应用:
// - HTML 文档类型(XHTML 过渡/严格)
// - RSS/Atom 订阅格式
// - SVG、配置文件的格式定义

// 校验失败的解析行为由解析器决定:
// - 严格解析器: 报错
// - 宽容解析器: 警告或忽略

遍历时的位置

// DOCTYPE 位于文档开头(根元素之前)
var children = doc.childNodes;
// children[0] 可能是 XML 声明或 DOCTYPE
// children[1] 通常是根元素

for (var i = 0; i < children.length; i++) {
    console.log(children[i].nodeType + ": " +
                children[i].nodeName);
}
// 可能输出:
// 10: bookstore (DOCTYPE)
// 1: bookstore  (根元素)

DocumentType 节点描述文档的"身份证"。多数场景只需读取 doc.doctype 判断类型,创建新文档时用 createDocumentType 声明 DTD。

DOM ProcessingInst

DOM ProcessingInst

ProcessingInstruction(处理指令)节点用于携带 XML 解析器与应用程序的指令,常见如 xml-stylesheet、xml 声明。

处理指令是什么

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="style.xsl"?>

// 以 <? 开头,?> 结尾
// 格式: <?目标 数据?>
// 目标(target): 指令名(如 xml-stylesheet)
// 数据(data): 指令内容(如 type="text/xsl" href="...")

ProcessingInstruction 接口

// 节点属性
pi.nodeType        // 7 (PROCESSING_INSTRUCTION_NODE)
pi.nodeName        // 目标名(如 "xml-stylesheet")
pi.target          // 同上(目标名)
pi.data            // 指令数据(如 type="text/xsl" ...)

// 示例
var pi = doc.firstChild;
if (pi.nodeType === 7) {
    console.log("目标: " + pi.target);
    console.log("数据: " + pi.data);
}

遍历找到处理指令

// 遍历文档顶层节点
function findPIs(doc) {
    var result = [];
    var nodes = doc.childNodes;
    for (var i = 0; i < nodes.length; i++) {
        if (nodes[i].nodeType === 7) {
            result.push(nodes[i]);
        }
    }
    return result;
}

var pis = findPIs(doc);
pis.forEach(function(pi) {
    console.log(pi.target + " → " + pi.data);
});

创建处理指令

// 创建并插入
var pi = doc.createProcessingInstruction(
    "xml-stylesheet",
    "type="text/xsl" href="style.xsl""
);

// 插入到文档开头(根元素之前)
doc.insertBefore(pi, doc.documentElement);

// 序列化输出:
// <?xml-stylesheet type="text/xsl" href="style.xsl"?>

XML 声明与处理指令

// 注意: XML 声明(<?xml?>)严格说是处理指令
// 但多数实现中不把它作为可访问的节点

// 实用场景:
// 1. 关联样式表(xml-stylesheet)
// 2. 关联 XSLT 转换
// 3. 传递解析器选项
// 4. 应用特定指令(PHP 的 <?php?> 类似概念)

修改处理指令

// 修改 data
pi.data = "type="text/xsl" href="new.xsl"";

// 删除处理指令
pi.parentNode.removeChild(pi);

// 替换
var newPi = doc.createProcessingInstruction(
    "xml-stylesheet", "href="new.xsl"");
pi.parentNode.replaceChild(newPi, pi);

处理指令是 XML 文档中的"元指令"。识别它(nodeType 7)、读取它(target/data)、创建它,即可操控这类特殊节点。

DOM Element

DOM Element

Element(元素)是最重要的 DOM 节点类型,代表 XML 中的标签。绝大多数 DOM 操作都围绕元素展开。

Element 概览

<book id="1" category="web">
  <title>Ruby</title>
</book>

var book = doc.getElementsByTagName("book")[0];
book.nodeType       // 1 (ELEMENT_NODE)
book.tagName        // "book"(等同 nodeName)
book.nodeName       // "book"

Element 属性操作

// 读取
book.getAttribute("id")            // "1"
book.getAttributeNode("id")        // 属性节点
book.hasAttribute("category")      // true
book.hasAttributes()               // true
book.attributes                    // NamedNodeMap

// 写入
book.setAttribute("lang", "zh")    // 新增/修改
book.removeAttribute("lang")       // 删除
book.setAttributeNode(attrNode)    // 用节点设置

// 遍历属性
for (var i = 0; i < book.attributes.length; i++) {
    var a = book.attributes[i];
    console.log(a.nodeName + "=" + a.nodeValue);
}

Element 内容操作

// 文本
title.textContent          // "Ruby"(全部文本)
title.firstChild.nodeValue // 第一个文本子节点

// 子元素
book.children              // 仅元素子节点
book.childNodes            // 所有子节点(含文本)
book.firstElementChild     // 第一个子元素
book.lastElementChild      // 最后一个子元素

// 结构操作
book.appendChild(newNode)
book.insertBefore(newNode, refNode)
book.removeChild(child)
book.replaceChild(newNode, oldNode)

按标签名搜索子元素

// 从元素开始向下搜索
var titles = book.getElementsByTagName("title");

// 遍历元素(HTML DOM 中的 children)
for (var i = 0; i < book.children.length; i++) {
    console.log(book.children[i].nodeName);
}

命名空间相关

<ns:book xmlns:ns="http://example.com/ns">

book.localName         // "book"(无前缀名)
book.prefix            // "ns"(前缀)
book.namespaceURI      // "http://example.com/ns"

// 带命名空间的属性操作
book.getAttributeNS("http://example.com/ns", "id")
book.setAttributeNS(ns, "ns:id", "1")

克隆与替换元素

// 克隆(见克隆章节)
var clone = book.cloneNode(true);

// 替换元素名(无直接 API,重建替换)
function renameElement(elem, newName) {
    var doc = elem.ownerDocument;
    var newElem = doc.createElement(newName);
    // 复制属性
    for (var i = 0; i < elem.attributes.length; i++) {
        var a = elem.attributes[i];
        newElem.setAttribute(a.nodeName, a.nodeValue);
    }
    // 移动子节点
    while (elem.firstChild) {
        newElem.appendChild(elem.firstChild);
    }
    elem.parentNode.replaceChild(newElem, elem);
    return newElem;
}

Element 实践模式

// 遍历元素树(仅元素)
function walkElements(elem, depth) {
    console.log("  ".repeat(depth) + elem.tagName);
    for (var i = 0; i < elem.children.length; i++) {
        walkElements(elem.children[i], depth + 1);
    }
}
walkElements(doc.documentElement, 0);

Element 是 DOM 编程的主角:属性用 get/setAttribute,内容用 textContent/children,结构用 append/remove。掌握 Element,就掌握了 DOM 的核心。

DOM Attribute

DOM Attribute

Attribute(属性节点)代表 XML 元素上的属性,如 id="1"。属性以 NamedNodeMap 形式挂在元素上,也可独立创建。

Attribute 节点特征

<book id="1">

var attr = book.getAttributeNode("id");

attr.nodeType      // 2 (ATTRIBUTE_NODE)
attr.nodeName      // "id"(属性名)
attr.nodeValue     // "1"(属性值)
attr.name          // "id"(等同 nodeName)
attr.value         // "1"(等同 nodeValue,现代)

// 属性节点的特殊性:
// - 不属于元素的 childNodes
// - 不能直接 appendChild 添加
// - 通过 attributes/setAttribute 管理

获取属性节点

// 方式1: getAttributeNode
var attr = book.getAttributeNode("id");

// 方式2: 从 NamedNodeMap 中取
var attr2 = book.attributes.getNamedItem("id");

// 方式3: 遍历
for (var i = 0; i < book.attributes.length; i++) {
    if (book.attributes[i].nodeName === "id") {
        // 找到 id 属性节点
    }
}

创建与设置属性节点

// 创建属性节点
var attr = doc.createAttribute("category");
attr.nodeValue = "web";

// 设置到元素
book.setAttributeNode(attr);
// 或
book.attributes.setNamedItem(attr);

// 效果等同
book.setAttribute("category", "web");

修改属性节点

// 修改 nodeValue 会同步到元素
var attr = book.getAttributeNode("id");
attr.nodeValue = "999";
book.getAttribute("id");    // "999" ✓ 已更新

// 部分实现支持 value 属性
attr.value = "888";

// 注意: 修改 name 可能不受支持(属性名不可改)
// 改属性名 = 删除旧 + 新建

删除属性节点

// 方式1
book.removeAttributeNode(attrNode);
// 方式2
book.attributes.removeNamedItem("id");
// 方式3
book.removeAttribute("id");

属性 vs 元素对比

| 维度       | 属性(Attribute)     | 元素(Element)        |
|------------|---------------------|----------------------|
| 节点类型   | 2                   | 1                    |
| 内容       | 简单字符串值        | 可嵌套子节点         |
| 遍历       | 不在 childNodes     | 在 childNodes        |
| 顺序       | 不保证顺序          | 文档顺序             |
| 用途       | 元数据/简单值       | 结构化内容           |

// 设计建议: 简单标量用属性,复杂结构用元素
// <price currency="CNY">59</price>
// 属性=货币单位, 元素=价格数值

实践示例

// 读取所有属性的键值
function getAttrMap(elem) {
    var map = {};
    var attrs = elem.attributes;
    for (var i = 0; i < attrs.length; i++) {
        map[attrs[i].nodeName] = attrs[i].nodeValue;
    }
    return map;
}
var attrs = getAttrMap(book);
// { id: "1", category: "web" }

Attribute 节点代表"元素的属性"。日常用 setAttribute/getAttribute 便捷方法,需要遍历或节点级操作时才直接接触 Attribute 节点。

DOM Text

DOM Text

Text(文本节点)代表元素中的文本内容。文本节点是叶子节点,没有子节点。

Text 节点特征

<title>Ruby 入门</title>

var title = doc.getElementsByTagName("title")[0];
var textNode = title.firstChild;

textNode.nodeType      // 3 (TEXT_NODE)
textNode.nodeName      // "#text"
textNode.nodeValue     // "Ruby 入门"
textNode.data          // "Ruby 入门"(等价,推荐)
textNode.length        // 文本长度

// 文本节点没有子节点
textNode.hasChildNodes()   // false

读取文本

// 通过文本节点
var t = title.firstChild;
t.nodeValue       // "Ruby 入门"
t.data            // "Ruby 入门"

// 便捷方式(推荐)
title.textContent  // "Ruby 入门"

// 注意: 元素可能包含多个文本节点
// 由多次 appendChild 或实体引起
// normalize() 可合并

修改文本

// 修改文本节点值
var t = title.firstChild;
t.nodeValue = "新标题";
t.data = "新标题";       // 等价

// 或用 textContent 直接改
title.textContent = "新标题";

// 拆分文本节点
var first = t.splitText(3);   // 前3字符为第一个节点
// t: "新标"  first: "题"

创建与追加文本

// 创建文本节点
var text = doc.createTextNode("Ruby 高级教程");
title.appendChild(text);

// 追加到已有文本
var t = title.firstChild;
t.appendData(" - 第二版");    // 末尾追加
t.insertData(5, "!!");        // 指定位置插入

// 常用 data 操作方法:
t.appendData(str)      // 追加
t.insertData(offset, str)  // 插入
t.deleteData(offset, count)  // 删除
t.replaceData(offset, count, str)  // 替换
t.substringData(offset, count)  // 提取

文本节点的空白问题

<book>
  <title>Ruby</title>
</book>
// 格式化后,book 的子节点包含:
// - 文本节点(换行与空格)
// - title 元素
// - 文本节点(换行与空格)

// 处理: 遍历时过滤
function getDirectText(elem) {
    var result = "";
    for (var i = 0; i < elem.childNodes.length; i++) {
        var n = elem.childNodes[i];
        if (n.nodeType === 3) {
            result += n.data;
        }
    }
    return result.trim();
}

CDATA 与 Text 的区别

// CDATA 段在 XML 中写法不同,但读取方式相似
<script><![CDATA[ if (a < b) {...} ]]></script>

// 遍历时:
// CDATA 节点 nodeType = 4
// 文本节点 nodeType = 3
// 处理时通常统一对待(都取 data/textContent)

Text 节点是内容的最终承载者。读取用 textContent 最省事,精细操作(拆分/插入/提取)才需要直接使用 Text 接口。

DOM CDATA

DOM CDATA

CDATA 段(Character Data)用于在 XML 中存放不需要转义的特殊字符,如代码片段。DOM 用 CDATASection 接口表示。

什么是 CDATA

<![CDATA[ 内容 ]]>

// 示例
<code><![CDATA[
  if (a < b && c > d) { ... }
]]></code>

// CDATA 中的 < & 等字符无需转义
// 普通文本节点中则必须写成 &lt; &amp;

CDATA 节点特征

// 解析后成为 CDATASection 节点
var cdata = codeElem.firstChild;

cdata.nodeType      // 4 (CDATA_SECTION_NODE)
cdata.nodeName      // "#cdata-section"
cdata.nodeValue     // "if (a < b ...)"
cdata.data          // 内容(推荐)

// CDATASection 继承自 Text
// 所以 Text 的方法都可用

创建 CDATA

// 创建 CDATA 段
var cdata = doc.createCDATASection(
    "if (a < b) { return; }");

codeElem.appendChild(cdata);

// 序列化输出:
// <![CDATA[if (a < b) { return; }]]>

读取 CDATA 内容

// 方式1: 通过 data
var content = cdata.data;

// 方式2: 通过 textContent(父元素)
var content = codeElem.textContent;

// 方式3: 遍历所有子节点,找 CDATA
function getCDATAs(elem) {
    var result = [];
    for (var i = 0; i < elem.childNodes.length; i++) {
        if (elem.childNodes[i].nodeType === 4) {
            result.push(elem.childNodes[i].data);
        }
    }
    return result;
}

CDATA vs 转义文本

// 两种方式存储相同内容:
// CDATA 方式
<code><![CDATA[a < b]]></code>

// 转义方式
<code>a &lt; b</code>

// 读取结果一致("a < b")
// 选择建议:
// - 含大量特殊字符(代码/脚本): CDATA
// - 少量特殊字符: 转义更简洁
// - 注意: CDATA 内不能包含 "]]>" 序列

修改与删除 CDATA

// 修改内容
cdata.data = "新内容";    // 直接改 data
cdata.nodeValue = "新内容";

// 替换为普通文本节点
var text = doc.createTextNode("普通文本");
codeElem.replaceChild(text, cdata);

// 删除
codeElem.removeChild(cdata);

// 拆分(继承自 Text)
var part1 = cdata.splitText(5);

CDATA 遍历注意

// 遍历时 CDATA 与 Text 常需一起处理
function getTextContent(elem) {
    var result = "";
    for (var i = 0; i < elem.childNodes.length; i++) {
        var n = elem.childNodes[i];
        if (n.nodeType === 3 || n.nodeType === 4) {
            result += n.data;
        }
    }
    return result;
}
// 通常 textContent 已自动处理两者

CDATA 是 XML 中存放"原始内容"的机制。创建用 createCDATASection,读取用 data/textContent,注意"]]>"不能出现在 CDATA 内部。

DOM Comment

DOM Comment

Comment(注释节点)代表 XML/HTML 中的注释,用于在文档中加入说明文字,注释不会显示给最终用户。

注释与节点

<!-- 这是注释 -->

var comment = doc.createComment("这是注释");

comment.nodeType      // 8 (COMMENT_NODE)
comment.nodeName      // "#comment"
comment.nodeValue     // "这是注释"
comment.data          // "这是注释"(推荐)

遍历发现注释

// 找到所有注释
function findComments(node, result) {
    if (node.nodeType === 8) {
        result.push(node.data);
    }
    for (var i = 0; i < node.childNodes.length; i++) {
        findComments(node.childNodes[i], result);
    }
    return result;
}

var comments = findComments(doc.documentElement, []);
// ["这是注释1", "这是注释2"]

创建与插入注释

// 创建
var comment = doc.createComment("记录修改原因");

// 插入到元素前
var book = doc.getElementsByTagName("book")[0];
book.parentNode.insertBefore(comment, book);

// 插入到元素后
book.parentNode.insertBefore(
    comment, book.nextSibling);

// 添加为子节点
elem.appendChild(doc.createComment("子注释"));

修改与删除注释

// 修改注释内容
comment.data = "新注释内容";
comment.nodeValue = "新注释内容";

// 删除
comment.parentNode.removeChild(comment);

// 替换
var newComment = doc.createComment("替换后的注释");
comment.parentNode.replaceChild(newComment, comment);

注释的实际用途

// 1. 代码说明: 复杂逻辑的解释
// 2. 区域标记: 便于定位大段结构
// 3. 临时禁用: 注释掉暂时不用的内容
// 4. 元信息: 版本、作者、生成时间
// 5. 配置开关: 部分解析器支持读取注释配置

// 注意: 注释可能包含敏感信息(如生成路径)
// 对外发布前清理

注释与空白

// 格式化 XML 中,注释与空白文本共存
// 遍历时需要同时过滤

function forEachRealNode(elem, fn) {
    for (var i = 0; i < elem.childNodes.length; i++) {
        var n = elem.childNodes[i];
        if (n.nodeType === 1) {        // 元素
            fn(n);
        }
        // nodeType 3(文本)/8(注释)可忽略
    }
}

注释节点用于"记录说明"。识别(nodeType 8)、读取(data)、创建(createComment)三个操作足够日常使用。

DOM ParseError Obj

DOM ParseError Obj

解析错误对象(ParseError)用于描述 XML 解析失败的信息。不同解析器暴露错误的方式不同,本节介绍常见模式。

浏览器中的解析错误

// 浏览器 DOMParser: 解析错误时生成
//  元素

var parser = new DOMParser();
var doc = parser.parseFromString(badXml, "text/xml");

// 检测错误
var errors = doc.getElementsByTagName("parsererror");
if (errors.length > 0) {
    console.log("解析失败:");
    console.log(errors[0].textContent);
}

parsererror 内容示例

<parsererror xmlns="http://www.mozilla.org/newlayout/xml/parsererror.xml">
  文档解析错误
  位置: line 3, column 5
  错误: 元素类型 "book" 后必须跟属性规范或 "/>"
</parsererror>

// 提取信息
var errText = errors[0].textContent;
// 可解析出行号与错误描述

XHR 加载错误检测

var xhr = new XMLHttpRequest();
xhr.open("GET", "data.xml", false);
xhr.send();

if (xhr.status !== 200) {
    console.log("HTTP 错误: " + xhr.status);
} else {
    var doc = xhr.responseXML;
    // responseXML 为 null 时说明解析失败
    if (doc === null) {
        console.log("XML 解析失败(响应非合法 XML)");
    } else {
        console.log("解析成功");
    }
}

Python 中的解析错误

from xml.dom import minidom
from xml.parsers.expat import ExpatError

try:
    doc = minidom.parseString(bad_xml)
except ExpatError as e:
    print(f"解析错误: {e}")
    print(f"行: {e.lineno}, 列: {e.offset}")
    print(f"错误码: {e.code}")
    # 输出示例:
    # 解析错误: mismatched tag: line 3, column 5

Node.js 中的解析错误

const { DOMParser } = require("xmldom");
const fs = require("fs");

const xml = fs.readFileSync("data.xml", "utf-8");
try {
    const doc = new DOMParser({
        locator: {},
        errorHandler: {
            warning: console.warn,
            error: console.error,    // 解析错误
            fatalError: console.error
        }
    }).parseFromString(xml, "text/xml");
} catch (e) {
    console.error("解析失败:", e);
}

解析错误处理最佳实践

// 1. 解析前校验(正则/预处理)
// 2. 解析后立即检测错误
// 3. 错误信息中包含行号与描述
// 4. 优雅降级: 提示用户而非崩溃
// 5. 记录错误场景便于排查

// 统一错误处理函数
function safeParse(xmlText) {
    var doc = parser.parseFromString(xmlText, "text/xml");
    var err = doc.getElementsByTagName("parsererror");
    if (err.length > 0) {
        throw new Error("XML 解析错误: " +
                        err[0].textContent);
    }
    return doc;
}

解析错误处理是健壮 XML 应用的必要环节:检测失败、提取信息、友好提示。不同环境用各自方式,但"检测-提取-提示"的思路一致。

DOM 解析器错误

DOM 解析器错误

本节汇总解析器常见错误类型、原因与排查方法,帮助你快速定位 XML 问题。

常见错误分类

1. 语法错误: XML 格式不合法
2. 结构错误: 标签不匹配/嵌套错误
3. 编码错误: 字符编码问题
4. 校验错误: 不符合 DTD/Schema
5. 资源错误: 引用的外部实体不可访问

常见错误与原因

# 错误: mismatched tag (标签不匹配)
<book><title>...</book>   ← 缺 </title>

# 错误: not well-formed (格式不正确)
<book id=1>              ← 属性必须加引号
<book></Book>            ← 大小写不一致

# 错误: invalid character (非法字符)
文本中含有控制字符或未转义 &

# 错误: encoding error (编码错误)
文件声明 UTF-8 但实际是 GBK 等

# 错误: entity not defined (实体未定义)
使用了未声明的实体 &foo;

快速排查清单

# 按顺序检查:
# 1. 标签配对: 每个开始标签都有结束标签
# 2. 大小写: XML 严格区分大小写
# 3. 引号: 属性值必须用引号包裹
# 4. 特殊字符: < & 等必须转义
# 5. 嵌套: 标签必须正确嵌套(不能交错)
# 6. 编码: 声明与实际编码一致
# 7. 根元素: 有且只有一个根元素
# 8. 注释: <!-- 和 --> 配对
# 9. CDATA: 内部不能有 ]]>
# 10. DOCTYPE/实体: 声明正确

定位错误位置

// 解析错误通常包含行号/列号
// 用行号定位到具体位置

function locateErrorLine(xmlText, lineNo) {
    var lines = xmlText.split("
");
    if (lineNo >= 1 && lineNo <= lines.length) {
        return lines[lineNo - 1];
    }
    return "";
}

// 示例: 第三行出错
var badLine = locateErrorLine(xmlText, 3);
console.log("出错行: " + badLine);

转义规则速查

XML 中必须转义的字符:
<   →  &lt;
>   →  &gt;(通常可省略,但建议转义)
&   →  &amp;
"   →  &quot;(属性值中)
'   →  &apos;(属性值中)

// 示例
<title>5 &lt; 10 &amp;&amp; ok</title>
// 解析结果: 5 < 10 && ok

编码问题处理

// 声明与实际编码一致
<?xml version="1.0" encoding="UTF-8"?>

// 文件另存为 UTF-8(无 BOM 最稳妥)
// Python 读取时指定编码
open("data.xml", encoding="utf-8").read()

// 浏览器 XHR 加载:
// 服务端返回正确的 Content-Type
// 或 xhr.overrideMimeType("text/xml; charset=utf-8")

工具推荐

# 在线/本地 XML 校验工具
# - W3C XML Validator
# - xmllint(Linux 命令行)
xmllint --noout data.xml
# 输出: data.xml:3: parser error : ...

# 编辑器插件实时校验
# VS Code 的 XML 插件

解析器错误绝大多数是"格式问题"。按排查清单逐项检查,配合行号定位与校验工具,通常几分钟就能找到并修复。

DOM 总结

DOM 总结

本节回顾 XML DOM 教程的核心知识点,帮助你建立完整的知识体系。

核心概念回顾

# 1. 节点树
# DOM 把 XML 表示为节点树:
# Document → 根元素 → 子元素 → 文本

# 2. 节点类型
# 1 元素 / 2 属性 / 3 文本 / 4 CDATA
# 8 注释 / 9 文档 / 10 DOCTYPE / 7 处理指令

# 3. 节点关系
# parentNode / childNodes / firstChild
# lastChild / nextSibling / previousSibling

核心操作回顾

# 加载
DOMParser.parseFromString / XHR.responseXML

# 查询
getElementsByTagName / getAttribute
textContent / attributes

# 修改
textContent / setAttribute / removeAttribute

# 创建
createElement / createTextNode / createAttribute

# 结构
appendChild / insertBefore / removeChild
replaceChild / cloneNode / normalize

常用接口一览

# 文档级
Document: documentElement, createElement, ...
DOMImplementation: createDocument, hasFeature

# 节点级(通用)
Node: nodeName/nodeValue/nodeType,
      parentNode/childNodes, appendChild...

# 元素级
Element: tagName, get/setAttribute, attributes

# 集合
NodeList: length, item
NamedNodeMap: getNamedItem, setNamedItem

# 内容
Text: data, splitText
CDATASection / Comment / ProcessingInstruction

实践技巧总结

# 1. 空白节点: 格式化 XML 会产生空白文本节点
#    → 用 children / nextElementSibling 过滤

# 2. 活集合: getElementsByTagName 返回活集合
#    → 删除时反向遍历或先转快照

# 3. 文本读取: 优先 textContent

# 4. 创建节点: 必须用所属文档的 create* 方法

# 5. 解析错误: 检测 parsererror / responseXML 为 null

应用场景

# XML DOM 的应用:
# - 解析配置文件与数据文件
# - 前后端数据交换(XML 格式)
# - 文档转换(XSLT 配合)
# - 网页数据抓取(HTML DOM 同源)

# 前端 HTML 场景使用更丰富的:
# HTML DOM(document.getElementById 等)

学习路线建议

# 已掌握: XML DOM 节点模型与操作
# 下一步可选:
# - HTML DOM: 网页操作(与 XML DOM 同源)
# - XSLT: XML 转换样式表
# - XPath: 更强大的节点查询
# - 语言库: Python lxml / Java DOM 等

XML DOM 的核心就一句话:文档是树,一切皆节点,操作靠方法。把这棵树的遍历、查询、修改练熟,XML 处理就不再是难题。

DOM 实例

DOM 实例

本节通过 5 个完整实例,综合运用 XML DOM 的解析、查询、修改、创建与序列化能力。

实例1: 解析并显示书籍列表

// books.xml
// <bookstore><book id="1"><title>Ruby</title><price>59</price></book>...</bookstore>

function loadXML(filename) {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", filename, false);
    xhr.send();
    return xhr.responseXML;
}

var doc = loadXML("books.xml");
var books = doc.getElementsByTagName("book");

var html = "<table>";
html += "<tr><th>ID</th><th>书名</th><th>价格</th></tr>";
for (var i = 0; i < books.length; i++) {
    var b = books[i];
    html += "<tr>";
    html += "<td>" + b.getAttribute("id") + "</td>";
    html += "<td>" + b.getElementsByTagName("title")[0]
        .textContent + "</td>";
    html += "<td>" + b.getElementsByTagName("price")[0]
        .textContent + "</td>";
    html += "</tr>";
}
html += "</table>";
document.getElementById("result").innerHTML = html;

实例2: 新增一本书

function addBook(id, titleText, priceText) {
    var doc = loadXML("books.xml");
    var root = doc.documentElement;

    // 创建 book
    var book = doc.createElement("book");
    book.setAttribute("id", id);

    var title = doc.createElement("title");
    title.textContent = titleText;
    book.appendChild(title);

    var price = doc.createElement("price");
    price.textContent = priceText;
    book.appendChild(price);

    root.appendChild(book);
    return doc;
}
// 用 XMLSerializer 输出新文档

实例3: 搜索书籍

function searchBooks(keyword) {
    var doc = loadXML("books.xml");
    var books = doc.getElementsByTagName("book");
    var result = [];

    for (var i = 0; i < books.length; i++) {
        var title = books[i]
            .getElementsByTagName("title")[0]
            .textContent;
        if (title.indexOf(keyword) !== -1) {
            result.push({
                id: books[i].getAttribute("id"),
                title: title
            });
        }
    }
    return result;
}
// searchBooks("Ruby") → [{id:"1", title:"Ruby 入门"}]

实例4: 修改价格

function updatePrice(id, newPrice) {
    var doc = loadXML("books.xml");
    var books = doc.getElementsByTagName("book");

    for (var i = 0; i < books.length; i++) {
        if (books[i].getAttribute("id") === id) {
            var price = books[i]
                .getElementsByTagName("price")[0];
            price.textContent = newPrice;
            return doc;   // 返回修改后的文档
        }
    }
    return null;   // 未找到
}

实例5: 遍历并统计

// 统计每个作者的书籍数量
function countByAuthor(doc) {
    var books = doc.getElementsByTagName("book");
    var stats = {};

    for (var i = 0; i < books.length; i++) {
        var author = books[i]
            .getElementsByTagName("author")[0]
            .textContent;
        stats[author] = (stats[author] || 0) + 1;
    }
    return stats;
}
// { "张三": 3, "李四": 2 }

实例要点

# 五个实例覆盖:
# - 解析加载(loadXML)
# - 查询(getElementsByTagName + getAttribute)
# - 创建组装(createElement + appendChild)
# - 修改(textContent)
# - 遍历统计(循环 + 对象聚合)

# 综合运用这些模式,
# 就能实现绝大多数 XML 数据处理需求

实例是最好的学习方式。动手运行一遍,把每个实例改造成自己的需求,XML DOM 的使用就真正上手了。

DOM 验证

DOM 验证

文档验证(Validation)是检查 XML 是否符合规则的过程:语法合法(well-formed)与结构合法(valid,符合 DTD/Schema)。

Well-formed vs Valid

# Well-formed(格式良好): 语法正确
# - 标签配对、大小写一致、正确嵌套
# - 属性带引号、特殊字符转义
# - 有唯一根元素
# 解析器基本都会检查这一层

# Valid(有效): 结构符合 DTD/Schema
# - 元素顺序、必需元素、属性约束
# - 数据类型、取值范围
# 需要校验器(validator)检查

DTD 校验

<!DOCTYPE bookstore [
  <!ELEMENT bookstore (book+)>
  <!ELEMENT book (title, price)>
  <!ELEMENT title (#PCDATA)>
  <!ELEMENT price (#PCDATA)>
  <!ATTLIST book id CDATA #REQUIRED>
]>

// 校验规则:
// - bookstore 必须有一个或多个 book
// - book 必须含 title 和 price(按顺序)
// - book 必须有 id 属性

// 校验工具(xmllint):
xmllint --valid --noout data.xml

Schema 校验(XSD)

// XSD 更强大,支持数据类型与命名空间
// books.xsd
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="book">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="title" type="xs:string"/>
        <xs:element name="price" type="xs:decimal"/>
      </xs:sequence>
      <xs:attribute name="id" type="xs:ID" use="required"/>
    </xs:complexType>
  </xs:element>
</xs:schema>

// Python 校验示例
from lxml import etree
schema = etree.XMLSchema(etree.parse("books.xsd"))
doc = etree.parse("data.xml")
print(schema.validate(doc))   # True/False

DOM 操作后的验证

// 程序修改文档后,可用校验器复查
// 场景: 增删改后确保结构仍合法

// 浏览器端(部分环境):
// 无内置 XSD 校验,通常服务端校验

// 前端常见做法:
// 1. 修改后检查关键结构(必备元素)
function checkBook(book) {
    var ok = true;
    ok = ok && book.hasAttribute("id");
    ok = ok && book.getElementsByTagName("title").length === 1;
    ok = ok && book.getElementsByTagName("price").length === 1;
    return ok;
}

// 2. 关键数据校验
function checkPrice(price) {
    var v = parseFloat(price.textContent);
    return !isNaN(v) && v >= 0;
}

验证实践建议

# 1. 数据入口验证: 外部 XML 先验证再入库
# 2. 生成输出验证: 序列化后跑一遍校验器
# 3. 增量验证: 修改后只验改动部分
# 4. 自动化: CI 中加入 XML 校验步骤

# CI 校验示例
xmllint --noout $(find . -name "*.xml")
xmllint --schema books.xsd --noout data.xml

验证与解析的关系

# 解析器: 检查 well-formed(自动)
# 校验器: 检查 valid(可选,需显式调用)
# 流程: 先解析(语法),后校验(结构)

# 没有 DTD/Schema 时:
# 只能保证 well-formed,无法保证 valid
# 靠程序逻辑在代码中做校验(如 checkBook)

验证是 XML 质量的保障:格式良好是底线,结构有效是要求。有 DTD/Schema 用工具校验,没有就在代码里写检查函数。

XML DOM 简介

XML DOM 简介

欢迎学习 XML DOM!本节作为课程的收尾,回顾 XML DOM 的定位,并给出学习后的行动建议。

XML DOM 是什么(回顾)

XML DOM = XML Document Object Model
= 操作 XML 文档的标准编程接口

核心思想:
- 文档 → 节点树
- 一切内容都是节点
- 通过标准接口遍历/查询/修改

学完本教程,你应该掌握

# 概念层
✓ 节点与节点树模型
✓ 12 种节点类型
✓ 节点之间的关系

# 操作层
✓ 解析 XML(DOMParser/XHR)
✓ 查询节点(getElementsByTagName 等)
✓ 读取内容(textContent/attributes)
✓ 修改节点(文本/属性/结构)
✓ 创建与插入节点
✓ 删除/替换/克隆节点

# 对象层
✓ Node / NodeList / NamedNodeMap
✓ Element / Attribute / Text
✓ Document / DocumentType / Comment
✓ CDATASection / ProcessingInstruction

# 实践层
✓ 错误处理与验证
✓ 实例应用(书籍列表/增删改查)

XML DOM 与 HTML DOM 的关系

HTML DOM = XML DOM + HTML 专属扩展

共同点:
- 节点树模型
- Node/Element/Text 等接口
- 遍历与修改方法

HTML 专属(更便捷):
- document.getElementById/querySelector
- document.forms/images/links
- 事件处理、样式操作

// 学了 XML DOM,HTML DOM 会非常容易上手

实践建议

# 1. 动手做: 把"DOM 实例"章节的代码跑起来
# 2. 扩展练: 做一个 XML 配置读取器
# 3. 进阶学: XPath 查询 / XSLT 转换
# 4. 换语言: Python lxml / Java DOM 对比学习
# 5. 应用: 前后端 XML 数据交互实战

常见场景提醒

# 实际开发中的选择:
# - 简单配置: 用 JSON/YAML 更简单(推荐)
# - 复杂文档/标准格式: XML + DOM/Schema
# - 网页操作: HTML DOM
# - 高性能解析: SAX / StAX(流式)或 lxml

# 选型原则: 适合场景的技术才是好技术

XML DOM 是文档编程的基础功,理解它,你不仅能处理 XML,也能更快掌握 HTML DOM 与各种语言中的 XML 库。现在动手写几个例子,巩固你的理解吧!

第 1 / 39 节