Замыкание js: Замыкания, функции изнутри

Содержание

Замыкания в JavaScript — Блог HTML Academy

Замыкания являются ключевой особенностью в 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. Начало 27 июля.

Класс, расскажите

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

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

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

В любой момент времени выполняется только один контекст функции (тело функции).

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

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

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

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

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

  • Оценка состояния кода — любое состояние необходимо выполнить, приостановить и возобновить определение кода, связанного с этим контекстом выполнения.
  • Функция — объект функции, который оценивает контекст выполнения или null, если контекст был определён как script или модуль.
  • Область — набор внутренних объектов, глобальное окружение ECMAScript, весь код ECMAScript, который находится в пределах этого глобального окружения и другие связанные с ним состояния и ресурсы.
  • Лексическое окружение — используется для разрешения ссылок идентификатора кода в этом контексте исполнения.
  • Переменное окружение — лексическое окружение, чья запись окружения — EnvironmentRecord имеет связи, созданные заявленными переменными — VariableStatements в этом контексте выполнения.

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

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

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

Дадим определение:

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

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

  • «используемый для связи идентификаторов»: целью лексического окружения является управление данными, то есть идентификаторами в коде. Говоря иначе, это придаёт им смысл. Например, у нас есть такая строка в консоли:
    console.log(x / 10)
    , x здесь бессмысленная переменная или идентификатор без чего-либо, что придавало бы ей смысл. Лексическое окружение обеспечивает смысл или «ассоциацию» через запись окружения. Смотрите ниже.
  • Лексическое окружение состоит из записи окружения: запись окружения — причудливый способ сказать, что она хранит записи всех идентификаторов и связей, которые существуют в лексической области видимости. Каждая лексическая область видимости имеет собственную запись окружения.
  • Лексическая структура вложенности: самый интересный момент, который говорит, что внутреннее окружение ссылается на внешнее окружение, и это внешнее окружение может иметь собственное внешнее окружение. В результате окружение может быть внешним окружением для более чем одного внутреннего окружения. Глобальное окружение является единственным лексическим окружением, которое не имеет внешнего окружения. Это сложно описать словами, поэтому давайте использовать метафоры и представим лексическое окружение как слои лука: глобальная среда — внешний слой луковицы, где каждый последующий слой находится ниже.

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

  • Новое лексическое окружение, которое создавалось каждый раз при разборе кода — каждый раз, когда вызываются внешние вложенные функции, создаётся новое лексическое окружение. Это важно, и мы вернёмся к этому моменту в конце. Примечание: функции — не единственный способ создать лексическое окружение. Другие типы содержат в себе оператор блока — block statement или условный оператор — catch clause. Для простоты, я сосредоточусь на окружении созданной нами функции на протяжении всего поста
    .

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

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

Исходя из приведённого выше определения, мы знаем, что окружение имеет доступ к окружению своего родителя, его родительское окружение имеет доступ к своему родительскому окружению и так далее. Этот набор идентификаторов, к которому каждое окружение имеет доступ, называется область видимости — 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 снаружи.

Tl;dr

  • Контекст выполнения — это абстрактный контекст, использовавшийся в спецификации ECMAScript для отслеживания времени выполнения кода. В любое время может быть только один контекст выполнения, который выполняет код.
  • Каждый контекст исполнения имеет лексическое окружение. Оно содержит связи идентификаторов, то есть переменные и их значения и имеет ссылку на него во внешнем окружении.
  • Набор идентификаторов, к которым у каждого окружения есть доступ, называют «область видимости». Мы можем вложить эти области в иерархическую цепь окружения, известной как «цепочки области видимости».
  • Каждая функция имеет контекст выполнения, который включает в себя лексическое окружение. Это придаёт смысл переменным в пределах этой функции и ссылку на родительское окружение. И это означает, что функции «запоминают» окружение или область видимости, так как они буквально ссылаются на это окружение. Это и есть замыкание.
  • Замыкания создаются каждый раз при вызове внешней функции. Другими словами, внутренняя функция не будет возвращена для замыкания, в котором была создана.
  • Область видимости замыканий в JavaScript лексическая, её смысл определяется статично в зависимости от нахождения в исходном коде.
  • Есть множество практических случаев использования замыканий. Один из важных случаев использования — это сохранение приватных ссылок к переменным во внешней среде.

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

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

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

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

Есть что-то ещё? Предлагайте.

Замыкания

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

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

Если говорить просто, то замыкание — это внутренняя функция. Ведь javascript разрешает создавать функции по ходу выполнения скрипта. И эти функции имеют доступ к переменным внешней функции.

В этом примере создается внутренняя функция func, изнутри которой доступны как локальные переменные, так и переменные внешней функции outer:

function outer() {
	var outerVar;

	var func = function() {
		var innerVar
		...
		x = innerVar + outerVar
	}
	return func
}

Когда заканчивает работать функция outer, внутренняя функция func остается жить, ее можно запускать в другом месте кода.

