Как стать автором
Обновить

Комментарии 54

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

На практике они регулярно нужны, но в весьма малых дозах. И код, хоть сколько-нибудь злоупотребляющий замыканиями — как правило, ужасен. Особенно когда это пытаются везде насаживать ради эмуляции private или подобного сахара. Окей, насадили, теперь всё красиво завёрнуто, и наружу только публичные интерфейсы. Зато код читать невозможно.
Еще недавно они использовались повсеместно: читаемый асинхронный код без них зачастую был невозможен.
Я бы сказал так: еще недавно серьезный асинхронный код был нечитаем что с замыканиями, что без.
Не очень понял следующий кусок:
Когда функция person() завершает работу, её контекст выполнения извлекается из стека. Но её лексическое окружение остаётся в памяти, так как ссылка на него есть в лексическом окружении её внутренней функции displayName(). В результате переменные, объявленные в этом лексическом окружении, остаются доступными.

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


Так в какой момент создается лексическое окружение displayNameLexicalEnvironment, которое хранит ссылку на personLexicalEnvironment? Если это происходит только в момент вызова peter(), то откуда тогда известно о существовании лексического окружения со ссылкой на personLexicalEnvironment в момент выхода из person()?
Лексическое окружение функции создается при ее вызове.

Ссылка на personLexicalEnvironment хранится в самой функции person. Ее существование следует из того факта, что personLexicalEnvironment не может появиться из воздуха в момент вызова peter(), а значит должно где-то храниться.
Ну если лексическое окружение функции создается при вызове, то получается, что на момент окончания person() на personLexicalEnvironment ссылается только сама функция person, а displayNameLexicalEnvironment еще не существует. Получается, что утверждение
Но её лексическое окружение остаётся в памяти, так как ссылка на него есть в лексическом окружении её внутренней функции displayName()
противоречит этому утверждению. Или я чего-то не понимаю.
У переменной peter, которая и есть внутренняя функция displayName, осталась ссылка на personLexicalEnvironment.

Окружения displayNameLexicalEnvironment еще не существует.
То есть ссылки на лексические окружения хранятся не только в других лексических окружениях?
Да. Они хранятся еще и в замыканиях.
Согласен с вами. Я как человек знакомый с классами — въехал в этот текст вообще раза с четвертого. Из всех представленных примеров — как я понимаю создается тип «person», потом создается экземпляр, объект этого типа и используется дальше. Причем тут — «Когда функция person() завершает работу, её контекст выполнения извлекается из стека»?
person — это не «тип» и не «класс» — это обычная функция, которая отработала и вернула результат. После этого из стека её удалили.
Хочу просто разобраться — сильно не пинайте, с java не знаком.
function person() {
let name = 'Peter';

return function displayName() {
console.log(name);
};
}
Это объявление функции — так? это не вызов функции — а объявление. Еще никакого пространства переменных нет.
Дальше
let peter = person();
объявили переменную, ну уж извините, типа person — так как у нас уже есть переменная (объект, экземпляр — называйте как хотите) — то создается и пространство имен (внутренних переменных — name в данном примере).
Дальше идет вызов внутренней функции person — displayName() — через код peter().
С точки зрения как написал автор объяснения — получается что функция displayName должна была вывести строку в консоль еще при «объявлении». Но вывод строки происходит только при непосредственном вызове — peter(). То же самое и со вторым примером — вывод и увеличение числа происходит при непосредственном вызове "++" через функцию count().
Я вот что не могу понять: вы утверждаете, что функция отработала и эту функцию выкидывает из стека. Как тогда происходит увеличение числа во втором примере?

В строке let peter = person(); была вызвана функция person. В строке peter() была вызвана функция peter, она же displayName.


Никакого типа person не существует, это функция а не тип.


И да, на этой странице нет никакой java. Javascript — это совершенно другой язык.

В строке let peter = person(); была вызвана функция person. В строке peter() была вызвана функция peter, она же displayName.

По вашим словам получается должно было 2 раза вывести строку «Peter» — но этого не происходит.
Никакого типа person не существует, это функция а не тип.

С точки зрения «классов» — функция person() используется как тип — я вижу служебное слово function ))
С чего бы строку «Peter» выводило два раза, если ее вывод есть только в функции displayName?
как с чего — displayName является частью функции person(), вы утверждаете что функция person() отработала — значит и должен выполниться весь код функции.

Хоть displayName и является частью person — но она внутри person ни разу не вызывается. Функция не может быть выполнена если ее не вызывали.

Объясните тогда, почему часть кода выполняется, а часть нет?
Если же представить функцию person() — как тип все встает на свои места: нет никакого выполнения функции, пока не сделали непосредственный вызов peter().
Выполняется тот код, который был вызван.

Рассмотрим сначала вот такой код:


console.log("outer");

function displayName() {
    console.log("inner");
}

Этот код выведет строку "outer". Строка "inner" выведена не будет, потому что функцию displayName никто не вызывал — она только объявлена.


Теперь запихнем код в функцию:


function person() {
    console.log("outer");

    function displayName() {
        console.log("inner");
    }
}

Теперь код не выводит ничего — потому что функцию person никто не вызывал.


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

Чем быстрее из JS выпилят подобную порнографию, тем лучше для мира.
Что выпилят? Возможность возвращать функцию в качестве результата выполнения функции или логичное поведение вообще?
Хм… ну во первых — это одно из преимуществ js, а во-вторых из js его никогда не выпилят — потому что там обратная совместимость должна работать, а в третьих чем будет лучше, да и еще миру?
НЛО прилетело и опубликовало эту надпись здесь
Ну и как хранение значений переменных в EnvRecord может помешать мутировать переменную?

