修改 DOM 元素:更新页面内容与属性
内容修改的多种方式
DOM 元素的内容可以通过多种方式修改,每种方式有不同的特点和适用场景。选择正确的方法不仅关系到代码的简洁性,还涉及安全性和性能。
textContent
textContent 用于获取或设置元素的纯文本内容,是最安全的内容修改方式:
const paragraph = document.querySelector("#intro");
// 获取文本
console.log(paragraph.textContent);
// 设置文本
paragraph.textContent = "This is the new content";textContent 会获取元素及其所有后代的文本内容:
<div id="container">
<p>First paragraph</p>
<p>Second paragraph</p>
</div>const container = document.getElementById("container");
console.log(container.textContent);
// 输出:
// "
// First paragraph
// Second paragraph
// "设置 textContent 时,所有子元素都会被移除,只留下纯文本:
container.textContent = "All HTML is gone";
// <div id="container">All HTML is gone</div>textContent 的安全性
textContent 会自动转义 HTML 字符,防止 XSS 攻击:
const userInput = '<script>alert("Hacked!")</script>';
// 安全:脚本标签会显示为文本
element.textContent = userInput;
// 显示: <script>alert("Hacked!")</script>innerText
innerText 与 textContent 类似,但有几个关键区别:
const element = document.querySelector(".content");
// 获取文本
console.log(element.innerText);
// 设置文本
element.innerText = "New text";textContent vs innerText:
| 特性 | textContent | innerText |
|---|---|---|
| 返回隐藏元素的文本 | 是 | 否 |
| 受 CSS 影响 | 否 | 是 |
| 触发重排 | 否 | 是 |
| 保留换行 | 保留原始格式 | 按渲染后的显示 |
| 性能 | 更快 | 较慢 |
<div id="example">
Visible text
<span style="display: none;">Hidden text</span>
</div>const div = document.getElementById("example");
console.log(div.textContent); // "Visible text Hidden text"
console.log(div.innerText); // "Visible text"一般情况下,推荐使用 textContent,除非你需要获取"用户可见"的文本内容。
innerHTML
innerHTML 可以获取或设置元素的 HTML 内容:
const container = document.querySelector(".container");
// 获取 HTML
console.log(container.innerHTML);
// "<p>First paragraph</p><p>Second paragraph</p>"
// 设置 HTML
container.innerHTML = "<h2>New Title</h2><p>New content</p>";使用 innerHTML 可以一次性创建复杂的 DOM 结构:
function renderCard(data) {
const card = document.createElement("div");
card.className = "card";
card.innerHTML = `
<img src="${data.image}" alt="${data.title}">
<h3>${data.title}</h3>
<p>${data.description}</p>
<button class="btn">Learn More</button>
`;
return card;
}innerHTML 的风险
直接使用用户输入设置 innerHTML 存在严重的安全风险:
// ❌ 危险:XSS 攻击
const userComment = '<img src=x onerror="stealCookies()">';
element.innerHTML = userComment; // 恶意代码会执行
// ✅ 安全:使用 textContent
element.textContent = userComment; // 显示原始文本
// ✅ 安全:手动转义
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
element.innerHTML = `<p>${escapeHtml(userComment)}</p>`;outerHTML
outerHTML 包含元素自身及其内容:
const paragraph = document.querySelector("p");
console.log(paragraph.outerHTML);
// "<p class="intro">Hello World</p>"
// 替换整个元素
paragraph.outerHTML = '<div class="intro">Hello World</div>';设置 outerHTML 后,原来的变量引用会失效:
const element = document.querySelector(".old");
element.outerHTML = '<div class="new">Replaced</div>';
console.log(element.className); // 仍然是 "old"
// element 仍然指向被移除的旧元素属性操作
DOM 元素有各种属性,可以通过多种方式读取和修改。
直接属性访问
许多 HTML 属性可以直接作为 DOM 对象的属性访问:
const link = document.querySelector("a");
// 读取属性
console.log(link.href); // 完整 URL
console.log(link.id); // ID 属性
console.log(link.className); // class 属性
// 设置属性
link.href = "https://example.com";
link.id = "main-link";
link.className = "external-link";
// 对于图片
const img = document.querySelector("img");
console.log(img.src); // 完整 URL
console.log(img.alt); // alt 文本
img.src = "/images/new-image.jpg";
// 对于输入框
const input = document.querySelector("input");
console.log(input.value); // 当前值
console.log(input.type); // 输入类型
console.log(input.disabled); // 是否禁用(布尔值)
input.value = "New value";
input.disabled = true;注意属性名的差异
有些 HTML 属性名与 JavaScript 属性名不同:
| HTML 属性 | JavaScript 属性 |
|---|---|
| class | className 或 classList |
| for | htmlFor |
| readonly | readOnly |
| maxlength | maxLength |
| tabindex | tabIndex |
const label = document.querySelector("label");
console.log(label.htmlFor); // 对应 HTML 的 for 属性
const input = document.querySelector("input");
console.log(input.readOnly); // 对应 HTML 的 readonly 属性getAttribute 和 setAttribute
对于非标准属性或需要获取原始属性值时,使用这些方法:
const element = document.querySelector(".card");
// 获取属性
const role = element.getAttribute("role");
const ariaLabel = element.getAttribute("aria-label");
const customAttr = element.getAttribute("data-custom");
// 设置属性
element.setAttribute("role", "button");
element.setAttribute("aria-label", "Click to expand");
element.setAttribute("data-id", "12345");
// 检查属性是否存在
if (element.hasAttribute("disabled")) {
console.log("Element is disabled");
}
// 移除属性
element.removeAttribute("disabled");getAttribute vs 直接属性访问
两种方式返回的值可能不同:
const link = document.querySelector("a");
// HTML: <a href="/about">About</a>
console.log(link.href); // "https://example.com/about" (完整 URL)
console.log(link.getAttribute("href")); // "/about" (原始值)
const input = document.querySelector('input[type="checkbox"]');
// HTML: <input type="checkbox" checked>
console.log(input.checked); // true (布尔值)
console.log(input.getAttribute("checked")); // "" 或 "checked" (字符串)布尔属性
像 disabled、checked、readonly 这样的布尔属性,直接属性访问返回布尔值:
const checkbox = document.querySelector('input[type="checkbox"]');
// 使用布尔值
checkbox.checked = true;
checkbox.disabled = false;
// ❌ 不要这样(会设置为字符串 "false",但仍被视为 true)
checkbox.setAttribute("checked", false);
// ✅ 正确的移除方式
checkbox.removeAttribute("checked");data-* 自定义属性
HTML5 引入了 data-* 属性来存储自定义数据,可以通过 dataset 访问:
<div
id="user-card"
data-user-id="123"
data-role="admin"
data-last-login="2024-01-15"
data-is-active="true"
></div>const card = document.getElementById("user-card");
// 读取 data 属性
console.log(card.dataset.userId); // "123"
console.log(card.dataset.role); // "admin"
console.log(card.dataset.lastLogin); // "2024-01-15"
console.log(card.dataset.isActive); // "true"
// 设置 data 属性
card.dataset.userId = "456";
card.dataset.newAttr = "value";
// 删除 data 属性
delete card.dataset.role;注意属性名的转换规则:
- HTML:
data-user-id→ JS:dataset.userId - HTML:
data-last-login→ JS:dataset.lastLogin - 连字符命名转换为驼峰命名
attributes 集合
可以遍历元素的所有属性:
const element = document.querySelector(".example");
// 遍历所有属性
for (const attr of element.attributes) {
console.log(`${attr.name} = ${attr.value}`);
}
// 获取属性数量
console.log(element.attributes.length);
// 按索引访问
console.log(element.attributes[0].name);
console.log(element.attributes[0].value);
// 按名称访问
console.log(element.attributes.getNamedItem("class").value);节点操作
除了修改内容和属性,还可以对节点本身进行操作。
移除节点
const element = document.querySelector(".to-remove");
// 现代方法
element.remove();
// 传统方法(兼容旧浏览器)
element.parentNode.removeChild(element);替换节点
const oldElement = document.querySelector(".old");
const newElement = document.createElement("div");
newElement.textContent = "New content";
// 现代方法
oldElement.replaceWith(newElement);
// 传统方法
oldElement.parentNode.replaceChild(newElement, oldElement);移动节点
将节点添加到新位置会自动从原位置移除:
const item = document.querySelector("#item");
const newContainer = document.querySelector("#new-container");
// 移动到新位置
newContainer.appendChild(item);清空元素内容
const container = document.querySelector(".container");
// 方法一:设置 innerHTML
container.innerHTML = "";
// 方法二:设置 textContent
container.textContent = "";
// 方法三:逐个移除子节点
while (container.firstChild) {
container.removeChild(container.firstChild);
}
// 方法四:使用 replaceChildren(现代浏览器)
container.replaceChildren();实际应用示例
实时表单反馈
class FormValidator {
constructor(formId) {
this.form = document.getElementById(formId);
this.init();
}
init() {
const inputs = this.form.querySelectorAll("input, textarea");
inputs.forEach((input) => {
input.addEventListener("input", () => this.validateField(input));
input.addEventListener("blur", () => this.validateField(input));
});
}
validateField(input) {
const errorElement = this.getOrCreateError(input);
const rules = input.dataset.rules?.split("|") || [];
for (const rule of rules) {
const error = this.checkRule(input, rule);
if (error) {
this.showError(input, errorElement, error);
return false;
}
}
this.hideError(input, errorElement);
return true;
}
checkRule(input, rule) {
const value = input.value.trim();
if (rule === "required" && !value) {
return "This field is required";
}
if (rule === "email" && value) {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(value)) {
return "Please enter a valid email address";
}
}
if (rule.startsWith("min:")) {
const min = parseInt(rule.split(":")[1]);
if (value.length < min) {
return `Minimum ${min} characters required`;
}
}
if (rule.startsWith("max:")) {
const max = parseInt(rule.split(":")[1]);
if (value.length > max) {
return `Maximum ${max} characters allowed`;
}
}
return null;
}
getOrCreateError(input) {
let errorElement = input.nextElementSibling;
if (!errorElement || !errorElement.classList.contains("field-error")) {
errorElement = document.createElement("span");
errorElement.className = "field-error";
input.after(errorElement);
}
return errorElement;
}
showError(input, errorElement, message) {
input.classList.add("invalid");
input.classList.remove("valid");
input.setAttribute("aria-invalid", "true");
errorElement.textContent = message;
errorElement.style.display = "block";
}
hideError(input, errorElement) {
input.classList.remove("invalid");
input.classList.add("valid");
input.removeAttribute("aria-invalid");
errorElement.textContent = "";
errorElement.style.display = "none";
}
}
// 使用
// <input data-rules="required|email" type="email" name="email">
const validator = new FormValidator("signup-form");内容编辑器
class SimpleEditor {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.init();
}
init() {
// 创建编辑器结构
this.container.innerHTML = `
<div class="toolbar">
<button data-command="bold" title="Bold"><b>B</b></button>
<button data-command="italic" title="Italic"><i>I</i></button>
<button data-command="underline" title="Underline"><u>U</u></button>
<button data-command="h1" title="Heading 1">H1</button>
<button data-command="h2" title="Heading 2">H2</button>
<button data-command="ul" title="Bullet List">• List</button>
</div>
<div class="editor" contenteditable="true"></div>
`;
this.editor = this.container.querySelector(".editor");
this.toolbar = this.container.querySelector(".toolbar");
this.toolbar.addEventListener("click", (e) => {
const button = e.target.closest("button");
if (button) {
this.executeCommand(button.dataset.command);
}
});
this.editor.addEventListener("input", () => {
this.onContentChange();
});
}
executeCommand(command) {
this.editor.focus();
switch (command) {
case "bold":
document.execCommand("bold", false, null);
break;
case "italic":
document.execCommand("italic", false, null);
break;
case "underline":
document.execCommand("underline", false, null);
break;
case "h1":
document.execCommand("formatBlock", false, "h1");
break;
case "h2":
document.execCommand("formatBlock", false, "h2");
break;
case "ul":
document.execCommand("insertUnorderedList", false, null);
break;
}
}
onContentChange() {
// 自动保存或触发事件
const content = this.getContent();
this.container.dispatchEvent(
new CustomEvent("contentchange", {
detail: { content },
})
);
}
getContent() {
return this.editor.innerHTML;
}
setContent(html) {
this.editor.innerHTML = html;
}
getPlainText() {
return this.editor.textContent;
}
}
// 使用
const editor = new SimpleEditor("editor-container");
editor.container.addEventListener("contentchange", (e) => {
console.log("Content changed:", e.detail.content);
});动态表格编辑
class EditableTable {
constructor(tableId) {
this.table = document.getElementById(tableId);
this.init();
}
init() {
this.table.addEventListener("dblclick", (e) => {
const cell = e.target.closest("td");
if (cell && !cell.classList.contains("editing")) {
this.startEdit(cell);
}
});
this.table.addEventListener("keydown", (e) => {
if (e.key === "Enter" && e.target.tagName === "INPUT") {
e.preventDefault();
this.finishEdit(e.target.closest("td"));
}
if (e.key === "Escape" && e.target.tagName === "INPUT") {
this.cancelEdit(e.target.closest("td"));
}
});
this.table.addEventListener(
"blur",
(e) => {
if (e.target.tagName === "INPUT") {
this.finishEdit(e.target.closest("td"));
}
},
true
);
}
startEdit(cell) {
const currentValue = cell.textContent;
cell.dataset.originalValue = currentValue;
cell.classList.add("editing");
const input = document.createElement("input");
input.type = "text";
input.value = currentValue;
input.className = "cell-editor";
cell.textContent = "";
cell.appendChild(input);
input.focus();
input.select();
}
finishEdit(cell) {
if (!cell || !cell.classList.contains("editing")) return;
const input = cell.querySelector("input");
const newValue = input.value;
const oldValue = cell.dataset.originalValue;
cell.classList.remove("editing");
cell.textContent = newValue;
delete cell.dataset.originalValue;
if (newValue !== oldValue) {
this.onCellChange(cell, oldValue, newValue);
}
}
cancelEdit(cell) {
if (!cell || !cell.classList.contains("editing")) return;
cell.classList.remove("editing");
cell.textContent = cell.dataset.originalValue;
delete cell.dataset.originalValue;
}
onCellChange(cell, oldValue, newValue) {
const row = cell.parentElement;
const rowIndex = row.rowIndex;
const cellIndex = cell.cellIndex;
console.log(
`Cell [${rowIndex}, ${cellIndex}] changed from "${oldValue}" to "${newValue}"`
);
// 触发自定义事件
this.table.dispatchEvent(
new CustomEvent("cellchange", {
detail: { cell, rowIndex, cellIndex, oldValue, newValue },
})
);
}
addRow(data) {
const row = this.table.insertRow();
data.forEach((value) => {
const cell = row.insertCell();
cell.textContent = value;
});
return row;
}
deleteRow(rowIndex) {
this.table.deleteRow(rowIndex);
}
getData() {
const data = [];
const rows = this.table.querySelectorAll("tbody tr");
rows.forEach((row) => {
const rowData = [];
row.querySelectorAll("td").forEach((cell) => {
rowData.push(cell.textContent);
});
data.push(rowData);
});
return data;
}
}
// 使用
const table = new EditableTable("data-table");
table.table.addEventListener("cellchange", (e) => {
console.log("Cell changed:", e.detail);
});动态属性切换
class ToggleManager {
constructor() {
this.init();
}
init() {
document.addEventListener("click", (e) => {
// 处理折叠面板
if (e.target.matches('[data-toggle="collapse"]')) {
this.toggleCollapse(e.target);
}
// 处理选项卡
if (e.target.matches('[data-toggle="tab"]')) {
this.toggleTab(e.target);
}
// 处理模态框
if (e.target.matches('[data-toggle="modal"]')) {
this.toggleModal(e.target);
}
});
}
toggleCollapse(trigger) {
const targetId = trigger.dataset.target;
const target = document.querySelector(targetId);
if (!target) return;
const isExpanded = trigger.getAttribute("aria-expanded") === "true";
trigger.setAttribute("aria-expanded", !isExpanded);
target.setAttribute("aria-hidden", isExpanded);
target.classList.toggle("collapsed", isExpanded);
}
toggleTab(trigger) {
const targetId = trigger.dataset.target;
const tabContainer = trigger.closest(".tabs");
if (!tabContainer) return;
// 移除所有活动状态
tabContainer.querySelectorAll('[data-toggle="tab"]').forEach((tab) => {
tab.classList.remove("active");
tab.setAttribute("aria-selected", "false");
});
tabContainer.querySelectorAll(".tab-panel").forEach((panel) => {
panel.classList.remove("active");
panel.setAttribute("aria-hidden", "true");
});
// 激活当前选项
trigger.classList.add("active");
trigger.setAttribute("aria-selected", "true");
const panel = document.querySelector(targetId);
if (panel) {
panel.classList.add("active");
panel.setAttribute("aria-hidden", "false");
}
}
toggleModal(trigger) {
const targetId = trigger.dataset.target;
const modal = document.querySelector(targetId);
if (!modal) return;
const isOpen = modal.classList.contains("open");
if (isOpen) {
modal.classList.remove("open");
modal.setAttribute("aria-hidden", "true");
document.body.classList.remove("modal-open");
} else {
modal.classList.add("open");
modal.setAttribute("aria-hidden", "false");
document.body.classList.add("modal-open");
// 聚焦到模态框
modal.focus();
}
}
}
// 初始化
new ToggleManager();性能优化建议
避免频繁读写 DOM
// ❌ 不好:每次循环都读写 DOM
for (let i = 0; i < 100; i++) {
element.style.left = element.offsetLeft + 1 + "px";
}
// ✅ 好:读取一次,最后写入
let left = element.offsetLeft;
for (let i = 0; i < 100; i++) {
left += 1;
}
element.style.left = left + "px";使用 textContent 而非 innerHTML
// ❌ 较慢且有安全风险
element.innerHTML = userInput;
// ✅ 更快且安全
element.textContent = userInput;批量修改属性
// ❌ 触发多次重排
element.style.width = "100px";
element.style.height = "100px";
element.style.padding = "10px";
element.style.margin = "20px";
// ✅ 使用 cssText 一次性设置
element.style.cssText =
"width: 100px; height: 100px; padding: 10px; margin: 20px;";
// ✅ 或者使用 CSS 类
element.className = "box-style";总结
修改 DOM 元素是 Web 开发的日常操作。掌握正确的方法可以让代码更安全、更高效:
| 需求 | 推荐方法 |
|---|---|
| 设置纯文本 | textContent |
| 设置 HTML | innerHTML(需转义用户输入) |
| 读写标准属性 | 直接属性访问(如 element.id) |
| 读写自定义属性 | dataset |
| 操作任意属性 | getAttribute / setAttribute |
| 移除元素 | element.remove() |
| 替换元素 | element.replaceWith(newElement) |
最佳实践:
- 对用户输入使用
textContent而非innerHTML - 批量操作时尽量减少 DOM 访问次数
- 利用
dataset存储自定义数据 - 注意属性名在 HTML 和 JavaScript 中的差异