如何在JavaScript中合并两个数组并去重

1962

我有两个 JavaScript 数组:

var array1 = ["Vijendra","Singh"];
var array2 = ["Singh", "Shakya"];

我希望输出结果是:

var array3 = ["Vijendra","Singh","Shakya"];

输出数组应该删除重复的单词。

我如何在JavaScript中合并两个数组,以便获取每个数组中唯一的项,并以它们插入原始数组的相同顺序返回?


32
在发布新答案之前,请考虑此问题已经有75个以上的答案。请确保您的答案提供的信息不在现有答案中。 - janniks
5
结果为 [1, 2, 3, 4],这行代码使用了 ES6 的 Set 数据结构和展开运算符将两个数组去重合并。 - Denis Giffeler
如果您想要一个更通用的解决方案,也包括深度合并,请查看这个问题。一些答案也涵盖了数组。 - Martin Braun
简而言之 - 合并数组 (ba) : a=a.concat(b); 从数组 a 中删除重复项 (就地操作) : a=a.filter((i,p)=>a.indexOf(i)===p); - ashleedawg
如果你不想再有更多答案,可以关闭问题。 - Janos Vinceller
问题仍然开放,等待不同、创新、前沿的答案。这也是免责声明的原因。 - Rodrigo Rodrigues
92个回答

2284

仅合并数组(不移除重复项)

ES5版本使用Array.concat

var array1 = ["Vijendra", "Singh"];
var array2 = ["Singh", "Shakya"];

array1 = array1.concat(array2);

console.log(array1);

2023更新

原始答案是多年前的。ES6已完全支持,IE终于退出历史舞台。以下是合并基本数组和对象数组的最简单方法:

const merge = (a, b, predicate = (a, b) => a === b) => {
    const c = [...a]; // copy to avoid side effects
    // add all items from B to copy C if they're not already present
    b.forEach((bItem) => (c.some((cItem) => predicate(bItem, cItem)) ? null : c.push(bItem)))
    return c;
}

merge(['a', 'b', 'c'], ['c', 'x', 'd']);
// => ['a', 'b', 'c', 'x', 'd']

merge([{id: 1}, {id: 2}], [{id: 2}, {id: 3}], (a, b) => a.id === b.id);
// [{id: 1}, {id: 2}, {id: 3}]

原始答案

ES6版本使用解构赋值

const array1 = ["Vijendra","Singh"];
const array2 = ["Singh", "Shakya"];
const array3 = [...array1, ...array2];

由于没有“内置”的方法来删除重复项(ECMA-262实际上有Array.forEach,这对此操作非常好),我们必须手动完成。请注意,这会污染Array原型,请谨慎使用。
Array.prototype.unique = function() {
    var a = this.concat();
    for(var i=0; i<a.length; ++i) {
        for(var j=i+1; j<a.length; ++j) {
            if(a[i] === a[j])
                a.splice(j--, 1);
        }
    }

    return a;
};

然后,要使用它:

var array1 = ["Vijendra","Singh"];
var array2 = ["Singh", "Shakya"];
// Merges both arrays and gets unique items
var array3 = array1.concat(array2).unique(); 

这样做还能保持数组的顺序(即不需要排序)。
由于很多人对Array.prototype的原型扩展和for in循环感到烦恼,所以这里提供了一种更少侵入性的使用方法:
function arrayUnique(array) {
    var a = array.concat();
    for(var i=0; i<a.length; ++i) {
        for(var j=i+1; j<a.length; ++j) {
            if(a[i] === a[j])
                a.splice(j--, 1);
        }
    }

    return a;
}

var array1 = ["Vijendra","Singh"];
var array2 = ["Singh", "Shakya"];
    // Merges both arrays and gets unique items
var array3 = arrayUnique(array1.concat(array2));

对于那些有幸使用支持ES5的浏览器的人来说,你可以像这样使用Object.defineProperty
Object.defineProperty(Array.prototype, 'unique', {
    enumerable: false,
    configurable: false,
    writable: false,
    value: function() {
        var a = this.concat();
        for(var i=0; i<a.length; ++i) {
            for(var j=i+1; j<a.length; ++j) {
                if(a[i] === a[j])
                    a.splice(j--, 1);
            }
        }

        return a;
    }
});

