Область видимости переменных, замыкание
JavaScript – язык с сильным функционально-ориентированным уклоном. Он даёт нам много свободы. Функция может быть динамически создана, скопирована в другую переменную или передана как аргумент другой функции и позже вызвана из совершенно другого места.
Мы знаем, что функция может получить доступ к переменным из внешнего окружения, эта возможность используется очень часто.
Но что произойдёт, когда внешние переменные изменятся? Функция получит последнее значение или то, которое существовало на момент создания функции?
И что произойдёт, когда функция переместится в другое место в коде и будет вызвана оттуда – получит ли она доступ к внешним переменным своего нового местоположения?
Разные языки ведут себя по-разному в таких случаях, и в этой главе мы рассмотрим поведение JavaScript.
Мы будем говорить о переменных let/const здесь
В JavaScript существует три способа объявить переменную: let , const (современные), и var (пережиток прошлого).
- В этой статье мы будем использовать переменные let в примерах.
- Переменные, объявленные с помощью const , ведут себя так же, так что эта статья и о них.
- Старые переменные var имеют несколько характерных отличий, они будут рассмотрены в главе Устаревшее ключевое слово «var».
Блоки кода
Если переменная объявлена внутри блока кода <. >, то она видна только внутри этого блока.
< // выполняем некоторые действия с локальной переменной, которые не должны быть видны снаружи let message = "Hello"; // переменная видна только в этом блоке alert(message); // Hello >alert(message); // ReferenceError: message is not defined
С помощью блоков <. >мы можем изолировать часть кода, выполняющую свою собственную задачу, с переменными, принадлежащими только ей:
Без блоков была бы ошибка
Обратите внимание, что без отдельных блоков возникнет ошибка, если мы используем let с существующим именем переменной:
// показать сообщение let message = "Hello"; alert(message); // показать другое сообщение let message = "Goodbye"; // SyntaxError: Identifier 'message' has already been declared alert(message);
Для if , for , while и т.д. переменные, объявленные в блоке кода <. >, также видны только внутри:
if (true) < let phrase = "Hello"; alert(phrase); // Hello >alert(phrase); // Ошибка, нет такой переменной!
В этом случае после завершения работы if нижний alert не увидит phrase , что и приведет к ошибке.
И это замечательно, поскольку это позволяет нам создавать блочно-локальные переменные, относящиеся только к ветви if .
То же самое можно сказать и про циклы for и while :
for (let i = 0; i < 3; i++) < // переменная i видна только внутри for alert(i); // 0, потом 1, потом 2 >alert(i); // Ошибка, нет такой переменной!
Визуально let i = 0; находится вне блока кода <. >, однако здесь в случае с for есть особенность: переменная, объявленная внутри (. ) , считается частью блока.
Вложенные функции
Функция называется «вложенной», когда она создаётся внутри другой функции.
Это очень легко сделать в JavaScript.
Мы можем использовать это для упорядочивания нашего кода, например, как здесь:
function sayHiBye(firstName, lastName) < // функция-помощник, которую мы используем ниже function getFullName() < return firstName + " " + lastName; >alert( "Hello, " + getFullName() ); alert( "Bye, " + getFullName() ); >
Здесь вложенная функция getFullName() создана для удобства. Она может получить доступ к внешним переменным и, значит, вывести полное имя. В JavaScript вложенные функции используются очень часто.
Что ещё интереснее, вложенная функция может быть возвращена: либо в качестве свойства нового объекта (если внешняя функция создаёт объект с методами), либо сама по себе. И затем может быть использована в любом месте. Не важно где, она всё так же будет иметь доступ к тем же внешним переменным.
Ниже, makeCounter создает функцию «счётчик», которая при каждом вызове возвращает следующее число:
function makeCounter() < let count = 0; return function() < return count++; // есть доступ к внешней переменной "count" >; > let counter = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 alert( counter() ); // 2
Несмотря на простоту этого примера, немного модифицированные его варианты применяются на практике, например, в генераторе псевдослучайных чисел и во многих других случаях.
Как это работает? Если мы создадим несколько таких счётчиков, будут ли они независимыми друг от друга? Что происходит с переменными?
Понимание таких вещей полезно для повышения общего уровня владения JavaScript и для более сложных сценариев. Так что давайте немного углубимся.
Лексическое окружение
Здесь водятся драконы!
Глубокое техническое описание – впереди.
Как бы мне ни хотелось избежать низкоуровневых деталей языка, любое представление о JavaScript без них будет недостаточным и неполным, так что приготовьтесь.
Для большей наглядности объяснение разбито на несколько шагов.
Шаг 1. Переменные
В JavaScript у каждой выполняемой функции, блока кода <. >и скрипта есть связанный с ними внутренний (скрытый) объект, называемый лексическим окружением LexicalEnvironment .
Объект лексического окружения состоит из двух частей:
- Environment Record – объект, в котором как свойства хранятся все локальные переменные (а также некоторая другая информация, такая как значение this ).
- Ссылка на внешнее лексическое окружение – то есть то, которое соответствует коду снаружи (снаружи от текущих фигурных скобок).
«Переменная» – это просто свойство специального внутреннего объекта: Environment Record . «Получить или изменить переменную», означает, «получить или изменить свойство этого объекта».
Например, в этом простом коде только одно лексическое окружение:
Это, так называемое, глобальное лексическое окружение, связанное со всем скриптом.
На картинке выше прямоугольник означает Environment Record (хранилище переменных), а стрелка означает ссылку на внешнее окружение. У глобального лексического окружения нет внешнего окружения, так что она указывает на null .
По мере выполнения кода лексическое окружение меняется.
Вот более длинный код:
Прямоугольники с правой стороны демонстрируют, как глобальное лексическое окружение изменяется в процессе выполнения кода:
- При запуске скрипта лексическое окружение предварительно заполняется всеми объявленными переменными.
- Изначально они находятся в состоянии «Uninitialized». Это особое внутреннее состояние, которое означает, что движок знает о переменной, но на нее нельзя ссылаться, пока она не будет объявлена с помощью let . Это почти то же самое, как если бы переменная не существовала.
- Появляется определение переменной let phrase . У неё ещё нет присвоенного значения, поэтому присваивается undefined . С этого момента мы можем использовать переменную.
- Переменной phrase присваивается значение.
- Переменная phrase меняет значение.
Пока что всё выглядит просто, правда?
- Переменная – это свойство специального внутреннего объекта, связанного с текущим выполняющимся блоком/функцией/скриптом.
- Работа с переменными – это на самом деле работа со свойствами этого объекта.
Лексическое окружение – объект спецификации
«Лексическое окружение» – это объект спецификации: он существует только «теоретически» в спецификации языка для описания того, как все работает. Мы не можем получить этот объект в нашем коде и манипулировать им напрямую.
JavaScript-движки также могут оптимизировать его, отбрасывать неиспользуемые переменные для экономии памяти и выполнять другие внутренние действия, но при этом видимое поведение остается таким, как описано.
Шаг 2. Function Declaration
Функция – это тоже значение, как и переменная.
Разница заключается в том, что Function Declaration мгновенно инициализируется полностью.
Когда создается лексическое окружение, Function Declaration сразу же становится функцией, готовой к использованию (в отличие от let , который до момента объявления не может быть использован).
Именно поэтому мы можем вызвать функцию, объявленную как Function Declaration, до самого её объявления.
Вот, к примеру, начальное состояние глобального лексического окружения при добавлении функции:
Конечно, такое поведение касается только Function Declaration, а не Function Expression, в которых мы присваиваем функцию переменной, например, let say = function(name) <. >.
Шаг 3. Внутреннее и внешнее лексическое окружение
Когда запускается функция, в начале ее вызова автоматически создается новое лексическое окружение для хранения локальных переменных и параметров вызова.
Например, для say(«John») это выглядит так (выполнение находится на строке, отмеченной стрелкой):
В процессе вызова функции у нас есть два лексических окружения: внутреннее (для вызываемой функции) и внешнее (глобальное):
- Внутреннее лексическое окружение соответствует текущему выполнению say . В нём находится одна переменная name , аргумент функции. Мы вызываем say(«John») , так что значение переменной name равно «John» .
- Внешнее лексическое окружение – это глобальное лексическое окружение. В нём находятся переменная phrase и сама функция.
У внутреннего лексического окружения есть ссылка на внешнее outer .
Когда код хочет получить доступ к переменной – сначала происходит поиск во внутреннем лексическом окружении, затем во внешнем, затем в следующем и так далее, до глобального.
Если переменная не была найдена, это будет ошибкой в строгом режиме ( use strict ). Без строгого режима, для обратной совместимости, присваивание несуществующей переменной создаёт новую глобальную переменную с таким же именем.
Давайте посмотрим, как происходит поиск в нашем примере:
- Для переменной name , alert внутри say сразу же находит ее во внутреннем лексическом окружении.
- Когда alert хочет получить доступ к phrase , он не находит её локально, поэтому вынужден обратиться к внешнему лексическому окружению и находит phrase там.
Шаг 4. Возврат функции
Давайте вернёмся к примеру с makeCounter :
function makeCounter() < let count = 0; return function() < return count++; >; > let counter = makeCounter();
В начале каждого вызова makeCounter() создается новый объект лексического окружения, в котором хранятся переменные для конкретного запуска makeCounter .
Таким образом, мы имеем два вложенных лексических окружения, как в примере выше:
Отличие заключается в том, что во время выполнения makeCounter() создается крошечная вложенная функция, состоящая всего из одной строки: return count++ . Мы ее еще не запускаем, а только создаем.
Все функции помнят лексическое окружение, в котором они были созданы. Технически здесь нет никакой магии: все функции имеют скрытое свойство [[Environment]] , которое хранит ссылку на лексическое окружение, в котором была создана функция:
Таким образом, counter.[[Environment]] имеет ссылку на лексического окружения. Так функция запоминает, где она была создана, независимо от того, где она вызывается. Ссылка на [[Environment]] устанавливается один раз и навсегда при создании функции.
Впоследствии, при вызове counter() , для этого вызова создается новое лексическое окружение, а его внешняя ссылка на лексическое окружение берется из counter.[[Environment]] :
Теперь, когда код внутри counter() ищет переменную count , он сначала ищет ее в собственном лексическом окружении (пустом, так как там нет локальных переменных), а затем в лексическом окружении внешнего вызова makeCounter() , где находит count и изменяет ее.
Переменная обновляется в том лексическом окружении, в котором она существует.
Вот состояние после выполнения:
Если мы вызовем counter() несколько раз, то в одном и том же месте переменная count будет увеличена до 2 , 3 и т.д.
В программировании есть общий термин: «замыкание», – который должен знать каждый разработчик.
Замыкание – это функция, которая запоминает свои внешние переменные и может получить к ним доступ. В некоторых языках это невозможно, или функция должна быть написана специальным образом, чтобы получилось замыкание. Но, как было описано выше, в JavaScript, все функции изначально являются замыканиями (есть только одно исключение, про которое будет рассказано в Синтаксис «new Function»).
То есть они автоматически запоминают, где были созданы, с помощью скрытого свойства [[Environment]] , и все они могут получить доступ к внешним переменным.
Когда на собеседовании фронтенд-разработчику задают вопрос: «что такое замыкание?», – правильным ответом будет определение замыкания и объяснения того факта, что все функции в JavaScript являются замыканиями, и, может быть, несколько слов о технических деталях: свойстве [[Environment]] и о том, как работает лексическое окружение.
Сборка мусора
Обычно лексическое окружение удаляется из памяти вместе со всеми переменными после завершения вызова функции. Это связано с тем, что на него нет ссылок. Как и любой объект JavaScript, оно хранится в памяти только до тех пор, пока к нему можно обратиться.
Однако если существует вложенная функция, которая все еще доступна после завершения функции, то она имеет свойство [[Environment]] , ссылающееся на лексическое окружение.
В этом случае лексическое окружение остается доступным даже после завершения работы функции.
function f() < let value = 123; return function() < alert(value); >> let g = f(); // g.[[Environment]] хранит ссылку на лексическое окружение // из соответствующего вызова f()
Обратите внимание, что если f() вызывается много раз и результирующие функции сохраняются, то все соответствующие объекты лексического окружения также будут сохранены в памяти. В приведенном ниже коде – все три:
function f() < let value = Math.random(); return function() < alert(value); >; > // 3 функции в массиве, каждая из которых ссылается на лексическое окружение // из соответствующего вызова f() let arr = [f(), f(), f()];
Объект лексического окружения исчезает, когда становится недоступным (как и любой другой объект). Другими словами, он существует только до тех пор, пока на него ссылается хотя бы одна вложенная функция.
В приведенном ниже коде после удаления вложенной функции ее окружающее лексическое окружение (а значит, и value ) очищается из памяти:
function f() < let value = 123; return function() < alert(value); >> let g = f(); // пока существует функция g, value остается в памяти g = null; // . и теперь память очищена.
Оптимизация на практике
Как мы видели, в теории, пока функция жива, все внешние переменные тоже сохраняются.
Но на практике движки JavaScript пытаются это оптимизировать. Они анализируют использование переменных и, если легко по коду понять, что внешняя переменная не используется – она удаляется.
Одним из важных побочных эффектов в V8 (Chrome, Edge, Opera) является то, что такая переменная становится недоступной при отладке.
Попробуйте запустить следующий пример в Chrome с открытой Developer Tools.
Когда код будет поставлен на паузу, напишите в консоли alert(value) .
function f() < let value = Math.random(); function g() < debugger; // в консоли: напишите alert(value); Такой переменной нет! >return g; > let g = f(); g();
Как вы можете видеть – такой переменной не существует! В теории, она должна быть доступна, но попала под оптимизацию движка.
Это может приводить к забавным (если удаётся решить быстро) проблемам при отладке. Одна из них – мы можем увидеть не ту внешнюю переменную при совпадающих названиях:
let value = "Сюрприз!"; function f() < let value = "ближайшее значение"; function g() < debugger; // в консоли: напишите alert(value); Сюрприз! >return g; > let g = f(); g();
Эту особенность V8 полезно знать. Если вы занимаетесь отладкой в Chrome/Edge/Opera, рано или поздно вы с ней столкнётесь.
Это не баг в отладчике, а скорее особенность V8. Возможно со временем это изменится. Вы всегда можете проверить это, запустив примеры на этой странице.
Задачи
Учитывает ли функция последние изменения?
важность: 5
Функция sayHi использует имя внешней переменной. Какое значение будет использоваться при выполнении функции?
let name = "John"; function sayHi() < alert("Hi, " + name); >name = "Pete"; sayHi(); // что будет показано: "John" или "Pete"?
Такие ситуации встречаются как при разработке для браузера, так и для сервера. Функция может быть назначена на выполнение позже, чем она была создана, например, после действия пользователя или сетевого запроса.
Итак, вопрос: учитывает ли она последние изменения?
Ответ: Pete.
Функция получает внешние переменные в том виде, в котором они находятся сейчас, она использует самые последние значения.
Старые значения переменных нигде не сохраняются. Когда функция обращается к переменной, она берет текущее значение из своего или внешнего лексического окружения.
Какие переменные доступны?
важность: 5
Приведенная ниже функция makeWorker создает другую функцию и возвращает ее. Эта новая функция может быть вызвана из другого места.
Будет ли она иметь доступ к внешним переменным из места своего создания, или из места вызова, или из обоих мест?
function makeWorker() < let name = "Pete"; return function() < alert(name); >; > let name = "John"; // создаём функцию let work = makeWorker(); // вызываем её work(); // что будет показано?
Какое значение будет показано? «Pete» или «John»?
Ответ: Pete.
Функция work() в приведенном ниже коде получает name из места его происхождения через ссылку на внешнее лексическое окружение:
Таким образом, в результате мы получаем «Pete» .
Но если бы в makeWorker() не было let name , то поиск шел бы снаружи и брал глобальную переменную, что мы видим из приведенной выше цепочки. В этом случае результатом было бы «John» .
Независимы ли счётчики?
важность: 5
Здесь мы делаем два счётчика: counter и counter2 , используя одну и ту же функцию makeCounter .
Они независимы? Что покажет второй счётчик? 0,1 или 2,3 или что-то ещё?
function makeCounter() < let count = 0; return function() < return count++; >; > let counter = makeCounter(); let counter2 = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 alert( counter2() ); // ? alert( counter2() ); // ?
Ответ: 0,1.
Функции counter и counter2 созданы разными вызовами makeCounter .
Так что у них независимые внешние лексические окружения, у каждого из которых свой собственный count .
Что такое замыкание в программировании
Сегодня разбираем полезное понятие из мира программирования — замыкание. Это нужно тем, кто хочет серьёзно заниматься разработкой и говорить со старшими товарищами на одном языке.
Для начала потребуется два термина: область видимости и стек. Если нужно вспомнить, раскрывайте:
Что такое область видимости
Область видимости определяет, к каким переменным функция, команда или другая переменная может получить доступ, а к каким нет. Проще говоря, что каждая функция «видит» — видит ли она те переменные и объекты, которые созданы за её пределами? Видит ли она то, что вложено в функции, которые запускаются внутри неё?
Обычно так: если переменная объявлена в самой программе, к ней можно получить доступ из вложенной функции. А если объявить переменную внутри функции, то не обратиться к ней извне не получится.
Что такое стек
Стек — это как список задач при выполнении программы. Сначала компьютер исполняет один код, внутри которого появляется какая-то функция — это как будто отдельная программа. Компьютер откладывает текущую программу, делает себе в стеке пометку «вернуться сюда, когда доделаю новую программу» и исполняет эту новую функцию. Исполнил — смотрит в стек, куда вернуться. Возвращается туда.
Про стек нужно думать, потому что он не безразмерный. Нельзя бесконечно вкладывать функции друг в друга.
Что такое замыкание
Замыкание в программировании — это когда одна функция возвращает как результат своей работы не переменную, а другую функцию. При этом хитрость в том, что внутренняя функция имеет доступ к переменным из внешней функции и может с ними работать в любой момент.
Чаще всего это используют, чтобы сделать переменную, которая на самом деле работает как функция.
Звучит сложно, объясним на примерах.
Пример замыкания в коде
Сделаем функцию, которая вернёт нам фразу «Привет, …» вместе с именем, которое мы в него отправим. Но сделаем это через замыкание — чтобы посмотреть, как именно всё устроено.
Обратите внимание на две переменные в конце: mike и maxim. По сути, эти переменные — ссылки на вызов результата функции hello(), но с конкретным параметром.
// внешняя функция function hello(name) < // внутренняя функция-замыкание // возвращаем её как результат работы внешней return function() < // внутренняя функция выводит сообщение на экран console.log("Привет, " + name); >> // создаём новые переменные, используя замыкание mike = hello("Миша"); maxim = hello("Максим") // запускаем внутреннюю функцию с нашими параметрами, просто указав имена переменной mike(); maxim();

