Это пятая статья в серии статей о 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 не было бы концом света. Тем не менее, как показывает этот пример, неточная область видимости переменных может привести к незаметному, но серьезному неожиданному поведению.