Замыкание для чего нужно

Замыкания в JavaScript

Если вы используете JavaScript, но при этом так до конца и не разобрались, что же это за чудная штука такая — замыкания, и зачем она нужна — эта статья для вас.

Как известно, в JavaScript областью видимости локальных переменных (объявляемых словом var) является тело функции, внутри которой они определены.

Если вы объявляете функцию внутри другой функции, первая получает доступ к переменным и аргументам последней:

function outerFn(myArg) <
var myVar;
function innerFn() <
//имеет доступ к myVar и myArg
>
>

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

Рассмотрим пример — функцию, возвращающую кол-во собственных вызовов:

function createCounter() <
var numberOfCalls = 0;
return function () <
return ++numberOfCalls;
>
>
var fn = createCounter();
fn(); //1
fn(); //2
fn(); //3

В данном примере функция, возвращаемая createCounter, использует переменную numberOfCalls, которая сохраняет нужное значение между ее вызовами (вместо того, чтобы сразу прекратить своё существование с возвратом createCounter).

Именно за эти свойства такие «вложенные» функции в JavaScript называют замыканиями (термином, пришедшим из функциональных языков программирования) — они «замыкают» на себя переменные и аргументы функции, внутри которой определены.

Применение замыканий

var fn = ( function () <
var numberOfCalls = 0;
return function () <
return ++ numberOfCalls;
>
>)();

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

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

var createHelloFunction = function (name) <
return function () <
alert( ‘Hello, ‘ + name);
>
>
var sayHelloHabrahabr = createHelloFunction( ‘Habrahabr’ );
sayHelloHabrahabr(); //alerts «Hello, Habrahabr»

Благодаря замыканию возвращаемая функция «запоминает» параметры, переданные функции создающей, что нам и нужно для подобного рода вещей.

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

Рассмотрим чуть более сложный пример — метод, привязывающий функцию к определённому контексту (т.е. объекту, на который в ней будет указывать слово this).

В этом примере с помощью замыканий функция, вощвращаемая bind’ом, запоминает в себе начальную функцию и присваиваемый ей контекст.

Следующее, принципиально иное применение замыканий — защита данных (инкапсуляция). Рассмотрим следующую конструкцию:

Очевидно, внутри замыкания мы имеем доступ ко всем внешним данным, но при этом оно имеет и собственные. Благодаря этому мы можем окружать части кода подобной конструкцией с целью закрыть попавшие внутрь локальные переменные от доступа снаружи. (Один из примеров ее использования вы можете увидеть в исходном коде библиотеки jQuery, которая окружает замыканием весь свой код, чтобы не выводить за его пределы нужные только ей переменные).

Есть, правда, одна связанная с таким применением ловушка — внутри замыкания теряется значение слова this за его пределами. Решается она следующим образом:

Рассмотрим еще один приём из той же серии. Повсеместно популяризовали его разработчики фреймворка Yahoo UI, назвав его «Module Pattern» и написав о нём целую статью в официальном блоге.

Пускай у нас есть объект (синглтон), содержащий какие-либо методы и свойства:

var MyModule = ( function () <
var name = ‘Habrahabr’ ;
function sayPreved() <
alert( ‘PREVED ‘ + name.toUpperCase());
>
return <
sayPrevedToHabrahabr: function () <
sayPreved(name);
>
>
>)();
MyModule.sayPrevedToHabrahabr(); //alerts «PREVED Habrahabr»

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

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

На деле же оказывается, что при клике на любую ссылку выводится одно и то же число — значение links.length. Почему так происходит? В связи с замыканием объявленная вспомогательная переменная i продолжает существовать, при чём и в тот момент, когда мы кликаем по ссылке. Поскольку к тому времени цикл уже прошёл, i остаётся равным кол-ву ссылок — это значение мы и видим при кликах.

Решается эта проблема следующим образом:

Здесь с помощью еще одного замыкания мы «затеняем» переменную i, создавая ее копию в его локальной области видимости на каждом шаге цикла. Благодаря этому всё теперь работает как задумывалось.

Вот и всё. Эта статья, конечно, не претендует на звание исчерпывающей, но кому-нибудь, надеюсь, всё-таки поможет разобраться. Спасибо за внимание!

Источник

Замыкания в JavaScript для начинающих

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

Материал, перевод которого мы публикуем сегодня, посвящён рассказу о внутренних механизмах замыканий и о том, как они работают в JavaScript-программах.

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

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

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

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

Понятие «лексическое окружение» или «статическое окружение» в JavaScript относится к возможности доступа к переменным, функциям и объектам на основе их физического расположения в исходном коде. Рассмотрим пример:

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

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

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

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

▍Пример №1

▍Пример №2

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

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

Для того чтобы понять замыкания, нам нужно разобраться с двумя важнейшими концепциями JavaScript. Это — контекст выполнения (Execution Context) и лексическое окружение (Lexical Environment).

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

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

В некий момент времени может выполняться код лишь в одном контексте выполнения (JavaScript — однопоточный язык программирования). Управление этими процессами ведётся с использованием так называемого стека вызовов (Call Stack).

Стек вызовов — это структура данных, устроенная по принципу LIFO (Last In, First Out — последним вошёл, первым вышел). Новые элементы можно помещать только в верхнюю часть стека, и только из неё же элементы можно изымать.

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

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

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

Пример контекста выполнения

Стек вызовов этого кода выглядит так:

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

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

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

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

Лексическое окружение содержит два компонента:

Взглянем на следующий фрагмент кода:

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

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

Подробный разбор примеров работы с замыканиями

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

▍Пример №1

Взгляните на данный фрагмент кода:

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

Когда вызывается функция peter() (соответствующая переменная хранит ссылку на функцию displayName() ), JS-движок создаёт для этой функции новый контекст выполнения и новое лексическое окружение. Это лексическое окружение будет выглядеть так:

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

▍Пример №2

Лексическое окружение функции getCounter() будет выглядеть так:

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

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

Итоги

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

Уважаемые читатели! Если вы обладаете опытом JS-разработки — просим поделиться с начинающими практическими примерами применения замыканий.

Источник

Замыкания

Замыкание — это комбинация функции и лексического окружения, в котором эта функция была определена. Другими словами, замыкание даёт вам доступ к Scope (en-US) внешней функции из внутренней функции. В JavaScript замыкания создаются каждый раз при создании функции, во время её создания.

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

Рассмотрим следующий пример:

Выполните этот код и обратите внимание, что команда alert() внутри displayName() благополучно выводит на экран содержимое переменной name объявленной в родительской функции. Это пример так называемой лексической области видимости (lexical scoping): в JavaScript область действия переменной определяется по её расположению в коде (это очевидно лексически), и вложенные функции имеют доступ к переменным, объявленным вовне. Этот механизм и называется Lexical scoping (область действия, ограниченная лексически).

Замыкание

Рассмотрим следующий пример:

Если выполнить этот код, то результат будет такой же, как и выполнение init() из предыдущего примера: строка «Mozilla» будет показана в JavaScript alert диалоге. Что отличает этот код и представляет для нас интерес, так это то, что внутренняя функция displayName() была возвращена из внешней до того, как была выполнена.

На первый взгляд, кажется неочевидным, что этот код правильный, но он работает. В некоторых языках программирования локальные переменные-функции существуют только во время выполнения этой функции. После завершения выполнения makeFunc() можно ожидать, что переменная name больше не будет доступна. Однако, поскольку код продолжает нормально работать, очевидно, что это не так в случае JavaScript.

А вот немного более интересный пример — функция makeAdder :

По существу makeAdder — это фабрика функций: она создаёт функции, которые могут прибавлять определённое значение к своему аргументу. В примере выше мы используем нашу фабричную функцию для создания двух новых функций — одна прибавляет 5 к своему аргументу, вторая прибавляет 10.

add5 и add10 — это примеры замыканий. Эти функции делят одно определение тела функции, но при этом они сохраняют различные окружения. В окружении функции add5 x — это 5, в то время как в окружении add10 x — это 10.

Замыкания на практике

Замыкания полезны тем, что позволяют связать данные (лексическое окружение) с функцией, которая работает с этими данными. Очевидна параллель с объектно-ориентированным программированием, где объекты позволяют нам связать некоторые данные (свойства объекта) с одним или несколькими методами.

Следовательно, замыкания можно использовать везде, где вы обычно использовали объект с одним единственным методом.

Давайте рассмотрим практический пример: допустим, мы хотим добавить на страницу несколько кнопок, которые будут менять размер текста. Как вариант, мы можем указать свойство font-size на элементе body в пикселах, а затем устанавливать размер прочих элементов страницы (таких, как заголовки) с использованием относительных единиц em:

Тогда наши кнопки будут менять свойство font-size элемента body, а остальные элементы страницы просто получат это новое значение и отмасштабируют размер текста благодаря использованию относительных единиц.

Используем следующий JavaScript:

Эмуляция частных (private) методов с помощью замыканий

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

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

Заметьте, что счётчики работают независимо друг от друга. Это происходит потому, что у каждого из них в момент создания функцией makeCounter() также создавался свой отдельный контекст исполнения (окружение). То есть приватная переменная privateCounter в каждом из счётчиков это действительно отдельная, самостоятельная переменная.

Используя замыкания подобным образом, вы получаете ряд преимуществ, обычно ассоциируемых с объектно-ориентированным программированием, таких как изоляция и инкапсуляция.

Создание замыканий в цикле: Очень частая ошибка

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

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

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

Соображения по производительности

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

Как пример, при написании нового класса есть смысл помещать все методы в прототип его объекта, а не описывать их в тексте конструктора. Если сделать по-другому, то при каждом создании объекта для него будет создан свой экземпляр каждого из методов, вместо того, чтобы наследовать их из прототипа.

Давайте рассмотрим не очень практичный, но показательный пример:

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

Методы вынесены в прототип. Тем не менее, переопределять прототип — само по себе является плохой привычкой, поэтому давайте перепишем всё так, чтобы новые методы просто добавились к уже существующему прототипу.

Код выше можно сделать аккуратнее:

В обоих примерах выше методы определяются один раз — в прототипе. И все объекты, использующие данный прототип, будут использовать это определение без дополнительного расхода вычислительных ресурсов. Смотрите подробное описание в статье Подробнее об объектной модели.

Источник

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

Замыкание. Как оно работает

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

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

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

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

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

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

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

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

В качестве примера поместим константу message в другую функцию:

В этом примере произойдёт ошибка, т.к. переменная message не будет найдена. Интерпретатор при её поиске перейдёт от текущего лексического окружения по ссылкам до глобального. А т.к. в нём этой переменной нет и ссылки на следующее окружение тоже (она равна null ), то интерпретатор выдаст ошибку и дальнейшее выполнение этого сценария прекратится.

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

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

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

Сборка мусора

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

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

Для чего нужны замыкания? Замыкания, например, могут использоваться для «запоминания» параметров, защиты данных (инкапсуляции), привязывания функции к определённому контексту и др. Замыкания положены в основу многих паттернов (шаблонов для написания кода).

Использование замыкания для создания приватных переменных и функций

Замыкания в JavaScript можно использовать для создания приватных переменных и функций.

Напрямую обратиться к _counter и _changeBy нельзя.

Примеры для подробного рассмотрения лексического окружения и замыкания

Какой ответ мы получим в результате выполнения этого примера?

Какой результат будет в результате выполнения этого примера?

Рассмотрим на примере, как происходит замыкание в JavaScript.

Посмотрим детальную информацию о функции:

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

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

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

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

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

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

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

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

Итоговый js-код рассмотренного примера:

Замыкания на практике

Замыкания в JavaScript являются очень интересной вещью. Они позволяют связать некоторые данные с функцией. Это очень похоже на то, как это реализовано в объекте, который позволяет связать свойства (переменные) и методы (действия над этими переменными). Такие задачи в веб-разработке попадаются очень часто. Давайте рассмотрим одну из подобных задач.

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

Кнопки, открывающие модальные окна:

Функция, возвращая в качестве результата другую функцию:

Код, который выполняет создание модальных окон и установлением каждому из них заголовка и некоторого содержимого:

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

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

Источник

Замыкания в JavaScript

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

Это перевод статьи Притти Кассириди «Let’s Learn JavaScript Closures».

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

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

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

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

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

Замыкания являются мощным инструментом в JavaScript и других языках программирования. Вот определение с MDN:

Замыкания — это функции, ссылающиеся на независимые (свободные) переменные. Другими словами, функция, определённая в замыкании, «запоминает» окружение, в котором она была создана.

Заметка: cвободные переменные — это переменные, которые не объявлены локально и не передаются в качестве параметра.

Давайте посмотрим на несколько примеров:

Пример 1

В примере функция numberGenerator создаёт локальную «свободную» переменную num (число) и checkNumber (функция, которая выводит число в консоль). Функция checkNumber не содержит собственной локальной переменной, но благодаря замыканию она имеет доступ к переменным внутри внешней функции, numberGenerator. Поэтому объявленная в numberGenerator переменная num будет успешно выведена в консоль, даже после того, как numberGenerator вернёт результат выполнения.

Пример 2

В этом примере видно, что замыкания содержат в себе все локальные переменные, которые были объявлены внутри внешней замкнутой функции — enclosing function.

Обратите внимание, как переменная hello определяется после анонимной функции, но эта функция всё равно может получить доступ к этой переменной hello. Это происходит из-за того, что переменная hello во время создания уже была определена в области видимости (scope), тем самым сделав её доступной на тот момент, когда анонимная функция будет выполнена. Не беспокойтесь, позже я объясню, что такое «область видимости». А пока просто смиритесь с этим.

Понимаем высокий уровень

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

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

Замыкания — только часть продвинутого JavaScript

Пройдите курс «JavaScript. Архитектура клиентских приложений» и прокачайте свои навыки. Скидка —1000 ₽ по промокоду SKUCHNO.

Нажатие на кнопку — согласие на обработку персональных данных

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

Контекст выполнения — это абстрактное понятие, которое используется в спецификации ECMAScript для оценки времени выполнения кода. Это может быть глобальный контекст — global context, в котором ваш код выполнится первым, или когда поток выполнения переходит в тело функции.

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

В любой момент времени выполняется только один контекст функции (тело функции). Вот почему JavaScript является однопотоковым, так как единовременно может выполняться только одна команда. Обычно браузеры поддерживают этот контекст с помощью стека — stack. Стек — структура данных, выполняемая в обратном порядке: LIFO — «последним пришёл — первым вышел». Последнее, что вы добавили в стек, будет удалено первым из него. Это происходит из-за того, что мы можем только добавить или удалить элементы из верхушки стека. Текущий или «выполняющийся» контекст исполнения — всегда верхний элемент стека. Он выскакивает из стека, когда код в текущем контексте полностью разобран, позволяя следующему верхнему элементу стека взять на себя контекст выполнения.

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

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

Для наглядности запустите в консоли код, который вы видите ниже:

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

Затем, когда boop возвратится, он удалится из стека, и bar продолжит работу:

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

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

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

Заметка: c технической точки зрения, окружение переменных и лексическая область видимости используются для реализации замыканий. Но для простоты мы заменим его на «окружение». Для детального объяснения разницы между лексическим и переменным окружением читайте статью Акселя Раушмайера.

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

Дадим определение: лексическое окружение — специфичный тип, используемый для связи идентификаторов с определёнными переменными и функциями на основе лексической структуры вложенности кода ECMAScript. Лексическое окружение состоит из записи окружения и, возможно, нулевой ссылки на внешнее лексическое окружение. Обычно лексическое окружение связано с определённой синтаксической структурой, например: FunctionDeclaration — объявление функции, BlockStatement — оператор блока, Catch clause — условный оператор, TryStatement — перехват ошибок и новым лексическим окружением, которое создавалось каждый раз при разборе кода. — ECMAScript-262/6.0

Давайте разберём это.

Так выглядит окружение в псевдокоде:

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

Цепочки областей видимости

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

Давайте рассмотрим эту структуру вложенности:

Как вы можете видеть, bar вложен в foo. Чтобы всё это представить посмотрите на диаграмму ниже:

Замыкание для чего нужно. Смотреть фото Замыкание для чего нужно. Смотреть картинку Замыкание для чего нужно. Картинка про Замыкание для чего нужно. Фото Замыкание для чего нужно

Мы вернёмся позже к этому примеру.

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

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

Идём в обход: динамическая область видимости против статической области видимости

У динамических языков программирования существует стековая архитектура — stack-based implementations, локальные переменные и функции хранятся в стеке. Поэтому, во время выполнения стека, программа определяет какую переменную вы имеете в виду. С другой стороны, статическая область видимости — это когда переменные ссылаются на контекст и фиксируются на момент создания. Другими словами, структура исходного кода программы определяет к каким переменным вы обращаетесь.

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

