По крайней мере так сказал блогер Demimurych у меня в комментариях:

Дальше была беседа в комментариях, из который я вынес два хороших замечания:
Тему замыканий в языках программирования стоит рассказывать с проблемы функционального аргумента.
Реализация этой проблемы наиболее и единственно полно описана в ECMAScript спецификации.
Я потратил время и покажу, что нашёл!
Несколько разборов примеров выложил на степике и сама статья есть на ютубе.
Почему все говорят о замыканиях?
Чтобы понять, зачем этот механизм вообще нужен, нам нужно вернуться к основам проектирования языков. Есть такая проблема, которую называют проблемой функционального аргумента.
У нас есть функция, которая использует переменную не из своих аргументов, а 'снаружи'. Мы передаем эту функцию в другое место и вызываем.
Вопрос: где она должна искать эту внешнюю переменную?
Есть два пути:
Нисходящий ФунАрг: Искать переменную там, где функция была вызвана. То есть смотреть вниз по стеку вызовов. Так работает, например, Bash.
Восходящий ФунАрг: Искать переменную там, где функция была объявлена в коде. То есть смотреть вверх по тексту программы. И это — выбор JavaScript.
Давайте почувствуем разницу!
Код на языке Bash
#!/bin/bash
x=10
printX() {
echo "$x"
}
run() {
local x=20
printX
}
run
Код на языке JavaScript
var x = 10;
function printX() {
console.log(x);
}
function run() {
var x = 20;
printX();
}
run();
Выведет: 10
.
Народные мифы или ECMAScript
Миф |
ECMAScript |
Замыкание — это функция, возвращающая функцию. |
Замыкание создаётся при создании любой функции в момент её определения, а не возврата. |
Замыкание нужно, чтобы 'замкнуть' (сохранить) переменную. |
Механизм позволяет функции иметь доступ к целой цепочке записей окружения, а не к одной переменной |
Замыкание — это особый объект с переменными |
Это не объект, а внутреннее поле функции — ссылка на её 'родную' запись окружения |
Демонстрируется только на примере с возвратом функции |
Демонстрируется на любом обращении к внешней переменной |
Хватит пустых слов, вперёд к академическим определениям!
Спецификация
Словарик
Я не люблю англицизмы, но не люблю и путанницу! Поэтому привожу определения из документации и их переводы.
BlockStatement = любой блок кода в {...}
Call Stack = Execution Context Stack = стек вызовов = стек контекстов исполненич
Closure = замыкание
Declarative Environment Record = декларативные записи окружения
ECMAScript = документация = спецификация
Environment Record = запись окружения = окружение
Execution Context = контекст выполнения
Function Environment Records = записи окружения функции
FunctionDeclaration = декларативное объявление функции
Global Environment Record = глобальные записи окружений
LexicalEnvironment = лексическое окружение
Module Environment Records = записи окружения модулей
OuterEnv = внешнее (родительское) окружение
VariableEnvironment = окружение переменных
Определение
Замыкание — это концептуальная пара, состоящая из функции и ссылки на её внешнее окружение, в котором эта функция была создана.
С точки зрения спецификации ECMA-262, это означает, что каждый объект функции при своём создании получает некое внутреннее свойство. Это свойство содержит ссылку на Environment Recodr того контекста выполнения, в котором функция была объявлена (скоро покажу всё на пальцах).
Благодаря этой ссылке, функция "помнит" свое "родное" окружение и может обращаться к переменным и функциям из этого окружения, даже если она вызывается в совершенно другом месте программы, где этих переменных уже нет в текущем окружении.
Механизм разрешения идентификаторов рекурсивно следует по цепочке внешних окружений, начиная с самой функции. Именно эта цепочка и составляет то, что мы на практике называем областью видимости.
Если обратиться к спецификации 1998 года, то там использовалась более простая модель с "Цепочкой областей видимости" (Scope Chain), которая была списком объектов. При создании функции она "захватывала" эту цепочку. Суть осталась той же, но современная модель с Environment Records более точна и позволяет элегантно описать блочную область видимости (let
, const
).
Таким образом, замыкание — это не какая-то особая сущность, а фундаментальный механизм работы записей окружения в JavaScript, реализованный через сохранение функцией ссылки на свою запись окружения в момент создания.
Изучая спецификацию
9.1 Environment Records
Environment Record is a specification type used to define the association of Identifiers to specific variables and functions, based upon the lexical nesting structure of ECMAScript code. Usually an Environment Record is associated with some specific syntactic structure of ECMAScript code such as a FunctionDeclaration, a BlockStatement, or a Catch clause of a TryStatement. Each time such code is evaluated, a new Environment Record is created to record the identifier bindings that are created by that code.
Every Environment Record has an [[OuterEnv]] field, which is either null or a reference to an outer Environment Record. This is used to model the logical nesting of Environment Record values. The outer reference of an (inner) Environment Record is a reference to the Environment Record that logically surrounds the inner Environment Record. An outer Environment Record may, of course, have its own outer Environment Record. An Environment Record may serve as the outer environment for multiple inner Environment Records. For example, if a FunctionDeclaration contains two nested FunctionDeclarations then the Environment Records of each of the nested functions will have as their outer Environment Record the Environment Record of the current evaluation of the surrounding function.
Environment Records are purely specification mechanisms and need not correspond to any specific artefact of an ECMAScript implementation. It is impossible for an ECMAScript program to directly access or manipulate such values.
Глава 9.1 “Environment Records” определяет понятие “Environment Record” (Запись окружения) как внутренний механизм для управления переменными, функциями и другими идентификаторами.
Каждый раз, когда движок JS встречает определенную синтаксическую конструкцию, создаётся новая Запись Окружения (условно: список переменных). К таким конструкциям относятся:
FunctionDeclaration (Декларативное объявление функции)
BlockStatement (Любой блок кода в
{...}
)
Ключевой элемент этой модели — поле [[OuterEnv]]
. Каждая Запись Окружения содержит ссылку на внешнее (родительское) окружение. Это создает связанный список или цепочку окружений, которая тянется от самого вложенного кода до глобального уровня. Именно эта цепочка и является реализацией замыкания.
Важно понимать, что Записи Окружения — это абстракция спецификации. Мы не можем получить к ним доступ из нашего JavaScript-кода или увидеть их в DevTools.
И вообще, в DevTools показывают лишь абстрактные понятия, которые могут не совпадать с именами из ECMAScript.
9.1.1 The Environment Record Type Hierarchy
Environment Records can be thought of as existing in a simple object-oriented hierarchy where Environment Record is an abstract class with three concrete subclasses:
Declarative Environment Record,
Function Environment Records,
Module Environment Records,
Global Environment Record.
Each Declarative Environment Record is associated with an ECMAScript program scope containing
var
iable,const
ant,let
, class, module, import, and/or function declarations.
A Declarative Environment Record binds the set of identifiers defined by the declarations contained within its scope.
Глава 9.1.1 “The Environment Record Type Hierarchy” определяет иерархию типов записей окружения:
-
Запись окружения
-
Декларативные записи окружения
Записи окружения функций - создается при вызове функции. Она дополнительно управляет
this
.Записи окружения модулей.
Глобальные записи окружений - глобальная область видимости. У нее
[[OuterEnv]]
равенnull
, так как выше ничего нет.
-
Каждая декларативная запись напрямую связывает идентификаторы (имена переменных) с их значениями в пределах своей области видимости.
9.1.2 Environment Record Operations
The following abstract operations are used in this specification to operate upon Environment Records:
9.1.2.1 GetIdentifierReference ( env, name, strict )
The abstract operation GetIdentifierReference takes arguments env (an Environment Record or null), name (a String), and strict (a Boolean) and returns either a normal completion containing a Reference Record or a throw completion. It performs the following steps when called:
If env is null, then
Return the Reference Record { unresolvable }.
Let exists be ? env.HasBinding(name).
If exists is true, then
Return the Reference Record { env }.
Else,
Let outer be env.[[OuterEnv]].
Return ? GetIdentifierReference(outer, name, strict).
Глава 9.1.2 “Environment Record Operations” определяет алгоритм поиска идентификатора (переменной):
Поискать идентификатор (переменную) в текущей Записи Окружения.
Если он найден, вернуть ссылку на него.
Если нет, взять ссылку на внешнее окружение из поля
[[OuterEnv]]
.Повторить поиск уже в этом внешнем окружении (рекурсивно).
Если мы дошли до самого верхнего уровня (где
[[OuterEnv]]
равенnull
) и ничего не нашли, значит, переменная не объявлена (возвращается unresolvable reference, что приведет кReferenceError
).
Этот простой рекурсивный обход по цепочке [[OuterEnv]]
и есть механизм, который позволяет коду внутри функции "видеть" переменные снаружи.
Table 26: Additional State Components for ECMAScript Code Execution Contexts
LexicalEnvironmentIdentifies the Environment Record used to resolve identifier references made by code within this execution context.VariableEnvironmentIdentifies the Environment Record that holds bindings created by VariableStatements within this execution context.
The LexicalEnvironment and VariableEnvironment components of an execution context are always Environment Records.
An execution context is purely a specification mechanism and need not correspond to any particular artefact of an ECMAScript implementation. It is impossible for ECMAScript code to directly access or observe an execution context.
Таблица 26 “Additional State Components for ECMAScript Code Execution Contexts” рассказывает, что каждый раз при выполнении функции создаётся контекст её выполнения. Это ещё одна абстракция, которая отслеживает состояние выполняемого кода. У каждого контекста есть как минимум два важных для нас компонента:
LexicalEnvironment (Лексическое окружение) — ссылка на Запись Окружения, используемую для разрешения имён. Именно здесь хранятся объявления
let
,const
,class
.VariableEnvironment (Окружение переменных) — ссылка на Запись Окружения для объявлений
var
иfunction
.
В чём разница между LexicalEnvironment и VariableEnvironment?
Это различие — ключ к пониманию разницы между var
и let
/const
.
VariableEnvironment создается для всей функции целиком и не меняется во время её выполнения. Именно сюда "всплываю��" все объявления
var
(народное понятие "hoisting" работает именно из-за этого).LexicalEnvironment является более динамичным. Изначально он совпадает с VariableEnvironment. Но когда выполнение входит в новый блок (например,
if
или циклfor
), создается новая LexicalEnvironment для этого блока, а её[[OuterEnv]]
указывает на предыдущую.
Это и есть механизм блочной области видимости. Переменные let
и const
существуют только в LexicalEnvironment своего блока.
И да, контекст выполнения функции — это временная сущность. Он создается при вызове функции и уничтожается после её завершения (удаляется из стека вызовов).
Как и Записи Окружения, Контекст Выполнения — это абстракция спецификации. Мы не можем получить к нему доступ из нашего JavaScript-кода или увидеть его в DevTools.
9.4.2 ResolveBinding ( name [ , env ] )
The abstract operation ResolveBinding takes argument name (a String) and optional argument env (an Environment Record or undefined) and returns either a normal completion containing a Reference Record or a throw completion. It is used to determine the binding of name. env can be used to explicitly provide the Environment Record that is to be searched for the binding. It performs the following steps when called:
If env is not present or env is undefined, then
Set env to the running execution context's LexicalEnvironment.
Assert: env is an Environment Record.
Return ? GetIdentifierReference(env, name).
Глава 9.4.2 “ResolveBinding” определяет точку входа для поиска идентификатора (переменной).
Алгоритм поиска записи окружения:
Взять LexicalEnvironment из текущего выполняемого контекста.
Запустить для этого окружения алгоритм поиска идентификатора из таблицы 26 “Additional State Components for ECMAScript Code Execution Contexts”.
Вывод
Фундаментальная идея — сохранение ссылки на цепочку внешних записей окружения при создании функции — осталась неизменной. Современная модель с Environment Records просто является более точной и гибкой, что позволило ввести блочные записи окружения, не ломая старую логику.
Объёмный пример