374
请注意,这个算法的时间复杂度是 O(n^2)。 - Gumbo
9
假设 [a, b, c][x, b, d] 是两个数组(请用引号表示),则它们的合并结果为 [a, b, c, x, b, d]。使用 unique() 函数后,输出结果为 [a, c, x, b, d],但是我认为这并不能保持原有顺序,我相信 OP 想要的输出结果应该是 [a, b, c, x, d] - Amarghosh
10
我最初点赞了这个帖子,但现在改变了主意。将原型分配给Array.prototype会导致“for ... in”语句失效。因此,最好的解决方案可能是使用这样的函数,但不要将其分配为原型。有些人可能会认为,“for ... in”语句本来就不应该用于迭代数组元素,但人们经常以这种方式使用它们,所以至少应该谨慎使用这个解决方案。 - Code Commander
17
在使用 hasOwnProperty 时,应该始终与 for ... in 一起使用,此时原型方法是可以的。 - mulllhausen
2
只需按照另一个答案中描述的方式使用Babel和Set()即可。 - cmcculloh
显示剩余17条评论

661

78
或许比下划线库更好的是与 API 兼容的 lodash 库。 - Brian M. Hunt
3
这是从 Lodash 文档中的摘录: "返回一个新的数组,其中包含一个或多个原始数组中存在的独特值,并以它们出现的顺序排序。" - Richard Ayotte
4
我更喜欢使用underscore.js。我最终使用的是underscore.flatten(),它比union更好,因为它可以接受一个数组对象作为参数。 - weaver
10
@weaver _.flatten会合并数组,但不会进行“去重”。 - GijsjanB
10
lodash与排名第一的答案之间的快速性能比较:http://jsperf.com/merge-two-arrays-keeping-only-unique-values - slickplaid
显示剩余5条评论

427
[...array1,...array2] //   =>  don't remove duplication 

或者

[...new Set([...array1 ,...array2])]; //   => remove duplication

3
第一/第二个示例根本没有union + 第一个示例对于大型Array会导致堆栈溢出 + 第三个示例非常缓慢并且消耗大量内存,因为必须构建两个中间的Array + 第三个示例只能用于在编译时已知数量的Arrayunion - user6445533
那么你会怎么做呢? - David Noreña
9
请注意,对于集合来说,除非它们是相同的对象引用,否则无法去重具有相同键值对的两个对象。 - Jun
14
无法使用对象数组进行合并,因为它只会合并对象的引用,并不关心对象本身是否相等。 - W Biggs
这真的是最好的答案,我尝试过使用array.find按id查找、array.concat等方法,但都不如你的解决方案快速,最棒的人,谢谢你。 - x-magix
显示剩余4条评论

421

首先将这两个数组连接起来,然后只筛选出唯一的项:

var a = [1, 2, 3], b = [101, 2, 1, 10]
var c = a.concat(b)
var d = c.filter((item, pos) => c.indexOf(item) === pos)

console.log(d) // d is [1, 2, 3, 101, 10]

编辑

如建议的那样,更有效率的解决方案是在将 ab 连接之前过滤掉 b 中的唯一项:

var a = [1, 2, 3], b = [101, 2, 1, 10]
var c = a.concat(b.filter((item) => a.indexOf(item) < 0))

console.log(c) // c is [1, 2, 3, 101, 10]


7
这里的原始解决方案具有在每个源数组内去重的好处。我猜这取决于您的情境,您会使用哪个。 - theGecko
1
提醒一下,对于那些担心IE6的人,请查看当前浏览器使用情况http://caniuse.com/usage-table。 - pmrotule
23
@Andrew: 更好的写法如下:1. var c = [...a, ...b.filter(o => !~a.indexOf(o))]; 2. var c = [...new Set([...a, ...b])]; - 7vujy0f0hy
如果更加晦涩和简短更好的话,那么可能吗? - Andrew
我们能在关联数组中使用它吗? let a = [{id: 1, name:"sam", id:2, name: "roy" }]; let b = [{id: 1, name:"sam", id:3, name: "john" }] 输出应该是 [{id: 1, name:"sam", id:2, name: "roy" , id:3, name: "john"}] - AbhimanuSharma
显示剩余3条评论

