ES6中如何获取数组的差异?

30

所以我有两个数组:

const allLanguages = [ 'ES', 'EN', 'DE' ]
const usedLanguages = [ { id: 1, lang: 'EN' } ]
什么是最快的方法来创建一个新数组,该数组是这两个数组之间的差异?在传统的JavaScript中,您需要在另一个循环内部使用for循环,我想......
例如:
const availableLanguages = [ 'ES', 'DE' ]

或者是最快的运行时性能?如果这真的是你的目标——换句话说,这段代码被执行了数百万次,并且对你的应用程序至关重要——那么你最好编写一些 for 循环。 - user663031
4个回答

39

你可以使用filter()find()来返回过滤后的数组。

const allLanguages = [ 'ES', 'EN', 'DE' ]
const usedLanguages = [ { id: 1, lang: 'EN' } ]

var result = allLanguages.filter(e => !usedLanguages.find(a => e == a.lang));
console.log(result)

你也可以使用map()方法处理第二个数组,然后使用includes()方法过滤掉重复项。

const allLanguages = [ 'ES', 'EN', 'DE' ]
const usedLanguages = [ { id: 1, lang: 'EN' } ].map(e => e.lang);

var result = allLanguages.filter(e => !usedLanguages.includes(e));
console.log(result)


4
为了避免虚假性,您可能希望使用.some()而不是.find()。具体请参考.some()文档 - 4castle
some short circuits in the exact same way as find - 4castle
@4castle 你在说什么愚蠢的事情?什么东西是虚假的? - user663031
2
@torazaburo 我指的是find返回值的虚假性。在这种情况下,对象永远不会是虚假的,但通常类型转换是不必要的,并且如果情况不同,容易出现错误。 - 4castle
1
@shai,关于Internet Explorer的不兼容性问题,我不同意使用“不幸”这个词。它从设计和意图上就不符合标准。 - maoizm
显示剩余8条评论

15

基于集合的方法

受@Ori Drori出色答案的启发,这里提供一种纯粹基于集合的解决方案。

const all = new Set(allLanguages);
const used = new Set(usedLanguages.map(({lang}) => lang));

const availableLanguages = setDifference(all, used);

在哪里

const setDifference = (a, b) => new Set([...a].filter(x => !b.has(x)));

availableLanguages 将会是一个集合,所以如果你想将其作为数组来处理,你需要使用 Array.from 或者 [...map]

如果你想要让它更加函数式,那么

const not = fn => x => !fn(x);
const isIn = set => x => set.has(x);

现在开始编写

const setDifference = (a, b) => new Set([...a].filter(not(isIn(b))));

有些人可能认为这种方法更语义化或可读性更强。

然而,这些解决方案有些令人不满意,并且可能不够优化。尽管Set#has的时间复杂度是O(1),而findsome的时间复杂度为O(n),但总体性能仍为O(n),因为我们必须遍历a的所有元素。最好的方法是从另一个答案中建议的那样,从a中删除b的元素。这将是:

const setDifference = (a, b) => {
  const result = new Set(a);
  b.forEach(x => result.delete(x));
  return result;
}

我们不能使用 `reduce`,因为集合上没有这个方法,而且我们也不想把集合转换成数组再使用它。但我们可以使用 `forEach`,因为它在集合上可用。如果 `a` 更大,而 `b` 更小,这种替代方案会更好。
很可能未来某个版本的 JS 会内置此功能,允许您直接使用。
const availableLanguages = all.difference(used)

基于生成器的方法

最后,如果我们有兴趣探索更多 ES6 特性,我们可以编写一个生成器来生成非重复值,例如:

function* difference(array, excludes) {
  for (let x of array) 
    if (!excludes.includes(x)) yield x;
}

现在我们可以编写

console.log([...difference(allLanguages, usedLanguages)]);

如果有一个很长的语言列表,可能是一个接一个地出现,并且您想获得未使用语言的流,则可能建议使用此解决方案。

使用字典

如果想要在不使用集合的情况下进行O(1)的查找排除列表中的内容,则经典方法是预先计算一个字典:

const dict = Object.assign({}, 
    ...usedLanguages.map(({lang}) => ({[lang]: true})));

const availableLanguages = allLanguages.filter(lang => lang in dict);

如果这种计算字典的方式对你来说太过神秘,那么有些人会使用reduce

const dict = usedLanguages.reduce((obj, {lang}) => {
  obj[lang] = true;
  return obj;
}, {});

Nina 喜欢使用逗号运算符来编写这个。
const dict = usedLanguages.reduce((obj, {lang}) => (obj[lang] = true, obj), {});

这可以省略一些花括号。

或者,由于JS仍然有for循环 :-)

const dict = {};
for (x of usedLanguages) {
  dict[x.lang] = true;
}

嘿,你是说你想使用ES6的人。


我感觉我的教育已经完成了 :) 喜欢创建字典的神秘方式。 - Ori Drori
字典方法可以通过使用Object.create(null)来改进空对象,以便来自Object.prototype的内容不会产生误报。 - 4castle

7
您可以使用以下代码:
availableLanguages = allLanguages.filter((lang1) => !usedLanguages.some((lang2) => lang2.lang === lang1))

some函数是find的一个不太常用的相关函数,它更适用于在数组中检查是否至少有一个元素满足条件的情况。


4

使用Array#reduce迭代usedLanguages,起始值为new Set(allLanguages)。每次迭代从used Set中删除语言。将结果展开到数组中。时间复杂度为O(n + m),其中n是usedLanguages数组的长度,m是allLanguages的长度:

const allLanguages = [ 'ES', 'EN', 'DE' ];
const usedLanguages = [{ id: 1, lang: 'EN' }];

const result = [...usedLanguages.reduce((r, { lang }) => {
  r.delete(lang);
  return r;
}, new Set(allLanguages))];

console.log(result);


我的命运啊。我正在汇聚我微薄的ES6知识来提供一个答案,却唯一得到回应的是一个循环专家 :) - Ori Drori
你已经使用了展开运算符、集合、箭头函数和解构,但是你遗漏了 Promise、代理、类和生成器。 :-) - user663031
正如我说的,“我微薄的ES6知识”。然而,我决定悔改!不再这样了!我会使用循环!我要向大师学习。当我发现这个链接时,我的震惊和沮丧可想而知 - http://stackoverflow.com/a/40944997/5157454 - Ori Drori

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