Что такое замыкание js
Перейти к содержимому

Что такое замыкание js

  • автор:

Область видимости переменных, замыкание

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 .

Объект лексического окружения состоит из двух частей:

  1. Environment Record – объект, в котором как свойства хранятся все локальные переменные (а также некоторая другая информация, такая как значение this ).
  2. Ссылка на внешнее лексическое окружение – то есть то, которое соответствует коду снаружи (снаружи от текущих фигурных скобок).

«Переменная» – это просто свойство специального внутреннего объекта: Environment Record . «Получить или изменить переменную», означает, «получить или изменить свойство этого объекта».

Например, в этом простом коде только одно лексическое окружение:

Это, так называемое, глобальное лексическое окружение, связанное со всем скриптом.

На картинке выше прямоугольник означает Environment Record (хранилище переменных), а стрелка означает ссылку на внешнее окружение. У глобального лексического окружения нет внешнего окружения, так что она указывает на null .

По мере выполнения кода лексическое окружение меняется.

Вот более длинный код:

Прямоугольники с правой стороны демонстрируют, как глобальное лексическое окружение изменяется в процессе выполнения кода:

  1. При запуске скрипта лексическое окружение предварительно заполняется всеми объявленными переменными.
    • Изначально они находятся в состоянии «Uninitialized». Это особое внутреннее состояние, которое означает, что движок знает о переменной, но на нее нельзя ссылаться, пока она не будет объявлена с помощью let . Это почти то же самое, как если бы переменная не существовала.
  2. Появляется определение переменной let phrase . У неё ещё нет присвоенного значения, поэтому присваивается undefined . С этого момента мы можем использовать переменную.
  3. Переменной phrase присваивается значение.
  4. Переменная 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 .

что такое замыкание js

«Замыкание» — это способность функции запоминать переменные, которые были определены внутри родительской функции, даже после того, как родительская функция была выполнена.

function createCounter()  // переменная, которую нужно запомнить let count = 0; function counter()  // увеличиваем нашу запомненную переменную count++; console.log(count); > // возвращаем функцию return counter; > // создаем новую функцию (с замыканием) const incrementCounter = createCounter(); incrementCounter(); // 1 incrementCounter(); // 2 incrementCounter(); // 3 

В этом примере мы создали функцию createCounter() , которая создает другую функцию counter() . Внутри функции мы создали переменную count , которая была определена внутри родительской функции. Функция counter() возвращает значение count , увеличивая его на 1 . Когда мы вызываем createCounter() , она возвращает функцию counter() , которая имеет доступ к count благодаря замыканию. Каждый раз, когда мы вызываем incrementCounter() ,

Если мы создадим новый счетчик с помощью функции createCounter() , то отсчет для него начнется заново.

// Создали еще одну новую функци-счетчик const newIncrementCounter = createCounter(); newIncrementCounter(); // 1 newIncrementCounter(); // 2 newIncrementCounter(); // 3 

Понимаем замыкания в JavaScript. Раз и навсегда

Тема измученная, старая, но всё равно в ней часто делаются ошибки. На самом деле замыкания могут показаться запутанными с первого взгляда, но по факту тут нет ничего сложного. В этой статье собраны переводы двух статей. Первая это теория, которая показывает то, что находится под капотом, а вторая это довольно интересные примеры для закрепления материала.

��Мой Твиттер — там много из мира фронтенда, да и вообще поговорим��. Подписывайтесь, будет интересно: ) ✈️

Замыкания это фундаментальная концепция JavaScript, которую должен понимать каждый разработчик пишущий на нем. Да, она сбивает с толку многих новичков в JS.

Имея четкое понимание замыканий, вы будете писать код лучше, куда эффективнее и чище. Что в итоге, поможет вам в продвижении своих профессиональных навыков.

В этой статье я попытаюсь объяснить внутреннюю структуру замыканий и то, как они на самом деле работают в JavaScript.

Давайте уже начнём.

Что такое замыкание?

Замыкание это функция у которой есть доступ к своей внешней функции по области видимости, даже после того, как внешняя функция прекратилась. Это говорит о том, что замыкание может запоминать и получать доступ к переменным, и аргументам своей внешней функции, даже после того, как та прекратит выполнение.

Перед тем как мы углубимся в замыкания, давайте сначала поймем лексическую область видимости.

Что такое лексическая область видимости?

