Получить разницу массивов в ES6?

Итак, у меня есть два массива:

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

Какой самый быстрый способ создать новый массив, в котором разница между этими двумя? Я думаю, что в старой школе JavaScript вам нужно было бы сделать цикл внутри другого цикла for...

Например:

const availableLanguages = [ 'ES', 'DE' ]

Ответ 1

Вы можете использовать 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)

Ответ 2

Установленный подход

Вдохновленный отличным ответом @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), по сравнению с O(n) для find или some, общая производительность по-прежнему O(n), так как мы должны выполнять итерацию по всем элементам a. Было бы лучше удалить элементы из b из a, как было предложено в другом ответе. Это будет

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;
}, {});

Нине нравится писать это с помощью оператора запятой как

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

который сохраняет некоторые фигурные скобки.

Или, поскольку JS все еще имеет for петли: -):

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

Эй, ты тот, кто сказал, что хочешь использовать ES6.

Ответ 3

Вы можете использовать следующий код:

availableLanguages = allLanguages.filter((lang1) => !usedLanguages.some((lang2) => lang2.lang === lang1))

Функция some менее известна относительно find, которая лучше подходит для случаев, когда вы хотите проверить, удовлетворено ли условие хотя бы одним элементом массива.

Ответ 4

Итерировать usedLanguages с помощью Array#reduce, с new Set(allLanguages) в качестве начального значения. Удалить язык из used Установить на каждой итерации. Распределите результат в массив. Сложность - это 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);