Как я могу проверить равенство двух NodeList?

Допустим, у меня есть пользовательская функция, которая, как я ожидаю, вернет NodeList:

getNodeList('foo');

Я ожидаю, что этот NodeList будет тем же самым NodeList, возвращенным из:

document.querySelectorAll('.foo');

Как я могу проверить, что мои ожидания верны?

Это не работает:

getNodeList('foo') == document.querySelectorAll('.foo')

Я уверен, что есть веская техническая причина, почему это не работает, поскольку document.querySelectorAll('.foo') == document.querySelectorAll('.foo') тоже не работает, я полагаю, что это ожидаемо.

Как я могу проверить, содержат ли два NodeList одни и те же узлы HTML?


person ESR    schedule 22.08.2018    source источник
comment
Много хороших ответов, а я не знаю, за кого голосовать :(   -  person ESR    schedule 22.08.2018


Ответы (6)


То, что у вас есть до сих пор, выглядит нормально, но это неэффективно (вы будете пересчитывать document.querySelectorAll(...) и indexOf, возможно, много раз).

Также есть ошибка: если querySelectorAll возвращает больше элементов, чем первый список узлов, но в остальном они равны, ваша функция вернет true.

Вы также можете упростить сравнение:

function nodeListsAreEqual( list1, list2 ) {
    if ( list1.length !== list2.length ) {
        return false;
    }
    return Array.from( list1 ).every( ( node, index ) => node === list2[ index ] );
}
person We Are All Monica    schedule 22.08.2018
comment
Спасибо - вы правы насчет ошибки, и спасибо, что предупредили меня о неэффективности - кажется хорошим ответом. - person ESR; 22.08.2018
comment
В итоге я выбрал этот подход, но ответ @mauroc8 кажется таким же хорошим - person ESR; 22.08.2018

Равенство массивов достигается по ссылке, а не по содержимому.

let a = [1, 2, 3], b = [1, 2, 3]
let c = a
a == c // => true, since both refer to `a`
a == b // => false

Если вы хотите сравнить два массивоподобных объекта, вам нужно сравнить их по индексу.

function eq(A, B) {
  if (A.length !== B.length) return false;
  for (let i = 0; i < A.length; i++) {
    if (A[i] !== B[i]) return false;
  }
  return true;
}

Конечно, вы всегда можете использовать магию функционального программирования:

let arrayEq = (A, B) => A.length === B.length && A.every((e, i) => e === B[i]);

Но это будет работать только в том случае, если A является массивом (а не NodeList).


Тогда попробуй

eq(getNodeList('foo'), document.querySelectorAll('.foo'))

or

arrayEq(Array.from(getNodeList('foo')), Array.from(document.querySelectorAll('.foo'))
person mauroc8    schedule 22.08.2018
comment
Просто вызовите Array.prototype.every с A и обратным вызовом, и он будет работать и с NodeLists. Нет необходимости создавать промежуточный массив. - person CertainPerformance; 22.08.2018
comment
Или, если вы не хотите заниматься магией кражи прототипов, arrayEq(Array.from(a), Array.from(b)) - person Amadan; 22.08.2018
comment
Спасибо, я добавил последнее предложение в ответ - person mauroc8; 22.08.2018
comment
Спасибо, кажется, хороший ответ. После того, как я поиграю с различными решениями, я отмечу одно из них как принятое. - person ESR; 22.08.2018

Если вы не возражаете против установки сторонней библиотеки, возьмите deep-equal из NPM и выполните:

deepEqual(Array.from(getNodeList('foo')), Array.from(document.querySelectorAll('.foo')))

Это гарантирует, что ваши списки вычисляются только один раз, и инкапсулирует все кровавые детали сравнения списков в отдельную функцию. Ваш код должен просто вызывать функцию равенства и не смешивать проблемы вашего приложения с низкоуровневым обходом структур списка. (Но вы, вероятно, уже знали это!)

Если вам не нравится многословие Array.from, используйте значки:

deepEqual([...getNodeList('foo')], [...document.querySelectorAll('.foo')])

Если эффективность имеет значение, вам нужно выполнить профилирование.

person Ray Toal    schedule 22.08.2018
comment
На самом деле я уже использую deep-extend, поэтому для меня может иметь смысл включить deep-equal тоже... - person ESR; 22.08.2018
comment
Стоит попробовать; обязательно профиль и тест. Как вы знаете, NodeList довольно странные (они, я думаю, живут как HTMLCollections), и они прекрасно работают при перечислении, но для таких вещей, как равенство Array.from, часто требуется. Но тогда вся производительность зависит от реализации фактического массива, поэтому YMMV. :) (По крайней мере, с вызовом deepEqual код выглядит лучше. :) ) - person Ray Toal; 22.08.2018
comment
Я только что попробовал это с nodejs.org/api/, который, вероятно, работает так же. образом, и это, к сожалению, не работает - оно не сравнивает фактические узлы DOM, поэтому любые 2 NodeList с одинаковым количеством узлов всегда будут возвращать true. - person ESR; 22.08.2018
comment
А, имеет смысл. Сравнение shallowEqual, вероятно, здесь намного лучше (поскольку именно это, по сути, и делает принятый ответ). Если у вас что-то не работает, возможно, это связано с тем, что узлы изменяемы, и что-то внутри узла отличается, когда вы вызываете querySelectorAll по сравнению с тем, когда вы создали NodeList, но идентификаторы объектов одинаковы.... Просто мысль. Рад, что вы нашли принятый ответ. - person Ray Toal; 23.08.2018

Я придумал этот метод, который, кажется, работает, используя функции ES6:

const isEqual = [...getNodeList('foo')].every((node, index) => {
    return Array.from(document.querySelectorAll('.foo')).indexOf(node) === index
});

По сути, мы проверяем, что каждый элемент в первом NodeList существует во втором NodeList с тем же индексом. Если есть какие-либо расхождения между NodeList, это должно вернуть false.

person ESR    schedule 22.08.2018
comment
Что делать, если второй NodeList длиннее? - person Ray Toal; 22.08.2018
comment
@RayToal кажется, что в этом случае мое условие не будет работать должным образом, как указано в ответе jnylen - person ESR; 22.08.2018

Я пересмотрел это после того, как потребовалось сравнить NodeLists, которые могут иметь различный порядок узлов, но в остальном узлы равны, и придумал это:

export default function nodeListsAreEqual(actual, expected) {
    if ((actual.length || Object.keys(actual).length) !== (expected.length || Object.keys(expected).length)) {
        return false;
    }

    return Array.from(actual).every(actualNode => {
        return Array.from(expected).some(expectedNode => actualNode === expectedNode)
    });
}
person ESR    schedule 08.03.2019

Более простая, но, вероятно, более быстрая версия @mauroc8:

     const nodeEq = (A, B) => {
        A = Array.from(A)
        B = Array.from(B)
        if (A.length !== B.length) return false

        return A.every((a, b) => a.innerHTML === B[b].innerHTML)
      }
person insign    schedule 25.07.2021