1. 节点层级(Node tree)
-
DOM 是一棵树:Document (根)→ Element (元素)→ Text/Comment 等。
-
常见
nodeType
:1
:Element3
:Text8
:Comment9
:Document10
:DocumentType(<!DOCTYPE html>
)11
:DocumentFragment2
:Attr(属性节点,遗留)
2. 节点类型与 Node 常用属性/关系
2.1 nodeName
与 nodeValue
-
nodeName
:标签名或节点名(元素为大写标签名如"DIV"
;Text 为#text
;Comment 为#comment
)。 -
nodeValue
:- Element:
null
- Text:文本内容
- Comment:注释文本
- Element:
ini
const div = document.createElement('div');
console.log(div.nodeName, div.nodeValue); // "DIV", null
const t = document.createTextNode('hello');
console.log(t.nodeName, t.nodeValue); // "#text", "hello"
2.2 节点关系
- 父子:
parentNode
、firstChild
、lastChild
、childNodes
(NodeList,包含文本/注释) - 同级:
previousSibling
、nextSibling
- 仅元素的集合:
children
、firstElementChild
、lastElementChild
、previousElementSibling
、nextElementSibling
ini
const ul = document.querySelector('ul');
ul.firstElementChild; // 第一个 <li>
ul.childNodes; // 包含换行产生的 Text 节点
2.3 操作节点(插入/删除/替换/移动)
- 经典 API:
appendChild
、insertBefore(newNode, refNode)
、replaceChild(newNode, oldNode)
、removeChild(node)
- 现代便捷:
append(...)
、prepend(...)
、before(...)
、after(...)
、replaceWith(...)
、remove()
ini
const li = document.createElement('li');
li.textContent = 'Item';
ul.appendChild(li);
const first = ul.firstElementChild;
first.before('标题:'); // 插到元素前
first.replaceWith(li); // 替换
li.remove(); // 删除
2.4 其它方法(克隆等)
cloneNode(deep)
:浅/深拷贝节点(deep=true
拷贝子树)。contains(node)
:是否为后代compareDocumentPosition(node)
:文档位置关系(位掩码)isEqualNode(node)
/isSameNode(node)
(后者≈===
)
go
const copy = ul.cloneNode(true); // 深拷贝整个列表
document.body.append(copy);
3. Document 类型
3.1 典型属性
nodeType=9
,nodeName="#document"
documentElement
(根<html>
)、head
、body
、doctype
URL
、domain
、title
、referrer
、lastModified
、cookie
、compatMode
(CSS1Compat
/BackCompat
)
ini
document.title = '新标题';
console.log(document.documentElement === document.childNodes[1]); // 通常 true(childNodes[0] 可能是 doctype)
3.1′ 文档子节点
document.childNodes[0]
常为DocumentType
(<!DOCTYPE html>
)document.childNodes[1]
通常为<html>
(documentElement
)
3.2 文档信息
javascript
console.log(document.URL, document.domain, document.referrer, document.lastModified);
3.3 定位元素(基于名字/ID的集合)
- 某些集合 支持按
name
或id
访问:如document.images['nameTest']
- 现代更推荐:
querySelector
/getElementById
3.4 特殊集合(HTMLCollection,部分是动态/live)
document.anchors
:<a name="...">
(遗留 ;现代多用id
)document.applets
:废弃document.forms
:所有<form>
document.images
:所有<img>
document.links
:所有带href
的<a>
⚠️ 这些多为 动态 集合(DOM 变化会实时反映),但
querySelectorAll
返回 静态 NodeList。
3.5 DOM 兼容性检测(特性检测)
- 推荐:判断 API 是否存在
javascript
if ('querySelector' in document && 'classList' in Element.prototype) {
// 安全使用
}
CSS.supports('display', 'grid')
检测 CSS 支持- ⚠️
document.implementation.hasFeature()
已不可靠
3.6 文档写入:write
/ writeln
(了解即可)
- 仅在解析阶段 使用;在加载后调用可能清空文档并重写!
xml
<script>
document.write('<p>构建时写入</p>');
document.writeln('<p>带换行</p>');
</script>
⚠️ 现代开发中应避免,改用 DOM API 或模板渲染。
4. Element 类型
4.1 常用属性
- 结构:
tagName
、id
、className
、classList
、dataset
、attributes
(NamedNodeMap,遗留 )、innerHTML
、outerHTML
、textContent
- 尺寸/位置(只读/计算):
clientWidth/Height
、offsetTop/Left
、getBoundingClientRect()
等
ini
const div = document.createElement('div');
div.id = 'box';
div.classList.add('card', 'primary');
div.dataset.role = 'panel';
div.innerHTML = '<strong>Hi</strong>';
document.body.append(div);
4.2 获取属性
less
div.getAttribute('data-role'); // "panel"
div.hasAttribute('hidden'); // boolean
4.3 设置/移除属性
less
div.setAttribute('aria-live', 'polite');
div.removeAttribute('aria-live');
4.4 attributes
(NamedNodeMap,不推荐新代码依赖)
- 方法:
getNamedItem(name)
、setNamedItem(attr)
、removeNamedItem(name)
、item(index)
ini
const attr = document.createAttribute('data-x');
attr.value = '1';
div.attributes.setNamedItem(attr);
现代用
getAttribute/setAttribute/hasAttribute/removeAttribute
更简单。
4.5 创建元素与插入
ini
const li = document.createElement('li');
li.textContent = 'A';
document.querySelector('ul').appendChild(li);
4.6 元素的后代
childNodes
(含文本/注释)children
(仅元素)- 遍历推荐:
for (const el of element.children) {}
或element.querySelectorAll(':scope > *')
5. Text 类型
-
创建:
document.createTextNode('text')
-
规范化:
parent.normalize()
合并相邻 Text 节点 -
拆分:
text.splitText(offset)
-
字符数据方法(Text 继承自 CharacterData):
appendData(s)
、deleteData(offset,count)
、insertData(offset,s)
replaceData(offset,count,s)
、substringData(offset,count)
小型富文本示例(基于选区拆分/替换)
ini
<div id="editable" contenteditable="true">前缀<strong>粗体</strong>后缀</div>
<script>
const el = document.getElementById('editable');
// 将选区用 <mark> 包裹(简化演示,未处理跨节点复杂情况)
function highlightSelection() {
const sel = window.getSelection();
if (!sel.rangeCount) return;
const range = sel.getRangeAt(0);
const frag = range.extractContents(); // 取出选中内容(可能含多个节点)
const mark = document.createElement('mark');
mark.append(frag);
range.insertNode(mark);
sel.removeAllRanges();
}
document.addEventListener('keydown', e => {
if (e.key.toLowerCase() === 'h' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
highlightSelection();
}
});
</script>
6. Comment 类型
- 创建与使用:
ini
const c = document.createComment('TODO: remove in prod');
document.body.append(c);
console.log(c.nodeType, c.nodeValue); // 8, "TODO: ..."
7. CDATASection 类型
- 仅适用于 XML/XHTML 文档。在 HTML 文档中
createCDATASection
会抛出NotSupportedError
。
ini
// 在 XML DOM 中:
const xmlDoc = document.implementation.createDocument(null, 'root');
const cdata = xmlDoc.createCDATASection('<raw>&chars</raw>');
xmlDoc.documentElement.append(cdata);
8. DocumentType 类型(DOCTYPE)
javascript
console.log(document.doctype.name); // "html"
- 创建(多用于生成 XML/离线文档):
ini
const dt = document.implementation.createDocumentType('html', '', '');
document.insertBefore(dt, document.documentElement);
9. DocumentFragment
- 轻量级容器,不会产生额外父节点,插入时一次性移动其子节点(减少重排/重绘)。
ini
const frag = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
frag.append(li);
}
document.querySelector('ul').append(frag);
- 结合
<template>
:
xml
<template id="tpl"><li class="row"><span></span></li></template>
<script>
const { content } = document.getElementById('tpl');
const ul = document.querySelector('ul');
const frag = document.createDocumentFragment();
for (const v of ['A','B','C']) {
const node = content.cloneNode(true);
node.querySelector('span').textContent = v;
frag.append(node);
}
ul.append(frag);
</script>
10. Attr 类型(属性节点,遗留)
ini
const attr = document.createAttribute('data-k');
attr.value = 'v';
const old = document.getElementById('box').setAttributeNode(attr);
新代码更推荐
setAttribute('data-k','v')
。
11. DOM 编程实战
11.1 动态脚本
ini
function loadScript(src, { async=true, defer=false, module=false } = {}) {
const s = document.createElement('script');
if (module) s.type = 'module';
s.src = src;
s.async = async;
s.defer = defer;
return new Promise((res, rej) => {
s.onload = () => res(s);
s.onerror = rej;
document.head.append(s);
});
}
// 使用:await loadScript('/assets/app.js', { module: true });
11.2 动态样式
ini
// 内联样式表
const style = document.createElement('style');
style.textContent = `
.badge { padding: 4px 8px; border-radius: 8px; }
`;
document.head.append(style);
// 外链样式表
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/styles/app.css';
document.head.append(link);
11.3 操作表格(详细示例)
ini
// 创建表格:thead / tbody / tfoot 与行列 API
const table = document.createElement('table');
table.createTHead();
const tbody = table.createTBody();
table.createTFoot();
// 头部
const hrow = table.tHead.insertRow();
['#','Name','Score'].forEach(t => hrow.insertCell().textContent = t);
// 数据
[
{ id:1, name:'Alice', score: 95 },
{ id:2, name:'Bob', score: 87 },
].forEach(r => {
const row = tbody.insertRow();
row.insertCell().textContent = r.id;
row.insertCell().textContent = r.name;
row.insertCell().textContent = r.score;
});
// 脚注
const frow = table.tFoot.insertRow();
const cell = frow.insertCell();
cell.colSpan = 3;
cell.textContent = 'Total rows: ' + tbody.rows.length;
document.body.append(table);
// 更新:删除第二行
// tbody.deleteRow(1);
11.4 使用 NodeList
- 来源:
querySelectorAll
(静态 )、childNodes
(可能 动态) - 迭代:
for...of
、NodeList.prototype.forEach
- 转数组与高阶方法:
ini
const list = document.querySelectorAll('li'); // 静态
list.forEach(li => li.classList.add('x'));
const arr = Array.from(list).map(li => li.textContent);
12. MutationObserver 接口
用于异步观察 DOM 变化(微任务队列中回调)。
12.1 基本用法
observe()
javascript
const target = document.getElementById('root');
const obs = new MutationObserver((mutations, observer) => {
for (const m of mutations) {
if (m.type === 'childList') {
console.log('childList:', m.addedNodes, m.removedNodes);
} else if (m.type === 'attributes') {
console.log('attr:', m.attributeName, 'old=', m.oldValue, 'new=', target.getAttribute(m.attributeName));
} else if (m.type === 'characterData') {
console.log('text old=', m.oldValue, 'new=', m.target.data);
}
}
});
obs.observe(target, {
childList: true,
attributes: true,
characterData: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true,
attributeFilter: ['class','data-state'] // 可选
});
- 回调与
MutationRecord
type
:'attributes'|'characterData'|'childList'
target
:发生变化的节点addedNodes
/removedNodes
(childList
)attributeName
、oldValue
(需启用对应OldValue
)previousSibling
/nextSibling
(在子节点变化时有值)
disconnect()
停止观察
ini
obs.disconnect();
- 复用/重用同一观察者
php
// 监听多个不同节点或范围
const nodes = document.querySelectorAll('.watch');
for (const n of nodes) obs.observe(n, { attributes:true, childList:true });
12.2 MutationObserverInit
与观察范围
attributes
:属性变化characterData
:文本节点数据变化childList
:子节点增删subtree
:是否包含整个子树attributeOldValue
:记录属性旧值(需attributes: true
)characterDataOldValue
:记录文本旧值(需characterData: true
)attributeFilter
:仅关注特定属性数组
12.3 异步回调与记录队列
- 变化会进入记录队列 ,在当前任务结束后以微任务回调一并派发。
takeRecords()
:立即取出队列中未派发的记录(不触发回调)。
ini
const pending = obs.takeRecords();
pending.forEach(r => console.log('手动取:', r));
12.4 性能、内存与垃圾回收
- 避免对
document
根节点全量subtree:true, childList:true
的长期观察;改为缩小范围或精确attributeFilter
。 - 回调中避免频繁 DOM 写操作(可批处理 或
requestAnimationFrame
合并)。 - 及时
disconnect()
,避免泄漏;不要在回调里递归触发大量新变更(可能引发振荡)。 - 将观察者实例存在需要的作用域;不再使用时移除引用,让 GC 回收。
附:常用对照与最佳实践
- 取元素 :
getElementById
(快)/querySelector
(灵活) - 遍历孩子 :结构化用
children
,包含文本则用childNodes
- 创建+插入 :批量用
DocumentFragment
;单个用append/prepend/before/after
- 属性 :首选
getAttribute/setAttribute/hasAttribute/removeAttribute
或直接属性(如el.id
、el.className
),少用attributes
- 文本 :首选
textContent
(安全、不会解析 HTML);需要 HTML 时用innerHTML
(注意 XSS) - 旧特性 :
document.write
、anchors
、applets
、NamedNodeMap
、Attr
节点 ------ 了解即可,不建议新项目依赖