样式与类操作:动态控制元素外观
样式操作的两种思路
在 JavaScript 中控制元素样式有两种主要方式:操作 CSS 类和直接修改内联样式。
操作 CSS 类更符合"关注点分离"的原则——样式定义在 CSS 中,JavaScript 只负责添加或移除类名。这种方式便于维护,也更容易实现复杂的样式变化。
直接修改样式适用于需要动态计算的场景,比如根据鼠标位置设置元素坐标,或者实现一些简单的即时效果。
两种方式各有优劣,实际开发中经常结合使用。
classList:类名操作
classList 是操作元素类名的现代 API,提供了直观的方法来添加、移除和切换类名。
基本操作
const element = document.querySelector(".box");
// 添加类
element.classList.add("active");
element.classList.add("highlight", "visible"); // 同时添加多个
// 移除类
element.classList.remove("hidden");
element.classList.remove("old", "deprecated"); // 同时移除多个
// 切换类(有则移除,无则添加)
element.classList.toggle("expanded");
// 强制添加或移除
element.classList.toggle("visible", true); // 强制添加
element.classList.toggle("visible", false); // 强制移除
// 检查是否包含某个类
if (element.classList.contains("active")) {
console.log("Element is active");
}
// 替换类
element.classList.replace("old-class", "new-class");遍历类名
const element = document.querySelector(".multi-class");
// 获取类名数量
console.log(element.classList.length);
// 通过索引访问
console.log(element.classList[0]);
console.log(element.classList.item(0)); // 相同效果
// 遍历所有类名
for (const className of element.classList) {
console.log(className);
}
// 转换为数组
const classArray = [...element.classList];
const classArray2 = Array.from(element.classList);className 属性
在 classList 出现之前,使用 className 操作类名:
const element = document.querySelector(".box");
// 获取完整的类名字符串
console.log(element.className); // "box active highlight"
// 设置类名(会覆盖原有的所有类)
element.className = "new-class another-class";
// 追加类名(需要手动处理空格)
element.className += " extra-class";className 的问题在于它返回和接受的是完整的类名字符串,添加或移除单个类需要进行字符串操作,容易出错。现代开发中应该优先使用 classList。
实际应用:切换主题
class ThemeManager {
constructor() {
this.body = document.body;
this.themes = ["light", "dark", "sepia"];
this.currentIndex = 0;
// 从本地存储恢复主题
const savedTheme = localStorage.getItem("theme");
if (savedTheme && this.themes.includes(savedTheme)) {
this.currentIndex = this.themes.indexOf(savedTheme);
}
this.applyTheme();
}
applyTheme() {
// 移除所有主题类
this.themes.forEach((theme) => {
this.body.classList.remove(`theme-${theme}`);
});
// 添加当前主题类
const currentTheme = this.themes[this.currentIndex];
this.body.classList.add(`theme-${currentTheme}`);
// 保存到本地存储
localStorage.setItem("theme", currentTheme);
}
setTheme(themeName) {
const index = this.themes.indexOf(themeName);
if (index !== -1) {
this.currentIndex = index;
this.applyTheme();
}
}
cycleTheme() {
this.currentIndex = (this.currentIndex + 1) % this.themes.length;
this.applyTheme();
}
getCurrentTheme() {
return this.themes[this.currentIndex];
}
}
// 使用
const themeManager = new ThemeManager();
document.querySelector("#theme-toggle").addEventListener("click", () => {
themeManager.cycleTheme();
});style 属性:内联样式
每个元素都有一个 style 属性,用于读写内联样式。
基本操作
const element = document.querySelector(".box");
// 设置单个样式
element.style.color = "red";
element.style.backgroundColor = "white";
element.style.fontSize = "16px";
element.style.marginTop = "20px";
// 读取样式
console.log(element.style.color); // "red"
// 清除样式(设为空字符串)
element.style.color = "";属性名转换
CSS 属性名使用连字符(如 background-color),而 JavaScript 使用驼峰命名(如 backgroundColor):
| CSS 属性 | JavaScript 属性 |
|---|---|
| background-color | backgroundColor |
| font-size | fontSize |
| border-radius | borderRadius |
| margin-top | marginTop |
| z-index | zIndex |
| -webkit-transform | webkitTransform |
// CSS: background-color → JS: backgroundColor
element.style.backgroundColor = "blue";
// CSS: border-top-left-radius → JS: borderTopLeftRadius
element.style.borderTopLeftRadius = "10px";
// 带厂商前缀的属性
element.style.webkitTransform = "rotate(45deg)";
element.style.MozTransform = "rotate(45deg)"; // 注意 Moz 的大小写cssText:批量设置样式
使用 cssText 可以一次性设置多个样式:
const element = document.querySelector(".box");
// 设置多个样式(会覆盖原有的 style 属性)
element.style.cssText = "color: red; background: white; font-size: 16px;";
// 追加样式
element.style.cssText += "padding: 10px; margin: 20px;";注意 cssText 使用 CSS 语法(连字符命名),而且会覆盖之前的所有内联样式。
使用 setProperty 和 getPropertyValue
这些方法是 style 对象提供的标准化接口:
const element = document.querySelector(".box");
// 设置样式(使用 CSS 属性名)
element.style.setProperty("background-color", "blue");
element.style.setProperty("--custom-color", "#ff0000"); // CSS 变量
// 获取样式
console.log(element.style.getPropertyValue("background-color"));
// 移除样式
element.style.removeProperty("background-color");
// 设置优先级
element.style.setProperty("color", "red", "important");style 属性的局限性
element.style 只能读取和设置内联样式,无法获取通过 CSS 文件或 <style> 标签定义的样式:
<style>
.box {
color: blue;
}
</style>
<div class="box" style="background: white;">Content</div>const box = document.querySelector(".box");
console.log(box.style.color); // ""(空字符串,不是 "blue")
console.log(box.style.background); // "white"(内联样式可以读取)getComputedStyle:获取计算样式
要获取元素的最终渲染样式(包括继承的样式、CSS 规则应用后的结果),需要使用 getComputedStyle:
const element = document.querySelector(".box");
const styles = getComputedStyle(element);
// 读取样式(使用 CSS 属性名或驼峰命名)
console.log(styles.color); // "rgb(255, 0, 0)"
console.log(styles.getPropertyValue("color")); // "rgb(255, 0, 0)"
console.log(styles.fontSize); // "16px"
console.log(styles.backgroundColor); // "rgb(255, 255, 255)"几点需要注意:
- 返回的是只读对象,不能用于设置样式
- 值是计算后的结果,单位会被转换(如
em转为px) - 颜色会被转换为 rgb/rgba 格式
- 简写属性可能不可用,需要使用具体属性
const styles = getComputedStyle(element);
// ❌ 简写属性可能不工作
console.log(styles.margin); // 可能是空字符串或分开的值
// ✅ 使用具体属性
console.log(styles.marginTop);
console.log(styles.marginRight);
console.log(styles.marginBottom);
console.log(styles.marginLeft);获取伪元素的样式
getComputedStyle 可以获取伪元素的样式:
const element = document.querySelector(".box");
// 获取 ::before 伪元素的样式
const beforeStyles = getComputedStyle(element, "::before");
console.log(beforeStyles.content);
// 获取 ::after 伪元素的样式
const afterStyles = getComputedStyle(element, "::after");
console.log(afterStyles.color);计算实际尺寸
结合 getComputedStyle 和元素的几何属性获取准确的尺寸信息:
const element = document.querySelector(".box");
const styles = getComputedStyle(element);
// 方法一:使用 getComputedStyle
const width = parseFloat(styles.width);
const height = parseFloat(styles.height);
const paddingLeft = parseFloat(styles.paddingLeft);
const paddingRight = parseFloat(styles.paddingRight);
console.log(`Content width: ${width}px`);
console.log(`Padding: ${paddingLeft}px + ${paddingRight}px`);
// 方法二:使用元素属性(通常更方便)
console.log(element.offsetWidth); // 包含边框和内边距
console.log(element.clientWidth); // 包含内边距,不含边框
console.log(element.scrollWidth); // 包含溢出内容
// getBoundingClientRect 提供最完整的尺寸信息
const rect = element.getBoundingClientRect();
console.log(rect.width); // 元素宽度(包含边框)
console.log(rect.height); // 元素高度(包含边框)
console.log(rect.top); // 距视口顶部的距离
console.log(rect.left); // 距视口左侧的距离CSS 变量操作
CSS 自定义属性(CSS 变量)可以通过 JavaScript 动态修改:
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--font-size-base: 16px;
--spacing-unit: 8px;
}
.box {
background-color: var(--primary-color);
font-size: var(--font-size-base);
padding: calc(var(--spacing-unit) * 2);
}// 获取根元素的 CSS 变量
const root = document.documentElement;
const styles = getComputedStyle(root);
const primaryColor = styles.getPropertyValue("--primary-color").trim();
console.log(primaryColor); // "#007bff"
// 设置 CSS 变量
root.style.setProperty("--primary-color", "#ff0000");
// 在特定元素上设置(会覆盖继承的值)
const element = document.querySelector(".box");
element.style.setProperty("--primary-color", "#00ff00");
// 移除 CSS 变量
root.style.removeProperty("--primary-color");实际应用:动态主题配色
class ColorSchemeManager {
constructor() {
this.root = document.documentElement;
this.defaultScheme = {
"--primary-color": "#007bff",
"--primary-light": "#4dabf7",
"--primary-dark": "#0056b3",
"--background": "#ffffff",
"--surface": "#f8f9fa",
"--text-primary": "#212529",
"--text-secondary": "#6c757d",
};
}
setScheme(scheme) {
Object.entries(scheme).forEach(([property, value]) => {
this.root.style.setProperty(property, value);
});
}
setColor(property, value) {
this.root.style.setProperty(property, value);
}
getColor(property) {
return getComputedStyle(this.root).getPropertyValue(property).trim();
}
reset() {
this.setScheme(this.defaultScheme);
}
// 从用户选择的颜色生成配色方案
generateFromPrimary(hexColor) {
const rgb = this.hexToRgb(hexColor);
this.setScheme({
"--primary-color": hexColor,
"--primary-light": this.lighten(rgb, 20),
"--primary-dark": this.darken(rgb, 20),
...this.defaultScheme,
});
}
hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
}
rgbToHex({ r, g, b }) {
return (
"#" +
[r, g, b]
.map((x) => {
const hex = Math.round(x).toString(16);
return hex.length === 1 ? "0" + hex : hex;
})
.join("")
);
}
lighten({ r, g, b }, percent) {
return this.rgbToHex({
r: Math.min(255, r + (255 - r) * (percent / 100)),
g: Math.min(255, g + (255 - g) * (percent / 100)),
b: Math.min(255, b + (255 - b) * (percent / 100)),
});
}
darken({ r, g, b }, percent) {
return this.rgbToHex({
r: Math.max(0, r * (1 - percent / 100)),
g: Math.max(0, g * (1 - percent / 100)),
b: Math.max(0, b * (1 - percent / 100)),
});
}
}
// 使用
const colorManager = new ColorSchemeManager();
document.querySelector("#color-picker").addEventListener("input", (e) => {
colorManager.generateFromPrimary(e.target.value);
});动画与过渡
通过 JavaScript 可以触发 CSS 动画和过渡效果。
触发过渡
const element = document.querySelector(".box");
// CSS 中定义过渡
// .box { transition: transform 0.3s ease, opacity 0.3s ease; }
// JavaScript 改变样式会触发过渡
element.style.transform = "translateX(100px)";
element.style.opacity = "0.5";
// 监听过渡结束
element.addEventListener("transitionend", (e) => {
console.log(`Transition ended: ${e.propertyName}`);
});强制触发动画
有时需要重新触发动画,可以通过移除并重新添加类来实现:
function replayAnimation(element, animationClass) {
element.classList.remove(animationClass);
// 强制重排以使动画重新开始
void element.offsetWidth; // 读取 offsetWidth 触发重排
element.classList.add(animationClass);
}
// 使用
const button = document.querySelector(".animated-button");
button.addEventListener("click", () => {
replayAnimation(button, "shake");
});使用 Web Animations API
现代浏览器支持更强大的 Web Animations API:
const element = document.querySelector(".box");
// 创建动画
const animation = element.animate(
[
{ transform: "translateY(0)", opacity: 1 },
{ transform: "translateY(-50px)", opacity: 0.5, offset: 0.5 },
{ transform: "translateY(0)", opacity: 1 },
],
{
duration: 1000,
easing: "ease-in-out",
iterations: Infinity,
}
);
// 控制动画
animation.pause();
animation.play();
animation.reverse();
animation.cancel();
// 调整播放速率
animation.playbackRate = 2; // 双倍速
// 监听事件
animation.onfinish = () => console.log("Animation finished");实际应用示例
交互式卡片效果
class InteractiveCard {
constructor(element) {
this.card = element;
this.shine = this.createShineElement();
this.card.appendChild(this.shine);
this.bindEvents();
}
createShineElement() {
const shine = document.createElement("div");
shine.className = "card-shine";
shine.style.cssText = `
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
135deg,
rgba(255,255,255,0) 0%,
rgba(255,255,255,0.1) 50%,
rgba(255,255,255,0) 100%
);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
`;
return shine;
}
bindEvents() {
this.card.addEventListener("mouseenter", () => this.onEnter());
this.card.addEventListener("mouseleave", () => this.onLeave());
this.card.addEventListener("mousemove", (e) => this.onMove(e));
}
onEnter() {
this.shine.style.opacity = "1";
this.card.style.transition = "transform 0.1s ease-out";
}
onLeave() {
this.shine.style.opacity = "0";
this.card.style.transform = "perspective(1000px) rotateX(0) rotateY(0)";
this.card.style.transition = "transform 0.5s ease-out";
}
onMove(e) {
const rect = this.card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const rotateX = (y - centerY) / 10;
const rotateY = (centerX - x) / 10;
this.card.style.transform = `
perspective(1000px)
rotateX(${rotateX}deg)
rotateY(${rotateY}deg)
scale3d(1.02, 1.02, 1.02)
`;
// 更新光泽位置
const percentX = (x / rect.width) * 100;
const percentY = (y / rect.height) * 100;
this.shine.style.background = `
radial-gradient(
circle at ${percentX}% ${percentY}%,
rgba(255,255,255,0.3) 0%,
rgba(255,255,255,0) 60%
)
`;
}
}
// 使用
document.querySelectorAll(".interactive-card").forEach((card) => {
new InteractiveCard(card);
});滚动进度指示器
class ScrollProgress {
constructor() {
this.progressBar = this.createProgressBar();
document.body.prepend(this.progressBar);
window.addEventListener("scroll", () => this.updateProgress());
window.addEventListener("resize", () => this.updateProgress());
this.updateProgress();
}
createProgressBar() {
const bar = document.createElement("div");
bar.className = "scroll-progress";
bar.style.cssText = `
position: fixed;
top: 0;
left: 0;
height: 4px;
background: linear-gradient(90deg, #007bff, #00bcd4);
z-index: 9999;
transition: width 0.1s ease-out;
box-shadow: 0 0 10px rgba(0, 123, 255, 0.5);
`;
return bar;
}
updateProgress() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const docHeight =
document.documentElement.scrollHeight - window.innerHeight;
const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
this.progressBar.style.width = `${progress}%`;
}
}
// 使用
new ScrollProgress();响应式样式管理
class ResponsiveStyleManager {
constructor() {
this.breakpoints = {
mobile: 576,
tablet: 768,
desktop: 992,
wide: 1200,
};
this.currentBreakpoint = null;
this.callbacks = [];
this.check();
window.addEventListener("resize", () => this.check());
}
check() {
const width = window.innerWidth;
let newBreakpoint;
if (width < this.breakpoints.mobile) {
newBreakpoint = "xs";
} else if (width < this.breakpoints.tablet) {
newBreakpoint = "mobile";
} else if (width < this.breakpoints.desktop) {
newBreakpoint = "tablet";
} else if (width < this.breakpoints.wide) {
newBreakpoint = "desktop";
} else {
newBreakpoint = "wide";
}
if (newBreakpoint !== this.currentBreakpoint) {
const oldBreakpoint = this.currentBreakpoint;
this.currentBreakpoint = newBreakpoint;
// 更新 body 类
document.body.classList.remove(
"bp-xs",
"bp-mobile",
"bp-tablet",
"bp-desktop",
"bp-wide"
);
document.body.classList.add(`bp-${newBreakpoint}`);
// 更新 CSS 变量
document.documentElement.style.setProperty(
"--current-breakpoint",
`"${newBreakpoint}"`
);
// 触发回调
this.callbacks.forEach((cb) => cb(newBreakpoint, oldBreakpoint));
}
}
onChange(callback) {
this.callbacks.push(callback);
// 立即调用一次
callback(this.currentBreakpoint, null);
// 返回取消订阅函数
return () => {
const index = this.callbacks.indexOf(callback);
if (index > -1) this.callbacks.splice(index, 1);
};
}
is(breakpoint) {
return this.currentBreakpoint === breakpoint;
}
isAtLeast(breakpoint) {
const order = ["xs", "mobile", "tablet", "desktop", "wide"];
return order.indexOf(this.currentBreakpoint) >= order.indexOf(breakpoint);
}
}
// 使用
const responsive = new ResponsiveStyleManager();
responsive.onChange((current, previous) => {
console.log(`Breakpoint changed from ${previous} to ${current}`);
if (current === "mobile" || current === "xs") {
// 移动端特定逻辑
document.querySelector(".sidebar")?.classList.add("collapsed");
} else {
document.querySelector(".sidebar")?.classList.remove("collapsed");
}
});样式验证器
class StyleValidator {
constructor(element) {
this.element = element;
this.rules = [];
}
addRule(property, validator, message) {
this.rules.push({ property, validator, message });
return this;
}
validate() {
const styles = getComputedStyle(this.element);
const errors = [];
this.rules.forEach((rule) => {
const value = styles.getPropertyValue(rule.property);
if (!rule.validator(value)) {
errors.push({
property: rule.property,
value,
message: rule.message,
});
}
});
return {
valid: errors.length === 0,
errors,
};
}
// 预设验证规则
static minFontSize(pixels) {
return (value) => parseFloat(value) >= pixels;
}
static hasColor(value) {
return value && value !== "rgba(0, 0, 0, 0)" && value !== "transparent";
}
static minContrast(background, foreground, ratio = 4.5) {
// 简化的对比度检查
return true; // 实际实现需要计算 WCAG 对比度
}
}
// 使用
const validator = new StyleValidator(document.querySelector(".content"));
validator
.addRule(
"font-size",
StyleValidator.minFontSize(14),
"Font size should be at least 14px"
)
.addRule("color", StyleValidator.hasColor, "Text color should be visible");
const result = validator.validate();
if (!result.valid) {
console.warn("Style validation failed:", result.errors);
}性能优化建议
避免频繁读写样式
// ❌ 不好:读写交替会触发多次重排
elements.forEach((el) => {
const height = el.offsetHeight; // 读取
el.style.height = height * 2 + "px"; // 写入
});
// ✅ 好:先批量读取,再批量写入
const heights = elements.map((el) => el.offsetHeight); // 批量读取
elements.forEach((el, i) => {
el.style.height = heights[i] * 2 + "px"; // 批量写入
});使用 CSS 类而非内联样式
// ❌ 较慢:多次修改内联样式
element.style.color = "red";
element.style.fontSize = "20px";
element.style.fontWeight = "bold";
element.style.marginTop = "10px";
// ✅ 更快:添加预定义的 CSS 类
element.classList.add("highlighted");使用 transform 和 opacity 进行动画
// ❌ 会触发布局重排
element.style.left = "100px";
element.style.top = "50px";
element.style.width = "200px";
// ✅ 只触发合成,性能更好
element.style.transform = "translate(100px, 50px) scale(2)";
element.style.opacity = "0.8";总结
样式与类操作是 JavaScript 与 CSS 交互的重要方式。选择合适的方法可以让代码更清晰、性能更好:
| 需求 | 推荐方法 |
|---|---|
| 添加/移除/切换类 | classList.add/remove/toggle |
| 检查是否有某个类 | classList.contains |
| 设置单个样式 | element.style.property |
| 批量设置样式 | element.style.cssText |
| 获取计算后的样式 | getComputedStyle |
| 操作 CSS 变量 | setProperty/getPropertyValue |
| 获取元素尺寸 | getBoundingClientRect |
最佳实践:
- 优先使用 CSS 类,将样式定义留在 CSS 中
- 使用
getComputedStyle读取最终渲染的样式 - 批量读写样式,避免频繁触发重排
- 动画优先使用
transform和opacity