在上一篇文章中,我们深入探索了矩阵与变换,学会了用 DOMMatrix 统一管理所有图形变换。但矩阵处理的是整体的、连续的变换数据——平移、旋转、缩放,这些数据在 SVG 属性中通常表现为单个值或一个 transform 字符串。
但SVG 中还有另一类数据: 列表(List)。
回想一下,你有没有遇到过这样的场景:
想给一个 SVG 元素动态添加或删除 CSS 类名——但 element.classList 是 DOMTokenList,而 SVG 元素上还有一个 className 属性,它是什么类型?
想动态修改 <polygon> 的顶点坐标——但 points 属性返回的不是一个数组,而是一个奇怪的对象。
想读取 <ellipse> 的 requiredExtensions 或 systemLanguage 属性——这些属性在 DOM 中是什么形态?
这些问题的答案,都指向 SVG API 中的两个核心接口:SVGStringList 和 SVGNumberList。
它们是 SVG 世界中处理“批量数据”的专用工具。SVGStringList 管理字符串列表(如类名、语言标签),SVGNumberList 管理数值列表(如多边形顶点坐标)。掌握了它们,你就掌握了 SVG 中所有“列表型属性”的读写与操作。
一、SVGStringList:字符串列表的管理者
SVGStringList 已废弃 - 现代 SVG 规范不再使用。
1.1 什么是 SVGStringList?
SVGStringList 接口定义了一个字符串列表。它在 SVG DOM 中用于表示那些值为多个字符串的属性。
一个 SVGStringList 对象可以被标记为只读——如果它对应的是一个只读属性,任何修改它的尝试都会抛出异常。不过,SVGStringList 对象是可索引的,可以像数组一样通过下标访问。
哪些 SVG 属性使用了 SVGStringList?
属性 | 所属接口 | 说明 |
|---|---|---|
className | SVGElement | 元素的类名列表(对应 class 属性) |
requiredExtensions | SVGGraphicsElement | 指定渲染该元素所需扩展的列表 |
systemLanguage | SVGGraphicsElement | 指定渲染该元素所需语言的列表 |
重要提示:在 SVG 2 中,推荐使用标准的 element.classList(DOMTokenList)来操作类名,而不是直接操作 className 属性。但 requiredExtensions 和 systemLanguage 仍然使用 SVGStringList。
1.2 核心属性
SVGStringList 提供了两个属性来获取列表长度:
属性 | 类型 | 说明 |
|---|---|---|
length | unsigned long | 列表中项目的数量 |
numberOfItems | unsigned long | 列表中项目的数量(与 length 互为别名) |
这两个属性的值是相同的。length 的存在是为了让 SVGStringList 更像 JavaScript 数组。
<svg width="200" height="200" class="class1 class2">
<circle id="myCircle" cx="100" cy="100" r="50" data-role="draggable" data-index="3" />
</svg>const svg = document.querySelector('svg');
// 现代标准方式 - SVGAnimatedString
const className = svg.className; // 现代浏览器是SVGAnimatedString而非SVGStringList
console.log(className.baseVal); // 获取当前类名字符串,如 "class1 class2"
console.log(className.animVal); // 获取动画中的类名字符串
// 更好的方式 - 使用 classList(推荐)
const classList = svg.classList; // DOMTokenList
console.log(classList.length); // 2
console.log([...classList]); // ["class1", "class2"]
console.log(classList.contains('class1')); // true
// 错误的方式- 现代浏览器下测试会得到以下结果
console.log(svg.className.length); // undefined
console.log(svg.className.numberOfItems); // undefined1.3 核心方法
SVGStringList 提供了完整的增删改查方法:
方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
initialize(newItem) | string | string | 清空列表并用单个字符串重新初始化 |
getItem(index) | number | string | 返回指定索引处的字符串 |
insertItemBefore(newItem, index) | string, number | string | 在指定位置插入新字符串 |
replaceItem(newItem, index) | string, number | string | 替换指定位置的字符串 |
removeItem(index) | number | string | 删除指定位置的字符串并返回它 |
appendItem(newItem) | string | string | 在列表末尾追加新字符串 |
clear() | 无 | void | 清空列表中的所有项目 |
重要行为:当一个项目被插入到列表中时,如果它已经在另一个列表中存在,它会先从原列表中被移除,再插入到新列表中。插入的项目是项目本身,而不是副本。
1.4 实战:操作 SVG 的 class 属性
注意:请使用旧版浏览器,比如ie等对下面示例代码进行测试学习。
虽然现代开发中更推荐使用 classList,但理解 className 作为 SVGStringList 的行为仍然有助于理解 SVG 的属性系统。
<svg id="mySvg" class="icon primary large">
<rect width="100" height="100" fill="blue" />
</svg>const svg = document.getElementById('mySvg');
// 获取 className 属性(SVGStringList)
const classList = svg.className;
console.log(classList.length); // 3
console.log(classList.getItem(0)); // "icon"
console.log(classList.getItem(1)); // "primary"
console.log(classList.getItem(2)); // "large"// 追加一个新类名
classList.appendItem('active');
console.log(svg.getAttribute('class')); // "icon primary large active"// 在索引 1 的位置插入
classList.insertItemBefore('new-class', 1);
console.log(svg.getAttribute('class')); // "icon new-class primary large active"// 替换索引 2 的项目
classList.replaceItem('updated', 2);
console.log(svg.getAttribute('class')); // "icon new-class updated large active"// 删除索引 3 的项目
classList.removeItem(3);
console.log(svg.getAttribute('class')); // "icon new-class updated active"// 清空所有类名
classList.clear();
console.log(svg.getAttribute('class')); // ""最佳实践:虽然 className 是 SVGStringList,但在实际开发中,操作 SVG 元素的类名推荐使用标准的 element.classList(DOMTokenList),它的 API 更加友好(支持 add、remove、toggle、contains 等方法)。
二、SVGNumberList:数值列表的管理者
2.1 什么是 SVGNumberList?
SVGNumberList 接口定义了一个 SVGNumber 对象列表。它在 SVG DOM 中用于表示那些值为多个数值的属性。
与 SVGStringList 类似 SVGNumberList 也可以被标记为只读,并且是可索引的,可以像数组一样访问。
哪些 SVG 属性使用了 SVGNumberList?
属性 | 所属接口 | 说明 |
|---|---|---|
points | SVGPolygonElement、SVGPolylineElement | 多边形的顶点坐标列表 |
动画属性的数值列表 | SVGAnimatedNumberList | 可动画的数值列表属性 |
关键认知:SVGNumberList 存储的是 SVGNumber 对象,而不是原始数字。每个 SVGNumber 对象都有一个 value 属性来存储实际的数值。这是 SVG 属性系统的统一设计——所有属性值都被封装为对象,而不是原始类型。
2.2 核心属性
与 SVGStringList 一样,SVGNumberList 也提供了两个属性:
属性 | 类型 | 说明 |
|---|---|---|
length | unsigned long | 列表中项目的数量 |
numberOfItems | unsigned long | 列表中项目的数量(与 length 互为别名) |
2.3 核心方法
SVGNumberList 的方法与 SVGStringList 非常相似,但操作的是 SVGNumber 对象而非字符串:
方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
initialize(newItem) | SVGNumber | SVGNumber | 清空列表并用单个 SVGNumber 重新初始化 |
getItem(index) | number | SVGNumber | 返回指定索引处的 SVGNumber |
insertItemBefore(newItem, index) | SVGNumber, number | SVGNumber | 在指定位置插入 SVGNumber |
replaceItem(newItem, index) | SVGNumber, number | SVGNumber | 替换指定位置的 SVGNumber |
removeItem(index) | number | SVGNumber | 删除指定位置的 SVGNumber 并返回它 |
appendItem(newItem) | SVGNumber | SVGNumber | 在列表末尾追加 SVGNumber |
clear() | 无 | void | 清空列表中的所有项目 |
重要行为:与 SVGStringList 相同,如果插入的 SVGNumber 已经在另一个列表中存在,它会先从原列表中被移除。插入的是对象本身,而不是副本。
2.4 如何创建 SVGNumber?
SVGNumber 没有公共构造函数。你需要通过 SVGSVGElement 的 createSVGNumber() 方法来创建:
const svg = document.querySelector('svg');
const num = svg.createSVGNumber();
num.value = 100;或者,你也可以通过 SVGPoint 来间接操作坐标(SVGPoint 有 x 和 y 属性,在某些场景下更直观)。不过对于 SVGNumberList 来说,标准的做法是使用 createSVGNumber()。
三、实战:操作 <polygon> 的 points 属性
<polygon> 和 <polyline> 的 points 属性是 SVGNumberList 最典型、最实用的应用场景。
3.1 读取多边形顶点
<svg viewBox="0 0 400 300">
<polygon id="triangle" points="200,10 380,290 20,290" fill="blue" />
</svg>const polygon = document.getElementById('triangle');
// 获取 points 属性(SVGAnimatedPoints 接口提供)
// 注意:points 是 SVGAnimatedPoints 接口的属性
const pointsList = polygon.points; // SVGPointList(现代实现)
// 或者通过 polygon.animatedPoints 获取动画值重要提示:在现代浏览器中,<polygon> 的 points 属性返回的是 SVGPointList 而不是 SVGNumberList。SVGPointList 是 SVGNumberList 的“升级版”——每个项目是一个 SVGPoint 对象(包含 x 和 y 属性),而不是单独的 SVGNumber。这是因为多边形的顶点是坐标对,用点列表比用数字列表更自然。
不过,SVGNumberList 仍然存在于 SVG 规范中,用于那些存储纯数值列表的场景(如某些动画属性)。理解 SVGNumberList 的原理,有助于你理解 SVGPointList 和其他列表类型的工作方式。
3.2 用 SVGPointList 操作多边形顶点
const polygon = document.getElementById('triangle');
const points = polygon.points; // SVGPointList// 读取顶点数量
console.log(points.length); // 3// 读取每个顶点
for (let i = 0; i < points.length; i++) {
const point = points.getItem(i);
console.log(`顶点 ${i}: (${point.x}, ${point.y})`);
}// 追加一个新顶点(将三角形变成四边形)
const svg = document.querySelector('svg');
const newPoint = svg.createSVGPoint();
newPoint.x = 200;
newPoint.y = 150;
points.appendItem(newPoint);
console.log(polygon.getAttribute('points')); // "200 10 380 290 20 290 200 150"// 在索引 1 的位置插入顶点
const anotherPoint = svg.createSVGPoint();
anotherPoint.x = 300;
anotherPoint.y = 150;
points.insertItemBefore(anotherPoint, 1);
console.log(polygon.getAttribute('points')); // "200 10 300 150 380 290 20 290 200 150"// 删除索引 2 的顶点
points.removeItem(2);
console.log(polygon.getAttribute('points')); // "200 10 300 150 20 290 200 150"// 替换索引 0 的顶点
const updatedPoint = svg.createSVGPoint();
updatedPoint.x = 100;
updatedPoint.y = 50;
points.replaceItem(updatedPoint, 0);
console.log(polygon.getAttribute('points')); // "100 50 300 150 20 290 200 150"3.3 直接修改顶点的坐标
getItem() 返回的是对象本身,而不是副本。这意味着你可以直接修改返回的 SVGPoint 对象,修改会立即反映到 DOM 中:
const polygon = document.getElementById('triangle');
const points = polygon.points;
// 获取第一个顶点并修改其坐标
const firstPoint = points.getItem(0);
firstPoint.x = 50;
firstPoint.y = 30;
// 不需要重新设置 points 属性,DOM 已经自动更新了!
console.log(polygon.getAttribute('points')); // "50 30 300 150 20 290 200 150"这个特性非常强大——你可以像操作普通 JavaScript 对象一样操作顶点,而无需手动序列化回字符串。
四、综合实战:构建一个多边形顶点编辑器
让我们综合运用所有知识,构建一个可视化的多边形顶点编辑器——用户可以通过拖拽顶点来实时调整多边形的形状。
4.1 HTML 结构
<style>
body { margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background: #f0f2f5; font-family: sans-serif; }
.container { background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); padding: 20px; }
svg { border: 1px solid #ddd; border-radius: 8px; }
.controls { display: flex; gap: 16px; margin-top: 16px; flex-wrap: wrap; align-items: center; }
.controls button { padding: 6px 16px; border: none; border-radius: 6px; background: #4A90D9; color: white; cursor: pointer; }
.controls button:hover { background: #357ABD; }
.controls button.danger { background: #E74C3C; }
.controls button.danger:hover { background: #C0392B; }
.controls span { font-size: 14px; color: #666; }
#info { font-size: 13px; color: #666; font-family: monospace; margin-top: 8px; }
</style><div class="container">
<svg id="canvas" viewBox="0 0 500 400" width="500" height="400">
<!-- 网格背景 -->
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#eee" stroke-width="0.5" />
</pattern>
<rect width="500" height="400" fill="url(#grid)" />
<!-- 多边形 -->
<polygon id="shape" points="250,50 400,150 350,320 150,320 100,150"
fill="rgba(74,144,217,0.3)" stroke="#4A90D9" stroke-width="2" />
<!-- 顶点标记(由 JavaScript 动态生成) -->
<g id="handles"></g>
</svg>
<div class="controls">
<button id="addVertexBtn">➕ 添加顶点</button>
<button id="removeVertexBtn" class="danger">➖ 删除顶点</button>
<button id="resetBtn">🔄 重置</button>
<span id="vertexCount">顶点数: 5</span>
</div>
<div id="info">拖拽顶点可调整形状</div>
</div>4.2 JavaScript 交互逻辑
<script>
// 应用状态管理 - 统一管理所有状态,避免全局变量污染
class PolygonEditor {
constructor() {
// DOM元素缓存
this.elements = {
canvas: document.getElementById('canvas'),
polygon: document.getElementById('shape'),
handlesContainer: document.getElementById('handles'),
vertexCount: document.getElementById('vertexCount'),
info: document.getElementById('info'),
addBtn: document.getElementById('addVertexBtn'),
removeBtn: document.getElementById('removeVertexBtn'),
resetBtn: document.getElementById('resetBtn')
};
// 拖拽状态
this.dragState = {
isDragging: false,
activeIndex: -1,
// 拖拽开始时,鼠标在SVG中的坐标
startX: 0,
startY: 0,
// 拖拽开始时,活动顶点的初始坐标
initialVertexX: 0,
initialVertexY: 0,
};
// 默认顶点配置
this.defaultVertices = [
{ x: 250, y: 50 },
{ x: 400, y: 150 },
{ x: 350, y: 320 },
{ x: 150, y: 320 },
{ x: 100, y: 150 }
];
// SVG视口边界
this.bounds = { width: 500, height: 400 };
// 初始化编辑器
this.init();
}
// ---------- 初始化方法 ----------
// 省略部分代码,需要请关注微信公众号 sharing pi 留言。
}
document.addEventListener('DOMContentLoaded', () => {
new PolygonEditor();
});
</script>4.3 运行效果
打开页面后,你会看到一个蓝色的五边形,每个顶点都有一个可拖拽的白色圆点标记:
拖拽顶点:点击任意顶点标记并拖动,多边形的形状会实时更新。
添加顶点:点击“添加顶点”按钮,会在多边形的中心位置插入一个新顶点。
删除顶点:点击“删除顶点”按钮,会移除最后一个顶点(至少保留 3 个)。
重置:一键恢复为默认的五边形。
所有的顶点操作(除了删除)都是通过 SVGPointList(SVGNumberList 的“升级版”)的 appendItem、getItem 等方法完成的。你不需要手动拼接 points 字符串——列表接口帮你处理了所有底层细节。
4.4 作者手欠拖拽效果图:

五、SVGAnimatedStringList 与 SVGAnimatedNumberList
在某些场景下,你会遇到 SVGAnimatedStringList 和 SVGAnimatedNumberList 接口。它们是 SVGStringList 和 SVGNumberList 的“动画包装器”,用于那些可以动画化的列表属性。
5.1 核心概念
与 SVGAnimatedLength、SVGAnimatedNumber 等接口类似,动画列表接口包含两个属性:
属性 | 类型 | 说明 |
|---|---|---|
baseVal | SVGStringList / SVGNumberList | 属性的基础值(静态值),可读写 |
animVal | SVGStringList / SVGNumberList | 属性的当前动画值,只读 |
如果属性当前没有被动画驱动,animVal 的内容与 baseVal 相同。
5.2 应用场景
SVGAnimatedNumberList 主要用于那些接受数值列表且可动画的属性。最常见的例子是 <polygon> 的 points 属性——通过 animatedPoints 可以访问其动画值。
const polygon = document.querySelector('polygon');
// 基础值(静态)
const basePoints = polygon.points; // SVGPointList
// 动画值(如果存在动画)
const animPoints = polygon.animatedPoints; // SVGPointList(只读)5.3 使用注意事项
animVal 是只读的——试图修改它会抛出异常。
如果属性没有动画,animVal 与 baseVal 指向相同的数据。
在大多数日常开发中,你直接操作 baseVal(即元素的标准属性)就足够了。
六、常见错误与避坑指南
错误一:试图直接赋值 points 属性
// 这样不会生效!
polygon.points = '100,100 200,200';正确做法:使用 setAttribute('points', ...) 或操作 SVGPointList。
错误二:忘记 createSVGPoint() / createSVGNumber()
// 错误:不能直接 new SVGPoint()
const point = new SVGPoint(); // TypeError
// 正确:通过 SVGSVGElement 创建
const svg = document.querySelector('svg');
const point = svg.createSVGPoint();
point.x = 100;
point.y = 100;错误三:混淆 SVGNumberList 和 SVGPointList
SVGNumberList 存储的是单个数值(SVGNumber 对象),而 SVGPointList 存储的是坐标对(SVGPoint 对象)。<polygon> 的 points 属性在现代浏览器中返回的是 SVGPointList,而不是 SVGNumberList。
// 错误:points 返回的是 SVGPointList,不是 SVGNumberList
const points = polygon.points; // SVGPointList
const firstNumber = points.getItem(0); // SVGPoint,不是 SVGNumber
// 正确:访问坐标
const firstPoint = points.getItem(0);
console.log(firstPoint.x, firstPoint.y);错误四:修改 getItem() 返回的对象后没有刷新视图
getItem() 返回的是对象本身,不是副本。修改它是有效的,但如果你使用的是框架(如 React、Vue),可能需要手动触发重新渲染才能看到变化。
解决方案:在修改顶点后,调用一个更新函数来刷新 UI。
错误五:在只读列表上调用修改方法
如果 SVGStringList 或 SVGNumberList 对应的是一个只读属性,调用 appendItem、removeItem 等方法会抛出 NoModificationAllowedError 异常。
// 如果 className 对应的 SVGStringList 是只读的
try {
classList.appendItem('new-class');
} catch (e) {
console.error('列表是只读的,无法修改');
}七、本章总结
接口 | 存储内容 | 典型属性 | 关键方法 |
|---|---|---|---|
SVGStringList | 字符串列表 | className、requiredExtensions、systemLanguage | appendItem、removeItem、insertItemBefore、replaceItem |
SVGNumberList | SVGNumber 对象列表 | 动画属性的数值列表 | 同上(操作 SVGNumber 对象) |
SVGPointList | SVGPoint 对象列表 | <polygon> 的 points | 同上(操作 SVGPoint 对象) |
SVGAnimatedStringList | 可动画的字符串列表 | 动画属性的字符串列表 | baseVal(可读写)、animVal(只读) |
SVGAnimatedNumberList | 可动画的数值列表 | 动画属性的数值列表 | baseVal(可读写)、animVal(只读) |
三个核心认知:
列表接口是“批量数据”的操作工具 ——SVGStringList 管理字符串列表,SVGNumberList 管理数值列表。它们提供了完整的增删改查方法,让你可以像操作数组一样操作 SVG 属性中的列表数据。
getItem() 返回的是对象本身,不是副本 ——修改返回的对象会立即反映到 DOM 中。这是一个强大的特性,但也需要注意不要在不该修改的地方误修改。
注意区分 SVGNumberList 和 SVGPointList——<polygon> 的 points 属性在现代浏览器中返回的是 SVGPointList(坐标对列表),而不是 SVGNumberList(纯数值列表)。理解它们的区别,有助于你正确选择操作方式。
下篇预告
在下一篇文章中,我们将进入 SVG 动画的世界—— SMIL 与动画接口。你将学会:
用 <animate> 和 <animateTransform> 元素实现属性动画。
用 JavaScript 动态创建和控制 SMIL 动画。
理解 TimeEvent 接口,监听动画的开始、重复和结束事件。
对比 SMIL 与 CSS Animation 的优劣,选择最适合你场景的方案。
互动话题:尝试用文中的多边形编辑器,画一个你喜欢的形状(比如星星、箭头、小房子)。你能通过拖拽顶点把它变成完全不同的形状吗?欢迎在评论区分享你的创作!