function hi() {
function say() {
var name = "John";
let middleName = "J.";
{
let surname = "Doe";
console.log(`Hi, ${name} ${middleName} ${surname}`);
}
}
return say();
}
hi();
Рассмотрим работу контекста выполнения, стека вызовов и записей окружения с точки зрения JavaScript движка.
Функция global()
В JavaScript всё является (объек..) функцией! Начиная с самой первой строчки кода мы уже в глобальной функции. И она всегда есть в механизме Стека Вызовов.
Стек вызовов |
|
|
|
|
Раз мы внутри функции и мы исполняем её – нам (как движку JS) полагается механизм исполнения: Контекст выполнения. У него есть два компонента, можем рассматривать их как прокси / каналы / указатели на нужную запись окружения. Соответственно и для каждого контекста выполнения полагается запись окружения. Мы исполняем функцию, значит и будет запись окружения функции.
Контекст выполнения функции |
|
Лексическое окружение |
|
Окружение переменных |
|
Запись окружения функции |
|
|
объект функции hi |
|
|
Рассмотрели всё, последней строкой идёт вызов этой функции hi()
.
Функция hi()
Стек вызовов |
|
|
|
|
Всё то же самое, очередная функция, очередной контекст исполнения, очередная запись окружения.
Контекст выполнения функции |
|
Лексическое окружение |
|
Окружение переменных |
|
Запись окружения функции |
|
|
объект функции say |
|
Запись окружения функции global() |
Рассмотрели всё, последней строкой идёт вызов этой функции say().
Функция say()
Стек вызовов |
|
|
|
|
Всё то же самое, очередная функция, очередной контекст исполнения, очередная запись окружения.
Контекст выполнения функции |
|
Лексическое окружение |
Запись окружения функции say() |
Окружение переменных |
|
Запись окружения функции |
|
|
|
|
|
|
Запись окружения функции hi() |
Но! Тут важно увидеть, что при заходе в блочную конструкцию {...}
мы откроем новую декларативную запись окружения. Туда попадут все let
const
переменные. И пока мы будем внутри этого блока, лексическое окружение будет ссылаться на неё.
Контекст выполнения функции |
|
Лексическое окружение |
Декларативная запись окружения блока {...} |
Окружение переменных |
|
Декларативная запись окружения блока |
|
|
|
|
Запись окружения функции say() |
Рассмотрели всё, последней строкой идёт вывод переменных в консоль.
Несколько разборов примеров выложил на степике и сама статья есть на ютубе.