Как сравнить массивы в JavaScript?
Я хочу сравнить два массива... желательно, эффективно. Ничего сложного, просто нужно получить true
, если они идентичны, и false
, если нет. Неудивительно, что оператор сравнения не работает должным образом.
Вот пример кода:
var a1 = [1,2,3];
var a2 = [1,2,3];
console.log(a1 == a2); // Возвращает false
console.log(JSON.stringify(a1) == JSON.stringify(a2)); // Возвращает true
Сравнение массивов с помощью JSON кодирования работает, но есть ли более быстрый или "лучший" способ просто сравнить массивы, не перебирая каждое значение?
5 ответ(ов)
Я думаю, что это самый простой способ сделать сравнение, используя JSON.stringify
, и в некоторых ситуациях это может быть наилучшим решением:
JSON.stringify(a1) === JSON.stringify(a2);
Этот код преобразует объекты a1
и a2
в строки, чтобы их можно было сравнить. В большинстве случаев порядок свойств важен, поэтому можно отсортировать объект с помощью алгоритма сортировки, показанного в одном из предыдущих ответов.
Обратите внимание, что вы больше не сравниваете объекты, а сравниваете их строковые представления. Это может быть не совсем то, что вам нужно.
Практический подход
Я считаю неправильным называть конкретную реализацию "Правильным Решением™", если она "правильна" только в том смысле, что является корректной по сравнению с "неправильным" решением. Решение Томаша ясно улучшает сравнение массивов на основе строк, но это не означает, что оно объективно "правильное". Что вообще означает правильный подход? Является ли он самым быстрым? Самым гибким? Самым простым для понимания? Самым легким для отладки? Использует ли он наименьшее количество операций? Имеет ли он какие-либо побочные эффекты? Ни одно решение не может иметь все эти преимущества одновременно.
Томаш может сказать, что его решение быстрое, но я бы также сказал, что оно чрезмерно усложнено. Оно пытается быть универсальным решением, которое работает для всех массивов, вложенных или нет. Фактически, оно даже принимает больше, чем просто массивы на вход, и все равно пытается дать "корректный" ответ.
Генерация для повторного использования
Мой ответ будет подходить к проблеме иначе. Я начну с обобщенной процедуры arrayCompare
, которая только и делает, что проходит по массивам. С этого момента мы будем строить наши другие базовые функции сравнения, такие как arrayEqual
и arrayDeepEqual
и т.д.
// arrayCompare :: (a -> a -> Bool) -> [a] -> [a] -> Bool
const arrayCompare = f => ([x,...xs]) => ([y,...ys]) =>
x === undefined && y === undefined
? true
: Boolean(f(x)(y)) && arrayCompare(f)(xs)(ys)
На мой взгляд, лучший код не нуждается в комментариях, и это не исключение. Здесь так мало происходящего, что вы можете понять поведение этой процедуры с почти нулевым усилием. Конечно, некоторый синтаксис ES6 может показаться вам непонятным, но это только потому, что ES6 относительно нов.
Как и предполагает тип, arrayCompare
принимает функцию сравнения f
и два входных массива xs
и ys
. По большей части, все, что мы делаем, это вызываем f(x)(y)
для каждого элемента во входных массивах. Мы возвращаем false
на раннем этапе, если функция, определенная пользователем f
, возвращает false
— благодаря краткому замыкающему вычислению &&
. Так что да, это означает, что компаратор может остановить итерацию раньше и избежать перебора оставшейся части входного массива, если это не нужно.
Стр comparing
Следующий шаг — используя нашу функцию arrayCompare
, мы можем легко создать другие функции, которые нам могут понадобиться. Начнем с элементарной arrayEqual
…
// equal :: a -> a -> Bool
const equal = x => y =>
x === y // обратите внимание: тройное равенство
// arrayEqual :: [a] -> [a] -> Bool
const arrayEqual =
arrayCompare(equal)
const xs = [1,2,3]
const ys = [1,2,3]
console.log(arrayEqual(xs)(ys)) //=> true
// (1 === 1) && (2 === 2) && (3 === 3) //=> true
const zs = ['1','2','3']
console.log(arrayEqual(xs)(zs)) //=> false
// (1 === '1') //=> false
Просто как это. arrayEqual
можно определить с помощью arrayCompare
и функции компаратора, которая сравнивает a
с b
, используя ===
(для строгого равенства).
Обратите внимание, что мы также определяем equal
как отдельную функцию. Это подчеркивает роль arrayCompare
как функции более высокого порядка, позволяющей использовать наш компаратор первого порядка в контексте другого типа данных (Массив).
Нечеткое сравнение
Мы могли бы так же легко определить arrayLooseEqual
, используя ==
вместо этого. Теперь, когда мы сравниваем 1
(число) и '1'
(строку), результат будет true
…
// looseEqual :: a -> a -> Bool
const looseEqual = x => y =>
x == y // обратите внимание: двойное равенство
// arrayLooseEqual :: [a] -> [a] -> Bool
const arrayLooseEqual =
arrayCompare(looseEqual)
const xs = [1,2,3]
const ys = ['1','2','3']
console.log(arrayLooseEqual(xs)(ys)) //=> true
// (1 == '1') && (2 == '2') && (3 == '3') //=> true
Глубокое сравнение (рекурсивное)
Вы, вероятно, заметили, что это только поверхностное сравнение. Наверняка, решение Томаша — это "Правильный Способ™", потому что оно выполняет неявное глубокое сравнение, верно?
Наше arrayCompare
достаточно универсально, чтобы использовать его так, чтобы сделать тест на глубокое равенство легким …
// isArray :: a -> Bool
const isArray =
Array.isArray
// arrayDeepCompare :: (a -> a -> Bool) -> [a] -> [a] -> Bool
const arrayDeepCompare = f =>
arrayCompare((a) => (b) =>
isArray(a) && isArray(b)
? arrayDeepCompare(f)(a)(b)
: f(a)(b))
const xs = [1,[2,[3]]]
const ys = [1,[2,['3']]]
console.log(arrayDeepCompare(equal)(xs)(ys)) //=> false
// (1 === 1) && (2 === 2) && (3 === '3') //=> false
console.log(arrayDeepCompare(looseEqual)(xs)(ys)) //=> true
// (1 == 1) && (2 == 2) && (3 == '3') //=> true
Просто как это. Мы строим глубокий компаратор с использованием другой функции более высокого порядка. В этот раз мы оборачиваем arrayCompare
, используя настраиваемый компаратор, который будет проверять, являются ли a
и b
массивами. Если да, то применяем arrayDeepCompare
повторно, в противном случае сравниваем a
и b
с помощью заданного пользователем компаратора (f
). Это позволяет нам сохранить поведение глубокого сравнения отдельно от того, как мы фактически сравниваем отдельные элементы. То есть, как показывает предыдущий пример, мы можем производить глубокое сравнение, используя equal
, looseEqual
или любой другой компаратор, который мы создадим.
Поскольку arrayDeepCompare
курирован, мы можем частично применять его так же, как это делали в предыдущих примерах.
// arrayDeepEqual :: [a] -> [a] -> Bool
const arrayDeepEqual =
arrayDeepCompare(equal)
// arrayDeepLooseEqual :: [a] -> [a] -> Bool
const arrayDeepLooseEqual =
arrayDeepCompare(looseEqual)
На мой взгляд, это уже явное улучшение по сравнению с решением Томаша, поскольку я могу явно выбрать поверхностное или глубокое сравнение для своих массивов, по мере необходимости.
Сравнение объектов (пример)
А что, если у вас есть массив объектов или что-то подобное? Возможно, вы хотите считать эти массивы "равными", если каждый объект имеет одно и то же значение id
…
// idEqual :: {id: Number} -> {id: Number} -> Bool
const idEqual = x => y =>
x.id !== undefined && x.id === y.id
// arrayIdEqual :: [a] -> [a] -> Bool
const arrayIdEqual =
arrayCompare(idEqual)
const xs = [{id:1}, {id:2}]
const ys = [{id:1}, {id:2}]
console.log(arrayIdEqual(xs)(ys)) //=> true
// (1 === 1) && (2 === 2) //=> true
const zs = [{id:1}, {id:6}]
console.log(arrayIdEqual(xs)(zs)) //=> false
// (1 === 1) && (2 === 6) //=> false
Просто как это. Здесь я использовал обычные объекты JS, но этот тип компаратора может работать с любым типом объектов, даже с вашими пользовательскими объектами. Решение Томаша необходимо полностью переработать, чтобы поддерживать такой тип теста равенства.
Глубокий массив с объектами? Не проблема. Мы построили высоко универсальные, обобщенные функции, поэтому они будут работать в широком диапазоне случаев.
const xs = [{id:1}, [{id:2}]]
const ys = [{id:1}, [{id:2}]]
console.log(arrayCompare(idEqual)(xs)(ys)) //=> false
console.log(arrayDeepCompare(idEqual)(xs)(ys)) //=> true
Произвольное сравнение (пример)
Или что, если вы хотите сделать какое-то совершенно произвольное сравнение? Возможно, я хочу знать, больше ли каждый x
, чем каждый y
…
// gt :: Number -> Number -> Bool
const gt = x => y =>
x > y
// arrayGt :: [a] -> [a] -> Bool
const arrayGt = arrayCompare(gt)
const xs = [5,10,20]
const ys = [2,4,8]
console.log(arrayGt(xs)(ys)) //=> true
// (5 > 2) && (10 > 4) && (20 > 8) //=> true
const zs = [6,12,24]
console.log(arrayGt(xs)(zs)) //=> false
// (5 > 6) //=> false
Меньше — значит больше
Вы можете видеть, что мы на самом деле делаем больше с меньшим количеством кода. В arrayCompare
нет ничего сложного, и каждый из пользовательских компараторов, которые мы создали, имеет очень простую реализацию.
С легкостью мы можем определить, как мы хотим сравнивать два массива — поверхностно, глубоко, строго, не строго, по некоторому свойству объекта или по какому-то произвольному вычислению, или любую комбинацию из этих подходов — все с использованием одной процедуры, arrayCompare
. Возможно, даже придумать компаратор на основе RegExp
! Я знаю, как дети обожают эти регулярные выражения…
Является ли это самым быстрым? Нет. Но, вероятно, и не должно быть. Если скорость — единственный критерий, по которому измеряется качество нашего кода, то много действительно хорошего кода будет отброшено — вот почему я называю этот подход Практическим Подходом. Или, может быть, более справедливо, Один Практический Подход. Это описание подходит для этого ответа, потому что я не говорю, что этот ответ практичен только
Не совсем ясно, что вы имеете в виду под "идентичными". Например, являются ли массивы a
и b
, приведенные ниже, идентичными (обратите внимание на вложенные массивы)?
var a = ["foo", ["bar"]], b = ["foo", ["bar"]];
Вот оптимизированная функция сравнения массивов, которая сравнивает соответствующие элементы каждого массива по очереди, используя строгое равенство, и не выполняет рекурсивное сравнение элементов массива, которые сами являются массивами. Это означает, что для приведенного выше примера вызов arraysIdentical(a, b)
вернет false
. Функция работает в общем случае, в отличие от решений на основе JSON и join()
:
function arraysIdentical(a, b) {
var i = a.length;
if (i != b.length) return false;
while (i--) {
if (a[i] !== b[i]) return false;
}
return true;
};
Таким образом, если ваша цель — сравнить массивы с учетом вложенных структур, вам может понадобиться более сложный подход, который рекурсивно проверяет элементы, если они тоже массивы.
Исходя из ответа Томаша Зато, я согласен с тем, что просто перебор массивов - самый быстрый способ. Кроме того (как уже отметили другие), функцию следует называть equals
или equal
, а не compare
. В связи с этим, я модифицировал функцию, чтобы она могла сравнивать массивы на предмет схожести, то есть, у них должны быть одинаковые элементы, но в произвольном порядке. Поделился ее реализацией для всеобщего обозрения.
Array.prototype.equals = function (array, strict) {
if (!array)
return false;
if (arguments.length == 1)
strict = true;
if (this.length != array.length)
return false;
for (var i = 0; i < this.length; i++) {
if (this[i] instanceof Array && array[i] instanceof Array) {
if (!this[i].equals(array[i], strict))
return false;
}
else if (strict && this[i] != array[i]) {
return false;
}
else if (!strict) {
return this.sort().equals(array.sort(), true);
}
}
return true;
}
Эта функция принимает дополнительный параметр strict
, который по умолчанию равен true
. Параметр strict
определяет, должны ли массивы быть полностью равны как по содержимому, так и по порядку этих содержимых, или же они могут просто содержать одинаковые элементы.
Пример:
var arr1 = [1, 2, 3, 4];
var arr2 = [2, 1, 4, 3]; // Слабо равен 1
var arr3 = [2, 2, 3, 4]; // Не равен 1
var arr4 = [1, 2, 3, 4]; // Строго равен 1
arr1.equals(arr2); // false
arr1.equals(arr2, false); // true
arr1.equals(arr3); // false
arr1.equals(arr3, false); // false
arr1.equals(arr4); // true
arr1.equals(arr4, false); // true
Я также создал быструю jsfiddle с этой функцией и примером:
Вопрос: Как правильно проверить, равны ли два массива, игнорируя порядок, и учитывая типы элементов?
Ответ:
Для проверки равенства двух массивов, где порядок элементов не имеет значения, можно воспользоваться методом join()
, как показано в следующем коде:
function checkArrays(arrA, arrB) {
// Проверяем, отличаются ли длины массивов
if (arrA.length !== arrB.length) return false;
// Копируем массивы, сортируем и соединяем в строку для сравнения
var cA = arrA.slice().sort().join(",");
var cB = arrB.slice().sort().join(",");
return cA === cB;
}
var a = [1, 2, 3, 4, 5];
var b = [5, 4, 3, 2, 1];
var c = [1, 2, 3, 4];
var d = [1, 2, 3, 4, 6];
var e = ["1", "2", "3", "4", "5"]; // будет возвращено true
console.log(checkArrays(a, b)); //true
console.log(checkArrays(a, c)); //false
console.log(checkArrays(a, d)); //false
console.log(checkArrays(a, e)); //true
Однако следует учесть, что данный способ не учитывает типы данных, как видно из последнего примера. Если вам важно, чтобы типы элементов совпадали, то следует использовать цикл для проверки каждого элемента:
function checkArrays(arrA, arrB) {
// Проверяем, отличаются ли длины массивов
if (arrA.length !== arrB.length) return false;
// Сортируем массивы без соединения в строку
var cA = arrA.slice().sort();
var cB = arrB.slice().sort();
for (var i = 0; i < cA.length; i++) {
if (cA[i] !== cB[i]) return false; // сравнение с учетом типов
}
return true;
}
var a = [1, 2, 3, 4, 5];
var b = [5, 4, 3, 2, 1];
var c = [1, 2, 3, 4];
var d = [1, 2, 3, 4, 6];
var e = ["1", "2", "3", "4", "5"]; // будет возвращено false
console.log(checkArrays(a, b)); //true
console.log(checkArrays(a, c)); //false
console.log(checkArrays(a, d)); //false
console.log(checkArrays(a, e)); //false
Если необходимо, чтобы порядок элементов оставался неизменным, достаточно просто провести итерацию по элементам массивов:
function checkArrays(arrA, arrB) {
// Проверяем, отличаются ли длины массивов
if (arrA.length !== arrB.length) return false;
for (var i = 0; i < arrA.length; i++) {
if (arrA[i] !== arrB[i]) return false; // сравнение с учетом типов
}
return true;
}
var a = [1, 2, 3, 4, 5];
var b = [5, 4, 3, 2, 1];
var c = [1, 2, 3, 4];
var d = [1, 2, 3, 4, 6];
var e = ["1", "2", "3", "4", "5"]; // будет возвращено false
console.log(checkArrays(a, a)); //true
console.log(checkArrays(a, b)); //false
console.log(checkArrays(a, c)); //false
console.log(checkArrays(a, d)); //false
console.log(checkArrays(a, e)); //false
Таким образом, в зависимости от ваших требований к порядку и типам элементов, вы можете выбрать соответствующий подход для проверки массивов.
Как перемешать (сделать случайным) массив в JavaScript?
Как объединить два массива в JavaScript и удалить дубликаты?
Как очистить массив в JavaScript?
Как получить доступ к вложенным объектам, массивам или JSON и обработать их?
Как удалить все дубликаты из массива объектов?