Недавно мне поручили повторно реализовать метод getElementsByClassName в веб-интерфейсе Document с нуля.
Чтобы полностью реализовать getElementsByClassName
, необходимо обойти каждый узел в DOM, каждый раз проверяя, относится ли этот узел к тому классу, который вы ищете.
В моей первой попытке реализации функции это выглядело так:
// Attempt #1 var getElementsByClassName = function(className) { var matchingNodes = []; function traverseDOM(node) { if (node.classList && node.classList.contains(className)) { matchingNodes.push(node); } if (node.firstChild) { traverseDOM(node.firstChild); } else if (node.nextSibling) { traverseDOM(node.nextSibling); } } traverseDOM(document.body); return matchingNodes; };
Эта функция работала примерно в половине случаев, в которых я ее тестировал. Я провел довольно много часов в недоумении, почему это не сработало во всех случаях.
Когда я пытался определить проблему с функцией, я создал следующие макеты примеров HTML-деревьев, которые я передал в свою функцию.
Пример №1
При передаче дерева в примере № 1 моя попытка № 1 ведет себя так, как я и ожидал.
Он успешно проходит каждый узел в DOM, используя встроенное в него условие:
if (node.firstChild) { traverseDOM(node.firstChild); } else if (node.nextSibling) { traverseDOM(node.nextSibling); } // *implicitly* if neither of these conditions is met, you have found a node with no children, and no siblings. You can safely pop this instance of traverseDOM off the call-stack.
Попытка № 1 также работает для следующего примера № 2:
Однако у меня возникли проблемы с примером № 3:
Вот как я понял, как моя Попытка № 1 реализовать getElementsByClassName
работала в этом случае.
На критическом временном шаге я ожидал, что моя функция выполнит следующий фрагмент кода, выделенный полужирным шрифтом:
if (node.firstChild) { traverseDOM(node.firstChild); } else if (node.nextSibling) { traverseDOM(node.nextSibling); } // *implicitly* if neither of these conditions is met, you have found a node with no children, and no siblings. You can safely pop this instance of traverseDOM off the call-stack.
Однако я неправильно понял, как операторы else if работают в Javascript.
В соответствии с этим ответом SE иначе, если операторы указывают новое условие для проверки, если первое условие ложно..
Мое невысказанное предположение состояло в том, что можно было бы «провалиться» через оператор if, а затем и оператор if-else. Поскольку это неверно, моя функция прекратила бы обход DOM в этот момент:
Другими словами, моя попытка № 1 в getElementsByClassName
не полностью обходила деревья, содержащие узлы как с дочерними элементами, так и с братьями и сестрами. Это было (неудачное) совпадение, что функция работала для Примера № 1 и Примера № 2.
Я решил проблему, просто изменив оператор else if на оператор if, как показано ниже в попытке №2. Это гарантирует, что функция не прекратит обход дерева, когда встретит узел, у которого есть как дочерние, так и одноуровневые узлы.
// Attempt #2 var getElementsByClassName = function(className) { var matchingNodes = []; function traverseDOM(node) { if (node.classList && node.classList.contains(className)) { matchingNodes.push(node); } if (node.firstChild) { traverseDOM(node.firstChild); } if (node.nextSibling) { traverseDOM(node.nextSibling); } } traverseDOM(document.body); return matchingNodes; };