JS数组增删改查详解:push/splice/slice等方法对原数组的影响

作者:拾光

时间:2026-11-04

浏览:310

深入解析JavaScript数组增删改查全量常用方法对原数组的影响,对比push、splice、slice等方法的底层执行机制、返回值、时间复杂度及不可变数据处理最佳实践。

文章目录
JavaScript 核心机制 内存与不可变性 标准参考:ECMA-262 / ECMAScript 2023
JS数组增删改查详解:push/splice/slice等方法对原数组的影响
图 0:JavaScript 数组增删改查与原数组影响全景缩略图
在 JavaScript 中操作数组时,最容易引发逻辑漏洞和难以排查的 Bug 的根源,在于混淆了“原地修改(In-Place Mutation)”与“返回新数组(Immutable Copy)”的本质区别。push()、splice() 等方法会直接在既有内存上修改原数组并产生连带副作用,而 slice()、concat() 等方法则会开辟全新的内存空间并返回浅拷贝切片。掌握这些 API 对原数组的破坏性与返回值规则,是写出健壮代码和正确维护现代前端状态(如 React、Vue)的基础前提。

一、核心机制:原地修改(In-Place)与浅拷贝副本的区别

JavaScript 数组属于引用类型(Reference Type)。当声明 const list = ['A', 'B'] 时,栈内存中存储的仅仅是一个指向堆内存地址的指针。

任何数组操作都可以归为以下两类行为:

  • 破坏性修改(In-Place Mutating):直接寻址到既有堆内存空间,增加、剔除或移动原有元素。原数组的 length 和内容立即发生物理改变。若其他变量持有该数组引用,它们所观察到的数据也会同步变化。
  • 非破坏性返回(Pure / Immutable):将原数组视作只读数据源,在堆内存中申请一块全新地址,将所需元素浅拷贝进去并返回该新数组。原数组指针及其内存块完全不受干扰。
JS数组增删改查详解:push/splice/slice等方法对原数组的影响
图 1:堆栈内存视角——原地修改 (In-Place) 与浅拷贝返回新数组的底层机制

了解了堆栈内存原理后,我们分门别类按“增、删、改、查”四大操作维度,逐一拆解高频方法的行为特征、返回值及避坑要点。

二、“增”——数组元素的添加与原数组变化

向数组中添加元素时,主要区别在于添加的位置(尾端、首端、中间任意位置)以及是否允许改动原数组。

1. 尾端添加:push()(破坏性)

push() 接收一个或多个参数,将它们按序追加到数组末尾。

  • 原数组影响:直接修改原数组,长度相应增加。
  • 返回值:添加新元素后数组的最新长度(length),而不是数组本身。
// 示例:push 追加操作
const fruits = ['apple', 'banana'];
const result = fruits.push('cherry', 'date');

// 输出验证
console.log(fruits); // 结果:['apple', 'banana', 'cherry', 'date'] (原数组被改变)
console.log(result); // 结果:4 (返回新数组的长度,非数组)

2. 首端插入:unshift()(破坏性)

unshift() 将一个或多个元素添加到数组开头。

  • 原数组影响:直接修改原数组。为腾出起始槽位,引擎需要将原有的所有元素向后平移,存在索引位移成本。
  • 返回值:插入新元素后的最新长度。
// 示例:unshift 首端插入
const queue = ['second', 'third'];
const newLen = queue.unshift('first');

console.log(queue);  // 结果:['first', 'second', 'third'] (原数组被改变)
console.log(newLen); // 结果:3

3. 任意位置插入:splice(index, 0, ...items)(破坏性)

当 splice() 的第二个参数(删除数量)传入 0 时,它充当纯插入工具。

  • 原数组影响:从指定索引开始插入新元素,原数组直接扩容。
  • 返回值:由于没有删除任何元素,返回空数组 []。
// 示例:splice 中间定点插入
const letters = ['A', 'D'];
const removed = letters.splice(1, 0, 'B', 'C');

console.log(letters); // 结果:['A', 'B', 'C', 'D'] (原数组被改变)
console.log(removed); // 结果:[] (返回空数组)

4. 保持原数组不变的添加方案:concat() 与扩展运算符

如果业务要求不得改动原始数组(例如 React 的 setList),应使用以下非破坏性方案:

// 方案 A:concat 方法
const base = [1, 2];
const addedByConcat = base.concat(3, [4, 5]);
console.log(base);          // 结果:[1, 2] (原数组保持原样)
console.log(addedByConcat);  // 结果:[1, 2, 3, 4, 5] (全新数组)

// 方案 B:ES6 扩展运算符(推荐)
const addedBySpread = [...base, 3];
console.log(base);          // 结果:[1, 2] (原数组保持原样)
console.log(addedBySpread);  // 结果:[1, 2, 3] (全新数组)