Лексическая область видимости это статическая область в JavaScript, имеющая прямое отношение к доступу к переменным, функциям и объектам, основываясь на их расположении в коде. Вот пример:

let a = 'global';function outer() let b = 'outer';function inner() let c = 'inner' 
console.log(c); // выдаст 'inner'
console.log(b); // выдаст 'outer'
console.log(a); // выдаст 'global'
>
console.log(a); // выдаст 'global'
console.log(b); // выдаст 'outer'
inner();
>
outer();
console.log(a); // выдаст 'global'

Тут функция inner имеет доступ к переменным в своей области видимости, в области видимости функции outer и глобальной области видимости. Функция outer имеет доступ к переменным, объявленным в собственной области видимости и глобальной области видимости.

В общем, цепочка области видимости выше будет такой:

Global outer inner 
>
>

Обратите внимание, что функция inner окружена лексической областью видимости функции outer , которая, в свою очередь, окружена глобальной областью видимости. Поэтому функция inner имеет доступ к переменным, определенным в функции outer и глобальной области видимости.

Практические примеры замыкания

Давайте взглянем на практические примеры замыканий, перед тем как углубляться в то, как они работают.

Пример 1:

function person() let name = 'Peter'; 

return function displayName() console.log(name);
>;
>
let peter = person();
peter(); // выведет 'Peter'

В этом примере мы вызываем функцию person , которая возвращает внутреннюю функцию displayName и сохраняет эту внутреннюю функцию в переменную peter . Когда мы вызываем функцию peter (которая на самом деле ссылается к функции displayName ), имя “Peter” выводится в консоль.

Но у нас же нет никакой переменной с именем name в displayName , так что эта функция как-то может получить доступ к переменной своей внешней функции person , даже после того, как та функция выполнится. Так что, функция displayName это ни что иное как замыкание.

Пример 2:

function getCounter() let counter = 0; 
return function() return counter++;
>
>
let count = getCounter();console.log(count()); // 0
console.log(count()); // 1
console.log(count()); // 2

И снова, мы храним анонимную внутреннюю функцию, возвращенную функцией getCounter в переменной count . Так как функция сount теперь замыкание, она может получать доступ к переменной counter в функции getCounter , даже после того, как та завершится.

Но обратите внимание, что значение counter не сбрасывается до 0 при каждом вызове count , как вроде бы она должна делать.

Так происходит, потому что при каждом вызове count() , создаётся новая область видимости, но есть только одна область видимости, созданная для getCounter , так как переменная counter объявлена в области видимости getCounter() , она увеличится при каждом вызове функции count , вместо того, чтобы сброситься до 0 .

Как работают замыкания?

До этого момента мы обсуждали то, чем являются замыкания и их практические примеры. Сейчас давайте поймём то, как замыкания на самом деле работают в JavaScript.

Чтобы реально это понять, нам надо разобраться в двумя самыми важными концепциями в JavaScript, а именно, 1) Контекст выполнения и 2) Лексическое окружение.

Контекст выполнения

Это абстрактная среда, в которой JavaScript код оценивается и выполняется. Когда выполняется “глобальный” код, он выполняется внутри глобального контекста выполнения, а код функции выполняется внутри контекста выполнения функции.

Тут может быть только один запущенный контекст выполнения (JavaScript это однопоточный язык), который управляется стеком запросов.

Стек выполнения это стек с принципом LIFO (Последний вошёл, первый вышел), в котором элементы могут быть добавлены или удалены только сверху стека.

Запущенный контекст выполнения будет всегда сверху стека и когда запущенная функция завершится, её контекст выполнения выкинется из стека, запустив контекст выполнения, который стоит ниже в очереди.

Давайте посмотрим на пример кода, чтобы лучше понять контекст выполнения и стек:

Во время выполнения этого кода, движок JavaScript создаёт глобальный контекст вызова, для того, чтобы выполнить глобальный код и когда он доходит до вызова функции first() , он создаёт новый контекст выполнения для этой функции и ставит её на вершину стека вызовов.

Так что он будет выглядеть таким образом для кода выше:

Когда функция first() завершится, её стек выполнения удалится и начнется выполнение кода ниже. Так что оставшийся код в глобальной области видимости будет выполнен.

Лексическое окружение

Каждый раз, когда движок JavaScript создаёт контекст выполнения, чтобы выполнить функцию или глобальный код, он также создаёт новое лексическое окружение, чтобы хранить переменную определенную в этой функции во время её выполнения.

