JS: Функциональное программирование
Теория: Лексическое окружение (LexicalEnvironment)
Понимание работы функций в немалой степени зависит от знания некоторых деталей реализации языка. К таким деталям относятся окружения (Environment). Разговор о них шел в соответствующем уроке «Введения в программирование». Советую освежить память, пересмотрев тот урок или перечитав конспект. После этого урока вы будете еще лучше разбираться в окружениях.
Познакомимся с термином «словарь». Словарь — это набор пар «ключ - значение». Зная ключ, можно получить значение. Точно так же, как и при работе с обычным бумажным словарем, где ключ — это слово, а значение — определение слова. В данном уроке важно только концептуальное понимание, без конкретных реализаций.
Каждый раз, когда в программе вызывается функция, внутри интерпретатора создается специальный словарь LexicalEnvironment (лексическое окружение), привязанный к этому вызову. Все определения констант, переменных и прочего внутри функции автоматически записываются в словарь. Имя определения (идентификатор, то есть имя константы, переменной и так далее) становится ключом, а значение определения становится значением в словаре. К таким определениям относятся аргументы, константы, функции, переменные и т.д. Лексическое окружение — это хранилище для данных в памяти и механизм для извлечения этих данных при обращении.
В примере ниже в комментариях показано состояние словаря перед выполнением каждой строчки кода. Не забывайте, что наполнение словаря происходит при вызове функции, а не при определении.
Код console.log(warning) активизирует поиск значения идентификатора warning в лексическом окружении.
В процессе выполнения функции значения переменных могут меняться, что сразу же отражается в лексическом окружении. После выполнения функции ее лексическое окружение уничтожается, а занятая им память освобождается.
Из этого поведения есть исключение — возврат функции. В следующем уроке мы рассмотрим связанный с ним механизм так называемых «замыканий». Ранее мы разбирали его во «Введении в программирование».
Окружение есть не только у функций. Любой идентификатор, определенный на уровне модуля, попадает в лексическое окружение модуля. Кроме того, существует и глобальное окружение. Благодаря ему мы с легкостью используем в JS такие функции, как console.log или Math.sqrt, даже особо не задумываясь, откуда они берутся.
Такой код работает — и это для нас не секрет, но как он вяжется с механизмом окружений? А вот как: интерпретатор производит поиск значения идентификатора не только в локальном лексическом окружении (в том, где используется идентификатор), но и во внешнем окружении. Поиск начинается с локального окружения, и если в нем не найден нужный идентификатор, то просмотр идет дальше, вплоть до уровня модуля, а затем и до глобального уровня.
Внешним окружением по отношению к функции считается окружение, в котором функция была объявлена (а не вызвана!). Если разбить пример выше на два файла, то разница станет очевидной.
Так сработает:
А так нет:
Если подумать логически, так и должно быть. Представьте: если бы сработал второй вариант, то автоматически это бы означало, что вы можете случайно создать константу с именем, совпадающим с именем константы внутри функции, написанной другим человеком. Как при этом будет работать код — предположить невозможно.
Попробуйте самостоятельно ответить на вопрос: сработает ли такой код, в котором константа определена позже ее использования внутри функции?
Ответ: сработает.
Окружение — это не «все, что было объявлено до функции, в которой мы используем эти объявления». Не важно, что number появился позже использования внутри функции. Главное, что вызов функции square происходит позже определения number, а значит к этому времени идентификатор уже был добавлен в окружение, внутри которого была создана функция square.
Переменные
Когда мы работаем с константами, все просто. Нет изменений — нет проблем. В случае с переменными ситуация становится сложнее.
Изменение переменной следует читать как «изменение значения ключа в окружении». Соответственно, обращение к number всегда вернет последнее присвоенное значение. Завязка на переменные, описанная в коде выше, должна восприниматься как абсолютное зло. Она порождает неявные зависимости, сложный код и отладку. Функция автоматически перестает быть чистой, так как начинает зависеть от внешнего контекста.
Вложенные функции
Напомню вам слегка модифицированный код курса «Введение в программирование»:
В этом коде реализовано вычисление факториала с применением итеративного процесса. Внутри функции factorial определяется внутренняя функция iter, которая накапливает аккумулятор, вызываясь рекурсивно. Условие выхода из рекурсии — попытка посчитать число большее, чем нужно.
В этой проверке используется переменная n, которая явно в iter не передавалась. Но благодаря тому, как работают окружения, любые функции (в том числе и вложенные), определенные внутри factorial, имеют к ней доступ. Как видно из кода, n используется как константа, а значит такое использование абсолютно безопасно.
Перекрытие (Shadowing)
Перекрытием называется ситуация, когда во внутреннем окружении создается идентификатор с таким же именем, как и во внешнем. Причем не важно, что это: аргумент функции, константа или переменная.
Несмотря на то, что сам код остается рабочим, перекрытие больше не позволяет обратиться к идентификатору из внешнего окружения, ведь поиск всегда происходит сначала в локальном окружении, а уже затем во внешних. Но еще большей проблемой является то, что такой код сложнее в анализе. Глядя на него недостаточно видеть имена, нужно также учитывать их контекст, так как одно и то же имя на разных строках может означать разные вещи. Если запустить линтер для подобного кода, то он укажет на перекрытие как на плохую практику программирования. Подробнее об этом можно прочитать в правилах Eslint.