Метод GetBindingValue и подобные введены в спеке только потому, что EnvRecord на самом деле не является объектом в понимании JavaScript. Но если в целях наглядности представить EnvRecord в виде обычного объекта — то все привязки будут обычными свойствами.
НЛО прилетело и опубликовало эту надпись здесь
Ну так формально-то захватывается не переменная, а весь скоуп целиком, вместе с его EnvRecord.
НЛО прилетело и опубликовало эту надпись здесь
Где вы вычитали в спеке, что биндинги пересчитываются при вызове функции? Значения биндингов меняются только при вызове SetMutableBinding.
НЛО прилетело и опубликовало эту надпись здесь
Там написано все ясно и понятно: контекст исполнения состоит из LexicalEnvironment и VariableEnvironment.

Где хоть слово про биндинги?
НЛО прилетело и опубликовало эту надпись здесь

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


Смотрим https://www.ecma-international.org/ecma-262/9.0/index.html#sec-environment-records:


Declarative Environment Records are used to define the effect of ECMAScript language syntactic elements such as FunctionDeclarations, VariableDeclarations, and Catch clauses that directly associate identifier bindings with ECMAScript language values.

С каждой привязкой напрямую связывается значение, которое и хранится в Environment Record.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Короче и понятнее по-моему. Не реклама, если что.

То что сначала кажется сложным и непонятным — с опытом использования дает невероятную свободу… Js —

Пожалуйста, спасите мой мозг.

В примере 2 —
если count() вызывает getCounter(), то почему let counter=0 не выполняется каждый раз?

function getCounter() {
  let counter = 0;
//^^^^^^
  return function() {
    return counter++;
  }
}
let count = getCounter();
console.log(count());  // 0
console.log(count());  // 1
результат выполнения getCounter() — функция (return function ...)
count() вызывает эту возвращаемую функцию, которая берет counter из ранее созданного окружения

Спасибо за ответ, но тут сразу несколько вопросов возникает.


1) функция getCounter возвращает анонимную функцию или значение? Если функцию, то почему в консоли значение?


2) эта возвращаемая функция вызывается после let, почему тогда let не вызывается второй раз?


Никак не могу понять, как компилятор это интерпретирует.

1. Функция getCounter возвращает функцию. Эта функция присваивается переменной count. То есть функция getCounter делает всего 2 вещи — обнуляет переменную counter и возвращает функцию, не вызывая её.
2. Возвращаемая функция вызывается, потому что мы присвоили её переменной count, то есть count — это теперь та самая функция, и вызываем её — count().
Строка let counter = 0; не может вызваться второй раз, потому что она содержится в функции getCounter, а эта функция вызывается всего 1 раз.

Круто, спасибо. Так абсолютно всё понятно.


Перевести бы ещё изначально closure не как "замыкание", а как "заключение", например, было бы интуитивно понятнее. "Функция, заключённая в контекст" и так далее...

Или так (поправьте, пожалуйста, если я ошибаюсь):
наличие анонимной функции в возвращаемом результате превращает функцию getCounter() в подобие класса, в котором объявляется переменная counter и инициализируется в конструкторе?

А дальше count = getCounter работает как создание экземпляра класса (главная привычным языком, = new getCounter() )?

Тогда механизм работы понимаю, но неясно, в какой момент эта магия случается — именно когда мы возвращаем анонимную функцию в качестве результата?
А когда освобождается память, занятая этими замыканиями?
В первом примере, если освободить переменную peter будет ли разрушено замыкание или память утечёт?
PS Был один интересный опыт в перле, там память текла очень неплохо и было не так просто поймать где. Оказалось замыкание.
Если присвоить переменной peter что-то еще, или она уйдет из области видимости — то захваченное лексическое окружение будет недоступно и когда-нибудь соберется сборщиком мусора.
Начиная читать текст, ожидал увидеть описание работы garbage collector'a и подсчёта ссылок, а тут как-то вскользь об этом упомянули.

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

Баг-то в чем? Багом было бы любое другое поведение.

Чтобы не заморачиваться с очищением памяти придумали «сборку мусора». Замыкание отключает сборку мусора для объекта, на который ссылки как бы нет, но она есть, и приводит к интересному эффекту.

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

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

Никакого "отключения" сборки мусора нет. На объект есть ссылка — из замыкания.


Если точнее, то, согласно стандарту, у Function Object есть скрытый слот [[Environment]], в котором хранится Lexical Environment родителя. У Lexical Environment есть свой Environment Record, где хранятся все переменные. И все эти ссылки учитываются сборщиком мусора.


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

Совершенно верно, это сознательно запрограммированное поведение.

Замыкание — это не баг и даже не какая-то особая возможность JS. Всё немного глубже. Эта штука присутствует в куче языков программирования. И это очень логичная штука.
Замыкания в JS — зло.
1. Мало кто вообще думает о памяти.
2. Такие функции медленнее работают, из-за того что их сложнее движку оптимизировать (забыл как эта штука называется).

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

«Мало кто вообще думает о памяти»

Это и есть зло, а не замыкания.

«Такие функции медленнее работают, из-за того что их сложнее движку оптимизировать „

Вы можете привести конкретный пример и показать насколько медленнее работают такие функции? Все функции работы с массивами(forEach, concat, filter и т.п.), работают медленнее, чем перебор с поиском, фильтрацией и т.д. Это не значит, что они — зло. Следить за скоростью и оптимизацией — задача в том числе и разработчика, не только движка.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий