Недавно мне поручили повторно реализовать метод 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;
};