三、“删”——数组元素的移除与截取差异

删除是开发中最容易引发副作用的环节。特别是 splice() 与 slice(),两者字面拼写仅相差一个字母,但内在机理截然相反。

1. 尾部剔除:pop() 与首部剔除:shift()(破坏性)

  • pop():移除末尾最后一个元素,原数组长度减 1;返回被移除的那一项。若数组为空,返回 undefined。
  • shift():移除开头第一个元素,剩余所有元素左移一位;返回被移除的那一项。
const stack = [10, 20, 30];
const last = stack.pop();
console.log(stack); // 结果:[10, 20] (原数组长度减少)
console.log(last);  // 结果:30 (返回被剔除项)

const first = stack.shift();
console.log(stack); // 结果:[20] (原数组被改变)
console.log(first); // 结果:10 (返回被剔除项)

2. 精准手术式删除:splice(start, deleteCount)(破坏性)

splice() 会直接从原数组中“剜掉”指定范围的数据,并将被切除的数据封装成新数组返回。

  • 原数组影响:指定区间元素被彻底移除,后续元素索引向前弥合,原数组发生改变。
  • 返回值:包含所有被删除元素组成的数组。若未删除任何项,返回 []。

3. 纯切片只读提取:slice(start, end)(非破坏性)

slice() 按照左闭右开区间 [start, end) 进行范围提取。

  • 原数组影响:原数组完全不发生任何改变,指针与内存数据保持不变。
  • 返回值:包含截取段元素浅拷贝的全新数组。
JS数组增删改查详解:push/splice/slice等方法对原数组的影响
图 2:splice() 原地切割增删 vs slice() 纯切片复制的步进推导

4. 常用无副作用删除:filter()

在实际项目(如待办事项删除、列表筛选)中,最为常用的非破坏性删除手段是 filter():

const todos = [
  { id: 1, text: '编写单元测试' },
  { id: 2, text: '重构老旧模块' },
  { id: 3, text: '发布上线版本' }
];

// 根据 ID 进行条件过滤删除(id !== 2)
const nextTodos = todos.filter(item => item.id !== 2);

console.log(todos.length);     // 结果:3 (原数组未被修改)
console.log(nextTodos.length); // 结果:2 (返回删除了目标项的全新数组)
JS数组增删改查详解:push/splice/slice等方法对原数组的影响
图 3:原地增删性能与索引位移机制对比:尾端操作 vs 首端操作

四、“改”——元素修改与顺序重排的陷阱

“改”不仅包括修改某个指定位置上的数值,也包括改变整个数组内部元素的相对顺序(排序与倒序)。

1. 下标索引直接赋值:arr[index] = val(破坏性)

通过下标直接指定新值是最原始的原地修改手段。原数组对应内存槽位的指针被覆写。

const colors = ['red', 'green', 'blue'];
colors[1] = 'yellow';
console.log(colors); // 结果:['red', 'yellow', 'blue'] (原数组被修改)
⚠️ 警惕 sort() 与 reverse() 的隐形破坏性:
许多初学者认为 arr.sort() 和 arr.reverse() 会返回一个排序/翻转后的“新数组”,从而直接书写类似 const sorted = original.sort() 的代码。实际上,这两个方法都是原地修改原数组的!同时它们会将同一个原数组引用作为返回值返回。这意味着 sorted === original 为 true,原数组原本的历史顺序已经被永久覆盖!
// 典型踩坑案例:sort 原地篡改
const scores = [80, 20, 100, 50];
const sortedScores = scores.sort((a, b) => a - b);

console.log(scores);        // 结果:[20, 50, 80, 100] (原数组顺序已被破坏!)
console.log(sortedScores === scores); // 结果:true (两者指向同一堆内存引用)

// 安全的做法:先拷贝再排序
const safeScores = [80, 20, 100, 50];
const pureSorted = [...safeScores].sort((a, b) => a - b);
console.log(safeScores);    // 结果:[80, 20, 100, 50] (原数组保持原样)
console.log(pureSorted);    // 结果:[20, 50, 80, 100] (独立的新排序数组)

2. ECMAScript 2023 新规:四大原生不可变操作方法

为了解决 JavaScript 长期以来修改数组必须手动 [...arr] 拷贝的繁琐问题,ECMAScript 2023 标准正式引入了 4 个原生非破坏性版本方法:

  • toSpliced(start, deleteCount, ...items):对应 splice(),但不改变原数组,直接返回修改后的新数组。
  • toSorted(compareFn):对应 sort(),不改变原数组,返回排好序的新数组。
  • toReversed():对应 reverse(),不改变原数组,返回倒序后的新数组。
  • with(index, value):对应 arr[i] = value,不改变原数组,返回替换指定索引位置元素后的新数组。