Лексическое окружение это структура данных, которая хранит информацию по идентификаторам переменных. Тут идентификатор обозначает имя переменных/функций, а переменная настоящий объект[включая тип функции] или примитивное значение.

У лексического окружения есть два компонента: (1) запись в окружении и (2) отсылка к внешнему окружению.

  1. Запись в окружении(environment record) это место хранятся объявления переменной или функции.

2. Отсылка к внешнему окружению (reference to the outer environment) означает то, что у него есть доступ к внешнему (родительскому) лексическому окружению. Этот компонент самый важный для понимания того, как работают замыкания.

Лексическое окружение на самом деле выглядит так:

lexicalEnvironment = environmentRecord:  : , 
:
>
outer: < Reference to the parent lexical environment>
>

Теперь снова, давайте посмотрим на пример кода выше:

let a = 'Hello World!';function first() let b = 25; 
console.log('Inside first function');
>
first();
console.log('Inside global execution context');

Когда движок JavaScript создаёт глобальный контекст выполнения, чтобы выполнить глобальный код, он также создаёт новое лексическое окружение, чтобы хранить переменные и функции, определенные в глобальной области видимости. Так что лексическое окружение для глобальной области видимости будет выглядеть примерно так:

globalLexicalEnvironment = environmentRecord: a : 'Hello World!', 
first : < reference to function object >
>
outer: null
>

Тут лексическое окружение выставлено на null , потому что нет внешнего лексического окружения для глобальной области видимости.

Когда движок создаёт контекст выполнения для функции first() , он также создаёт лексическое окружение для хранения переменных, объявленных в этой функции во время выполнения. Таким образом, лексическое окружение функции будет выглядеть вот так:

functionLexicalEnvironment = environmentRecord: b : 25, 
>
outer:
>

Внешнее лексическое окружение функции указывается в глобальном лексическом окружении, потому что функция окружена глобальной областью видимости в исходном коде.

Обратите внимание — когда функция выполняется, её контекст выполнения удаляется из стека, но её лексическое окружение может или не может быть удалено из памяти, в зависимости от того, ссылается ли на это лексическое окружение другое лексическое окружение.

А теперь детально о примерах замыканий

Теперь, когда мы поняли контекст выполнения и лексическое окружение, давайте вернёмся к замыканиям.

Пример 1:

function person() let name = 'Peter'; 

return function displayName() console.log(name);
>;
>
let peter = person();
peter(); // prints 'Peter'

Когда выполняется функция person , JavaScript создаёт новый контекст выполнения и лексическое окружение для функции. После того, как эта функция завершится, она вернёт displayName функцию и назначится на переменную peter .

Таким образом, её лексическое окружение будет выглядеть так:

personLexicalEnvironment = environmentRecord: name : 'Peter', 
displayName: < displayName function reference>
>
outer:
>

Когда функция person завершится, её контекст выполнения выкинется из стека. Но её лексическое окружение всё ещё останется в памяти, так как на него ссылается лексическое окружение его внутренней функции displayName . Таким образом, её переменные всё ещё будут доступны в памяти.

При выполнении функции peter (которая на самом деле является отсылкой к функции displayName ), JavaScript создаёт новый контекст выполнения и лексическое окружение для этой функции.

Так что его лексическое окружение будет выглядеть таким образом:

displayNameLexicalEnvironment = environmentRecord:  
>
outer:
>

В функции displayName нет переменной, её запись окружения будет пуста. Во время выполнения этой функции, JavaScript будет пытаться найти переменную name в лексическом окружении функции.

Так как там нет переменных в лексическом окружении функции displayName, она будет искать во внешнем лексическом окружении, то есть, лексическом окружении функции person , которое до сих пор в памяти. JavaScript найдёт эту переменную и name выводится в консоль.

Пример 2:

function getCounter() let counter = 0; 
return function() return counter++;
>
>
let count = getCounter();console.log(count()); // 0
console.log(count()); // 1
console.log(count()); // 2

Снова, лексическое окружение для функции getCounter будет выглядеть таким образом:

getCounterLexicalEnvironment = environmentRecord: counter: 0, 
: < reference to function>
>
outer:
>

Эта функция возвращает анонимную функцию и назначает её на переменную count .

Когда функция count выполняется, её лексическое окружение будет выглядеть таким образом:

countLexicalEnvironment = environmentRecord:  
>
outer:
>

