JS数组增删改查详解:push/splice/slice等方法对原数组的影响
深入解析JavaScript数组增删改查全量常用方法对原数组的影响,对比push、splice、slice等方法的底层执行机制、返回值、时间复杂度及不可变数据处理最佳实践。
push()、splice() 等方法会直接在既有内存上修改原数组并产生连带副作用,而 slice()、concat() 等方法则会开辟全新的内存空间并返回浅拷贝切片。掌握这些 API 对原数组的破坏性与返回值规则,是写出健壮代码和正确维护现代前端状态(如 React、Vue)的基础前提。
一、核心机制:原地修改(In-Place)与浅拷贝副本的区别
JavaScript 数组属于引用类型(Reference Type)。当声明 const list = ['A', 'B'] 时,栈内存中存储的仅仅是一个指向堆内存地址的指针。
任何数组操作都可以归为以下两类行为:
- 破坏性修改(In-Place Mutating):直接寻址到既有堆内存空间,增加、剔除或移动原有元素。原数组的
length和内容立即发生物理改变。若其他变量持有该数组引用,它们所观察到的数据也会同步变化。 - 非破坏性返回(Pure / Immutable):将原数组视作只读数据源,在堆内存中申请一块全新地址,将所需元素浅拷贝进去并返回该新数组。原数组指针及其内存块完全不受干扰。
了解了堆栈内存原理后,我们分门别类按“增、删、改、查”四大操作维度,逐一拆解高频方法的行为特征、返回值及避坑要点。
二、“增”——数组元素的添加与原数组变化
向数组中添加元素时,主要区别在于添加的位置(尾端、首端、中间任意位置)以及是否允许改动原数组。
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) 进行范围提取。
- 原数组影响:原数组完全不发生任何改变,指针与内存数据保持不变。
- 返回值:包含截取段元素浅拷贝的全新数组。
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 (返回删除了目标项的全新数组)
四、“改”——元素修改与顺序重排的陷阱
“改”不仅包括修改某个指定位置上的数值,也包括改变整个数组内部元素的相对顺序(排序与倒序)。
1. 下标索引直接赋值:arr[index] = val(破坏性)
通过下标直接指定新值是最原始的原地修改手段。原数组对应内存槽位的指针被覆写。
const colors = ['red', 'green', 'blue'];
colors[1] = 'yellow';
console.log(colors); // 结果:['red', 'yellow', 'blue'] (原数组被修改)
许多初学者认为
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():逻辑断言,返回布尔值。
尽管
slice()、filter()、map() 或 [...arr] 不会修改原数组外壳的槽位和指针,但它们执行的是浅拷贝。如果数组中存放的是对象(Object)或数组(Array),新旧数组内部的每个槽位保存的仍然是同一个对象的堆内存地址!如果在遍历或切片后,执行类似
copy[0].status = 'done',原数组 orig[0].status 也将同步变动。若需要彻底割裂内部嵌套数据,需配合深拷贝(如 structuredClone())使用。
六、综合决策矩阵与全量方法速查
在实际项目架构设计中,面对具体场景应如何迅速选型?以下决策树与速查表提供了直观指导:
| 分类 | 方法名称 | 是否改变原数组 | 返回值类型及内容 | 典型适用场景 |
|---|---|---|---|---|
| 增 | 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 数组方法的原数组影响机制,核心就在于厘清“数据的生命周期与所有权”:
- 破坏性修改方法(
push、pop、shift、unshift、splice、sort、reverse)适用于私有局部作用域、高性能计算或对内存分配极度敏感的场景。在大型应用中,避免在函数间传递此类数组并就地修改。 - 非破坏性方法(
slice、concat、filter、map、扩展运算符以及 ES2023 的toSorted、toSpliced、with)则是函数式编程、组件驱动框架和状态管理的核心基石,能够杜绝不可控的共享状态副作用。 - 在编写任何数组操作代码前,养成自问“此处是否允许修改原数组?”以及“该方法的返回值究竟是数组本身、新长度还是被删除的项?”的习惯,能彻底避开大部分由于数组引用混淆造成的工程隐患。
JS处理含Emoji或中文的字符串转数组乱码问题
2026-11-05 16:12:28
JS怎么使用展开运算符合并对象
2026-11-05 14:42:18
ES6+环境下判断JS对象为空的简洁写法
2026-11-05 12:12:23
怎么通过replacer函数自定义JS中JSON的格式化输出
2026-11-05 11:19:44
JS正则表达式基础:字面量与RegExp构造函数的区别及常用元字符
2026-11-05 11:16:58
相关标签:








![JS创建二维数组:为什么Array(n).fill([])会导致数据联动](https://img.javascript.cn/tit/articles/13236/20261102/f618c991891ff37efb12df6af2ce0ccb0da8779a907fe1d2f7275d60c84fdd77.webp)