JavaScript Array.splice() 深度实战指南:从基础用法到 refine 项目源码中的真实应用
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
Array.prototype.splice()是 JavaScript 中用于原地(in-place)删除、替换或插入数组元素的核心方法,也是 refine 这类以数据操作为核心的 React 框架中高频使用的底层工具。本篇以 refine 仓库官方博客教程为主体,结合仓库真实源码中的 6 处调用场景,从方法签名、参数语义、边界情况到多维数组与解构组合,系统讲解splice()的完整用法,读完你既能熟练驾驭该方法,也能理解它在真实项目中的落地模式。
JavaScript Splice:方法签名与返回语义
当数组调用splice()时,它接受三类可选参数:startIndex(起始索引)、deleteCount(删除数量)以及可变的items 插入列表。其中startIndex是从零开始的索引,表示变更动作从哪里开始;deleteCount表示要删除的条目数量;items 则是可能无限多个、需要插入的条目。
JSArray.prototype.splice()方法签名
Array.prototype.splice()的合法调用签名如下:
splice() splice(startIndex) splice(startIndex, deleteCount, item1) splice(startIndex, deleteCount, item1, item2, /*...*/, itemN)Array.prototype.splice()的所有参数都是可选的。不过,如果我们希望调用数组真正发生变更,至少要传入startIndex。是否传deleteCount,取决于我们想不想删除条目;是否传items 列表,取决于我们想不想插入或追加条目。三个参数也可以同时传入,后续章节会逐一覆盖这些情况。
JSsplice()的返回值
splice()会变更原始数组,并返回一个包含被删除条目的数组。这是它与slice()最本质的区别之一:它不返回新数组的副本,而是"切除"下来的部分。
无参数调用splice()
虽然没有参数时splice()不会对调用数组做任何修改,但它能帮助我们直观理解返回值语义——返回的是"被删除条目的数组":
const mnemonic = [ "Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked", ]; // 返回值 console.log(mnemonic.splice()); // [] console.log(mnemonic); // ["Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked"]可以看到,无参数调用返回空数组[]。原因在于没有传入startIndex,没有任何条目被移除,因此"被删除条目数组"自然是空的。这也印证了一点:虽然splice()天生用于移除条目,但没有startIndex时它不会改变调用数组。
只传startIndex:清空第n项之后的所有元素
当只传入startIndex时,从startIndex(含)开始到数组末尾的所有条目都会被移除:
const mnemonic = [ "Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked", ]; // 返回值 console.log(mnemonic.splice(4)); // ["And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked"] // 调用数组 console.log(mnemonic); // ["Please", "Send", "Cats", "Monkeys"]这里的关键在于:移除从startIndex处(含)开始。因此零基的startIndex恰好等于调用数组中保留的前n个条目数量,即n = startIndex:
const mnemonic = [ "Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked", ]; // 只保留前 4 项 console.log(mnemonic.splice(4)); // ["And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked"] console.log(mnemonic); // ["Please", "Send", "Cats", "Monkeys"] // 只保留前 2 项 console.log(mnemonic.splice(2)); // ["Cats", "Monkeys"] console.log(mnemonic); // ["Please", "Send"] // 清空整个数组 console.log(mnemonic.splice(0)); // ["Please", "Send"] console.log(mnemonic); // []在上面的例子中,传入的每个startIndex值,都代表着我们希望在原数组头部保留的条目数量。mnemonic.splice(0)则是彻底清空原数组的快捷方式。
仓库实战:这种"只传startIndex截断尾部"的写法在 packages/strapi/src/hooks/upload.ts 中有直接应用。Strapi 数据提供者的beforeUpload会统计待上传文件与已上传文件的总数,当超过maxCount上限时,用files.splice(deleteItemCount)一步把超出配额的多余文件从待上传列表中切除:
const beforeUpload = (_file: any, files: any[]): boolean => { const totalFiles = fileList.length; const filesCount = files.length; if (totalFiles + filesCount > maxCount) { const excessFileCount = totalFiles + filesCount - maxCount; // 转成负数,从尾部截断 const deleteItemCount = excessFileCount - excessFileCount * 2; files.splice(deleteItemCount); } setFileList([...fileList, ...files]); return true; };这正是"从第 n 项之后全部移除"语义的典型工程化用法:用一个负数(超过数组长度的负值实际等效于0,详见下文"边界情况")或正索引,快速裁剪数组尾部。
传入deleteCount:删除中间条目
通过第二个参数deleteCount,我们可以删除从startIndex开始、指定数量的中间条目:
const mnemonic = [ "Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked", ]; console.log(mnemonic.splice(4, 2)); // ["And", "Zebras"] console.log(mnemonic); // ["Please", "Send", "Cats", "Monkeys", "In", "Large", "Cages", "Make", "Sure", "Padlocked"]上面的调用传入了2作为deleteCount。删除从startIndex开始,直到删满deleteCount个为止。注意,删除后deleteCount之后的条目会向左侧滑动补齐空位。
仓库实战:按索引删除单个元素是splice(index, 1)最常见的形态。Mantine 数据提供者的通知管理实现中,packages/mantine/src/providers/notificationProvider.tsx 的removeNotification先用indexOf找到通知 key 的位置,再删除该元素:
const removeNotification = (key?: string) => { if (key) { const index = activeNotifications.indexOf(key); if (index > -1) { activeNotifications.splice(index, 1); } } };同样,refine 的 codemod 在把 v1 代码迁移到 v2 时,需要从<Refine>组件的 JSX 属性数组中剔除多余的routes属性,packages/codemod/src/transformations/refine1-to-refine2.ts 正是用openingElement.attributes?.splice(routesAttributeIndex, 1)完成的。由于splice()是原地修改,配合indexOf/findIndex定位索引,即可在真实数据数组上实现精准删除。
替换条目:删除与插入的优先级
在第二个参数之后传入条目列表,就可以用新条目替换被删除的条目。所有传入的条目都会被添加到startIndex及之后的位置:
const mnemonic = [ "Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked", ]; console.log(mnemonic.splice(4, 0, "Ants", "Zebus")); // [] console.log(mnemonic); // ["Please", "Send", "Cats", "Monkeys", "Ants", "Zebus", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked"] console.log(mnemonic.splice(4, 2)); // ["Ants", "Zebus"] console.log(mnemonic); // ["Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked"] // 先删除、再替换 console.log(mnemonic.splice(4, 2, "Alpacas", "Zorillas")); console.log(mnemonic); // ["Please", "Send", "Cats", "Monkeys", "Alpacas", "Zorillas", "In", "Large", "Cages", "Make", "Sure", "Padlocked"]观察上面的连续变更可以看出:删除优先于插入。当deleteCount为正数且后续传入替换条目时,引擎先删除指定数量的条目,再把新条目放进startIndex及之后。在上面的序列中,"Ants", "Zebus"先以0删除量插入到索引4;随后以startIndex = 4, deleteCount = 2且不带替换条目被删除;最后一次变更又在索引4删除了 2 个条目,并用新条目替换。
我们还可以让插入的条目多于删除的条目,从而"撑大"(overload)数组:
const mnemonic = [ "Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked", ]; // 插入的条目数 > 删除的条目数 console.log(mnemonic.splice(4, 2, "Ants", "Zebus", "Donkeys", "Bears")); // ["And", "Zebras"] console.log(mnemonic); // ["Please", "Send", "Cats", "Monkeys", "Ants", "Zebus", "Donkeys", "Bears", "In", "Large", "Cages", "Make", "Sure", "Padlocked"]注意,因为替换/扩容发生在中间位置,startIndex之后的原有条目会被向右推挤。
仓库实战:这是splice()的"替换片段"用法。refine 的查询 key 构建器(packages/core/src/definitions/helpers/keys/index.ts)在arrayReplace中先浅拷贝数组,再借助arrayFindIndex定位需要替换的子数组起始位置,然后一次调用完成"删除旧片段 + 展开插入新片段":
export function arrayReplace<T>( array: T[], partToBeReplaced: T[], newPart: T[], ): T[] { const newArray: T[] = [...array]; const startIndex = arrayFindIndex(array, partToBeReplaced); if (startIndex !== -1) { newArray.splice(startIndex, partToBeReplaced.length, ...newPart); } return newArray; }newArray.splice(startIndex, partToBeReplaced.length, ...newPart)是"先删后插"的标准写法:deleteCount等于被替换片段的长度,配合展开运算符把新片段逐项插入——这正是 replace 语义在框架底层(如动态更新查询 key 分段)的典型实现。注意这里对副本调用splice(),避免了对外部传入数组的原地污染,属于防御性编程的最佳实践。
纯注入:deleteCount传0
如果只想在某个位置注入条目而不删除任何元素,把deleteCount设为0即可:
const mnemonic = [ "Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked", ]; console.log(mnemonic.splice(4, 0, "Ants", "Zebus", "Donkeys", "Bears")); // [] console.log(mnemonic); // ["Please", "Send", "Cats", "Monkeys", "Ants", "Zebus", "Donkeys", "Bears", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked"]由于deleteCount为0,没有任何条目被删除,新条目直接从startIndex位置开始插入。
仓库实战:codemod 在 v5 迁移脚本中需要把新声明的变量语句插到指定语句之后,packages/codemod/src/transformations/v5/use-query-and-result-fields-in-list-hooks.ts 使用的正是statements.splice(currentIndex + 1, 0, newDeclaration)—— 以0删除量、目标位置为起点完成纯注入。
尾部追加:用length作为startIndex
使用调用数组的length作为startIndex,可以往数组尾部追加条目。因为length大于零基数组的最后一个索引,所以它天然指向"末尾之后"的位置:
const mnemonic = [ "Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked", ]; console.log( mnemonic.splice(mnemonic.length, 0, "Ants", "Zebus", "Donkeys", "Bears"), ); // [] console.log(mnemonic); // ["Please", "Send", "Cats", "Monkeys", "In", "Large", "Cages", "Make", "Sure", "Padlocked", "Ants", "Zebus", "Donkeys", "Bears"]像这样结合length使用splice(),是Array.prototype.push()的一个不错的替代方案——尤其当你希望保持"统一用splice()处理插入"的代码风格时。
⚠️重要警告:用-1作为startIndex不能实现尾部追加。因为当startIndex < 0时,变更从startIndex + mnemonic.length开始,即-1 + 12 = 11,也就是最后一个元素的位置。于是最后一个元素总会被挤到末尾,永远无法真正"追加到尾部":
const mnemonic = [ "Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked", ]; console.log(mnemonic.splice(-1, 0, "Ants", "Zebus", "Donkeys", "Bears")); // [] console.log(mnemonic); // ["Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Ants", "Zebus", "Donkeys", "Bears", "Padlocked"]负数startIndex:在最后n项之前注入
将-n作为startIndex,可以在最后n个条目之前注入新条目,这是startIndex < 0的又一典型场景:
const mnemonic = [ "Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked", ]; // 在最后 2 项之前注入 console.log(mnemonic.splice(-2, 0, "Ants", "Zebus", "Donkeys", "Bears")); // [] console.log(mnemonic); // ["Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Ants", "Zebus", "Donkeys", "Bears", "Sure", "Padlocked"] // 在最后 4 项之前注入 console.log(mnemonic.splice(-4, 0, "Apacas", "Zorillas")); // [] console.log(mnemonic); // ["Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Ants", "Zebus", "Apacas", "Zorillas", "Donkeys", "Bears", "Sure", "Padlocked"]负数startIndex会换算为-n + mnemonic.length,因此变更从距数组末尾n个条目(注意是条数,不是索引)处开始,从而保证末尾n个条目保持不动。
仓库实战:把"负数索引注入"与"删除 + 头部插入"组合,可以实现"把某个元素移动到数组首位"。kbar 命令面板的 packages/kbar/src/hooks/useRefineKbar/index.tsx 中,moveActionToFirst先复制资源数组,findIndex定位当前路由对应的资源,然后两次splice完成移动:
const moveActionToFirst = (): IResourceItem[] => { const orderedResources = [...resources]; const fromIndex = orderedResources?.findIndex( (resource) => (resource.identifier ?? resource?.name) === (resourceFromParams?.identifier ?? resourceFromParams?.name), ); if (fromIndex > 0) { const element = orderedResources[fromIndex]; orderedResources.splice(fromIndex, 1); orderedResources.splice(0, 0, element); } return orderedResources; };splice(fromIndex, 1)取出目标元素,splice(0, 0, element)再把它注入到头部——两个操作都基于"原地修改 + 删除优先于插入"的语义,拼装出"移动到首位"的效果,同时保留其余资源的相对顺序。
边界情况:极端的startIndex值
过高负值:startIndex < -length时等效于0
当startIndex小于-mnemonic.length时,它会被钳制为0:
const mnemonic = [ "Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked", ]; console.log(mnemonic.splice(-100, 10, "Ants", "Zebus", "Donkeys", "Bears")); // ["Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make"] console.log(mnemonic); // ["Ants", "Zebus", "Donkeys", "Bears", "Sure", "Padlocked"]这里startIndex为-100,但数组长度只有 12,-100 + 12 = -88仍为负数,最终按0处理。因此删除从索引0开始、共移除前 10 个条目;传入的条目在删除后从索引0处插入。这正是上文 Strapi 上传示例中deleteItemCount取负值的数学基础:只要负得足够大,splice(负数)就等价于splice(0),即"从头开始切除"。
过高正值:startIndex > length时退化为尾部追加
当startIndex大于数组长度时,即使传入了deleteCount也不会删除任何条目,所有传入条目会被追加到尾部:
const mnemonic = [ "Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked", ]; console.log(mnemonic.splice(100, 10, "Ants", "Zebus", "Donkeys", "Bears")); // [] console.log(mnemonic); // ["Please", "Send", "Cats", "Monkeys", "And", "Zebras", "In", "Large", "Cages", "Make", "Sure", "Padlocked", "Ants", "Zebus", "Donkeys", "Bears"]如我们所见,100远大于mnemonic.length,而且我们要求删除 10 个"并不存在"的条目,所以数组内部没有任何元素被删除,传入条目被追加到了尾部。
需要提醒的是:用任意过大的索引做尾部追加并不安全,因为它无法保证startIndex一定大于等于调用数组的length(例如传入的数组可能比预期更长)。更稳妥的做法是显式传arr.length。
Bonus:splice()vsslice()
splice()和slice()经常被初学者混淆,原因很简单——它们的名字太像了。但二者的行为截然不同:
splice()
- 会修改原始数组。
- 可用于在指定索引处删除、替换或插入元素。
- 原地修改数组,并返回被删除元素的数组。
示例:
let arr = [1, 2, 3, 4]; let removed = arr.splice(2, 2); // 从索引 2 开始,删除 2 个元素 console.log(arr); // [1, 2] - 原始数组被修改 console.log(removed); // [3, 4] - 被删除的元素slice()
- 不会修改原始数组。
- 返回从起始索引到结束索引之间一部分数组的浅拷贝。
- 原始数组保持不变。
示例:
let arr = [1, 2, 3, 4]; let sliced = arr.slice(2, 4); // 复制索引 2 到 4(不含 4)之间的元素 console.log(arr); // [1, 2, 3, 4] - 原始数组未被修改 console.log(sliced); // [3, 4] - 切片部分关键区别
splice()会修改原始数组,slice()不会。- 需要变更数组(删除、插入、替换)时用
splice();需要读取数组的一部分而不改变原数组时用slice()。
多维数组中的splice()
当数组的元素本身还是数组(多维数组)时,splice()既可以作用于外层数组(增删整个内层数组),也可以先通过索引访问到内层数组再对其调用splice()。
修改外层数组
用splice()从外层数组中整体增删内层数组:
let matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ];弹出第二行(即数组[4, 5, 6]):
let removedRow = matrix.splice(1, 1); // 删除索引 1 处的 1 个元素 console.log(matrix); // 输出: [[1, 2, 3], [7, 8, 9]] console.log(removedRow); // 输出: [[4, 5, 6]]修改内层数组
也可以对内层数组的元素执行splice():
// 删除第一行的第二个元素 let removedElement = matrix[0].splice(1, 1); // 删除第一个内层数组中索引 1 处的 1 个元素 console.log(matrix); // 输出: [[1, 3], [7, 8, 9]] console.log(removedElement); // 输出: [2]向内层数组插入元素
同理,可以向内层数组注入元素:
// 填充第二行 matrix[1].splice(2, 0, 10, 11); // 在第二个内层数组的索引 2 处插入 10 和 11 console.log(matrix); // 输出: [[1, 3], [7, 8, 10, 11, 9]]应用示例:日程表
假设我们用多维数组表示一周日程,每个内层数组代表当天的任务:
let weeklySchedule = [ ["Meeting", "Coding", "Review"], // 周一 ["Planning", "Design"], // 周二 ["Deployment", "Testing"], // 周三 ];如果要把周二日程中的 "Design" 划掉:
weeklySchedule[1].splice(1, 1); // 删除周二数组索引 1 处的 1 个元素 console.log(weeklySchedule); // 输出: [["Meeting", "Coding", "Review"], ["Planning"], ["Deployment", "Testing"]]要点总结
- 访问内层数组:先通过外层数组索引拿到内层数组,再对它调用
splice()。 - 替换内层元素:
splice()可以增、删、改内层数组中的元素。 - 修改外层结构:
splice()可以整体增删外层数组中的内层数组。
结合解构使用splice()
Array.prototype.splice()返回"被删除元素数组"这一特性,配合解构赋值可以写出非常简洁的代码——一次调用同时完成"移除 + 变量绑定"。
基础用法:解构被移除的元素
let arr = [1, 2, 3, 4, 5]; // 移除前两个元素,并解构返回值 let [first, second] = arr.splice(0, 2); console.log(first); // 输出: 1 console.log(second); // 输出: 2 console.log(arr); // 输出: [3, 4, 5] - 原始数组已被修改元素交换
splice()和解构也可以组合出更实际的用法,例如交换数组元素:
let numbers = [10, 20, 30, 40, 50]; // 交换第 2 个和第 4 个元素 [numbers[1], numbers[3]] = [numbers[3], numbers[1]]; console.log(numbers); // 输出: [10, 40, 30, 20, 50]批量提取多个元素
可以用splice()一次提取数组的多段内容并立即解构使用:
let fruits = ["apple", "banana", "cherry", "date", "elderberry"]; // 从索引 1 开始删除 3 个元素 const [firstFruit, secondFruit, thirdFruit] = fruits.splice(1, 3); console.log(firstFruit); // 输出: 'banana' console.log(secondFruit); // 输出: 'cherry' console.log(thirdFruit); // 输出: 'date' console.log(fruits); // 输出: ['apple', 'elderberry']删除 + 插入一步完成
deleteCount为 0 时返回值是空数组,而配合正数deleteCount则可以在"删除并替换"的同时把被删除项解构出来:
let arr = [1, 2, 3, 4, 5]; // 从索引 2 删除 2 个元素,并替换为 "a"、"b",同时解构被删除元素 let [removed1, removed2] = arr.splice(2, 2, "a", "b"); console.log(removed1); // 输出: 3 console.log(removed2); // 输出: 4 console.log(arr); // 输出: [1, 2, 'a', 'b', 5] - 数组已被新元素替换这个例子中,元素3和4被删除,'a'和'b'插入到它们的位置,同时被删除的元素被解构到变量removed1和removed2中。
为什么推荐这种写法
- 代码更简洁:配合解构,可以少写很多"先删、再取引用"的样板代码。
- 直接访问:被移除的元素立即可用于后续逻辑,数组在同一语句中就完成了变更,逻辑连贯、可读性强。
总结
本篇围绕Array.prototype.splice()展开了系统梳理:它是数组方法,没有对应的 String 版本;它会原地修改原数组,并返回被删除元素的数组。
我们掌握了以下模式:
- 只传
n作为startIndex,可以清空前n项之后的所有条目(splice(0)即清空整个数组); - 传
deleteCount作为第二参数,可以删除指定数量的中间条目; - 在第三参数及之后传入新条目,可以实现替换,甚至可以"撑大"数组;
deleteCount传0则实现纯注入;- 用调用数组的
length作为startIndex可以尾部追加,作为push()的替代; - 用
-n作为startIndex可以在最后n项之前注入; - 极端值方面:
startIndex < -length时等效为0,startIndex > length时不会删除任何条目、新条目直接进入尾部。
在 refine 仓库中,splice()的这些语义被真实地应用于框架与工具链:core的查询 key 构建器用它做数组片段替换(keys/index.ts),kbar命令面板用它实现资源"移动到首位"(useRefineKbar/index.tsx),Mantine 通知提供者用它按索引关闭通知(notificationProvider.tsx),Strapi 上传钩子用它裁剪超配额文件(upload.ts),codemod 迁移脚本则用它删除 JSX 属性与注入新语句(refine1-to-refine2.ts、use-query-and-result-fields-in-list-hooks.ts)。掌握了这些真实调用形态,你就能在自己的项目中更自信地选择splice()处理数组变更。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考