Получается, что при запуске func используется переменная уже отработавшей функции outer, т.е самим фактом своего существования, func замыкает на себя переменные внешней функции (а точнее — всех внешних функций).

Наиболее часто замыкания применяются для назначения функций-обработчиков событий:

function addHideHandler(sourceId, targetId) {
	var sourceNode = document.getElementById(sourceId)
	var handler = function() {
		var targetNode = document.getElementById(targetId)
		targetNode.style.display = ‘none’
	}
	sourceNode.onclick = handler
}

Эта функция принимает два ID элементов HTML и ставит первому элементу обработчик onclick, который прячет второй элемент.

Т.е,

// при клике на элемент с
// будет спрятан элемент с
addHideHandler("clickToHide", "info")

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

.. Если Вы хотите углубиться поглубже и разбираться подольше..

..На самом деле происходящее в интерпретаторе Javascript гораздо сложнее и содержит куда больше деталей, чем здесь описано…

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

Каждое выполнение функции хранит все переменные в специальном объекте с кодовым именем [[scope]], который нельзя получить в явном виде, но он есть .

Каждый вызов var… — всего лишь создае

Что ты такое, замыкания в JavaScript? / Хабр

В этой статье я постараюсь подробно разобрать механизм реализации замыканий в JavaScript. Для этого я буду использовать браузер Chrome.

Начнем с определения:

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

Если вам что-то не понятно в этом определении, это не страшно. Просто читайте дальше.

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

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

Итак, приступим:


Рисунок 1

Мы находимся в глобальном контексте вызова, он же Global (он же Window в браузере) и видим, что функция main уже лежит в текущем контексте и готова к работе.


Рисунок 2

Происходит это потому, что все Function Declaration (далее FD) всегда поднимаются наверх в любом контексте, сразу инициализируются и готовы к работе. Тоже самое происходит с переменными, объявленными через var, только их значения инициализируются как undefined.

Также важно понимать, что JavaScript точно также ‘поднимает’ переменные, объявленные через let и const. Разница лишь в том, что он не инициализирует их как var или как FD. Поэтому, когда мы пытаемся обратиться к ним до инициализации, получаем Reference Error.

Также у main мы видим внутренне скрытое свойство [[Scopes]] — это список внешних контекстов, к которым main имеет доступ. В нашем случае там лежит Global, так как main запущен в глобальном контексте.

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

Идем дальше:


Рисунок 3

Заходим в функцию main и первое, что бросается в глаза — это объект Local (в спецификации — localEnv). Там мы видим a, так как эта переменная объявлена через var и она ‘всплыла’ наверх, ну и по традиции видим все 3 FD (foo, bar, baz). Теперь давайте разберемся, откуда это все взялось.

При запуске любого контекста запускается абстрактная операция NewDeclarativeEnvironment, которая позволяет инициализировать LexicalEnvironment (далее LE) и VariableEnvironment. Также NewDeclarativeEnvironment принимает 1 аргумент — внешний LE, для того чтобы создать [[Scopes]], о котором мы говорили выше. LE — это API, который позволяет нам определять связь между идентификаторами и отдельными переменными, функциями. LE состоит из 2 составляющих:

  1. Record Environment — запись окружения, которая позволяет определить связи между идентификаторами и тем, что нам доступно в текущем контексте вызова
  2. Ссылка на внешний LE. У каждой функции при создании есть внутреннее свойство [[Scopes]]

VariableEnvironment — чаще всего это то же, что и LE. Разница между ними в том, что значение VariableEnvironment никогда не меняется, а LE может меняться во время выполнения кода. Для упрощения дальнейшего понимая предлагаю объеденить эти компоненты в один — LE.

Также в текущем Local есть this благодя тому, что произошел вызов ThisBinding — это тоже абстрактный метод, который инициализирует this в текущем контексте.

Разумеется, каждая FD сразу получила [[Scopes]]:


Рисунок 4

Видим, что все FD получили в [[Scopes]] массив [Closure main, Global], что логично.

Также на рисунке мы видим Call Stack — это структура данных, которая работает по принципу LIFO — last in first out. Так как JavaScript однопоточный, то одновременно может выполняться только один контекст. В нашем случае это контекст функции main. Каждый новый вызов функции создает новый контекст, который складывается в stack.

Наверху стека находится всегда текущий контекст исполнения. После того, как функция закончила свое выполнение и интерпретатор вышел из нее, контекст вызова удаляется из стека. Это все, что нам нужно знать о Call Stack в этой статье 🙂

Резюмируем произошедшее в текущем контексте:

  • Во время создания main получил [[Scopes]] со ссылками на внешнее окружение
  • Интерпретатор вошел в тело функции main
  • В Call Stack попал контекст выполнения main
  • Произошла инициализация this
  • Произошла инициализация LE

На самом деле, самое сложное уже позади. Переходим к следующему шагу в коде:

Теперь нам необходимо вызвать baz, чтобы получить результат.


