Vue 和 React 封装了 DOM 操作,但在做富文本编辑器、拖拽排序、Canvas 交互或不依赖框架的轻量组件时,原生 DOM 的知识仍然绕不开。BOM 则负责与浏览器窗口和导航相关的交互。

DOM 的本质

DOM(Document Object Model)是浏览器将 HTML 解析后生成的一棵对象树。HTML 本身是一种特定语法的 XML,规定了标签名称和嵌套规则。浏览器的渲染管线大致是:

HTML → 解析 → DOM Tree
CSS  → 解析 → CSSOM
DOM Tree + CSSOM → Render Tree → Layout(布局)→ Paint(绘制)

所以当我们操作 DOM 时,实际上是在操作一棵内存中的对象树。修改 DOM 不一定立即触发重绘——浏览器有自己的批量优化机制。

节点操作

获取节点

// 单个元素(通过 ID,最快)
const div1 = document.getElementById('div1');
 
// 动态集合(DOM 变化时实时更新)
const divList = document.getElementsByTagName('div');
const containers = document.getElementsByClassName('container');
 
// 静态快照(CSS 选择器,最灵活)
const pList = document.querySelectorAll('p');
const specific = document.querySelector('.container > p:first-child');

getElementsBy* 返回动态 HTMLCollection,querySelectorAll 返回静态 NodeList。在遍历过程中删除 DOM 节点时,动态集合的行为可能出乎意料:

const items = document.getElementsByTagName('li');
// items 是动态的,删除第一个 li 后,原来的 items[1] 变成了新的 items[0]
// 如果正向遍历并删除,会跳过元素
// 解决办法:反向遍历,或使用 querySelectorAll(静态快照)

Property vs Attribute

const p = document.querySelector('p');
 
// Property — JS 对象属性,不修改 HTML 标签
p.className = 'highlight'; // class 的 Property 名是 className
p.style.color = 'red'; // style 对象映射 CSS 属性
p.nodeName; // 'P'(只读)
p.nodeType; // 1 = 元素, 3 = 文本, 8 = 注释, 9 = 文档
 
// Attribute — 直接修改 HTML 标签属性
p.setAttribute('data-id', '123');
p.getAttribute('data-id'); // '123'
p.setAttribute('style', 'font-size: 20px;'); // 这会把内联 style 整个替换

两者的关系和差异:

维度PropertyAttribute
本质JS 对象的属性HTML 标签上的特性
同步少数属性(id、className、value——但 value 不同步回 Attribute)多数自定义属性不同步
修改 HTML不体现会改变 HTML 结构
获取element.propelement.getAttribute()

element.value 是一个典型的不对称例子:修改 input.value = 'hello' 会更新 Property 但不会改变 HTML 中的 value Attribute。getAttribute('value') 始终返回 HTML 源码中的初始值。用 element.defaultValue 获取初始值更可靠。

推荐操作:优先用 Property(element.prop),需要操作自定义 data-* 属性时用 dataset

p.dataset.id = '123'; // 等价于 data-id="123"
p.dataset.userName = 'test'; // 等价于 data-user-name="test"(驼峰 → 连字符)
console.log(p.dataset.id); // '123'

textContent vs innerText vs innerHTML

element.textContent = 'hello'; // 纯文本,安全,不解析 HTML
element.innerText = 'hello'; // 纯文本,但会触发回流(考虑 CSS 样式)
element.innerHTML = '<b>hello</b>'; // 解析 HTML,有 XSS 风险
  • textContent:最快,直接设置文本
  • innerText:会考虑 CSS(如 display: none 的文本不返回),会触发回流
  • innerHTML:用于需要插入 HTML 结构的场景,必须确保内容安全

结构操作

// 创建
const newP = document.createElement('p');
newP.textContent = 'hello';
div1.appendChild(newP);
 
// 移动 —— appendChild 是移动而非复制!
div2.appendChild(document.getElementById('p2'));
 
// 克隆
const clone = p1.cloneNode(true); // true = 深克隆(含子节点)
 
// 插入
div1.insertBefore(newNode, div1.firstChild);
parent.insertBefore(newNode, referenceNode); // 新节点插入到参考节点之前
 
// 替换
parent.replaceChild(newNode, oldNode);
 
// 查询关系
p1.parentNode; // 父节点
p1.parentElement; // 父元素(节点是元素时两者等价,父节点是 Document 时 parentElement 为 null)
div.children; // HTMLCollection,只含元素子节点
div.childNodes; // NodeList,含文本、注释等所有类型节点
div.firstChild; // 第一个子节点(含文本节点)
div.firstElementChild; // 第一个子元素
div.previousSibling; // 前一个兄弟节点
div.nextElementSibling; // 后一个兄弟元素
 
// 删除
element.remove(); // 现代写法
parent.removeChild(child); // 传统写法

DOM 性能优化

DOM 操作昂贵,每次修改都可能触发回流(reflow)和重绘(repaint)。回流涉及布局重新计算,比重绘(仅重新绘制)开销更大。

1. 缓存查询

