硬核 DOM2/DOM3 全解析:从命名空间到 Range,前端工程师必须掌握的底层知识

这是一份"能抄能跑能查"的长文。每个 API 都尽量给出最小可复现示例兼容性/注意点 。示例默认运行在现代浏览器控制台或内联 <script> 中。


1. DOM 的演进(超简史)

  • DOM Level 1(1998) :定义了核心节点模型(Node/Document/Element/Attr/Text ...)与基础 HTML 接口。
  • DOM Level 2(2000) :模块化(Core/Views/Events/Traversal and Range/Style/CSS 等),引入 XML 命名空间遍历器NodeIterator/TreeWalker)、RangeCSSOM(样式对象模型)。
  • DOM Level 3(2004) :完善 Core,新增/规范了 isEqualNodeisSameNodesetUserData/getUserDataDocument.importNodeDocument.implementation.createDocument()DocumentType 扩展等。

今日实用性提示:部分 DOM3 API(如 isSameNodesetUserData/getUserData)在现代 Web 中已废弃或很少实现,下文会给出替代方案。


2. XML 命名空间(xmlns)与相关 API

2.1 xmlns 是什么?

  • 通过 XML 命名空间可区分同名元素/属性来自哪个"词汇表"。

  • 形式:

    • 默认命名空间xmlns="http://example.com/ns"(影响未带前缀的元素名)
    • 具名前缀xmlns:xlink="http://www.w3.org/1999/xlink"(为属性或元素赋予前缀)
  • 预留前缀:xmlxmlns

在 HTML 文档中操作 SVG/MathML 时经常会用到命名空间方法(createElementNS/setAttributeNS ...)。


2.2 Node 的变化(含示例)

涉及:localNamenamespaceURIprefix(DOM2);isDefaultNamespacelookupNamespaceURIlookupPrefix(DOM3 新增)

xml 复制代码
<div id="host"></div>
<script>
// 1) 创建一个 SVG 元素(带命名空间)
const SVG_NS = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(SVG_NS, "svg");     // 合法的合格名(qualifiedName) "svg"
const rect = document.createElementNS(SVG_NS, "rect");   // 同上
svg.appendChild(rect);
document.getElementById("host").appendChild(svg);

// 2) 查看 Node 的命名空间相关属性
console.log(rect.localName);      // "rect"(不含前缀的本地名)
console.log(rect.namespaceURI);   // "http://www.w3.org/2000/svg"
console.log(rect.prefix);         // 若创建时名为 "svg:rect" 则可能为 "svg"; 这里多为 null

// 3) DOM3:命名空间查找辅助
console.log(svg.isDefaultNamespace(SVG_NS)); // true: svg 元素树默认命名空间是 SVG
console.log(svg.lookupNamespaceURI("svg"));  // 某些文档中可能返回 SVG_NS;若无此前缀绑定则为 null
console.log(svg.lookupPrefix(SVG_NS));       // 返回与该 URI 绑定的前缀(若有,否则 null)
</script>

要点

  • localName 永远不含前缀;prefix 可能是 null
  • 对于 HTML 里使用 createElement("svg") 创建的元素,在部分浏览器会落入 HTML namespace,不等价于 SVG 元素;务必用 createElementNS

2.3 Document 的变化(命名空间相关:每个 API 有示例)

涉及:getElementsByTagNameNScreateElementNScreateAttributeNS

ini 复制代码
<div id="host"></div>
<script>
const SVG_NS = "http://www.w3.org/2000/svg";
const XLINK_NS = "http://www.w3.org/1999/xlink";

// createElementNS:在特定命名空间里创建元素
const svg = document.createElementNS(SVG_NS, "svg");
const use = document.createElementNS(SVG_NS, "use");

// createAttributeNS:在特定命名空间里创建属性(较少直接用,通常用 setAttributeNS)
const hrefAttr = document.createAttributeNS(XLINK_NS, "xlink:href");
hrefAttr.value = "#icon-id";
use.setAttributeNodeNS(hrefAttr);

svg.appendChild(use);
document.getElementById("host").appendChild(svg);