284
这是一个使用 扩展运算符 和数组泛型的 ECMAScript 6 解决方案。
目前仅适用于 Firefox,并可能与 Internet Explorer Technical Preview 兼容。
但如果您使用 Babel,您现在就可以使用它。

const input = [
  [1, 2, 3],
  [101, 2, 1, 10],
  [2, 1]
];
const mergeDedupe = (arr) => {
  return [...new Set([].concat(...arr))];
}

console.log('output', mergeDedupe(input));


18
应将此内容添加到答案中。这种解决方案比目前可能的解决方案更加高效和优雅,但它是我们必将能够做到的(也应该这样做,以保持在这个领域的竞争力)。 - heckascript
6
既然问题是从2009年提出的,很难说这应该成为被认可的答案。但是,是的,这不仅更具“性能”,而且更加“优雅”。 - Cezar Augusto
20
可以使用Array.from代替展开运算符:Array.from(new Set([].concat(...arr))) - Henry Blyth
1
这非常优雅。不幸的是,Typescript目前还不支持这个。https://dev59.com/NVwX5IYBdhLWcg3wvBdj#33464709 - Ben Carp
1
为什么不直接使用 return [...new Set(arr)]; 而不是 return [...new Set([].concat(...arr))];?并不是说这样做是错误的,只是想知道为什么你要这样做。 - user210757
显示剩余9条评论

143
使用Set(ECMAScript 2015),就像这样简单:

const array1 = ["Vijendra", "Singh"];
const array2 = ["Singh", "Shakya"];
console.log(Array.from(new Set(array1.concat(array2))));


7
我认为这是使用ES6的“被接受的答案”。 - mwieczorek
12
如何这样写:const array3 = [...new Set(array1.concat(array2))]意思是将 array1array2 合并成一个数组,并去除其中的重复元素,然后将结果赋值给 array3 - Robby Cornelissen
8
如果你正在使用对象数组,那么它无法运行。 - carkod
1
用于合并不重复对象的代码:stackoverflow.com/a/54134237/3131433 - Rakibul Haq

47

你可以使用ECMAScript 6轻松完成它,

var array1 = ["Vijendra", "Singh"];
var array2 = ["Singh", "Shakya"];
var array3 = [...new Set([...array1 ,...array2])];
console.log(array3); // ["Vijendra", "Singh", "Shakya"];
  • 使用 扩展操作符 来连接数组。
  • 使用 Set 来创建一个不重复的元素集合。
  • 再次使用扩展操作符将 Set 转换为数组。

3
我收到了一个错误:类型 'Set<string>' 不是数组类型。 - gattsbr
3
如果出于某种原因您不想使用扩展运算符,也可以使用以下代码:Array.from(new Set(array1.concat(array2)))。这将合并两个数组并去除重复项。 - kba
@gattsbr,在TypeScript中的tsconfig.json文件中,你可以在“compilerOptions”中添加"downlevelIteration": true来启用下级迭代。 - Vince

43
这里对循环进行了一些不同的处理方式。在最新版Chrome中进行了一些优化后,这是解决两个数组并集的最快方法(Chrome 38.0.2111)。

JSPerf:“合并两个数组,仅保留唯一值”(已存档)

var array1 = ["Vijendra", "Singh"];
var array2 = ["Singh", "Shakya"];
var array3 = [];

var arr = array1.concat(array2),
  len = arr.length;

while (len--) {
  var itm = arr[len];
  if (array3.indexOf(itm) === -1) {
    array3.unshift(itm);
  }
}

while循环:每秒约589k次操作
过滤器:每秒约445k次操作
lodash:每秒约308k次操作
for循环:每秒约225k次操作

一条评论指出,我的设置变量之一导致循环领先于其他操作,因为它不必初始化一个空数组来写入。我同意这一点,所以我重写了测试以平衡竞争条件,并包括了一个更快的选项。

