Это пятая статья в серии статей о JavaScript, и изначально она была опубликована здесь.

Функция JavaScript обход DOM обходит все узлы в дереве DOM, подчиненные данному узлу, выполняя некоторую операцию на каждом узле. Одним из способов создания такой функции является рекурсия с реализацией, которая выглядит примерно так:

function walk(node, callback) {
  // perform operation on node
  callback(node);

  // visit each of the child nodes
  for (i = 0; i < node.childNodes.length; i++) {
    // call walk for each child node
    walk(node.childNodes[i], callback);
  }
  // exit function when for loop is complete
}

Для HTML-страницы с заданной иерархией элементов:

HTML
  HEAD
    TITLE
  BODY
    DIV
      P1
      P2
      P3

обход DOM с помощью этой рекурсивной реализации (начиная с корневого элемента в документе) должен посещать узлы в следующем порядке.

HTML
HEAD
TITLE
BODY
DIV
P1
P2
P3

Однако функция выше (которую я в качестве упражнения скопировал (ошибочно) по памяти из материалов урока) посещает узлы в следующем порядке, попадая в бесконечный цикл после достижения TITLE.

HTML
HEAD
TITLE
TITLE
TITLE
TITLE
... etc.

Оказывается, в моей версии пропущено одно важное ключевое слово: var. Это, казалось бы, тривиальное упущение приводит к такому отрицательному результату из-за природы рекурсивных функций и правил области видимости переменных в JavaScript.

Рекурсия

Рекурсивным функциям требуется некоторое условие, выполнение которого позволяет выйти из функции. В приведенной выше реализации завершение цикла for является этим условием, и поэтому функция необходима для работы; если JavaScript не может выйти из функции, то рекурсия никогда не остановится, создавая бесконечный цикл внутренних рекурсивных вызовов функций.

Кажется, это то, что происходит в версии А, но почему?

Область видимости переменных JavaScript

В JavaScript любая переменная, которая не объявлена явным образом (инициализирована ключевым словом var или параметром функции), присваивается глобальной области видимости, то есть набору переменных, объектов и функций. виден из любой точки программы. Опуская ключевое слово var при инициализации i в цикле for:

(for ([var] i = 0; i < node.childNodes.length; i++)

i был добавлен в глобальную область, а не в локальную область (функции). Однако для того, чтобы функция работала правильно, i должна иметь локальную область действия, чтобы функция могла выполнять итерацию по каждому дочернему узлу и должна содержать отдельное значение в каждой функции. Как написано, i переназначается 0 каждый раз, когда функция вызывается для узла с дочерними узлами, и, поскольку каждый экземпляр i ссылается на одно и то же глобальное значение, это сбрасывает циклы for глобально - на каждом уровне. рекурсии.

Отрицательный результат

С практической точки зрения это означает, что функция будет рекурсивно перемещаться по DOM, пока не встретит бездетный узел, после чего она попадет в ловушку, перемещаясь вверх и вниз между бездетным узлом и его родителем, поскольку i не увеличивается, предотвращая for цикла от повторения следующего дочернего элемента или постоянного выхода и перехода на уровень вверх в рекурсии.

Это именно то, что происходит с нашим примером выше — walk начинается с HTML, вызывает себя для дочернего элемента HEAD, а затем снова для дочернего элемента HEAD TITLE, у которого нет дочерних элементов. В TITLE функция инициализирует i и присваивает его 0, а затем выходит из функции, поскольку условие while уже выполнено (поскольку node.childNodes.length равно 0). Однако при возврате на уровень HEAD значение i не увеличилось (поскольку оно было "сброшено" до 0), и поэтому функция снова вызывается на уровне TITLE (нулевой дочерний элемент), цикл, который продолжается бесконечно.

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