// getElementsByTagNameNS:按命名空间和本地名查找节点
const uses = document.getElementsByTagNameNS(SVG_NS, "use");
console.log(uses.length); // 1
</script>

2.4 Element 的变化(命名空间属性方法:每个 API 有示例)

涉及:getAttributeNSsetAttributeNSremoveAttributeNS

xml 复制代码
<div id="host"></div>
<script>
const SVG_NS = "http://www.w3.org/2000/svg";
const XLINK_NS = "http://www.w3.org/1999/xlink";

const svg = document.createElementNS(SVG_NS, "svg");
const use = document.createElementNS(SVG_NS, "use");

// setAttributeNS(namespace, qualifiedName, value)
use.setAttributeNS(XLINK_NS, "xlink:href", "#logo");

// getAttributeNS(namespace, localName)
console.log(use.getAttributeNS(XLINK_NS, "href")); // "#logo"

// removeAttributeNS(namespace, localName)
use.removeAttributeNS(XLINK_NS, "href");

svg.appendChild(use);
document.getElementById("host").appendChild(svg);
</script>

2.5 NamedNodeMap 的变化(每个 API 有示例)

涉及:getNamedItemNSsetNamedItemNSremoveNamedItemNS(用于 Element.attributes

ini 复制代码
<div id="host"></div>
<script>
const SVG_NS = "http://www.w3.org/2000/svg";
const XLINK_NS = "http://www.w3.org/1999/xlink";

const svg = document.createElementNS(SVG_NS, "svg");
const use = document.createElementNS(SVG_NS, "use");
svg.appendChild(use);
document.getElementById("host").appendChild(svg);

// attributes 是 NamedNodeMap("实时"集合)
// 1) 创建 Attr 节点(命名空间)
const hrefAttr = document.createAttributeNS(XLINK_NS, "xlink:href");
hrefAttr.value = "#icon";

// 2) setNamedItemNS:把 Attr 放进 attributes(等价于 setAttributeNS)
use.attributes.setNamedItemNS(hrefAttr);

// 3) getNamedItemNS:按 ns+localName 取回
const got = use.attributes.getNamedItemNS(XLINK_NS, "href");
console.log(got.value); // "#icon"

// 4) removeNamedItemNS:删除该属性节点
use.attributes.removeNamedItemNS(XLINK_NS, "href");
</script>

现代实践:尽量使用 get/set/removeAttributeNS 。直接操作 NamedNodeMap 语义更绕且较少用。


3. 其他变化

3.1 DocumentType(DOM3:publicIdsystemIdinternalSubset

javascript 复制代码
console.log(document.doctype.name);        // "html"
console.log(document.doctype.publicId);    // 通常为空字符串(HTML5)
console.log(document.doctype.systemId);    // 通常为空字符串(HTML5)
console.log(document.doctype.internalSubset); // 常为 null(很少用,DTD 内联子集)

3.2 Document 新/扩展 API(逐个示例)

涉及:importNodedefaultViewparentWindow(IE)、
implementationimplementation.createDocument()

xml 复制代码
<div id="a"></div><iframe id="f" srcdoc="<div id='b'></div>"></iframe>
<script>
// 1) defaultView:指向 window
console.log(document.defaultView === window); // true

// parentWindow(仅老 IE):现代浏览器无此属性,了解即可
// console.log(document.parentWindow); // undefined in modern

// 2) importNode:把外文档的节点导入到当前 document
const frame = document.getElementById("f");
frame.addEventListener("load", () => {
  const foreignDoc = frame.contentDocument;      // 来自 iframe 的 document
  const foreignDiv = foreignDoc.createElement("div");
  foreignDiv.textContent = "来自外文档";

  // 若直接 appendChild(foreignDiv) -> WRONG(跨文档)
  const imported = document.importNode(foreignDiv, /* deep */ true);
  document.getElementById("a").appendChild(imported);
});

// 3) document.implementation:DOM 实现入口
// 创建一个"纯 XML 文档"(常用于生成 SVG/MathML、或离线处理)
const xmlDoc = document.implementation.createDocument(
  "http://www.w3.org/2000/svg", // namespaceURI
  "svg",                        // qualifiedName
  null                          // doctype
);
console.log(xmlDoc.documentElement.localName); // "svg"
</script>

3.3 Node 新/扩展 API(逐个示例)

涉及:isEqualNodeisSameNodesetUserData/getUserData(DOM3)

xml 复制代码
<div id="x"></div>
<script>
// isEqualNode:结构与属性"值"相等(不比较引用)
const div1 = document.createElement("div");
div1.setAttribute("data-x", "1");
const div2 = document.createElement("div");
div2.setAttribute("data-x", "1");
console.log(div1.isEqualNode(div2)); // true

// isSameNode:是否同一对象(现代直接用 ===)
console.log(div1.isSameNode(div1)); // true(已过时)
console.log(div1 === div1);         // 推荐

// setUserData/getUserData:已基本废弃 → 用 WeakMap 代替
const data = new WeakMap();
data.set(div1, { foo: 42 });
console.log(data.get(div1).foo); // 42
</script>

3.4 内嵌窗格(iframe):DOM2 增加 contentDocument

xml 复制代码
<iframe id="f" srcdoc="<p id='p'>hi</p>"></iframe>
<script>
const iframe = document.getElementById("f");
iframe.addEventListener("load", () => {
  // 标准
  const doc = iframe.contentDocument;
  // 兼容(非常老的 IE)
  const docCompat = iframe.contentDocument || iframe.contentWindow.document;

  console.log(doc.getElementById("p").textContent); // "hi"
});
</script>

4. 样式(style 属性、<style><link>

4.1 存取元素样式(element.style

xml 复制代码
<div id="box" style="width:100px;height:50px"></div>
<script>
const box = document.getElementById("box");
// 读/写行内样式(只影响 style 特性,不含外链/类的计算结果)
console.log(box.style.width); // "100px"
box.style.border = "1px solid";
box.style.setProperty("background-color", "lightblue", "important");
console.log(box.style.getPropertyValue("background-color")); // "lightblue"
console.log(box.style.getPropertyPriority("background-color")); // "important"
</script>

4.2 DOM 样式属性和方法(重点:通过规则访问类名样式

涉及:cssTextlengthparentRulegetPropertyValuegetPropertyPrioritysetPropertyremoveProperty

备注:getPropertyCSSValue 已过时,现代请用 getPropertyValue

xml 复制代码
<style id="app-style">
  .btn { padding: 8px 12px; color: white; background: #09f; }
  .btn.primary { background: #07c; }
</style>

<button class="btn primary">Click</button>

<script>
// 1) 拿到样式表与规则集合
const sheet = document.getElementById("app-style").sheet; // CSSStyleSheet
const rules = sheet.cssRules; // CSSRuleList(只读)

// 2) 查找选择器为 ".btn" 的规则
const btnRule = Array.from(rules).find(r => r.type === CSSRule.STYLE_RULE && r.selectorText === ".btn");
const styleDecl = btnRule.style; // CSSStyleDeclaration

// 3) 操作声明
console.log(styleDecl.length);                    // 已声明的属性数,例如 3
console.log(styleDecl.getPropertyValue("color")); // "white"
console.log(styleDecl.parentRule === btnRule);    // true
styleDecl.setProperty("border-radius", "12px");
styleDecl.removeProperty("background");

// 4) 一次性覆写(谨慎使用):cssText
styleDecl.cssText += ";background:#0a0"; // 追加也可
</script>

4.3 计算样式(getComputedStylecurrentStyle

xml 复制代码
<div id="box" class="panel" style="padding:10px"></div>
<style>
  .panel { margin: 12px; padding: 4px; }
</style>
<script>
const box = document.getElementById("box");
// 计算样式:综合了所有来源,单位已解析
const cs = getComputedStyle(box);
console.log(cs.marginLeft);      // 如 "12px"(来自类)
console.log(cs.paddingLeft);     // 如 "10px"(行内覆盖类)

// 老 IE(了解即可)
// const paddingLeft = box.currentStyle.paddingLeft;
</script>

5. 操作样式表(每个属性/方法有示例)

涉及:disabledhrefmediaownerNodeparentStyleSheettitletypecssRulesownerRuledeleteRuleinsertRule

xml 复制代码
<link id="link-css" rel="stylesheet" href="data:text/css,.x{color:red}" title="theme">
<style id="inline-css">@import url("data:text/css,.y{color:green}"); .z{font-weight:bold}</style>
<div class="x y z">Hello</div>
<script>
// 取所有样式表
for (const sheet of document.styleSheets) {
  console.log({
    disabled: sheet.disabled,            // 是否禁用
    href: sheet.href,                    // 外链样式表的 URL(内联为 null)
    media: sheet.media.mediaText,        // 适用媒体列表
    ownerNode: sheet.ownerNode.tagName,  // <link> 或 <style>
    title: sheet.ownerNode.title || null,
    type: sheet.type                     // "text/css"
  });

  // 规则集合与层级
  for (const rule of sheet.cssRules) {
    // @import 的子样式表
    if (rule.type === CSSRule.IMPORT_RULE) {
      const imported = rule.styleSheet;
      console.log(imported.parentStyleSheet === sheet); // true
      console.log(imported.ownerRule === rule);         // CSSStyleSheet.ownerRule 指向 @import 规则
    }
  }
}

// 插入与删除规则(对可写样式表生效:同源策略可能限制外链)
const inlineSheet = document.getElementById("inline-css").sheet;
const idx = inlineSheet.insertRule(".added { text-decoration: underline }", inlineSheet.cssRules.length);
console.log("inserted at", idx);

// 删除刚插入的规则
inlineSheet.deleteRule(idx);
</script>

若跨域外链样式表没有 CORS 许可,访问其 cssRules 会抛异常;可改为把规则放在可控的 <style> 中操作。


6. 元素尺寸(逐项示例)

6.1 偏移尺寸(offset*

xml 复制代码
<div id="wrap" style="position:relative; padding:10px; border:5px solid">
  <div id="box" style="width:100px;height:50px;border:2px solid;margin:3px"></div>
</div>
<script>
const box = document.getElementById("box");
console.log(box.offsetWidth, box.offsetHeight); // 包含 padding + border(不含 margin)
console.log(box.offsetLeft, box.offsetTop);     // 相对 offsetParent 的偏移
console.log(box.offsetParent === document.getElementById("wrap")); // 取决于布局
</script>

6.2 客户端尺寸(client*

rust 复制代码
console.log(box.clientWidth, box.clientHeight); // 内容 + padding(不含 border/scrollbar)
console.log(box.clientLeft, box.clientTop);     // 左/上边框宽度(+方向性影响)

6.3 滚动尺寸(scroll*

ini 复制代码
box.style.overflow = "auto";
box.innerHTML = "<div style='height:200px'></div>";
console.log(box.scrollWidth, box.scrollHeight); // 内容区域的总大小(含溢出)
box.scrollTop = 30;                             // 设置滚动位置
console.log(box.scrollLeft, box.scrollTop);

6.4 确认元素尺寸(getBoundingClientRect()

arduino 复制代码
const rect = box.getBoundingClientRect(); // 相对视口的布局盒
console.log(rect.left, rect.top, rect.width, rect.height);
// 注意:包含变换与缩放后的视觉大小;需结合 window.scrollX/Y 得到文档坐标

7. 遍历

7.1 NodeIteratorcreateNodeIterator

xml 复制代码
<ul id="tree"><li>A<b>A1</b></li><li>B</li></ul>
<script>
// 遍历元素节点(过滤掉文本节点)
const it = document.createNodeIterator(
  document.getElementById("tree"),
  NodeFilter.SHOW_ELEMENT,                         // 只看元素
  node => node.tagName === "B"
          ? NodeFilter.FILTER_REJECT               // 跳过 B 及其子树
          : NodeFilter.FILTER_ACCEPT,
);
let cur;
while (cur = it.nextNode()) {
  console.log("NodeIterator:", cur.tagName);
}
</script>

7.2 TreeWalkercreateTreeWalker

xml 复制代码
<ul id="tree"><li>A<b>A1</b></li><li>B</li></ul>
<script>
const walker = document.createTreeWalker(
  document.getElementById("tree"),
  NodeFilter.SHOW_ELEMENT,
  null
);
let n = walker.currentNode; // 初始为 root
while (n = walker.nextNode()) {
  console.log("TreeWalker:", n.tagName);
}
// 与 NodeIterator 不同:TreeWalker 支持 parentNode/firstChild/lastChild/nextSibling/previousSibling 等导航
</script>

8. DOM2 定义的 Range 接口(简介)

Range 表示文档中的一个起点-终点边界区间,可跨元素与文本节点。可用来做选择、高亮、剪切/复制等。


9. 关键属性(使用场景)

  • startContainer / startOffset:起点所在节点与偏移
  • endContainer / endOffset:终点所在节点与偏移
  • commonAncestorContainer:起止的最近公共祖先
xml 复制代码
<p id="p">Hello <b>world</b> !</p>
<script>
const rng = document.createRange();
const p = document.getElementById("p");
rng.selectNode(p.childNodes[1]); // 选中 <b>world</b>
console.log(rng.startContainer, rng.endContainer);
console.log(rng.commonAncestorContainer === p); // true
</script>

10. 简单选择(边界锚定 API)

涉及:selectNodesetStartBeforesetStartAftersetEndBeforesetEndAfter

xml 复制代码
<p id="p">A <i>be</i> C</p>
<script>
const p = document.getElementById("p");
const rng = document.createRange();

rng.selectNode(p.querySelector("i")); // 整个 <i>...</i>
rng.setStartBefore(p.firstChild);     // 起点移到第一个子节点之前("A " 文本之前)
rng.setEndAfter(p.lastChild);         // 终点移到最后一个子节点之后(" C" 文本之后)
console.log(rng.toString());          // "A be C"
</script>

使用场景:基于元素边界进行"包围式"选择(如把某个元素连同两侧兄弟都选中)。


11. 复杂选择(精准到文本偏移)

涉及:setStart(node, offset)setEnd(node, offset)

xml 复制代码
<p id="p">Hello <b>world</b> !</p>
<script>
const p = document.getElementById("p");
const rng = document.createRange();
const textNode = p.firstChild; // "Hello "

rng.setStart(textNode, 1);     // 从 "ello " 的开头
rng.setEnd(textNode, 5);       // 到 "Hello" 后(空格前)
console.log(rng.toString());   // "ello"
</script>

使用场景:做富文本编辑器、代码高亮、精确拼接/替换文本。


12. 操作范围内容:deleteContents / extractContents / cloneContents

xml 复制代码
<p id="p">Hello <b>world</b> !</p>
<div id="bin"></div>
<script>
const p = document.getElementById("p");
const rng = document.createRange();

// 选中 <b>world</b>
rng.selectNode(p.querySelector("b"));

// 1) cloneContents:克隆一个 DocumentFragment,不改原文档
const fragClone = rng.cloneContents();
document.getElementById("bin").appendChild(fragClone);

// 2) extractContents:剪切(从文档移除并返回 fragment)
rng.extractContents(); // 文档中删除了 <b>world</b>

// 3) deleteContents:仅删除(不返回 fragment)
rng.setStart(p.firstChild, 0);
rng.setEnd(p.firstChild, 2);   // 删除 "He"
rng.deleteContents();
</script>

13. 将内容插入或包裹:insertNode / surroundContents

ini 复制代码
<p id="p">Hello world!</p>
<script>
const p = document.getElementById("p");
const rng = document.createRange();

// 在 "Hello " 与 "world!" 间插入一个 <span>
const text = p.firstChild; // "Hello world!"
rng.setStart(text, 6);     // 在空格后
rng.collapse(true);        // 折叠成插入点

const span = document.createElement("span");
span.textContent = "🌟";
rng.insertNode(span);

// 包裹内容:把 "world" 包成 <mark>world</mark>
const rng2 = document.createRange();
rng2.setStart(text, 7);  // w
rng2.setEnd(text, 12);   // d 后
const mark = document.createElement("mark");
rng2.surroundContents(mark);
</script>

surroundContents 要求范围内不能产生无效 DOM(如把两个兄弟节点的一部分一起包裹会抛错)。必要时用 extractContents() + 手工拼装。


14. 折叠范围:collapse(toStart)

xml 复制代码
<p id="p">abc</p>
<script>
const t = document.getElementById("p").firstChild; // "abc"
const rng = document.createRange();
rng.setStart(t, 1); // 在 "b" 前
rng.setEnd(t, 2);   // 在 "c" 前
console.log(rng.toString()); // "b"

rng.collapse(true); // 折叠到起点(光标落在 b 前)
console.log(rng.collapsed);  // true
</script>

15. 比较边界:compareBoundaryPointscollapsed

ini 复制代码
<p id="p">Hello world</p>
<script>
const t = document.getElementById("p").firstChild; // "Hello world"
const a = document.createRange();
const b = document.createRange();
a.setStart(t, 0); a.setEnd(t, 5);  // "Hello"
b.setStart(t, 6); b.setEnd(t, 11); // "world"

const BEFORE = Range.START_TO_START; // 0
const cmp = a.compareBoundaryPoints(BEFORE, b);
console.log(cmp); // -1(a 开始 在 b 开始 之前)

console.log(a.collapsed); // false(顺便:是否折叠)
</script>

返回值-1(在前)、0(相等)、1(在后)。常用于判断两个范围相对位置。


16. 复制范围:cloneRange

ini 复制代码
const r1 = document.createRange();
// ... 配置 r1
const r2 = r1.cloneRange(); // 独立副本,可单独修改

使用场景:保存/恢复用户选区;在不改变原范围的前提下做试验性操作。


17. 清理范围:detach(历史 API)

ini 复制代码
const r = document.createRange();
r.detach(); // 现代浏览器基本为 no-op;习惯上将引用置空利于 GC
// r = null; // 让 GC 回收

现代浏览器无需刻意调用 detach()正确释放引用 即可(变量置 null / 超出作用域)。


▲ 附:使用 Selection API 与 Range 协作(常用)

ini 复制代码
// 读取并修改用户当前选区
const sel = window.getSelection();
if (!sel.rangeCount) {
  // 创建一个选区
  const r = document.createRange();
  const p = document.querySelector("p");
  r.selectNodeContents(p);
  sel.removeAllRanges();
  sel.addRange(r);
} else {
  const r = sel.getRangeAt(0);
  console.log("selected text:", r.toString());
}

常见坑与最佳实践

  • SVG/MathML :创建元素/属性请用 *NS 版本,避免落入 HTML 命名空间导致属性无效(如 xlink:href)。

  • 样式表跨域 :没有 CORS 许可的外链样式表不可读 cssRules。把规则放到 <style> 或同源文件。

  • 已过时 APIisSameNode===getPropertyCSSValuecurrentStylesetUserData/getUserDatadetach 等仅作历史了解。

    • 替代WeakMap 存放"挂靠数据";getPropertyValue 代替 getPropertyCSSValue
  • 尺寸计算offset* 包含边框;client* 不含边框;scroll* 反映滚动内容大小;getBoundingClientRect() 受 CSS 变换影响。

  • Range 包裹surroundContents 必须是"良好"范围,复杂情况用 extractContents() + newEl.append(frag) + insertNode(newEl)

相关推荐
江城开朗的豌豆1 分钟前
React的渲染时机:聊透虚拟DOM的更新机制
前端·javascript·react.js
anyup8 分钟前
🔥🔥 uView Pro:Vue3+TS重构的uni-app开源组件库,文档免费无广告!
前端·vue.js·uni-app
CodeSheep18 分钟前
我天,Java 已沦为老四。。
前端·后端·程序员
前端小巷子1 小时前
Vue 逻辑抽离全景解析
前端·vue.js·面试
excel1 小时前
前端事件机制入门到精通:事件流、冒泡捕获与事件委托全解析
前端
Moment1 小时前
Next.js 15.5 带来 Turbopack Beta、Node 中间件稳定与 TypeScript 强化 🚀🚀🚀
前端·javascript·react.js
yzzzzzzzzzzzzzzzzz2 小时前
初识javascript
前端·javascript
专注API从业者10 小时前
Python + 淘宝 API 开发:自动化采集商品数据的完整流程
大数据·运维·前端·数据挖掘·自动化
烛阴11 小时前
TypeScript高手密技:解密类型断言、非空断言与 `const` 断言
前端·javascript·typescript