Рисунок 5

В Call Stack добавился новый контекст вызова baz. Мы видим, что появился новый объект Closure. Сюда попадает то, что нам доступно из [[Scopes]]. Вот мы и подобрались к сути. Это и есть замыкания. Как вы видите на Рисунке 4 Closure (main) идет первым в списке ‘резервных’ контекстов у baz. Снова никакой магии.

Давайте вызовем foo:


Рисунок 6

Важно знать, что из какого бы места мы ни вызывали foo, она всегда будет ходить за ненайденными идентификаторами по своей цепочке [[Scopes]]. А именно в main и потом в Global, если в main не найдено.

После выполнения foo, она вернула значение, и ее контекст выбросился из Call Stack.
Переходим к вызову функции bar. В контексте выполнения bar есть переменная, с таким же именем, как и у переменной в LE foo — a. Но, как вы уже догадались, это ни на что не влияет. foo все равно будет брать значение из своего [[Scopes]].

Место вызова не влияет на Scope, только место создания
logachyova


Рисунок 7

В итоге baz вернет 300 и будет выброшен из Call Stack. Затем тоже самое произойдет с контекстом main, наш фрагмент кода закончит выполняться.

Резюмируем:

  • Во время создания функции происходит установка [[Scopes]]. Это очень важно для понимания замыканий, так как при поиске значений интерпретатор сразу идет по этим ссылкам
  • Затем, когда эта функция вызывается, создается активный контекст исполнения, который помещается в Call Stack
  • Выполняется ThisBinding и устанавливается this для текущего контекста
  • Выполняется инициализация LE, и все аргументы функции, переменные, объявленные через var и FD становятся доступными. Далее, если встречаются переменные, объявленные через let или const, они тоже добавляются в LE
  • Если интерпретатор не найдет в текущем контексте какой-либо идентификатор, то для дальнейшего поиска используется [[Scopes]], которые перебираются все, по очереди. Если значение найдено, то ссылка на него попадает в специальный объект Closure. При этом для каждого контекста, на который замыкается текущий, создается отдельный Closure с нужными переменными
  • Если значение не найдено ни в одном Scopes, включая Global, возвращается ReferenceError

Вот и все!

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

Всем пока 🙂 И до новых встреч. Ставьте лайки и подписывайтесь на мой канал 🙂

Замыкания в Javascript

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

Давайте начнем.

Итак представьте, что у нас есть функция, которая добавляет 2 к числу, которое мы указали в функции.

var addTo = function (passed) {
  var inner = 2
  return passed + inner
}

console.log(addTo(3))

То есть при вызове функции с аргументом 3 мы добавляем 3 + 2 и получаем 5.

Если мы посмотрим в браузер, то оно так и работает.

Теперь изменим код. Давайте уберем аргумент passed. Теперь вопрос: «Откуда мы можем его получить, чтобы он появился внутри функции addTo»?

Мы можем написать passed, как переменную вверху нашей функции и тогда она будет доступна внутри нашей функции.

var passed = 3

var addTo = function () {
  var inner = 2
  return passed + inner
}

console.log(addTo())

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

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

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

var addTo = function (passed) {
  var add = function () {

  }
}

И вернем созданную функцию не вызывая ее.

var addTo = function (passed) {
  var add = function () {

  }

  return add
}

Теперь давайте добавим в эту функцию один параметр inner и так как мы помним из предыдущего пример, что внутри функции нам доступны внешние переменные, то нам доступна переменная passed в этой функции.

var addTo = function (passed) {
  var add = function (inner) {
    return passed + inner
  }

  return add
}

Итак еще раз. Мы описали функцию внутри функции, которая принимает аргумент inner и возвращает passed + inner. passed мы берем извне.

Теперь давайте напишем

var addThree = addTo(3)
console.log(addThree)

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

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

var addThree = addTo(3)
console.dir(addThree)

Теперь в консоли мы увидим не тело функции, а обьект. И у него есть одно свойство, которое нас интересует. Это свойство [[Scopes]]. Если мы его откроем, то увидим Closure внутри. И внутри Closure у нас и будет наша переменная passed, которую мы передавали как аргумент.

Собственно, Closure в переводе на русский и означает замыкание.

И

Я раньше никогда не понимал замыкания в JavaScript …

Что такое замыкание и как оно работает? Очередной достаточно частый вопрос на собеседованиях. Я всегда думал что я знаю что ответить на этот вопрос. И всегда отвечал что типа такого… Замыкание это когда функция низшего порядка получается доступе к переменным функции высшего порядка. Но знать приблизительное определение и реально разбираться в замыкание не совсем одно и тоже. Давайте рассмотрим простейший пример:

function createCounter() {
   let counter = 0
   const myFunction = function() {
     counter = counter + 1
     return counter
   }
   return myFunction
 }
const increment = createCounter()
const c1 = increment()
const c2 = increment()
const c3 = increment()
console.log('example increment', c1, c2, c3)

