Комментарии 54
На практике они регулярно нужны, но в весьма малых дозах. И код, хоть сколько-нибудь злоупотребляющий замыканиями — как правило, ужасен. Особенно когда это пытаются везде насаживать ради эмуляции private или подобного сахара. Окей, насадили, теперь всё красиво завёрнуто, и наружу только публичные интерфейсы. Зато код читать невозможно.
Когда функция person() завершает работу, её контекст выполнения извлекается из стека. Но её лексическое окружение остаётся в памяти, так как ссылка на него есть в лексическом окружении её внутренней функции displayName(). В результате переменные, объявленные в этом лексическом окружении, остаются доступными.
Когда вызывается функция peter() (соответствующая переменная хранит ссылку на функцию displayName()), JS-движок создаёт для этой функции новый контекст выполнения и новое лексическое окружение. Это лексическое окружение будет выглядеть так:
Так в какой момент создается лексическое окружение displayNameLexicalEnvironment, которое хранит ссылку на personLexicalEnvironment? Если это происходит только в момент вызова peter(), то откуда тогда известно о существовании лексического окружения со ссылкой на personLexicalEnvironment в момент выхода из person()?
Ссылка на personLexicalEnvironment хранится в самой функции person. Ее существование следует из того факта, что personLexicalEnvironment не может появиться из воздуха в момент вызова peter(), а значит должно где-то храниться.
Но её лексическое окружение остаётся в памяти, так как ссылка на него есть в лексическом окружении её внутренней функции displayName()противоречит этому утверждению. Или я чего-то не понимаю.
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 ))
Хоть 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", потому что один и тот же код внутри функции и снаружи нее ведет себя совершенно одинаково.
Метод GetBindingValue и подобные введены в спеке только потому, что EnvRecord на самом деле не является объектом в понимании JavaScript. Но если в целях наглядности представить EnvRecord в виде обычного объекта — то все привязки будут обычными свойствами.
Где хоть слово про биндинги?
А, да, точно. Но тут нет ни слова о том, что биндинги пересчитываются при вызове функций. Осталось понять, что такое эти биндинги из себя представляют.
Смотрим 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
count() вызывает эту возвращаемую функцию, которая берет counter из ранее созданного окружения
Спасибо за ответ, но тут сразу несколько вопросов возникает.
1) функция getCounter возвращает анонимную функцию или значение? Если функцию, то почему в консоли значение?
2) эта возвращаемая функция вызывается после let, почему тогда let не вызывается второй раз?
Никак не могу понять, как компилятор это интерпретирует.
2. Возвращаемая функция вызывается, потому что мы присвоили её переменной count, то есть count — это теперь та самая функция, и вызываем её — count().
Строка let counter = 0; не может вызваться второй раз, потому что она содержится в функции getCounter, а эта функция вызывается всего 1 раз.
наличие анонимной функции в возвращаемом результате превращает функцию getCounter() в подобие класса, в котором объявляется переменная counter и инициализируется в конструкторе?
А дальше count = getCounter работает как создание экземпляра класса (главная привычным языком, = new getCounter() )?
Тогда механизм работы понимаю, но неясно, в какой момент эта магия случается — именно когда мы возвращаем анонимную функцию в качестве результата?
В первом примере, если освободить переменную peter будет ли разрушено замыкание или память утечёт?
PS Был один интересный опыт в перле, там память текла очень неплохо и было не так просто поймать где. Оказалось замыкание.
В любом случае, замыкания больше похоже на баг, чем на фичу. Немного странно, что этот баг называют «одной из важнейших фундаментальных концепций JavaScript».
Баг-то в чем? Багом было бы любое другое поведение.
Но да, это не баг, а недокументированная возможность, которая стала документированной, поскольку достичь подобного эффекта другими способами не получилось.
Или я заблуждаюсь, и замыкание — не побочный эффект, а сознательно запрограммированное поведение?
Никакого "отключения" сборки мусора нет. На объект есть ссылка — из замыкания.
Если точнее, то, согласно стандарту, у Function Object есть скрытый слот [[Environment]], в котором хранится Lexical Environment родителя. У Lexical Environment есть свой Environment Record, где хранятся все переменные. И все эти ссылки учитываются сборщиком мусора.
Или я заблуждаюсь, и замыкание — не побочный эффект, а сознательно запрограммированное поведение?
Совершенно верно, это сознательно запрограммированное поведение.
1. Мало кто вообще думает о памяти.
2. Такие функции медленнее работают, из-за того что их сложнее движку оптимизировать (забыл как эта штука называется).
Такие штуки довольно просто оптимизируются. Намного проще чем, например, доступ к свойствам объектов. Потому что захваченная переменная всегда переменной и останется, а свойство объекта кто-то может переопределить.
Это и есть зло, а не замыкания.
«Такие функции медленнее работают, из-за того что их сложнее движку оптимизировать „
Вы можете привести конкретный пример и показать насколько медленнее работают такие функции? Все функции работы с массивами(forEach, concat, filter и т.п.), работают медленнее, чем перебор с поиском, фильтрацией и т.д. Это не значит, что они — зло. Следить за скоростью и оптимизацией — задача в том числе и разработчика, не только движка.
Замыкания в JavaScript для начинающих