Когда функция count вызывается, JavaScript начнет поиск в лексическом окружении этой функции на наличие переменной counter . Снова, если ее запись окружения пуста, то движок пойдёт искать во внешнем лексическом окружении функции.

Движок находит переменную, выводит её в консоль и увеличивает переменную counter в лексическом окружении getCounter функции.

Таким образом, лексическое окружение для функции getCounter после первого вызова функции count будет выглядеть таким образом:

getCounterLexicalEnvironment = environmentRecord: counter: 1, 
: < reference to function>
>
outer:
>

На каждом вызове функции count , JavaScript создаёт новое лексическое окружение для функции count, увеличивает переменную count и обновляет лексическое окружения функции getCounter , чтобы соответствовать изменениям.

Закрепляем понимание замыканий в JavaScript на примерах

Замыкания это такая вещь в JavaScript, что сколько бы вы про неё не читали, всё равно полностью не поймете. Как и с многим в программировании, вам нужно покрутить-повертеть какие-нибудь примеры, чтобы полностью принять и понять нужную концепцию.

Теперь давайте посмотрим на некоторые примеры того, как же используются замыкания и пройдёмся по процессу создания вашего приложения, где мы воспользуемся преимуществами вышеупомянутых замыканий.

Снова, что такое замыкание?

Мне очень нравится определение из Secrets of the JavaScript:

Замыкание это способ получения доступа и управления внешними переменными из функции.

Мне нравится рассматривать замыкания как функцию языка программирования, которая позволяет нам делать крутые вещи, такие как:

Объектно-ориентированное программирование в 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(‘Вася’) нам будет возвращено «Привет, Вася!» .

Что такое лексическое окружение в JavaScript

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

JavaScript

console.log(result('Петя')); // "Привет, Петя!"

Лексическое окружение в JavaScript

Изменим немного пример:

JavaScript

const message = 'Привет, '; function sayHello() { return function(name) { return message + name + '!'; } } const result = sayHello(); // ƒ (name) { return message + name + '!'; } console.log(result('Вася')); // "Привет, Вася!"

В этом примере выполнение result(‘Вася’) нам также вернёт «Привет, Вася!» . Это произойдёт потому, что при поиске переменной message , интерпретатор, будет переходить по ссылкам, от одного лексического окружения к другому, начиная с текущего, пока не найдёт её. В данном случае он найдёт эту переменную в глобальном окружении.

Поиск переменной в коде JavaScript

Поиск переменной

Как же происходит поиск переменной? Поиск переменной всегда начинается с текущего лексического окружения. Т.е., если переменная будет сразу найдена в текущем лексическом окружении, то её дальнейший поиск прекратится и возвратится значение, которая эта переменная имеет здесь. Если искомая переменная в текущем окружении не будет найдена, то произойдёт переход к следующему окружению (ссылка на которое имеется в текущем). Если она не будет найдена в этом, то опять произойдёт переход к следующему окружению, и т.д. Если при поиске переменной, она будет найдена, то её дальнейший поиск прекратится и возвратится значение, которая она имеет здесь.

В качестве примера поместим константу 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

Ещё один важный момент заключается в том, что лексические окружения создаются и изменяются в процессе выполнения кода. Рассмотрим это на следующем примере:

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);

JavaScript - Информация о функции c1 (console.dir)

На изображение видно, что внутренняя функция запомнила окружение, в котором была создана. Она имеет доступ к переменной x родительской функции. Значение данной переменной ( x ) равно числу 2.

Теперь выведем в консоль значение функции c1(5) :

JavaScript

console.log(c1(5));

Данная инструкция отобразит в консоли результат сложения значений параметров x и y . Значение x функция f2 будет брать из родительской области видимости.

JavaScript - Вывод в консоль значения функции c1(5)

Повторим вышепредставленные действия, но уже используя другую переменную ( c2 ):

JavaScript

var c2= f1(5); console.dir(c2); console.log(c2(5));

JavaScript - Вывод информации о функции c2 и значения функции c2(5) в консоль

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

JavaScript - Переменные и функции вышепредставленного примера в виде схемы

Итоговый 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.

'); });

Итоговый код (кнопки + скрипт):

    

Если необходимо изменить при наступлении каких-то событий заголовок и содержимое модального окна (например, второго), то это будет выглядеть так:

JavaScript
modal2('Другой заголовок','

Другое содержимое.

');

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *