На этом занятии
подробно разберем области видимости данных в JavaScript и такое важное
понятие как замыкание.
Для начала
представим, что у нас имеется вот такая программа:
let name = "Иван";
getName("Привет!");
function getName(say) {
console.log(name + ": " + say);
}
Как это работает
в деталях. Как мы уже говорили, все функции, объявленные как Function Declaration, создаются JavaScript-движком в
первую очередь. Но что значит создаются? В действительности, при запуске любого
скрипта создается специальный объект (на уровне JavaScript-машины и
недоступный программисту), называемый глобальным лексическим окружением (Lexical Envirnoment). Он состоит из
двух частей:
- Environment Record – объект, в
котором хранятся локальные данные как свойства этого объекта;
- Ссылка на
внешнее лексическое окружение (если его нет, то эта ссылка равна null).
Так вот, объявление
функции getName – это не что
иное, как создание свойства getName в глобальном лексическом
окружении:
Далее, начинает
выполняться скрипт. Создается переменная name. Значит, в
лексическом окружении появляется еще одно свойство – name:
Затем идет вызов
функции getName("Привет!").
В лексическом окружении ищется свойство getName со ссылкой на
функцию. Так как оно там уже есть, то функция успешно вызывается. Но, каждый
вызов функции создает свое лексическое окружение и картина будет выглядеть так:
Здесь аргументы
функции становятся свойствами лексического окружения функции. Далее,
выполняется строчка
console.log(name + ": " + say);
Переменная name сначала ищется
в лексическом окружении функции. Не находит ее, тогда процесс поиска
повторяется на следующем уровне, на который ссылается outer. В глобальном
окружении переменная name существует,
берется ее значение для вывода в консоль. То же самое происходит и с переменной
say. Но она имеется
в лексическом окружении самой функции, поэтому поиск здесь и завершается, не
переходя к глобальному пространству. В результате мы видим сообщение «Иван:
Привет!».
Как только
функция завершила свою работу, ее лексическое окружение автоматически
уничтожается с помощью встроенного в JavaScript механизма,
называемый «сборщик мусора».
Из этого примера
следуют такие важные моменты:
- Внешнее
лексическое окружение не имеет доступа к данным внутренних окружений.
- Поиск переменных
начинается с текущего окружения и при необходимости последовательно переходит к
внешним окружениям. Останавливается, как только переменная найдена.
- Лексическое
окружение автоматически уничтожается, когда в нем более нет необходимости (на него
нет внешних ссылок).
Здесь нужно лишь
добавить: если переменная не находится, то при включенном режиме "use
strict" она принимает значение undefined. Иначе, будет
ссылаться на глобальный объект (в браузере – это window).
Исходя из всего
сказанного, легко понять, что выведет в консоль следующий скрипт:
let name = "Иван";
getName("Привет!");
function getName(say) {
let name = "Федор";
console.log(name + ": " + say);
}
Да, мы видим
строчку: «Федор: Привет!». То есть, переменная name была найдена в локальном
окружении и взято значение «Федор», а не «Иван».
А вот, если мы
выведем что-то в глобальном лексическом окружении:
console.log(name);
console.log(say);
то увидим
«Иван», а переменная say не будет найдена. Вот так это работает.
Причем, обратите внимание, свои лексические окружения создают не только
вызываемые функции, но и условные операторы:
if(...) {
// внутреннее лексическое
окружение
}
операторы
циклов:
while, for, do while(...) {
// внутреннее лексическое
окружение
}
И в современных
браузерах даже просто проставленные фигурные скобки:
{
//
внутреннее лексическое окружение
}
Однако, старые
браузеры блок кода внутри фигурных скобок помещают в текущее лексическое
окружение, не создавая нового. Поэтому, в старых программах для изоляции кода
от внешнего скрипта, часто применялся такой прием с анонимными функциями:
(function() {
// код внутри изолирован от внешнего скрипта
let name = "Григорий";
console.log(name);
})();
Причем, он
исполнялся в момент, когда интерпретатор JavaScript-машины доходил
до этого места программы. Но теперь, в этом приеме нет необходимости и можно
просто использовать фигурные скобки.
Еще раз обращу
ваше внимание, что внутреннее лексическое окружение создается каждый раз при
вызове функции. Если она вызывается несколько раз, то столько же и будет
создано лексических окружений. Все лексические окружения автоматически
удаляются при отсутствии внешних ссылок на них. Это важный момент!
Мы можем даже
создавать функции внутри других функций. Они называются вложенными функциями. В
JavaScript – это обычная
практика:
function getName(say) {
function getSay() {
return ": " + say;
}
console.log(name + getSay());
}
Здесь функция getSay создана для
удобства – вывода сообщения по определенному шаблону. Ее лексическое окружение
будет вложено в лексическое окружение функции getName и, конечно же,
она будет иметь доступ ко всем внутренним переменным функции getName.
Теперь, когда мы
со всем этим разобрались, посмотрим на работу вот такой программы:
function createCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = createCounter();
console.log( counter() );
Вначале здесь
объявляется функция createCounter, то есть, в глобальном окружении
появляется такое свойство. Далее, эта функция вызывается. Создается ее
локальное лексическое окружение с переменной count и анонимной
функцией:
На эту анонимную
функцию ссылается переменная counter из глобального
окружения. В результате, после завершения выполнения функции createCounter() ее локальное
окружение не уничтожается, так как все еще остается ссылка counter.
Далее, в
программе идет вызов анонимной функции. Появляется еще одно лексическое пространство
этой функции и картина выглядит уже вот так:
Анонимная
функция берет переменную count из внешнего лексического пространства,
возвращает ее и увеличивает на 1. Вот такое обращение в теле функции к
переменной из внешнего лексического окружения в программировании называют замыканием.
При завершении,
лексическое пространство анонимной функции уничтожается и остается два
пространства:
Но значение count теперь равно 1.
Если мы вызовем counter() еще раз, то получим значение 1 и
переменная count станет равной
2. И так далее. Вот так работают замыкания в JavaScript. Причем,
обратите внимание, если мы создадим еще один счетчик:
let counter2 = createCounter();
то будет создано
еще одно лексическое пространство и оба счетчика будут работать независимо друг
от друга:
console.log( counter2() );
Я постарался
предельно просто изложить материал о лексических окружениях и замыканиях,
опуская некоторые несущественные на мой взгляд детали. Если вы хотите научиться
грамотно писать код на JavaScript, то этот материал следует хорошо
знать. Особенно, часто на собеседованиях соискателя любят спрашивать о
замыканиях и особенностях работы лексических окружений. И теперь, вы все это
знаете.