JSPerf:“合并两个数组,仅保留唯一值”(已归档)

let whileLoopAlt = function (array1, array2) {
    const array3 = array1.slice(0);
    let len1 = array1.length;
    let len2 = array2.length;
    const assoc = {};

    while (len1--) {
        assoc[array1[len1]] = null;
    }

    while (len2--) {
        let itm = array2[len2];

        if (assoc[itm] === undefined) { // Eliminate the indexOf call
            array3.push(itm);
            assoc[itm] = null;
        }
    }

    return array3;
};

在这个备选方案中,我将一个答案的关联数组解决方案与第二个循环结合起来,以消除在循环中使用.indexOf()调用时导致速度变慢的情况,并包含其他用户在他们的答案中建议的一些优化。
顶部答案中使用双重循环(i-1)仍然明显较慢。lodash仍然表现出色,我仍然会推荐它给那些不介意向项目添加库的人。对于那些不想要的人,我的while循环仍然是一个很好的答案,filter答案在这里表现非常强劲,在最新的Canary Chrome(44.0.2360)测试中,击败了所有答案。
如果您想加快速度,请查看Mike's answerDan Stocker's answer。在经过几乎所有可行答案的测试后,它们是迄今为止最快的。

你的方法存在一个漏洞:将array3的创建放在设置阶段,而这个成本只应该是基于while循环解决方案的一部分。如果将此1行代码移动,你的解决方案的速度将下降到基于for循环的解决方案的速度。我知道数组是可以重复使用的,但也许其他算法也能从不必声明和初始化每个必要的构建块中受益。 - doldt
我同意你的前提@doldt,但不同意你的结果。循环删除条目存在根本设计缺陷,因为在删除项目后必须重新检查数组的长度,导致执行时间变慢。倒序工作的while循环没有这些影响。这是一个例子,尽可能删除设置变量,而不会太大程度地改变它们的原始答案:http://jsperf.com/merge-two-arrays-keeping-only-unique-values/19 - slickplaid
@ slickplaid 链接的测试为空,并且在 jsperf 的下一个版本中,while 循环挂起。 - doldt
@ slickpaid 谢谢,扩展性能页面做得很好,非常全面! - doldt
1
@ slickplaid 感谢您设置了扩展性能页面。除非我漏掉了什么,否则“whileLoopAlt2”函数不起作用?它创建一个包含第一个数组和第二个数组(以相反的顺序)的新数组。为避免混淆,我进行了另一次修订,删除了损坏的函数。我还添加了一个附加示例:http://jsperf.com/merge-two-arrays-keeping-only-unique-values/22 - Stephen S
显示剩余3条评论

38

我简化了这个答案中的最佳部分,并将其转化为一个好用的函数:


function mergeUnique(arr1, arr2){
    return arr1.concat(arr2.filter(function (item) {
        return arr1.indexOf(item) === -1;
    }));
}

3
我认为这比被接受的答案更加简洁。此外,它似乎在 ECMAScript 5.1 + 中支持过滤器,而这现在得到了广泛的支持。 - Tom Fobear
1
这要简明得多。 - Mox
4
合并唯一值的函数: const mergeUnique = (a, b) => a.concat(b.filter(v => a.indexOf(v) === -1)) - mad.meesh
3
这并不会从arr1中移除重复的元素,它只会添加来自arr2的独特元素。 - Konstantin Glukhov
可以使用 findIndexfilter() 函数中通过对象属性值查找对象的索引,如下所示: const index = arr1.findIndex(i => i.id === item.id); - askepott

35
ES6提供了一种通过解构和set来合并多个数组且不包含重复项的单行解决方案。

The ES6提供了一种通过解构和set来合并多个数组且不包含重复项的单行解决方案。

const array1 = ['a','b','c'];
const array2 = ['c','c','d','e'];
const array3 = [...new Set([...array1,...array2])];
console.log(array3); // ["a", "b", "c", "d", "e"]

1
这并没有为2016年已经提供的相同答案增添任何内容。 - Forage
3
不适用于对象数组。 - Flummiboy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接