13 KiB
sidebar_position
| sidebar_position |
|---|
| 6 |
Область видимости переменных, замыкание
Источник: https://learn.javascript.ru/closure
Блоки кода
Если переменная объявлена внутри блока кода {...}, то она видна только внутри этого блока.
С помощью блоков {...} мы можем изолировать часть кода, выполняющую свою собственную задачу, с переменными, принадлежащими только ей
Для if, for, while и т.д. переменные, объявленные в блоке кода {...}, также видны только внутри
Вложенные функции
Функция называется «вложенной», когда она создаётся внутри другой функции. Она может получить доступ к внешним переменным. Вложенная функция может быть возвращена: либо в качестве свойства нового объекта (если внешняя функция создаёт объект с методами), либо сама по себе. И затем может быть использована в любом месте. Не важно где, она всё так же будет иметь доступ к тем же внешним переменным.
Лексическое окружение
Переменные
В JavaScript у каждой выполняемой функции, блока кода {...} и скрипта есть связанный с ними внутренний (скрытый) объект, называемый *лексическим окружением LexicalEnvironment.
Объект лексического окружения состоит из двух частей:
- Environment Record – объект, в котором как свойства хранятся все локальные переменные (а также некоторая другая информация, такая как значение this).
- Ссылка на внешнее лексическое окружение – то есть то, которое соответствует коду снаружи (снаружи от текущих фигурных скобок).
«Переменная» – это просто свойство специального внутреннего объекта: Environment Record.
«Получить или изменить переменную», означает, «получить или изменить свойство этого объекта».
- Переменная – это свойство специального внутреннего объекта, связанного с текущим выполняющимся блоком/функцией/скриптом.
- Работа с переменными – это на самом деле работа со свойствами этого объекта.
Function Declaration
Разница заключается в том, что Function Declaration мгновенно инициализируется полностью.
Когда создается лексическое окружение, Function Declaration сразу же становится функцией, готовой к использованию (в отличие от let, который до момента объявления не может быть использован).
Поэтому мы можем вызвать функцию, объявленную как Function Declaration, до самого её объявления.
Такое поведение касается только Function Declaration, а не Function Expression, в которых мы присваиваем функцию переменной.
Внутреннее и внешнее лексическое окружение
Когда запускается функция, в начале ее вызова автоматически создается новое лексическое окружение для хранения локальных переменных и параметров вызова.
В процессе вызова функции у нас есть два лексических окружения: внутреннее (для вызываемой функции) и внешнее (глобальное):
- Внутреннее лексическое окружение соответствует текущему выполнению say.
В нём находится одна переменная name, параметр функции. Мы вызываем say("John"), так что значение переменной name равно "John". - Внешнее лексическое окружение – это глобальное лексическое окружение.
В нём находятся переменная phrase и сама функция.
У внутреннего лексического окружения есть ссылка на внешнее outer.
Когда код хочет получить доступ к переменной – сначала происходит поиск во внутреннем лексическом окружении, затем во внешнем, затем в следующем и так далее, до глобального.
Если переменная не была найдена, это будет ошибкой в строгом режиме (use strict). Без строгого режима, для обратной совместимости, присваивание несуществующей переменной создаёт новую глобальную переменную с таким же именем.
Возврат функции
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
В начале каждого вызова makeCounter() создается новый объект лексического окружения, в котором хранятся переменные для конкретного запуска makeCounter.
Таким образом, мы имеем два вложенных лексических окружения, как в примере выше:
Отличие заключается в том, что во время выполнения makeCounter() создается крошечная вложенная функция, состоящая всего из одной строки: return count++. Мы ее еще не запускаем, а только создаем.
Все функции помнят лексическое окружение, в котором они были созданы. Технически здесь нет никакой магии: все функции имеют скрытое свойство Environment, которое хранит ссылку на лексическое окружение, в котором была создана функция:

Таким образом, counter.Environment имеет ссылку на {count: 0} лексического окружения. Так функция запоминает, где она была создана, независимо от того, где она вызывается. Ссылка на Environment устанавливается один раз и навсегда при создании функции.
Впоследствии, при вызове counter(), для этого вызова создается новое лексическое окружение, а его внешняя ссылка на лексическое окружение берется из counter.Environment:
Теперь, когда код внутри counter() ищет переменную count, он сначала ищет ее в собственном лексическом окружении (пустом, так как там нет локальных переменных), а затем в лексическом окружении внешнего вызова makeCounter(), где находит count и изменяет ее.
Переменная обновляется в том лексическом окружении, в котором она существует.
Если мы вызовем counter() несколько раз, то в одном и том же месте переменная count будет увеличена до 2, 3 и т.д.
💥 Замыкания
Замыкание – это функция, которая запоминает свои внешние переменные и может получить к ним доступ. В некоторых языках это невозможно, или функция должна быть написана специальным образом, чтобы получилось замыкание. Но, как было описано выше, в JavaScript, все функции изначально являются замыканиями.
То есть они автоматически запоминают, где были созданы, с помощью скрытого свойства [[Environment]], и все они могут получить доступ к внешним переменным.
Когда на собеседовании фронтенд-разработчику задают вопрос: «что такое замыкание?», – правильным ответом будет определение замыкания и объяснения того факта, что все функции в JavaScript являются замыканиями, и, может быть, несколько слов о технических деталях: свойстве [[Environment]] и о том, как работает лексическое окружение.
Есть только одно исключение, когда функция создаётся с использованием new Function, в её [[Environment]] записывается ссылка не на внешнее лексическое окружение, в котором она была создана, а на глобальное. Поэтому такая функция имеет доступ только к глобальным переменным.
Сборка мусора
Обычно лексическое окружение удаляется из памяти вместе со всеми переменными после завершения вызова функции. Это связано с тем, что на него нет ссылок. Как и любой объект JavaScript, оно хранится в памяти только до тех пор, пока к нему можно обратиться.
Однако если существует вложенная функция, которая все еще доступна после завершения функции, то она имеет свойство [[Environment]], ссылающееся на лексическое окружение.
В этом случае лексическое окружение остается доступным даже после завершения работы функции.
Объект лексического окружения исчезает, когда становится недоступным (как и любой другой объект). Другими словами, он существует только до тех пор, пока на него ссылается хотя бы одна вложенная функция.
Оптимизация на практике
Но на практике движки JavaScript пытаются это оптимизировать. Они анализируют использование переменных и, если легко по коду понять, что внешняя переменная не используется – она удаляется.
Одним из важных побочных эффектов в V8 (Chrome, Edge, Opera) является то, что такая переменная становится недоступной при отладке.
