属性与样式操作
页面交互离不开两件事:读写元素的属性、控制元素的样式。本文先厘清最容易混淆的 attribute 与 property 概念,再系统讲解属性 API、class 操作、内联样式与计算样式,最后给出性能建议。
一、attribute 与 property:HTML 属性 vs DOM 属性
这是理解属性操作的核心。attribute 是 HTML 源码里写在标签上的属性,property 是浏览器解析后挂在元素对象上的 JavaScript 属性:
| 对比项 | attribute(属性) | property(特性) |
|---|---|---|
| 来源 | HTML 源码 / setAttribute | DOM 对象自动生成的属性 |
| 类型 | 都是字符串 | 有真实类型(布尔、数字、对象) |
| 访问方式 | getAttribute / setAttribute | 点号直接访问 el.id |
| 同步关系 | 两者多数情况下联动 | 少数属性不同步(见下) |
几个典型差异:
<input id="name" type="text" value="默认值">
<script>
const input = document.getElementById("name");
// 用户输入后
input.value = "新值"; // property 改变
input.getAttribute("value"); // 仍是 "默认值"——attribute 没变!
input.value; // "新值"——property 变了
// 反例:id 会同步
input.id = "newId";
input.getAttribute("id"); // "newId",attribute 跟随 property
</script>规律:多数标准属性"用户读写的 property 是影子",attribute 是初始值。value、checked、selected 这类"输入状态"属性,两者不同步;而 id、class、title 等始终同步。布尔属性(如 disabled、checked)在 attribute 侧是"存在即 true,不存在即 false":
const btn = document.querySelector("button");
btn.setAttribute("disabled", ""); // 只要存在就算禁用
btn.disabled; // true(property 有布尔语义)二、attribute 的读写与判断
const link = document.querySelector("a");
link.getAttribute("href"); // 读取,返回字符串或 null
link.setAttribute("href", "https://example.com"); // 写入
link.removeAttribute("href"); // 删除
link.hasAttribute("href"); // 是否存在,返回布尔| 方法 | 作用 | 注意 |
|---|---|---|
getAttribute(name) | 读取属性值 | 不存在返回 null |
setAttribute(name, value) | 写入属性 | value 会被转成字符串 |
removeAttribute(name) | 删除属性 | 删除后使用默认行为 |
hasAttribute(name) | 判断属性是否存在 | 常用作特征检测 |
attributes 属性还能拿到所有属性的实时集合(NamedNodeMap),遍历自定义配置时很有用:
const el = document.querySelector("img");
el.attributes; // NamedNodeMap
for (const attr of el.attributes) {
console.log(attr.name, attr.value);
}三、dataset:自定义数据属性
以 data- 开头的属性专用于在元素上携带业务数据,JS 通过 dataset 访问,命名自动转驼峰:
<div id="card" data-user-id="1001" data-level="high" data-role="admin">
卡片内容
</div>const card = document.querySelector("#card");
card.dataset.userId; // "1001"(data-user-id → userId)
card.dataset.level; // "high"
card.dataset.role; // "admin"
// 写入
card.dataset.level = "low";
card.getAttribute("data-level"); // "low",与 attribute 联动
// 删除
delete card.dataset.role;| dataset 命名规则 | 示例 |
|---|---|
data-user-id → dataset.userId | 连字符转驼峰 |
data-role → dataset.role | 单段保持原名 |
写入 dataset.userId → attribute 变成 data-user-id | 自动加 data- 前缀 |
注意:dataset 只能访问 data-* 属性,值都是字符串;布尔场景请用 "true" / "false" 字符串后自行转换。
四、className 与 classList
操作 class 属性有两套方式。直接改字符串太脆弱,classList 提供了一组语义化方法:
const box = document.querySelector(".box");
// 传统方式
box.className = "box active"; // 覆盖全部 class
box.className += " dark"; // 追加(易出错)
// 现代方式
box.classList.add("active"); // 加一个或多个
box.classList.remove("active"); // 移除
box.classList.toggle("active"); // 有则删、无则加
box.classList.contains("active"); // 判断,返回布尔
box.classList.replace("box", "card"); // 替换某个类名| 方法 | 作用 | 返回 |
|---|---|---|
add(...names) | 追加类名(重复自动去重) | 无 |
remove(...names) | 移除类名 | 无 |
toggle(name, force?) | 切换;force 可强制加/删 | 布尔(切换后的状态) |
contains(name) | 判断是否存在 | 布尔 |
replace(old, new) | 替换类名 | 布尔(是否替换成功) |
const btn = document.querySelector("button");
btn.addEventListener("click", () => {
btn.classList.toggle("pressed"); // 切换按下状态
const isPressed = btn.classList.contains("pressed");
btn.classList.toggle("loading", isPressed); // 联动开关
});className 是一次性读取/写入整个 class 字符串,适合整体重置;classList 适合细粒度增删。样式逻辑优先用 classList + CSS 类,比直接改 style 更好维护。
五、内联样式 style 属性
element.style 只反映内联样式(style 属性里写的),CSS 规则和计算值不在此列。属性名用驼峰命名:
const box = document.querySelector(".box");
box.style.color = "red"; // 驼峰命名
box.style.backgroundColor = "#fff"; // background-color → backgroundColor
box.style.fontSize = "16px"; // 必须带单位
box.style.width = "100px";
// 读取
box.style.color; // "red"(只读内联,读不到样式表里的值)| CSS 写法 | JS 写法 | 说明 |
|---|---|---|
background-color | backgroundColor | 连字符转驼峰 |
-webkit-transform | webkitTransform | 前缀保留大写 |
float | cssFloat | float 是保留字,需改名 |
font-size | fontSize | 数值属性必须带单位 |
cssText 可以一次性读写整段内联样式:
box.style.cssText = "color: red; font-size: 16px; padding: 8px;";
// 注意:cssText 会覆盖该元素全部内联样式批量设置多个样式更推荐 Object.assign 配合 style:
Object.assign(box.style, {
color: "blue",
margin: "10px auto",
borderRadius: "8px",
});六、getComputedStyle:读取计算样式
想拿到"样式表 + 继承 + 默认值"综合计算后的最终值,用 getComputedStyle:
const box = document.querySelector(".box");
const styles = getComputedStyle(box);
styles.color; // 计算后的颜色,如 "rgb(255, 0, 0)"
styles.backgroundColor; // "rgb(255, 255, 255)"
styles.getPropertyValue("font-size"); // 读取 CSS 属性原文| 对比项 | el.style | getComputedStyle(el) |
|---|---|---|
| 内容 | 仅内联样式 | 全部计算后的最终值 |
| 单位 | 原样保留(px) | 多为绝对值(px) |
| 颜色 | 原样 | 归一化成 rgb()/rgba() |
| 可写性 | 可写 | 只读 |
| 是否触发重排 | 写入时 | 首次读取会强制计算布局 |
两点注意:
- 读取
getComputedStyle会强制刷新布局(reflow),循环里避免反复调用,先缓存结果。 - 页面刚加载、动画进行中读取,拿到的可能是中间值;需要"样式就绪"时先等
requestAnimationFrame。
七、CSS 变量读写
CSS 自定义属性(变量)同样可以通过 style 与 getComputedStyle 读写:
:root {
--primary-color: #4a90d9;
--spacing: 16px;
}const root = document.documentElement;
// 读取(计算样式)
const primary = getComputedStyle(root).getPropertyValue("--primary-color").trim();
// "#4a90d9"
// 写入
root.style.setProperty("--spacing", "24px");
root.style.setProperty("--primary-color", "#e74c3c");
// 删除变量
root.style.removeProperty("--primary-color");| 方法 | 作用 |
|---|---|
style.getPropertyValue(name) | 读取变量(需配合 getComputedStyle 读计算值) |
style.setProperty(name, value) | 写入/覆盖变量 |
style.removeProperty(name) | 删除变量 |
利用 CSS 变量可以做"一键换肤"——改根节点的几个变量,全站颜色随之变化,避免逐个元素改 style。
八、样式操作的性能注意
- 避免频繁逐条改 style:连续
el.style.left = ...会反复触发重排;改为合并类名或用cssText一次写入。 - 读写分离:在同一帧内先批量读布局值、再批量写,避免"读-写-读-写"造成多次强制布局。
- 触发重排的属性:
offsetWidth、offsetHeight、getBoundingClientRect()、getComputedStyle等读取会强制计算布局;scrollTop、clientWidth同理。 - 触发重绘的属性:
color、background-color、visibility、box-shadow等通常只触发重绘,开销小于重排。 - 动画优先用
transform与opacity,它们在合成器上执行,不触发重排重绘。 - 一次循环中统一处理:
// 不好的写法:循环内读写交错
for (const item of items) {
item.style.width = item.offsetWidth + 10 + "px"; // 每轮都强制布局
}
// 更好的写法:先读完,再统一写
const widths = items.map((item) => item.offsetWidth);
items.forEach((item, i) => {
item.style.width = widths[i] + 10 + "px";
});九、综合示例:选项卡样式切换
把 classList、dataset 与样式 API 组合成一个可用的选项卡:
<div class="tabs">
<button data-tab="a" class="tab active">标签 A</button>
<button data-tab="b" class="tab">标签 B</button>
<button data-tab="c" class="tab">标签 C</button>
</div>const tabs = document.querySelectorAll(".tab");
const styleRoot = document.documentElement;
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
// 1. 用 classList 切换激活态
tabs.forEach((t) => t.classList.toggle("active", t === tab));
// 2. 用 dataset 取业务数据
const name = tab.dataset.tab;
const color = name === "a" ? "#4a90d9" : name === "b" ? "#e67e22" : "#27ae60";
// 3. 写入 CSS 变量实现主题联动
styleRoot.style.setProperty("--tab-active", color);
});
});属性与样式是 DOM 编程的两大抓手:属性层面分清 attribute/property 的同步规则,样式层面记住"classList 优先、样式表优先、批量与读写分离",页面交互的代码质量和性能都会有明显提升。