Ещё один пример, посложнее
Только что мы сделали просто — привязали сообщение к переменной без возможности на него повлиять. Но можно создавать такие переменные, у которых могут быть свои параметры — и в них уже передавать любые значения.
Для примера сделаем замыкание и заведём две переменные — каждая будет выдавать сообщение со своим началом фразы, а продолжение будем передавать им в виде параметра:
// внешняя функция, у которой есть свой параметры function greeting(hi) < // внутренняя функция-замыкание, тоже со своим параметром // возвращаем её как результат работы внешней return function(name) < // внутренняя функция выводит сообщение на экран console.log(hi + ", " + name); >> // создаём новые переменные, используя замыкание morning = greeting("Доброе утро"); thnx = greeting("Спасибо за комментарий") // запускаем внутреннюю функцию с нашими параметрами, указав параметры вызова morning("коллеги"); thnx("Павел");

Причём здесь область видимости
Из последнего примера видно, что объявление переменных происходит так:
- переменной присваивается результат выполнения внешней функции;
- этот результат зависит от параметра, который мы ей передали;
- потом, при обращении к переменной, происходит обработка только второго параметра, а первый хранится где-то в памяти и подставляется в нужный момент.
Но как такое может быть, когда внутренняя функция использует переменную из области видимости внешней функции, которой уже нет?
Всё дело в том, что при замыкании создаётся как бы виртуальное окружение, в котором и хранится значение из внешней функции. А вот как это работает с точки зрения области видимости:

Зачем нужны замыкания
На замыканиях строится около половины алгоритмов в функциональном программировании. А ещё на них можно построить много разного:
- изолировать логику выполнения фрагментов кода, если это не позволяют сделать встроенные возможности языка (как в JavaScript);
- лучше структурировать код, особенно при организации функций, которые отличаются только несколькими элементами;
- реализовать инкапсуляцию в тех языках, где её нет.
Что дальше
Следующий шаг — попробовать замыкания в деле. Напишем небольшой код и проверим, как работают замыкания и для чего они могут пригодиться.
JavaScript — Что такое замыкание?
Урок, в котором рассмотрим что такое замыкание в JavaScript и зачем оно нужно. После этого выполним несколько практических примеров. В первом примере разберём, как происходит замыкание, а во втором — некоторую реальную задачу с использованием front-end фреймворка Bootstrap. В конце урока познакомимся с тем, как можно использовать замыкания для создания приватных переменных и функций.
Замыкание. Как оно работает
В JavaScript функции могут находиться внутри других функций. Когда одна функция находится внутри другой, то внутренняя функция имеет доступ к переменным внешней функции. Другими словами, внутренняя функция, при вызове как бы «запоминает» место в котором она родилась (имеет ссылку на внешнее окружение).
Замыкание — это такой механизм в JavaScript, который даёт нам доступ к переменным внешней функции из внутренней.
В качестве примера рассмотрим функцию, которая в качестве результата будет возвращать другую функцию:
JavaScript
function sayHello() { const message = 'Привет, '; return function(name) { return message + name + '!'; } } const result = sayHello(); // ƒ (name) { return message + name + '!'; } console.log(result('Вася')); // "Привет, Вася!"
В этом примере внутреннюю функцию мы создали анонимной, т.к. к ней мы не будем обращаться по её имени. Это действие мы будем выполнять, используя result . Эту функцию при необходимости мы сможем вызвать в любом месте кода. При этом где бы мы это не делали, она всегда будет иметь доступ к своим внешним переменным.
Лексическое окружение
Чтобы разобраться, как этот пример работает, необходимо сначала рассмотреть, что такое лексическое окружение и когда оно создаётся. Лексическое окружение — это скрытый объект, который связан с функцией и создаётся при её запуске. В нём находятся все локальные переменные этой функции, ссылка на внешнее лексическое окружение, а также некоторая другая информация. Кстати, лексическое окружение в JavaScript создаётся также для скрипта и блоков кода.
- Глобальное лексическое окружение (1) будет создано самим скриптом, в нём будет находиться функция sayHello и константа result . У глобального окружения нет внешнего окружения (ссылка на внешнее окружение равна null ).
- Одно внутреннее лексическое окружение (2) будет создано при вызове функции sayHello , которая нам в качестве результата возвратит другую функцию (её мы сохраним в константу result ). В этом лексическом окружении (2) будет находиться переменная message со значением «Привет, » , и ссылка на внешнее (глобальное) окружение (1).
- Другое внутреннее лексическое окружение (3) соответствует вызову result(‘Вася’) . В нём находится одна переменная name со значением ‘Вася’ и ссылка на внешнее лексическое окружение (2), т.к. в JavaScript функция «запоминает» то место, в котором она была создана.
Таким образом, когда мы вызываем result(‘Вася’) , то создаётся лексическое окружение (3), в котором находится не только name со значением «Вася» , но и ссылка на внешнее окружение (2). Это внешнее окружение (2) было создано при запуске функции sayHello . Оно содержит переменную message со значением «Привет, » . Не смотря на то, что функция sayHello уже выполнилась, её лексическое окружение (2) нам доступно, т.к. у нас есть ссылка на него. А т.к. в лексическом окружении (3) нет переменной message , то оно будет искаться в следующем окружении, на которое указывает текущее. Т.е. в лексическом окружении (2). В этом окружении оно есть. Таким образом, в результате выполнения result(‘Вася’) нам будет возвращено «Привет, Вася!» .

