JavaScript 合并两个数组:新建数组、原地追加与去重怎么选

编程狮(w3cschool.cn) 2026-09-11 17:55:16 浏览数 (36)
反馈

你要保留原数组,就用展开语法或 concat;需要修改原数组,就逐项 push。合并后的数组不会自动去重,也不会深拷贝里面的对象。这三个区别,比记住三种写法更重要。

数组合并先决定改不改原数组

分页接口返回了第二批商品,你准备把它接到现有列表后面。代码只写了一行,页面却出现重复商品;另一次修改新列表里的名称,旧列表也跟着变了。这些现象往往不是合并失败,而是你对顺序、对象引用和重复规则的预期没有写进代码。本文用商品列表拆解三种做法,并给出按编号合并的明确规则。编程狮这次把例子限定在普通数组,适用于现代浏览器和 Node.js,稀疏数组差异会单独说明。

一、合并两个数组前,先确定哪一个对象会改变

把两个购物篮倒进一个新篮子,与把第二篮倒进第一篮,看上去都有相同数量的商品,原来的篮子却处于不同状态。JavaScript 里也一样:返回新数组和修改原数组,是两种不同的接口约定。

例如列表组件接收父组件传下来的数组,你在子组件直接追加元素,父组件持有的也是同一个数组对象。即使暂时没有报错,后续撤销、比较前后状态、缓存结果都会变得难解释。因此,动手前先回答:调用者是否还需要旧数组?多个模块是否共享它?页面更新是否依赖数组引用变化?

还要约定合并顺序。本文所有示例都把第一批排在前面,第二批接在后面,不按价格或名称排序。如果输入可能为空值,应在数据入口确认 Array.isArray,而不是让每个合并函数偷偷把错误输入变成空数组。接口返回格式异常时尽早报错,比静默丢失半批数据容易排查。

二、用展开语法新建数组,写法直接但只复制一层

展开语法适合数量可控的普通列表。保存下列代码为 merge.mjs,使用 Node.js 运行,或把它放进浏览器控制台:

const first = [{ id: 1, name: "基础课" }];
const second = [{ id: 2, name: "进阶课" }];
const merged = [...first, ...second];
console.assert(merged.length === 2);
console.assert(first.length === 1);
console.assert(merged !== first);
merged[0].name = "基础课修订版";
console.assert(first[0].name === "基础课修订版");

前三个断言验证“新容器、旧数组长度不变”。最后一个断言说明浅拷贝:商品对象没有被复制,两个数组中的第一项指向同一个对象。修改对象字段会同时被两边观察到;给新数组追加第三项,则不会改变旧数组长度。

如果业务只修改一层对象属性,可以为需要修改的那一项重新构造对象,而不是为了拼接列表把整棵数据深拷贝。对象内还有嵌套属性时,一层对象展开仍然不够。先辨清更新路径,再决定复制哪几层。基础语法不熟悉时,可以配合 JavaScript 教程 理解数组与对象的引用关系。

三、concat 也返回新数组,但不要误认成递归展开

concat 的名字直接表达连接操作。它没有修改调用它的数组,所以必须接住返回值。只写一次调用却继续渲染原数组,是新手常见的“代码执行了、页面没变化”原因。

const a = [1, [2, 3]];
const b = [4];
const joined = a.concat(b);
console.log(joined); // [1, [2, 3], 4]
console.assert(joined[1] === a[1]);
console.assert(a.length === 2);

这里第二项仍然是嵌套数组。concat 对普通数组参数连接一层,不会递归拍平成所有数字。如果你真正需要扁平化,要另外定义深度,并使用适合的转换步骤,不能把“连接”和“拍平”当成同一个需求。

普通密集数组中,concat 和展开语法常可互换;稀疏数组则不同。数组中的空位不等于显式的 undefined:concat 通常保留空位,展开语法通过迭代会把空位读成 undefined。对接口返回的正常 JSON 数组,这通常不是主要矛盾;对手动设置 length 或使用 delete 留下空位的数组,就值得加一个专门用例。

也不要直接宣布某种写法一定最快。引擎版本、元素类型和数组大小都会影响表现。业务列表优先选团队容易理解的表达;性能敏感路径再用真实数据测量,记录运行时和输入规模。

四、原地追加和按编号去重,要分别写清规则

当你明确拥有目标数组、允许改变它时,逐项 push 可避免把整个源数组展开成函数参数。尤其对大数组,不建议依赖一次 push 加展开的写法,因为不同引擎有函数参数数量限制。

const target = [1, 2];
const incoming = [3, 4];
for (const value of incoming) target.push(value);
console.log(target); // [1, 2, 3, 4]

const oldRows = [{ id: 1, name: "旧名称" }];
const newRows = [{ id: 1, name: "新名称" }, { id: 2, name: "新课" }];
const byId = new Map();
for (const row of oldRows.concat(newRows)) byId.set(row.id, row);
const uniqueRows = [...byId.values()];
console.assert(uniqueRows.length === 2);
console.assert(uniqueRows[0].name === "新名称");

第二段给出数组去重的业务版本:编号相同,后出现的数据覆盖先出现的数据。Map 中已有键被更新时不会改变首次插入的位置,因此编号 1 仍排在前面。如果产品希望“更新过的排最后”,就需要先删除该键再写入,并把这个顺序约定加入测试。

数字或字符串可以用 Set 去重,对象则按引用判断。两个内容相同、分别创建的对象仍然是两个元素。另一个容易漏掉的边界是编号类型:数字 1 和字符串 "1" 是不同键;是否统一类型应该由接口契约决定,不能随意转换后掩盖数据错误。相关方法可以在 JavaScript 参考手册 中对照查阅。

需求 建议 需要接受的行为
保留旧数组 展开语法 元素对象仍共享引用
表达连接操作 concat 不递归展开嵌套数组
修改已有容器 循环 push 所有引用者都能看到追加
商品按编号合并 Map 必须决定覆盖和排序规则

先确定数据契约,再选操作

总结

合并两个数组之前,先约定是否修改输入,再约定重复项和顺序。展开语法与 concat 解决新建容器,循环 push 解决原地追加,Map 解决按业务编号保留哪一条数据。它们不是同一种操作的性能排名。

把“合并后有几项”“原数组长度是否变化”“同编号保留哪条”“修改对象会不会影响旧数据”写成四个断言,往往比换一种语法更有价值。下次处理分页列表时,先把这四项预期写下来,再接接口;如果还需要排序,把排序作为合并之后的独立步骤,别让两个规则互相干扰。

延伸学习

  1. JavaScript 速查手册 对照数组方法的返回值。
  2. 通过 JavaScript 基础实战 继续练习列表处理。
  3. 延伸阅读 JavaScript 数组去重的四种方法,区分基本值与对象去重。

常见问题

Q:合并后再修改新数组,旧数组一定安全吗?

A:数组容器是新的,不代表里面的对象也是新的。追加或删除新数组元素不会改变旧数组长度;修改共享对象的字段却会影响两边,应按具体更新路径复制对象。

Q:两个数组都是空的,需要额外判断吗?

A:正常空数组可以直接参与上述操作,结果仍是空数组。需要区分的是 null、undefined 和非数组输入,它们不是空数组,建议在接口入口校验。

Q:可以一边遍历数组,一边把它追加到自身吗?

A:不要用本文的循环方式这样做。数组迭代会观察新增元素,可能导致循环持续增长。源和目标可能相同时,先复制源数据,再执行原地追加。

0 人点赞