Как по вашему что должно быть выведено в консоль? Если вы точно знаете ответ, то можете пропустить эту статью. А если думаете что 1,1,1 или если у вас возникло какое либо сомнение, рекомендую продолжить чтение далее….

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

До того как мы начнем

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

Это статья хорошо объясняет что такое Контекст исполнения (Execution Context). Основные понятия из статьи:

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

Глобальный код (Global code —  Среда по умолчанию, где код выполняется в первый раз.

Код функции (Function code) —  всякий раз, когда поток выполнения входит в тело функции.

(…), давайте думать о термине контекст исполнения как о среде/области видимости (scope), в которой выполняется текущий код.

Другими словами, когда запускается программа, ее код начинается исполнятся в глобальном контексте. Как правило некоторое количество переменных объявляются в глобальном контексте выполнения. Мы называем их глобальными переменными. А что происходит когда программа вызывает функцию? Вот список шагов:

  1. JavaScript создает новый контекст исполнения, который становится локальным для текущего потока
  2. Этот локальный контекст исполнения будет иметь свой собственный набор переменных, и они будут локальными для этого контекста.
  3. Новый контекст переносится в стек исполнения. Думайте о стеке как о механизме, позволяющем отслеживать, где в текущий момент исполняется программа.

Что происходит когда функция заканчивает выполнение? Выполнение заканчивается когда встречается оператор return или соответствующая закрывающая скобка }:

  1. Локальные контексты исполнения извлекается из стека выполнения
  2. Функции отправляют возвращаемое значение обратно в вызывающий контекст. Вызывающий контекст — это контекст который вызвал эту функцию. Им может быть глобальный контекст или другой локальный контекст выполнения. Возвращаемое значение может быть объектом, массивом, функцией, boolean или чем угодно. Если функция не имеет оператора возврата return, возвращается  undefined.
  3. Локальный контекст уничтожается. Все переменные, которые были объявлены в локальном контексте выполнения, удаляются и они больше не доступны. Вот почему они называются локальными переменными.

Очень простой пример

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

1: let a = 3
2: function addTwo(x) {
3:   let ret = x + 2
4:   return ret
5: }
6: let b = addTwo(a)
7: console.log(b)

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

  1. В строке 1 объявляется новая переменная  a  в глобальном контексте выполнения и ей присваивается значение 3
  2. Далее становится чуть сложнее. В строках со 2 по 5 объявляется новая переменная с именем  addTwo  в глобальном контексте выполнения. И что ей назначается? Определение функции. Все, что находится между двумя скобками,  { } присваивается  addTwo. Код внутри функции на данный момент не выполняется, он просто сохраняется в переменной для будущего использования.
  3. Итак, теперь мы находимся на строке 6. Тут объявляется новая переменная  b в глобальном контексте. Как только переменная объявлена, она имеет значение undefined.
  4. Далее, все еще в строке 6, мы видим оператор присваивания. Далее идет вызов функции. Когда вы видите переменную, за которой следуют круглые скобки  (…), это сигнал о том, что это вызов функции. Забегая вперед, заметим что каждая функция возвращает что-либо (значение, объект или  undefined). Все, что возвращается из функции, будет присвоено переменной b.
  5. Но сначала нам нужно вызвать функцию с именем  addTwo. Что бы ее запустить JavaScript нужно найти переменную с именем  addTwo в глобальном контексте. Он ее найдет, так как мы ее объявили в строке 2 (или строки 2–5). Обратите внимание, что переменная  a  передается в качестве аргумента функции. JavaScript так же будет искать переменную a  в глобальном контекста, и после того как, найдет ее, передаст число 3 в качестве аргумента функции. Теперь все готово что бы выполнить функцию.
  6. Далее произойдет переключения контекста выполнения. Вначале создается новый локальный контекст. Затем он помещается в стек вызовов. Что в первую очередь должно быть выполнено в локальном контексте?
  7. У вас может возникнуть соблазн сказать: «В локально контексте будет объявлена новая переменная  ret. Это не совсем верный ответ. Правильный ответ: сначала объявляются параметры функции. То есть в нашем случае объявляется новая переменная  x  в локальном контексте. И поскольку значение 3 было передано в качестве аргумента, переменной  x  присваивается значение 3.
  8. Следующий шаг: новая переменная ret  объявляется в локальном контексте. Ее значение устанавливается как undefined. (строка 3)
  9. Все еще на строка 3, сначала нам нужно значение переменной  x. JavaScript начинает искать переменную  x. В начале она будет искаться в локальном контексте. И сразу же там будет найдена со значение 3. А вторым операндом будет число 2. Результат сложения (5) присваивается переменной ret.
  10. Строка 4. Возвращается содержимое переменной ret. Еще один поиск в локальном контексте. ret содержит значение 5. Функция возвращает число 5. И на этом шаге функция завершится.
  11. Строки 4–5. Функция завершится. Локальный контекст будет уничтожен. Переменные x и ret так же уничтожаются. Контекст извлекается из стека вызовов, а возвращаемое значение возвращается в вызывающий контекст. В этом случае вызывающий контекст будет глобальным контекстом выполнения, потому что функция  addTwo была вызвана из глобального контекста.
  12. Мы все еще на шестой строчке. Возвращаемое значение (число 5) присваивается переменной b.
  13. В строке 7 содержимое переменной b выводится на консоль. В нашем примере это число 5.

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