// ES2023 新标准示例
const originalNums = [3, 1, 4, 1, 5];

const s1 = originalNums.toSorted((a, b) => a - b);
console.log(originalNums); // 结果:[3, 1, 4, 1, 5] (完全未受影响)
console.log(s1);           // 结果:[1, 1, 3, 4, 5]

const s2 = originalNums.with(0, 999);
console.log(originalNums); // 结果:[3, 1, 4, 1, 5] (完全未受影响)
console.log(s2);           // 结果:[999, 1, 4, 1, 5]

五、“查”——只读检索与切片提取(天然安全)

查询类方法在设计理念上属于纯只读操作,没有任何内部逻辑会触碰或写入原数组的内存槽位。

  • slice(start, end):切片提取,返回截取的新浅拷贝数组。
  • indexOf(item) / lastIndexOf(item):返回指定项在数组中第一次/最后一次出现的索引;若未找到返回 -1。
  • includes(val):检测是否包含特定值,返回布尔值 true / false(支持精确识别 NaN)。
  • find(predicate):返回首个满足匹配条件的元素;无匹配返回 undefined。
  • findIndex(predicate):返回首个满足匹配条件的索引;无匹配返回 -1。
  • some() / every():逻辑断言,返回布尔值。
⚠️ 关键避坑提醒:浅拷贝(Shallow Copy)的对象引用陷阱
尽管 slice()、filter()、map() 或 [...arr] 不会修改原数组外壳的槽位和指针,但它们执行的是浅拷贝。如果数组中存放的是对象(Object)或数组(Array),新旧数组内部的每个槽位保存的仍然是同一个对象的堆内存地址!
如果在遍历或切片后,执行类似 copy[0].status = 'done',原数组 orig[0].status 也将同步变动。若需要彻底割裂内部嵌套数据,需配合深拷贝(如 structuredClone())使用。

六、综合决策矩阵与全量方法速查

在实际项目架构设计中,面对具体场景应如何迅速选型?以下决策树与速查表提供了直观指导:

JS数组增删改查详解:push/splice/slice等方法对原数组的影响
图 4:JavaScript 数组增删改查方法选型决策矩阵与场景速查
分类 方法名称 是否改变原数组 返回值类型及内容 典型适用场景
增 push(...items) 是 (In-Place) number (新增后数组 length) 局部队列构建、追加高吞吐日志
增 unshift(...items) 是 (In-Place) number (新增后数组 length) 小规模首端插入(大数据量慎用 O(n))
增 concat() / [...arr] 否 (Immutable) Array (拼接生成的全新数组) React / Redux / Vue 状态更新
删 pop() 是 (In-Place) Any (被弹出的末尾元素) 实现栈结构 (Stack LIFO)
删 shift() 是 (In-Place) Any (被弹出的首部元素) 实现先入先出队列 (FIFO)
删 / 增 / 改 splice(i, c, ...items) 是 (In-Place) Array (被剔除的所有元素组成的数组) 任意索引位置的原地缝合增删
删 / 查 slice(start, end) 否 (Immutable) Array (截取区间的全新切片) 分页切片、无损浅拷贝、提取片段
删 filter(predicate) 否 (Immutable) Array (满足条件的元素组成的新数组) 根据 ID 或状态非破坏性排除元素
改 sort() / reverse() 是 (In-Place) Array (原数组本身的引用) 性能敏感的单次局部重排
改 map(callback) 否 (Immutable) Array (转换映射后的全新数组) 非破坏性数据整形与按条件替换字段
改 (ES2023) toSorted() / toSpliced() / with() 否 (Immutable) Array (变更后的全新数组) 现代不可变数据处理首选标准 API
查 indexOf() / includes() 否 (只读) number / boolean 基础值存在性校验与定位
查 find() / findIndex() 否 (只读) Any / number 对象数组复杂条件检索

七、总结

区分 JavaScript 数组方法的原数组影响机制,核心就在于厘清“数据的生命周期与所有权”:

  1. 破坏性修改方法(push、pop、shift、unshift、splice、sort、reverse)适用于私有局部作用域、高性能计算或对内存分配极度敏感的场景。在大型应用中,避免在函数间传递此类数组并就地修改。
  2. 非破坏性方法(slice、concat、filter、map、扩展运算符以及 ES2023 的 toSorted、toSpliced、with)则是函数式编程、组件驱动框架和状态管理的核心基石,能够杜绝不可控的共享状态副作用。
  3. 在编写任何数组操作代码前,养成自问“此处是否允许修改原数组?”以及“该方法的返回值究竟是数组本身、新长度还是被删除的项?”的习惯,能彻底避开大部分由于数组引用混淆造成的工程隐患。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章