Пример 1

Как мы видим из примера выше, статическая и динамическая область видимости возвращают разные значения при вызове функции bar.

В статической области видимости возврат значения bar зависит от значения x. Это происходит из-за того, что статическая и лексическая структура исходного кода приводит x и к 10, и к 15.

Динамическая область видимости даёт нам стек определённых переменных, которые отслеживаются во время выполнения. Поэтому x, которую мы используем, зависит от того, что находится в её области видимости и как она была динамично определена во время выполнения. Выполнение функции bar выталкивает x = 2 на верхушку стека, заставляя foo вернуть 7.

Пример 2

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

Как вы можете заметить, динамическая область видимости часто создаёт некоторую двусмысленность. Она не даёт точно понять, какая свободная переменная будет передана.

Замыкания

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

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

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

Возвратимся к примеру вложенной структуры:

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

Когда мы вызываем функцию test, мы получаем 45, и она возвращает значение из вызова функции bar (потому что foo возвращает bar). bar имеет доступ к свободной переменной y даже после того, как функция foo вернётся, так как bar имеет ссылку на y через его внешнее окружение, которое является окружением foo! bar так же имеет доступ к глобальной переменной x потому, что у окружения foo есть доступ к глобальному окружению. Это называют «поиск цепочки области видимости».

Подведём итог обсуждения динамической области видимости против статической: для замыканий, которые будут выполняться, нельзя использовать динамическую область с помощью динамического стека. Это сохранит наши переменные. Причина такого поведения кроется в том, что когда функция возвращается, переменные будут удалены из стека и больше не будут доступны, а это противоречит нашему определению замыкания. Это происходит из-за того, что замкнутость данных в родительском контексте сохраняется в так называемой «куче», и это позволяет сохранять данные после вызова функции, делая их возможными для возврата, то есть даже после того, как контекст выполнения извлекается из стека выполнения вызова.

Теперь, когда мы понимаем внутренности на абстрактном уровне, давайте рассмотрим ещё пару примеров:

Пример 1

Вот типичное заблуждение: в цикле for мы пробуем связать переменную счётчика с какой-либо функцией.

На основе всего что было ранее, мы можем с лёгкостью найти ошибку здесь. Абстрактно говоря, вот так выглядит окружение, во время выхода из цикла for:

Было бы неверно предполагать, что область видимости отличается для всех пяти функций в результирующем массиве. Вместо того, что происходит по факту, окружение или контекст/область видимости является тем же самым для всех пяти функций в пределах результирующего массива. Поэтому каждый раз, когда переменная i увеличивается, обновляется область видимости, а она является общей для всех функций. Из-за этого любая из пяти функций, пытающихся получить доступ к i, возвращает 5, i равна 5, когда цикл завершается.

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

Ура, мы исправили это.

Есть ещё одно решение, в котором мы используем let вместо var, let находится в операторе блока и поэтому новая привязка идентификатора замыкания создаётся для каждой итерации в цикле for.

Пример 2

В этом примере мы покажем как каждый вызов в функции создаёт новое отдельное замыкание:

Мы видим что каждый вызов в функции iCantThinkOfAName создаёт новое замыкание, а именно foo и bar. Последующие вызовы каждой замкнутой функции обновляют замкнутые переменные в пределах самого замыкания, демонстрируя, что переменные в каждом замыкании используются функции iCantThinkOfAName’s doSomething после того, как вернулась iCantThinkOfAName.

Пример 3

Обратите внимание, что mysteriousCalculator находится в глобальной области и возвращает две функции. Говоря иначе, окружение для кода выше будет выглядеть так:

Это происходит из-за того, что наши функции add и substract ссылаются на среду mysteriousCalculator, и они в состоянии использовать переменные этой среды для расчёта результата.

Пример 4

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

Это очень мощная техника — она даёт замыкающей функции guessPassword исключительный доступ к переменной password, делая невозможным доступ к password снаружи.

Замыкающая ремарка

Я надеюсь этот пост был полезным и дал представление о том, как замыкания реализованы в JavaScript. Как вы видите, понимание внутреннего устройства замыканий и особенностей их работы делают их поиск проще. Теперь у вас будет меньше головной боли при отладке.

Дополнительная литература

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

Источник

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

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