Области видимости переменных в JavaScript и их подводные камни
В этом посте мы разберемся с областями видимости в JavaScript. Обратим внимание на две вещи, которые могут приводить к проблемам, если вы о них не позаботитесь: область видимости переменных в JavaScript - функции, и они могут быть захвачены в замыкания.
Область видимости переменных определяет, где эта переменная доступна. Например, если переменная определена в начале функции, то она доступная внутри этой функции, но не снаружи, и обычно "умирает" с завершением функции. В этом случае, область видимости переменной - функция. Когда поток управления доходит до области видимости, создается новое окружение, которое отображает имена переменных на их значения. Области видимости могут быть вложенными. Переменная доступна в своей области видимости и во всех вложенных областях видимости.
Ловушка 1: Область видимости переменных функций
Большинство популярных языков имеют блочную область видимости: новое окружение создается при входе в блок и области видимости составлены из вложенных блоков. В отличии от JavaScript, где переменные имеют область видимости в функции, новое окружение создается только при входе в функцию и составлено из вложенных функций. Это значит, что даже если вы определите переменную внутри блока "тогда" оператора if , то она будет доступна везде в окружающей функции. Код ниже демонстрирует это:
Как вы видите, даже первое обращение к myvar ссылается на локальную переменную (значение которой еще не присвоено на (*) ). Причина в том, что определение var переменной в JavaScript поднимается (hoisting): var myvar = "local" равносильно определению переменной myvar на начале функции f() , а присвоение происходит на строке (**) . Поэтому, передовые практики в JavaScript рекомендуют использовать только один var в начале функции.
Ловушка 2: Замыкания
В JavaScript области видимости статические, они определены вложенностью синтаксических конструкций. Для обеспечения статической области видимости, окружение связано с значениями, которые имеют доступ к переменными в окружении. Пример такого значения - возвращенная функция в следующем коде:
Переменная x доступная в возвращенной функции, и к ней невозможно получить доступ, кроме как в этой функции. Присоединив окружение, можно получить значение x , как мы ожидали, с учетом статической области видимости. Эта пара значения и окружения называется замыкание, потому что переменные замкнуты. Замыкания в JavaScript очень мощны. Вы их можете использовать даже для сохранения свойств объектов, как продемонстрировано в коде ниже:
Значение цвета сохраняется в окружении, которое было создано при вызове objMaker() . Это окружение присоединяется к возвращаемому значению, поэтому color все еще доступен даже после того, как закончилось исполнение objMaker() .
Нечаянный обмен средой
Замыкания и функциональная область видимости приводит к неожиданному поведению, имея следующий код:
Эта функция возвращает коллекцию из двух функций. Обе функции все еще имеют доступ к окружению f и, как следствие, к arr . По факту, они имеют одинаковые окружения. Но в том окружении i имеет значение 2, и поэтому функции возвращают значение "blue" при вызове:
Это не то что нам нужно. Для того чтоб это полечить нам необходимо сделать снимок того что находится в индексе i , перед созданием функции которая его использует.
Создание нового окружения
В языке с блочной областью видимости, следующий код был бы рабочим:
JavaScript имеет функциональную область видимости, поэтому color обрабатывается так, если бы был определен в начале функции f() , и мы не получим другого окружения для каждой возвращаемой функции. Только функции могут создавать окружения, поэтому мы используем функцию для симуляции блока. Это выглядит следующим образом:
Так, как мы сразу вызываем функцию, поведение обернутого кода такое же, как и в блока: код выполняется сразу. Зачем нужны круглые скобки вокруг конструкции? Они там для избежания синтаксической ошибки. Если выражение начинается с функции, то ожидается определение функции. Таким образом, без открытия круглых скобок, синтаксический анализатор будет жаловаться на отсутствующее имя. Вас может посетить идея дописать необходимое имя - в таком случае, анализатор будет жаловаться на круглые скобки, которые следуют за функцией. В обоих случаях, функция не будет выполнена. Таким образом открытие круглых скобок необходимо для того, чтоб указать, что далее будет следовать выражение-функция-выражение. Это называется Немедленно Вызываемые Функции (Immediately Invoked Function Expression aka IIFE). Переписав в пример выше, на псевдокоде с помощью IIFE получим:
Теперь новое окружение будет создано для каждой итерации в цикле. Теперь получим ожидаемый результат:
Так же вы можете использовать color как параметр для IIFE:
Обратите внимание, пример имеет практический смысл, потому что похожи сценарии имеют место при добавлении обработчиков на DOM элементы.