Лексический контекст.

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

1: let val1 = 2
2: function multiplyThis(n) {
3:   let ret = n * val1
4:   return ret
5: }
6: let multiplied = multiplyThis(6)
7: console.log('example of scope:', multiplied)

Идея в том, что у нас есть переменные в локальном контексте и переменные в глобальном контексте. Сложность для JavaScript состоит в поиске переменных. Если JavaScript не может найти переменную в локальном контексте выполнения, она будет искать ее в вызывающем контексте, если она не найдет ее там далее она будет искать переменную в глобальном контексте. (И если JavaScript не найдет ее там, переменная станет undefined). Рассмотрим пример выше. Если вы понимаете, как работает поиск, вы можете пропустить рассмотрения примера.

  1. Объявление новой переменной val1 в глобальном контексте и назначение ей значение 2.
  2. Строки 2–5. Объявление новой переменной multiplyThis и назначение ей определение функции.
  3. Строка 6. Объявление новой переменной multiplied в глобальном контексте.
  4. Получение переменной multiplyThis  из глобального контекста и выполнение функции. Передача значения  в качестве аргумента.
  5. Вызов функции = новый контекст . Создается новый локальный контекст.
  6. В локальном контексте объявляется переменная n и ей назначается значение 6.
  7. Строка 3. В локальном контексте объявляется переменная ret.
  8. Строка 3 (продолжение). Выполняется умножение с двумя операндами; переменными n и val1. Переменная n берется из локального контекста. Мы объявили ее в строке 6. Ее значение 6. Ищется переменная val1 в локальном контексте. В локальном контексте ее нет. Далее ищется в вызываемом контексте. В данном случае вызываемый контекст является глобальным. И там она находится. Она была определена в строке 1. Ее значение равно 2.
  9. Строка 3 (продолжение). Умножаются два операнда и результат назначается переменной ret. 6 * 2 = 12. Значение ret в данный момент 12.
  10. Далее из функции возвращается значение ret. Локальный контекст уничтожается вместе с переменными ret и n. Переменная val1 не уничтожается, так как она находится в глобальном контексте.
  11. Строка 6. В вызывающем контексте переменной  multiplied назначается значение 12.
  12. И наконец строка 7, значение переменной multiplied отображается в консоле.

Таким образом, в этом примере мы продемонстрировали, что функция имеет доступ к переменным, которые определены в контексте вызова. Формальное название этого явления — лексическая область видимости (lexical scope).

А что если функция возвращает функцию

В первом примере функция addTwo возвращает номер. Мы знаем что функция может вернуть что угодно. Давайте рассмотрим пример когда функция возвращает функцию.

1: let val = 7
2: function createAdder() {
3:   function addNumbers(a, b) {
4:     let ret = a + b
5:     return ret
6:   }
7:   return addNumbers
8: }
9: let adder = createAdder()
10: let sum = adder(val, 8)
11: console.log('example of function returning a function: ', sum)
  1. Строка 1. Объявлена переменная val в глобальном контексте и ей назначили значение 7.
  2. Строки 2–8. Объявлена переменная createAdder в глобальном контексте и ей назначили определение функции. Строки с 3 по 7 определяют тело функции. Как и ранее на этом шаге сама функция не запускается. Только сохраняется ее определение в переменной createAdder.
  3. Строка 9. Объявляется новая переменная с именем adder, в глобальном контексте. Временно ее значение в текущий момент undefined.
  4. Все еще строка 9. Так как мы видим скобки (); мы запускаем функцию на выполнение. В глобальном контексте ищется переменная createAdder. Она была определена в строке 2. Далее запускается функцию.
  5. Строка 2. Создается новый локальные контекст. Движок JavaScript добавляет новый контекст в стек выполнения.
  6. Строки 3–6. Объявляется новая функция. В локальном контексте создается новая переменная addNumbers. Это важно! addNumbers существует только в локальном контексте. Далее определение функции сохраняется в локальной переменной  addNumbers.
  7. Строка 7. Возвращается содержимое переменной addNumbers. В ней находится определение функции (строки с 4 по 5). Далее удаляется локальный контекст из стека вызовов.
  8. Удаляется локальный контекст и переменная addNumbers более не существует. Однако определение функции все еще существует, он возвращается из функции и назначается переменной adder; это переменная было создана в строке 3.
  9. Строка 10. Определяется новая переменная sum в глобальном контексте. И ей временно назначается значение undefined.
  10. Далее нам нужно выполнить функцию которая была определена в переменной adder. Движок ищет и находит ее в глобальном контексте. У этой функции есть два параметра.
  11. Давайте разберемся с параметрами. Первый переменная val определена в строке 1, ее значение 7, вторая переменная просто значение 8.
  12. Далее идет выполнение функции. Функция определена в строках 3–5. Вначале создается новый локальный контекст. Внутри локального контекста создается две новые переменные: a и b. У них значения 7 и 8 соотвественно.
  13. Строка 4. В новом локальном контексте объявляется новая переменная ret.
  14. Строка 4. Выполняется сложение переменных a и b. Результат сложения (15) назначается переменной ret.
  15. Переменная ret возвращается из функции. Локальный контекст уничтожается и удаляется из стека выполнения, переменные ab и ret более не существует.
  16. Возвращаемое значение назначается переменной sum определенной в строке 9.
  17. Значение переменной sum выводится в консоле.

Как и ожидалось, в консоли отобразиться 15. Этим примером я пытаюсь проиллюстрировать несколько моментов. Во-первых, определение функции может быть сохранено в переменной, определение функции невидимо для программы, пока функция не будет вызвано. Во-вторых, каждый раз, когда вызывается функция, создается (временно) локальный контекст выполнения. После завершения функции этот контекст исчезает. Функция завершает свое выполнение, когда в коде встречается  return  или соответствующую закрывающую скобку}.

Ну и наконец, замыкание

Посмотрим на следующий пример и попытайтесь понять, что здесь происходит.

1: function createCounter() {
2:   let counter = 0
3:   const myFunction = function() {
4:     counter = counter + 1
5:     return counter
6:   }
7:   return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)

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

  1. Строки 1–8. Создается новая переменная createCounter в глобальном контексте и ей назначается определение функции.
  2. Строка 9. В глобальном контексте объявляется новая переменная increment .
  3. Строка 9. Вызывается функция createCounter и ее возвращаемое значение назначается переменной increment.
  4. Строки 1–8. Вызов функции. Создается новый локальный контекст.
  5. Строка 2. Внутри локального контекста, объявляется новая переменная counter. Ей назначается значение 0.
  6. Строки 3–6. В локальном контексте объявляется новая переменная myFunction. Значение переменной объявление еще одной функции (строки 4 и 5).
  7. Строка 7. Возвращается содержимое переменной myFunction. Локальный контекст удаляется. Переменные myFunction и counter больше не существуют. Контроль передается в вызывающий контекст.
  8. Строка 9. Вызывающий контекст в данном случае является глобальным контекстом, значение возвращаемое createCounter назначается increment. Сейчас в этой переменной определение функции. И оно больше не помечено именем myFunction, внутри глобального контекста оно названо increment.
  9. Строка 10. Объявляется новая переменная (c1).
  10. Строка 10. Ищется переменная increment, после того как оно будет найдено и определено что это функция, она будет запущена.
  11. Создается новый контекст. Так как у функции нет параметров перейдем к ее выполнению.
  12. Строка 4. counter = counter + 1. Начинается поиск  counter в локальном контексте. Мы только что его создали и в нем пока еще ничего нет. Далее ищется в глобальном контексте. Но там тоже нет переменной counter. Javascript будет рассматривать это выражение как  counter = undefined + 1, далее объявит новую локальную переменную counter и назначит ей значение 1, так как undefined воспринимается как 0.
  13. Строка 5. Далее возвращается переменная counter, а точнее ее значение 1. Уничтожается локальный контекст и переменная counter.
  14. Строка 10. Возвращаемое значение (1) назначается переменной c1.
  15. Строка 11. Мы повторяем строки 10–14, c2 получает 1.
  16. Строка 12. Снова повторяются строки 10–14, c3 получает 1.
  17. Строка 13. Выводятся переменные c1c2 и c3.

Попробуйте сами запустить этот пример и посмотрите, что получится. Вы заметите, что на самом деле выводится не  11, и 1 как мы только что рассмотрели выше. Вместо этого выводится 12 и 3. Так что здесь происходит?

Каким-то образом функция increment запоминает значение счетчика.

Переменная counter находится в глобальном контексте ? Попытайтесь выполнить console.log(counter)и вы получите counter is not defined. Так что нет, не находится.

Может быть, когда мы вызываем increment, каким-то образом он возвращается к функции, в которой он был создан (createCounter)? Как это вообще работает? Переменная increment содержит определение функции, а не то откуда она вызвалась.

Так что должен быть другой механизм. И он называется Замыкание Мы наконец дошли до этого!

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

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

1: function createCounter() {
2:   let counter = 0
3:   const myFunction = function() {
4:     counter = counter + 1
5:     return counter
6:   }
7:   return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
  1. Строки 1–8. В глобальном контексте создается новая переменная createCounter и ей назначается определение функции.
  2. Строка 9. В глобальном контексте объявляется новая переменная increment.
  3. Строка 9. Вызывается функция createCounter и ее возвращаемое значение передается переменной increment .
  4. Строки 1–8 . Вызов функции. Создается новый локальный контекст.
  5. Строка 2. Внутри локального контекста, объявляется новая переменная counter. Ее назначается значение 0.
  6. Строка 3–6. В локальном контексте объявляется новая переменная myFunction. Переменная содержит объявление еще одной функции строки 4 и 5. Сейчас так же создается замыкание и включается в определение функции. Замыкание содержит переменные, находящиеся в области видимости, в данном случае переменная  counter  (со значением  0).
  7. Строка 7. Возвращается содержимое переменной myFunction. Локальный контекст удаляется. Переменные myFunction и counter перестают существовать. Контроль передается в вызывающий контекст. Таким образом, мы возвращаем определение функции и ее замыкание, «рюкзак» с переменными, которые находились в области видимости в момент ее создания.
  8. Строка 9. В вызывающем контексте, то есть в глобальном возвращаемое значение функцией createCounter передается в переменную increment. Переменная increment сейчас содержит определение функции (и замыкание).
  9. Строка 10. Объявляется переменная c1.
  10. Строка 10. Ищется переменная increment, после того как она будет найдена будет определенно ее содержимое. Она содержит определение функции в строках 4–5. (а так же содержит «рюкзак» с переменными)
  11. Создается новый контекст выполнения. У функции нет параметров поэтому сразу переходим к ее выполнению.
  12. Строка 4. counter = counter + 1. Ищется переменная counter. Прежде чем начнется поиск в локальном и глобальном контексте давай те посмотрим что в нашем «рюкзаке». Точнее посмотрим что в замыкании. А вот замыкание содержит переменную  counter, со значением 0. Поэтому выражению в строке 4, назначается значение 1. И это значение сохраняется в «рюкзаке». Замыкание сейчас содержит переменную counter со значением 1.
  13. Строка 5. Возвращается содержимое переменной counter, а точнее ее значение 1. Далее уничтожается локальный контекст.
  14. Строка 10. Возвращаемое значение (1) назначается переменной c1.
  15. Строка 11. Повторяются строки 10–14. На этот раз, когда мы смотрим на наше замыкание, мы видим, что переменная  counter  имеет значение 1. Она была установлена в строке 12 или строке 4 программы. Его значение увеличивается и сохраняется со значением 2  при закрытии функции increment. И переменная  c2 получает значение 2.
  16. Строка 12. Повторяются строки 10–14, c3 получает значение 3.
  17. Строка 13. Выводится значение переменных c1c2 и c3.

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

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

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

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

Не самые обычные замыкания

Иногда замыкания появляются, когда вы даже не замечаете этого. Возможно, вы видели похожий пример:

let c = 4
const addX = x => n => n + x
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)

В случае, если стрелочные функции сбивает вас с толку, напишем эквивалент.

let c = 4
function addX(x) {
  return function(n) {
     return n + x
  }
}
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)

Мы объявляем функцию сумматора addX, которая принимает один параметр (x) и возвращает другую функцию.

Возвращаемая функция также принимает один параметр и добавляет его к переменной x.

Переменная x является частью замыкания. Когда переменная  addThree  объявляется в локальном контексте, ей назначается определение функции и замыкание. Замыкание содержит переменную  x.

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

В этом примере в консоль будет выведено число 7.

Заключение

Аналогия о которой я всегда буду помнить в случае замыканий — это аналогия с рюкзаком. Когда функция создается и передается или возвращается из другой функции, она несет с собой этот самый «рюкзак». И в этом «рюкзаке» находятся все переменные, которые были в области видимости, когда функция была объявлена.

Статья написана на основе I never understood JavaScript closures

Была ли вам полезна эта статья?

[0 / 0]


замыкания, лексическое окружение, вложенные функции

На этом занятии подробно разберем области видимости данных в JavaScript и такое важное понятие как замыкание.

Для начала представим, что у нас имеется вот такая программа:

let name = "Иван";
getName("Привет!");
 
function getName(say) {
    console.log(name + ": " + say);
}

Как это работает в деталях. Как мы уже говорили, все функции, объявленные как Function Declaration, создаются JavaScript-движком в первую очередь. Но что значит создаются? В действительности, при запуске любого скрипта создается специальный объект (на уровне JavaScript-машины и недоступный программисту), называемый глобальным лексическим окружением (Lexical Envirnoment). Он состоит из двух частей:

  1. Environment Record – объект, в котором хранятся локальные данные как свойства этого объекта;
  2. Ссылка на внешнее лексическое окружение (если его нет, то эта ссылка равна null).

Так вот, объявление функции getName – это не что иное, как создание свойства getName в глобальном лексическом окружении:

Далее, начинает выполняться скрипт. Создается переменная name. Значит, в лексическом окружении появляется еще одно свойство – name:

Затем идет вызов функции getName(«Привет!»). В лексическом окружении ищется свойство getName со ссылкой на функцию. Так как оно там уже есть, то функция успешно вызывается. Но, каждый вызов функции создает свое лексическое окружение и картина будет выглядеть так:

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

console.log(name + ": " + say);

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

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

Из этого примера следуют такие важные моменты:

  1. Внешнее лексическое окружение не имеет доступа к данным внутренних окружений.
  2. Поиск переменных начинается с текущего окружения и при необходимости последовательно переходит к внешним окружениям. Останавливается, как только переменная найдена.
  3. Лексическое окружение автоматически уничтожается, когда в нем более нет необходимости (на него нет внешних ссылок).

Здесь нужно лишь добавить: если переменная не находится, то при включенном режиме «use strict» она принимает значение undefined. Иначе, будет ссылаться на глобальный объект (в браузере – это window).

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

let name = "Иван";
getName("Привет!");
 
function getName(say) {
    let name = "Федор";
    console.log(name + ": " + say);
}

Да, мы видим строчку: «Федор: Привет!». То есть, переменная name была найдена в локальном окружении и взято значение «Федор», а не «Иван».

А вот, если мы выведем что-то в глобальном лексическом окружении:

console.log(name);
console.log(say);

то увидим «Иван», а переменная say не будет найдена. Вот так это работает. Причем, обратите внимание, свои лексические окружения создают не только вызываемые функции, но и условные операторы:

if(…) {
// внутреннее лексическое окружение
}

операторы циклов:

while, for, do while(…) {
// внутреннее лексическое окружение
}

И в современных браузерах даже просто проставленные фигурные скобки:

{
// внутреннее лексическое окружение     
}

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

(function() {
    // код внутри изолирован от внешнего скрипта
    let name = "Григорий";
    console.log(name);
})();

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

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

Мы можем даже создавать функции внутри других функций. Они называются вложенными функциями. В JavaScript – это обычная практика:

function getName(say) {
    function getSay() {
         return ": " + say;
    }
 
    console.log(name + getSay());
}

Здесь функция getSay создана для удобства – вывода сообщения по определенному шаблону. Ее лексическое окружение будет вложено в лексическое окружение функции getName и, конечно же, она будет иметь доступ ко всем внутренним переменным функции getName.

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

function createCounter() {
    let count = 0;
 
    return function() {
         return count++;
    };
}
 
let counter = createCounter();
console.log( counter() );

Вначале здесь объявляется функция createCounter, то есть, в глобальном окружении появляется такое свойство. Далее, эта функция вызывается. Создается ее локальное лексическое окружение с переменной count и анонимной функцией:

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

Далее, в программе идет вызов анонимной функции. Появляется еще одно лексическое пространство этой функции и картина выглядит уже вот так:

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

При завершении, лексическое пространство анонимной функции уничтожается и остается два пространства:

Но значение count теперь равно 1. Если мы вызовем counter() еще раз, то получим значение 1 и переменная count станет равной 2. И так далее. Вот так работают замыкания в JavaScript. Причем, обратите внимание, если мы создадим еще один счетчик:

let counter2 = createCounter();

то будет создано еще одно лексическое пространство и оба счетчика будут работать независимо друг от друга:

console.log( counter2() );

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

Область

— закрытие JavaScript против анонимных функций

Переполнение стека
  1. Около
  2. Продукты
  3. Для команд
  1. Переполнение стека Общественные вопросы и ответы
  2. Переполнение стека для команд Где разработчики и технологи делятся частными знаниями с коллегами
  3. Вакансии Программирование и связанные с ним технические возможности карьерного роста
  4. Талант Нанимайте технических специалистов и создавайте свой бренд работодателя
  5. Реклама Обратитесь к разработчикам и технологам со всего мира
  6. О компании
.

javascript — Замыкания и асинхронные функции node.js

Переполнение стека
  1. Около
  2. Продукты
  3. Для команд
  1. Переполнение стека Общественные вопросы и ответы
  2. Переполнение стека для команд Где разработчики и технологи делятся частными знаниями с коллегами
  3. Вакансии Программирование и связанные с ним технические возможности карьерного роста
  4. Талант Нанимайте технических специалистов и создавайте свой бренд работодателя
  5. Реклама Обратитесь к разработчикам и технологам со всего мира
  6. О компании

Загрузка…

  1. Авторизоваться зарегистрироваться
  2. текущее сообщество

.

javascript — Получение компилятора закрытия и Node.js для хорошей игры

Переполнение стека
  1. Около
  2. Продукты
  3. Для команд
  1. Переполнение стека Общественные вопросы и ответы
  2. Переполнение стека для команд Где разработчики и технологи делятся частными знаниями с коллегами
  3. Вакансии Программирование и связанные с ним технические возможности карьерного роста
  4. Талант Нанимайте технических специалистов и создавайте свой бренд работодателя
  5. Реклама Обратитесь к разработчикам и технологам со всего мира
.

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

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