在上一篇文章中,我们深入探索了矩阵与变换,学会了用 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); // undefined
1.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(只读)

三个核心认知:

  1. 列表接口是“批量数据”的操作工具 ——SVGStringList 管理字符串列表,SVGNumberList 管理数值列表。它们提供了完整的增删改查方法,让你可以像操作数组一样操作 SVG 属性中的列表数据。

  2. getItem() 返回的是对象本身,不是副本 ——修改返回的对象会立即反映到 DOM 中。这是一个强大的特性,但也需要注意不要在不该修改的地方误修改。

  3. 注意区分 SVGNumberList 和 SVGPointList——<polygon> 的 points 属性在现代浏览器中返回的是 SVGPointList(坐标对列表),而不是 SVGNumberList(纯数值列表)。理解它们的区别,有助于你正确选择操作方式。

下篇预告

在下一篇文章中,我们将进入 SVG 动画的世界—— SMIL 与动画接口。你将学会:

  • 用 <animate> 和 <animateTransform> 元素实现属性动画。

  • 用 JavaScript 动态创建和控制 SMIL 动画。

  • 理解 TimeEvent 接口,监听动画的开始、重复和结束事件。

  • 对比 SMIL 与 CSS Animation 的优劣,选择最适合你场景的方案。

互动话题:尝试用文中的多边形编辑器,画一个你喜欢的形状(比如星星、箭头、小房子)。你能通过拖拽顶点把它变成完全不同的形状吗?欢迎在评论区分享你的创作!