Если после console.log(result(‘Вася’)) мы поместим ещё один вызов функции result , то для него создастся лексическое окружение (4), которое то же будет иметь ссылку на внешнего окружение (2). Но, так как в лексическом окружение (4) переменной message нет, то оно будет взято из окружения (2). В результате, нам в консоль будет выведено «Привет, Петя!» :
JavaScript
console.log(result('Петя')); // "Привет, Петя!"

Изменим немного пример:
JavaScript
const message = 'Привет, '; function sayHello() { return function(name) { return message + name + '!'; } } const result = sayHello(); // ƒ (name) { return message + name + '!'; } console.log(result('Вася')); // "Привет, Вася!"
В этом примере выполнение result(‘Вася’) нам также вернёт «Привет, Вася!» . Это произойдёт потому, что при поиске переменной message , интерпретатор, будет переходить по ссылкам, от одного лексического окружения к другому, начиная с текущего, пока не найдёт её. В данном случае он найдёт эту переменную в глобальном окружении.

Поиск переменной
Как же происходит поиск переменной? Поиск переменной всегда начинается с текущего лексического окружения. Т.е., если переменная будет сразу найдена в текущем лексическом окружении, то её дальнейший поиск прекратится и возвратится значение, которая эта переменная имеет здесь. Если искомая переменная в текущем окружении не будет найдена, то произойдёт переход к следующему окружению (ссылка на которое имеется в текущем). Если она не будет найдена в этом, то опять произойдёт переход к следующему окружению, и т.д. Если при поиске переменной, она будет найдена, то её дальнейший поиск прекратится и возвратится значение, которая она имеет здесь.
В качестве примера поместим константу message в другую функцию:
JavaScript
function getMessage() { const message = 'Привет, '; return message; } function sayHello() { return function(name) { return message + name + '!'; // Uncaught ReferenceError: message is not defined } } console.log(getMessage()); const result = sayHello(); // ƒ (name) { return message + name + '!'; } // произойдёт ошибка когда мы вызовем функцию result('Вася') console.log(result('Вася'));
В этом примере произойдёт ошибка, т.к. переменная message не будет найдена. Интерпретатор при её поиске перейдёт от текущего лексического окружения по ссылкам до глобального. А т.к. в нём этой переменной нет и ссылки на следующее окружение тоже (она равна null ), то интерпретатор выдаст ошибку и дальнейшее выполнение этого сценария прекратится.

Ещё один важный момент заключается в том, что лексические окружения создаются и изменяются в процессе выполнения кода. Рассмотрим это на следующем примере:
JavaScript
function sayHello() { return function(name) { return message + name + '!'; } } const result = sayHello(); // ƒ (name) { return message + name + '!'; } let message = 'Привет, '; console.log(result('Вася')); // "Привет, Вася!" message = 'Здравствуйте, '; console.log(result('Вася')); // "Здравствуйте, Вася!"
В этом примере, когда мы первый раз вызываем функцию result(‘Вася’) , в глобальном лексическом окружении переменная message имеет значение ‘Привет, ‘ . В результате мы получим строку «Привет, Вася!» . При втором вызове переменная message имеет уже значение ‘Здравствуйте, ‘ . В результате мы уже получим строку «Здравствуйте, Вася!» .
В JavaScript все функции, кроме функций-конструкторов, являются замыканиями. При вызове функций-конструкторов им в качестве внешнего окружения присваивается ссылка на глобальное окружение, и следовательно, они имеют доступ только к глобальным переменным.
Сборка мусора
В JavaScript лексическое окружение обычно удаляется после того, как функция выполнилась. Это происходит только тогда, когда у нас нет ссылок на это окружение. Как например, в этом примере:
JavaScript
function sayHello(name) { return 'Привет, ' + name + '!'; } console.log(sayHello('Вася')); // "Привет, Вася!"
Но в вышеприведённых примерах со вложенными функциями, у нас лексическое окружение внешней функции оставалась доступным после её выполнения. Т.к. на на неё оставалась ссылка у вложенной функции. А пока есть доступ к лексическому окружению, автоматический сборщик мусора не может его удалить, и оно остаётся держаться в памяти.
Для чего нужны замыкания? Замыкания, например, могут использоваться для «запоминания» параметров, защиты данных (инкапсуляции), привязывания функции к определённому контексту и др. Замыкания положены в основу многих паттернов (шаблонов для написания кода).
Использование замыкания для создания приватных переменных и функций
Замыкания в JavaScript можно использовать для создания приватных переменных и функций.
JavaScript
const counter = () => { // приватная переменная _counter let _counter = 0; // приватная функция _changeBy (изменяет значение переменой _counter на переданное ей значение в качестве аргумента) const _changeBy = (value) => { _counter += value; }; // возвращаемое значение функции (объект, состоящий из 3 методов) return { // публичный метод (функция) increment (для увеличения счетчика на 1) increment() { _changeBy(1); }, // публичный метод (функция) decrement (для уменьшения счетчика на 1) decrement() { _changeBy(-1); }, // публичный метод (функция) value (для получения текущего значения _counter) value() { return _counter; }, }; }; // создадим счетчик 1 const counter1 = counter(); // создадим счетчик 2 const counter2 = counter(); counter1.increment(); counter1.increment(); console.log(counter1.value()); // 2 counter1.decrement(); console.log(counter1.value()); // 1 counter2.decrement(); counter2.decrement(); console.log(counter2.value()); // -2
Напрямую обратиться к _counter и _changeBy нельзя.
JavaScript
console.log(counter1._counter); // undefined counter1._changeBy(1); // Uncaught TypeError: counter1._changeBy is not a function
Обратиться к ним можно только через функции increment , decrement и value .
Примеры для подробного рассмотрения лексического окружения и замыкания
JavaScript
function one() { console.log(num); // Uncaught ReferenceError: num is not defined } function two() { const num = 5; one(); } two();
В этом примере мы получим ошибку. Т.к. функция one имеет в качестве внешнего окружения глобальное, и, следовательно, не может получить доступ к переменной num даже не смотря на то, что вызываем мы её внутри функции two .
JavaScript
function one(num1) { console.log(num1 + num2); } function two() { const num2 = 20; one(num2); } two(); // ?
Какой ответ мы получим в результате выполнения этого примера?
JavaScript
const num2 = 3; function one(num1) { console.log(num1 + num2); } function two() { const num2 = 20; one(num2); } two(); // ?
Какой результат будет в результате выполнения этого примера?
JavaScript — Замыкание на примере
Рассмотрим на примере, как происходит замыкание в JavaScript.
Объявим некоторую функцию, например f1 . Внутри этой функции объявим ещё одну функцию f2 (внутреннюю) и вернём её в качестве результата первой. Функция f1 пусть имеет параметр (переменную) x , а функция f2 — параметр (переменную) y . Функция f2 кроме доступа к параметру x имеет ещё доступ и к параметру y (по цепочки областей видимости).
JavaScript
//родительская функция для f2 function f1(x) { //внутренняя функция f2 по отношению к f1 function f2(y) { return x + y; } //родительская функция возвращает в качестве результата внутреннюю функцию return f2; }
Теперь перейдём к самому интересному, а именно рассмотрим, что произойдёт, если некоторой переменной c1 присвоить вызов функции f1(2) .
JavaScript
var c1 = f1(2);
В результате выполнения функция f1(2) вернёт другую (внутреннюю) функцию f2 . Но, функция f2 в данном контексте позволяет получить значения переменных родительской функции ( f1 ) даже несмотря на то, что функция f1 уже завершила своё выполнение.
Посмотрим детальную информацию о функции:
JavaScript
console.dir(c1);

На изображение видно, что внутренняя функция запомнила окружение, в котором была создана. Она имеет доступ к переменной x родительской функции. Значение данной переменной ( x ) равно числу 2.
Теперь выведем в консоль значение функции c1(5) :
JavaScript
console.log(c1(5));
Данная инструкция отобразит в консоли результат сложения значений параметров x и y . Значение x функция f2 будет брать из родительской области видимости.

Повторим вышепредставленные действия, но уже используя другую переменную ( c2 ):
JavaScript
var c2= f1(5); console.dir(c2); console.log(c2(5));

Представим переменные и функции рассмотренного примера для наглядности в виде следующей схемы:

Итоговый js-код рассмотренного примера:
JavaScript
//родительская функция function f1(x) { //внутренняя функция f2 function f2(y) { return x + y; } //родительская функция возвращает в качестве результата внутреннюю функцию return f2; } var c1 = f1(2); var c2 = f1(5); //отобразим детальную информацию о функции c1 console.dir(c1); //отобразим детальную информацию о функции c2 console.dir(c2); console.log(c1(5)); //7 console.log(c2(5)); //10
Замыкания на практике
Замыкания в JavaScript являются очень интересной вещью. Они позволяют связать некоторые данные с функцией. Это очень похоже на то, как это реализовано в объекте, который позволяет связать свойства (переменные) и методы (действия над этими переменными). Такие задачи в веб-разработке попадаются очень часто. Давайте рассмотрим одну из подобных задач.
Допустим, необходимо создать несколько модальных окон на странице с привязкой их к конкретным кнопкам. Кроме этого в задании говорится ещё о том, что необходимо сделать так, чтобы можно было легко менять при необходимости заголовок и содержимое модального окна.
Кнопки, открывающие модальные окна:
Функция, возвращая в качестве результата другую функцию:
JavaScript
function modalContent(idModal,idButton){ //переменная, содержащая код модального окна Bootstrap var modal=''+ ''+ ''+ ''+ ' '+ ''+ ''+ ''; //инструкция, добавляющая HTML-код модального окна сразу после открывающего тега body $(modal).prependTo('body'); //связываем модальное окно с кнопкой: $('#'+idButton).click(function(){ $('#'+idModal).modal('show'); }); // функция modalContent возвращает в качестве результата другую функцию return function(modalTitle,modalBody) { //устанавливаем заголовок модальному окну $('#'+idModal).find('.modal-title').html(modalTitle); //устанавливаем модальному окну содержимое $('#'+idModal).find('.modal-body').html(modalBody); } }
Код, который выполняет создание модальных окон и установлением каждому из них заголовка и некоторого содержимого:
JavaScript
$(function(){ //1 модальное окно var modal1 = modalContent('modal1','myButton1'); modal1('Заголовок 1','Содержимое 1.
'); //2 модальное окно var modal2 = modalContent('modal2','myButton2'); modal2('Заголовок 2','Содержимое 2.
'); //3 модальное окно var modal3 = modalContent('modal3','myButton3'); modal3('Заголовок 3','Содержимое 3.
'); });
Итоговый код (кнопки + скрипт):
Если необходимо изменить при наступлении каких-то событий заголовок и содержимое модального окна (например, второго), то это будет выглядеть так:
JavaScriptmodal2('Другой заголовок','Другое содержимое.
');Замыкания
Замыкание — это комбинация функции и лексического окружения, в котором эта функция была определена. Другими словами, замыкание даёт вам доступ к Scope (en-US) внешней функции из внутренней функции. В JavaScript замыкания создаются каждый раз при создании функции, во время её создания.
Лексическая область видимости
Рассмотрим следующий пример:
function init() var name = "Mozilla"; // name - локальная переменная, созданная в init function displayName() // displayName() - внутренняя функция, замыкание alert(name); // displayName() использует переменную, объявленную в родительской функции > displayName(); > init();init() создаёт локальную переменную name и определяет функцию displayName() . displayName() — это внутренняя функция — она определена внутри init() и доступна только внутри тела функции init() . Обратите внимание, что функция displayName() не имеет никаких собственных локальных переменных. Однако, поскольку внутренние функции имеют доступ к переменным внешних функций, displayName() может иметь доступ к переменной name , объявленной в родительской функции init() .
Выполните этот код и обратите внимание, что команда alert() внутри displayName() благополучно выводит на экран содержимое переменной name объявленной в родительской функции. Это пример так называемой лексической области видимости (lexical scoping): в JavaScript область действия переменной определяется по её расположению в коде (это очевидно лексически), и вложенные функции имеют доступ к переменным, объявленным вовне. Этот механизм и называется Lexical scoping (область действия, ограниченная лексически).
Замыкание
Рассмотрим следующий пример:
function makeFunc() var name = "Mozilla"; function displayName() alert(name); > return displayName; > var myFunc = makeFunc(); myFunc();Если выполнить этот код, то результат будет такой же, как и выполнение init() из предыдущего примера: строка "Mozilla" будет показана в JavaScript alert диалоге. Что отличает этот код и представляет для нас интерес, так это то, что внутренняя функция displayName() была возвращена из внешней до того, как была выполнена.
На первый взгляд, кажется неочевидным, что этот код правильный, но он работает. В некоторых языках программирования локальные переменные-функции существуют только во время выполнения этой функции. После завершения выполнения makeFunc() можно ожидать, что переменная name больше не будет доступна. Однако, поскольку код продолжает нормально работать, очевидно, что это не так в случае JavaScript.
Причина в том, что функции в JavaScript формируют так называемые замыкания. Замыкание — это комбинация функции и лексического окружения, в котором эта функция была объявлена. Это окружение состоит из произвольного количества локальных переменных, которые были в области действия функции во время создания замыкания. В рассмотренном примере myFunc — это ссылка на экземпляр функции displayName , созданной в результате выполнения makeFunc . Экземпляр функции displayName в свою очередь сохраняет ссылку на своё лексическое окружение, в котором есть переменная name . По этой причине, когда происходит вызов функции myFunc , переменная name остаётся доступной для использования и сохранённый в ней текст "Mozilla" передаётся в alert .
А вот немного более интересный пример — функция makeAdder :
function makeAdder(x) return function (y) return x + y; >; > var add5 = makeAdder(5); var add10 = makeAdder(10); console.log(add5(2)); // 7 console.log(add10(2)); // 12Здесь мы определили функцию makeAdder(x) , которая получает единственный аргумент x и возвращает новую функцию. Эта функция получает единственный аргумент y и возвращает сумму x и y .
По существу makeAdder — это фабрика функций: она создаёт функции, которые могут прибавлять определённое значение к своему аргументу. В примере выше мы используем нашу фабричную функцию для создания двух новых функций — одна прибавляет 5 к своему аргументу, вторая прибавляет 10.
add5 и add10 — это примеры замыканий. Эти функции делят одно определение тела функции, но при этом они сохраняют различные окружения. В окружении функции add5 x — это 5, в то время как в окружении add10 x — это 10.
Замыкания на практике
Замыкания полезны тем, что позволяют связать данные (лексическое окружение) с функцией, которая работает с этими данными. Очевидна параллель с объектно-ориентированным программированием, где объекты позволяют нам связать некоторые данные (свойства объекта) с одним или несколькими методами.
Следовательно, замыкания можно использовать везде, где вы обычно использовали объект с одним единственным методом.
Такие ситуации повсеместно встречаются в web-разработке. Большое количество front-end кода, который мы пишем на JavaScript, основано на обработке событий. Мы описываем какое-то поведение, а потом связываем его с событием, которое создаётся пользователем (например, клик мышкой или нажатие клавиши). При этом наш код обычно привязывается к событию в виде обратного/ответного вызова (callback): callback функция - функция выполняемая в ответ на возникновение события.
Давайте рассмотрим практический пример: допустим, мы хотим добавить на страницу несколько кнопок, которые будут менять размер текста. Как вариант, мы можем указать свойство font-size на элементе body в пикселах, а затем устанавливать размер прочих элементов страницы (таких, как заголовки) с использованием относительных единиц em:
body font-family: Helvetica, Arial, sans-serif; font-size: 12px; > h1 font-size: 1.5em; > h2 font-size: 1.2em; >Тогда наши кнопки будут менять свойство font-size элемента body, а остальные элементы страницы просто получат это новое значение и отмасштабируют размер текста благодаря использованию относительных единиц.
Используем следующий JavaScript:
function makeSizer(size) return function () document.body.style.fontSize = size + "px"; >; > var size12 = makeSizer(12); var size14 = makeSizer(14); var size16 = makeSizer(16);Теперь size12 , size14 , и size16 - это функции, которые меняют размер текста в элементе body на значения 12, 14, и 16 пикселов, соответственно. После чего мы цепляем эти функции на кнопки примерно так:
.getElementById("size-12").onclick = size12; document.getElementById("size-14").onclick = size14; document.getElementById("size-16").onclick = size16;a href="#" id="size-12">12a> a href="#" id="size-14">14a> a href="#" id="size-16">16a>Эмуляция частных (private) методов с помощью замыканий
Языки вроде Java позволяют нам объявлять частные (private) методы . Это значит, что они могут быть вызваны только методами того же класса, в котором объявлены.
JavaScript не имеет встроенной возможности сделать такое, но это можно эмулировать с помощью замыкания. Частные методы полезны не только тем, что ограничивают доступ к коду, это также мощное средство глобальной организации пространства имён, позволяющее не засорять публичный интерфейс вашего кода внутренними методами классов.
Код ниже иллюстрирует, как можно использовать замыкания для определения публичных функций, которые имеют доступ к закрытым от пользователя (private) функциям и переменным. Такая манера программирования называется модульное программирование:
var Counter = (function () var privateCounter = 0; function changeBy(val) privateCounter += val; > return increment: function () changeBy(1); >, decrement: function () changeBy(-1); >, value: function () return privateCounter; >, >; >)(); alert(Counter.value()); /* Alerts 0 */ Counter.increment(); Counter.increment(); alert(Counter.value()); /* Alerts 2 */ Counter.decrement(); alert(Counter.value()); /* Alerts 1 */Тут много чего поменялось. В предыдущем примере каждое замыкание имело свой собственный контекст исполнения (окружение). Здесь мы создаём единое окружение для трёх функций: Counter.increment , Counter.decrement , и Counter.value .
Единое окружение создаётся в теле анонимной функции, которая исполняется в момент описания. Это окружение содержит два приватных элемента: переменную privateCounter и функцию changeBy(val) . Ни один из этих элементов не доступен напрямую, за пределами этой самой анонимной функции. Вместо этого они могут и должны использоваться тремя публичными функциями, которые возвращаются анонимным блоком кода (anonymous wrapper), выполняемым в той же анонимной функции.
Эти три публичные функции являются замыканиями, использующими общий контекст исполнения (окружение). Благодаря механизму lexical scoping в Javascript, все они имеют доступ к переменной privateCounter и функции changeBy .
Заметьте, мы описываем анонимную функцию, создающую счётчик, и тут же запускаем её, присваивая результат исполнения переменной Counter . Но мы также можем не запускать эту функцию сразу, а сохранить её в отдельной переменной, чтобы использовать для дальнейшего создания нескольких счётчиков вот так:
var makeCounter = function () var privateCounter = 0; function changeBy(val) privateCounter += val; > return increment: function () changeBy(1); >, decrement: function () changeBy(-1); >, value: function () return privateCounter; >, >; >; var Counter1 = makeCounter(); var Counter2 = makeCounter(); alert(Counter1.value()); /* Alerts 0 */ Counter1.increment(); Counter1.increment(); alert(Counter1.value()); /* Alerts 2 */ Counter1.decrement(); alert(Counter1.value()); /* Alerts 1 */ alert(Counter2.value()); /* Alerts 0 */Заметьте, что счётчики работают независимо друг от друга. Это происходит потому, что у каждого из них в момент создания функцией makeCounter() также создавался свой отдельный контекст исполнения (окружение). То есть приватная переменная privateCounter в каждом из счётчиков это действительно отдельная, самостоятельная переменная.
Используя замыкания подобным образом, вы получаете ряд преимуществ, обычно ассоциируемых с объектно-ориентированным программированием, таких как изоляция и инкапсуляция.
Создание замыканий в цикле: Очень частая ошибка
До того, как в версии ECMAScript 6 ввели ключевое слово let , постоянно возникала следующая проблема при создании замыканий внутри цикла. Рассмотрим пример:
p id="help">Helpful notes will appear herep> p>E-mail: input type="text" id="email" name="email" />p> p>Name: input type="text" id="name" name="name" />p> p>Age: input type="text" id="age" name="age" />p>function showHelp(help) document.getElementById("help").innerHTML = help; > function setupHelp() var helpText = [ id: "email", help: "Ваш адрес e-mail" >, id: "name", help: "Ваше полное имя" >, id: "age", help: "Ваш возраст (Вам должно быть больше 16)" >, ]; for (var i = 0; i helpText.length; i++) var item = helpText[i]; document.getElementById(item.id).onfocus = function () showHelp(item.help); >; > > setupHelp();Массив helpText описывает три подсказки для трёх полей ввода. Цикл пробегает эти описания по очереди и для каждого из полей ввода определяет, что при возникновении события onfocus для этого элемента должна вызываться функция, показывающая соответствующую подсказку.
Если вы запустите этот код, то увидите, что он работает не так, как мы ожидаем интуитивно. Какое поле вы бы ни выбрали, в качестве подсказки всегда будет высвечиваться сообщение о возрасте.
Проблема в том, что функции, присвоенные как обработчики события onfocus , являются замыканиями. Они состоят из описания функции и контекста исполнения (окружения), унаследованного от функции setupHelp . Было создано три замыкания, но все они были созданы с одним и тем же контекстом исполнения. К моменту возникновения события onfocus цикл уже давно отработал, а значит, переменная item (одна и та же для всех трёх замыканий) указывает на последний элемент массива, который как раз в поле возраста.
В качестве решения в этом случае можно предложить использование функции, фабричной функции (function factory), как уже было описано выше в примерах:
function showHelp(help) document.getElementById("help").innerHTML = help; > function makeHelpCallback(help) return function () showHelp(help); >; > function setupHelp() var helpText = [ id: "email", help: "Ваш адрес e-mail" >, id: "name", help: "Ваше полное имя" >, id: "age", help: "Ваш возраст (Вам должно быть больше 16)" >, ]; for (var i = 0; i helpText.length; i++) var item = helpText[i]; document.getElementById(item.id).onfocus = makeHelpCallback(item.help); > > setupHelp();Вот это работает как следует. Вместо того, чтобы делить на всех одно окружение, функция makeHelpCallback создаёт каждому из замыканий своё собственное, в котором переменная item указывает на правильный элемент массива helpText .
Соображения по производительности
Не нужно без необходимости создавать функции внутри функций в тех случаях, когда замыкания не нужны. Использование этой техники увеличивает требования к производительности как в части скорости, так и в части потребления памяти.
Как пример, при написании нового класса есть смысл помещать все методы в прототип его объекта, а не описывать их в тексте конструктора. Если сделать по-другому, то при каждом создании объекта для него будет создан свой экземпляр каждого из методов, вместо того, чтобы наследовать их из прототипа.
Давайте рассмотрим не очень практичный, но показательный пример:
function MyObject(name, message) this.name = name.toString(); this.message = message.toString(); this.getName = function () return this.name; >; this.getMessage = function () return this.message; >; >Поскольку вышеприведённый код никак не использует преимущества замыканий, его можно переписать следующим образом:
function MyObject(name, message) this.name = name.toString(); this.message = message.toString(); > MyObject.prototype = getName: function () return this.name; >, getMessage: function () return this.message; >, >;Методы вынесены в прототип. Тем не менее, переопределять прототип — само по себе является плохой привычкой, поэтому давайте перепишем всё так, чтобы новые методы просто добавились к уже существующему прототипу.
function MyObject(name, message) this.name = name.toString(); this.message = message.toString(); > MyObject.prototype.getName = function () return this.name; >; MyObject.prototype.getMessage = function () return this.message; >;Код выше можно сделать аккуратнее:
function MyObject(name, message) this.name = name.toString(); this.message = message.toString(); > (function () this.getName = function () return this.name; >; this.getMessage = function () return this.message; >; >).call(MyObject.prototype);В обоих примерах выше методы определяются один раз — в прототипе. И все объекты, использующие данный прототип, будут использовать это определение без дополнительного расхода вычислительных ресурсов. Смотрите подробное описание в статье Подробнее об объектной модели.
Found a content problem with this page?
- Edit the page on GitHub.
- Report the content issue.
- View the source on GitHub.
This page was last modified on 7 авг. 2023 г. by MDN contributors.
Your blueprint for a better internet.
