Skip to content

修改 DOM 元素:更新页面内容与属性

内容修改的多种方式

DOM 元素的内容可以通过多种方式修改,每种方式有不同的特点和适用场景。选择正确的方法不仅关系到代码的简洁性,还涉及安全性和性能。

textContent

textContent 用于获取或设置元素的纯文本内容,是最安全的内容修改方式:

javascript
const paragraph = document.querySelector("#intro");

// 获取文本
console.log(paragraph.textContent);

// 设置文本
paragraph.textContent = "This is the new content";

textContent 会获取元素及其所有后代的文本内容:

html
<div id="container">
  <p>First paragraph</p>
  <p>Second paragraph</p>
</div>
javascript
const container = document.getElementById("container");
console.log(container.textContent);
// 输出:
// "
//   First paragraph
//   Second paragraph
// "

设置 textContent 时,所有子元素都会被移除,只留下纯文本:

javascript
container.textContent = "All HTML is gone";
// <div id="container">All HTML is gone</div>

textContent 的安全性

textContent 会自动转义 HTML 字符,防止 XSS 攻击:

javascript
const userInput = '<script>alert("Hacked!")</script>';

// 安全:脚本标签会显示为文本
element.textContent = userInput;
// 显示: <script>alert("Hacked!")</script>

innerText

innerTexttextContent 类似,但有几个关键区别:

javascript
const element = document.querySelector(".content");

// 获取文本
console.log(element.innerText);

// 设置文本
element.innerText = "New text";

textContent vs innerText:

特性textContentinnerText
返回隐藏元素的文本
受 CSS 影响
触发重排
保留换行保留原始格式按渲染后的显示
性能更快较慢
html
<div id="example">
  Visible text
  <span style="display: none;">Hidden text</span>
</div>
javascript
const div = document.getElementById("example");

console.log(div.textContent); // "Visible text Hidden text"
console.log(div.innerText); // "Visible text"

一般情况下,推荐使用 textContent,除非你需要获取"用户可见"的文本内容。

innerHTML

innerHTML 可以获取或设置元素的 HTML 内容:

javascript
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 结构:

javascript
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 存在严重的安全风险:

javascript
// ❌ 危险: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 包含元素自身及其内容:

javascript
const paragraph = document.querySelector("p");

console.log(paragraph.outerHTML);
// "<p class="intro">Hello World</p>"

// 替换整个元素
paragraph.outerHTML = '<div class="intro">Hello World</div>';

设置 outerHTML 后,原来的变量引用会失效:

javascript
const element = document.querySelector(".old");
element.outerHTML = '<div class="new">Replaced</div>';

console.log(element.className); // 仍然是 "old"
// element 仍然指向被移除的旧元素

属性操作

DOM 元素有各种属性,可以通过多种方式读取和修改。

直接属性访问

许多 HTML 属性可以直接作为 DOM 对象的属性访问:

javascript
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 属性
classclassName 或 classList
forhtmlFor
readonlyreadOnly
maxlengthmaxLength
tabindextabIndex
javascript
const label = document.querySelector("label");
console.log(label.htmlFor); // 对应 HTML 的 for 属性

const input = document.querySelector("input");
console.log(input.readOnly); // 对应 HTML 的 readonly 属性

getAttribute 和 setAttribute

对于非标准属性或需要获取原始属性值时,使用这些方法:

javascript
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 直接属性访问

两种方式返回的值可能不同:

javascript
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" (字符串)

布尔属性

disabledcheckedreadonly 这样的布尔属性,直接属性访问返回布尔值:

javascript
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 访问:

html
<div
  id="user-card"
  data-user-id="123"
  data-role="admin"
  data-last-login="2024-01-15"
  data-is-active="true"
></div>
javascript
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 集合

可以遍历元素的所有属性:

javascript
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);

节点操作

除了修改内容和属性,还可以对节点本身进行操作。

移除节点

javascript
const element = document.querySelector(".to-remove");

// 现代方法
element.remove();

// 传统方法(兼容旧浏览器)
element.parentNode.removeChild(element);

替换节点

javascript
const oldElement = document.querySelector(".old");
const newElement = document.createElement("div");
newElement.textContent = "New content";

// 现代方法
oldElement.replaceWith(newElement);

// 传统方法
oldElement.parentNode.replaceChild(newElement, oldElement);

移动节点

将节点添加到新位置会自动从原位置移除:

javascript
const item = document.querySelector("#item");
const newContainer = document.querySelector("#new-container");

// 移动到新位置
newContainer.appendChild(item);

清空元素内容

javascript
const container = document.querySelector(".container");

// 方法一:设置 innerHTML
container.innerHTML = "";

// 方法二:设置 textContent
container.textContent = "";

// 方法三:逐个移除子节点
while (container.firstChild) {
  container.removeChild(container.firstChild);
}

// 方法四:使用 replaceChildren(现代浏览器)
container.replaceChildren();

实际应用示例

实时表单反馈

javascript
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");

内容编辑器

javascript
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);
});

动态表格编辑

javascript
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);
});

动态属性切换

javascript
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

javascript
// ❌ 不好:每次循环都读写 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

javascript
// ❌ 较慢且有安全风险
element.innerHTML = userInput;

// ✅ 更快且安全
element.textContent = userInput;

批量修改属性

javascript
// ❌ 触发多次重排
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
设置 HTMLinnerHTML(需转义用户输入)
读写标准属性直接属性访问(如 element.id
读写自定义属性dataset
操作任意属性getAttribute / setAttribute
移除元素element.remove()
替换元素element.replaceWith(newElement)

最佳实践:

  1. 对用户输入使用 textContent 而非 innerHTML
  2. 批量操作时尽量减少 DOM 访问次数
  3. 利用 dataset 存储自定义数据
  4. 注意属性名在 HTML 和 JavaScript 中的差异