// ❌ 每次循环都查询 DOM(getElementsByTagName 返回动态集合,更是雪上加霜)
for (let i = 0; i < document.getElementsByTagName('p').length; i++) {}
 
// ✅ 缓存查询
const pList = document.getElementsByTagName('p');
const len = pList.length;
for (let i = 0; i < len; i++) {}

2. DocumentFragment:批量插入

const list = document.getElementById('list');
// DocumentFragment 是"虚拟容器",不在真实 DOM 树中,修改它不触发渲染
const frag = document.createDocumentFragment();
 
for (let i = 0; i < 100; i++) {
  const li = document.createElement('li');
  li.textContent = 'item ' + i;
  frag.appendChild(li); // 操作虚拟节点,零渲染开销
}
 
list.appendChild(frag); // 一次性插入,只触发一次回流

3. 读写分离:避免强制同步布局

// ❌ 交替读写:每次写后立即读,浏览器被迫立刻计算布局(强制同步布局)
elements.forEach((el) => {
  el.style.width = el.offsetWidth + 1 + 'px'; // 读 → 写 → 下一轮又读...
});
 
// ✅ 先批量读,再批量写
const widths = elements.map((el) => el.offsetWidth);
elements.forEach((el, i) => (el.style.width = widths[i] + 1 + 'px'));

强制同步布局(Forced Synchronous Layout)的性能影响在循环中尤为明显。每次读取布局属性(如 offsetWidth、clientHeight、getComputedStyle)后立即写入样式,浏览器必须同步重新计算布局才能返回正确的读取值。

4. 使用 CSS class 而非逐条修改 style

// ❌ 逐条设置
el.style.width = '100px';
el.style.height = '100px';
el.style.backgroundColor = 'red';
 
// ✅ 定义 class,一次性切换
el.classList.add('active');
// el.classList.toggle('visible');
// el.classList.remove('hidden');
// el.classList.replace('old', 'new');

BOM

BOM(Browser Object Model)是与浏览器窗口交互的 API 集合,没有 W3C 标准,但主流浏览器实现基本一致。

// 浏览器信息
navigator.userAgent; // UA 字符串
navigator.language; // 浏览器语言
navigator.onLine; // 是否在线
navigator.cookieEnabled; // cookie 是否启用
 
// 推荐:特性检测代替 UA 检测
const supportsTouch = 'ontouchstart' in window;
const supportsWebP = false; // 需要异步检测

screen

screen.width; // 屏幕宽度
screen.height; // 屏幕高度
screen.availWidth; // 可用宽度(减去系统任务栏等)
screen.colorDepth; // 色彩深度
screen.pixelDepth; // 像素深度
 
// 设备像素比(Retina 屏幕为 2 或 3)
window.devicePixelRatio;

location

// 假设 URL: https://example.com:8080/path/page?q=hello&page=1#section
location.href; // 完整 URL
location.protocol; // 'https:'
location.host; // 'example.com:8080'(含端口)
location.hostname; // 'example.com'
location.port; // '8080'
location.pathname; // '/path/page'
location.search; // '?q=hello&page=1'
location.hash; // '#section'
location.origin; // 'https://example.com:8080'(只读)
 
// 跳转方式
location.href = '/new'; // 可以后退
location.assign('/new'); // 同上
location.replace('/new'); // 不可后退(替换历史记录)
location.reload(); // 重新加载(等同于 F5)
location.reload(true); // 强制重新加载(等同于 Ctrl+F5,已废弃)
 
// 解析查询参数
const params = new URLSearchParams(location.search);
params.get('q'); // 'hello'
params.get('page'); // '1'
params.has('q'); // true

history

history.length; // 历史记录条数
history.back(); // 后退
history.forward(); // 前进
history.go(-2); // 后退两页
 
// SPA 路由基础
history.pushState({ page: 1 }, '', '/page/1'); // 新增记录
history.replaceState({ page: 2 }, '', '/page/2'); // 替换当前记录
 
// 监听前进/后退
window.addEventListener('popstate', (e) => {
  console.log(e.state); // pushState/replaceState 传入的 state 对象
});

pushStatereplaceState 不会触发 popstate 事件,也不会导致页面重新加载——这正是 SPA 路由能工作的基础。

window 对象常用属性和方法

window.innerWidth; // 视口宽度(含滚动条)
window.innerHeight; // 视口高度
window.scrollX; // 水平滚动距离(pageXOffset 的别名)
window.scrollY; // 垂直滚动距离
 
window.open('/popup', '_blank', 'width=400,height=300');
window.close(); // 只能关闭 window.open 打开的窗口
window.scrollTo(0, 500); // 滚动到指定位置
window.scrollTo({ top: 500, behavior: 'smooth' }); // 平滑滚动
 
// 定时器
const id = setTimeout(() => {}, 1000);
const intervalId = setInterval(() => {}, 1000);
clearTimeout(id);
clearInterval(intervalId);
 
// requestAnimationFrame —— 在下一帧绘制前执行
requestAnimationFrame(() => {
  // 适合做动画,浏览器会优化执行频率
});

相关笔记