На собеседовании сказали, что не все функции — замыкания. Так ли это?

Сказали: покажите замыкание
Пишу:
let x = 10
function fn(){
  console.log(x);
}
fn()


вот, говорю, функция со своим лексическим окружением, в котором есть x, и есть замыкание.

Отвечают: нет тут замыкания, замыкание это функция, ссылающаяся на переменные внешней функции даже после её завершения, а у тебя вообще только одна функция, какое тут замыкание.
  • Вопрос задан
  • 4516 просмотров
Решения вопроса 11
@Arlekcangp
Разработчик, Лид, Архитектор ПО
Похоже вас поймали на том что в js кто-то решил переопределить термин "замыкание"... Т к javascript далеко не первый язык, в котором есть нечто, претендующее называться термином "замыкание", то следует в первую очередь рассмотреть общее определение. Возьмём его из википедии:

Замыкание (англ. closure) в программировании — функция первого класса, в теле которой присутствуют ссылки на переменные, объявленные вне тела этой функции в окружающем коде и не являющиеся её параметрами. Говоря другим языком, замыкание — функция, которая ссылается на свободные переменные в своей области видимости.
Замыкание, так же как и экземпляр объекта, есть способ представления функциональности и данных, связанных и упакованных вместе.
Замыкание — это особый вид функции. Она определена в теле другой функции и создаётся каждый раз во время её выполнения. Синтаксически это выглядит как функция, находящаяся целиком в теле другой функции. При этом вложенная внутренняя функция содержит ссылки на локальные переменные внешней функции. Каждый раз при выполнении внешней функции происходит создание нового экземпляра внутренней функции, с новыми ссылками на переменные внешней функции.
В случае замыкания ссылки на переменные внешней функции действительны внутри вложенной функции до тех пор, пока работает вложенная функция, даже если внешняя функция закончила работу, и переменные вышли из области видимости.[1]
Замыкание связывает код функции с её лексическим окружением (местом, в котором она определена в коде). Лексические переменные замыкания отличаются от глобальных переменных тем, что они не занимают глобальное пространство имён. От переменных в объектах они отличаются тем, что привязаны к функциям, а не объектам.


Как видим из этого длинного определения (а не только его первого предложения) интервьюер оказался формально прав (что является неожиданностью и для меня самого. Кто знает, возможно это веяние нового или всё же творчество чрезмерного ЧСВ js-кодерков, но не будем предвзятыми) Посмотрим дальше...Переходим в английскую версию той же страницы в википедии:

In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function[a] together with an environment.[1] The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.[b] Unlike a plain function, a closure allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.


О! А тут нет никаких "функций, вложенных в функцию" и нет никаких "создающихся каждый раз" Хм... Интересно. Ну за то есть абзац о том как появились замыкания:

The concept of closures was developed in the 1960s for the mechanical evaluation of expressions in the λ-calculus and was first fully implemented in 1970 as a language feature in the PAL programming language to support lexically scoped first-class functions.[2]

Peter J. Landin defined the term closure in 1964 as having an environment part and a control part as used by his SECD machine for evaluating expressions.[3] Joel Moses credits Landin with introducing the term closure to refer to a lambda expression whose open bindings (free variables) have been closed by (or bound in) the lexical environment, resulting in a closed expression, or closure.[4][5] This usage was subsequently adopted by Sussman and Steele when they defined Scheme in 1975,[6] a lexically scoped variant of Lisp, and became widespread.


Ага. Оказывается первоначально они появились в такой не конвенциональной, с сегодняшней точки зрения VM, как SECD... И их создатель прямо говорит, что это код плюс данные. Далее термин применяют к лямбда-выражениям. Ничего не говорится, должны ли эти выражения возвращаться функцией, или достаточно того, что бы они захватывали переменные из своего лексического (или динамического, как в common lisp) окружения. Далее нам сообщают, что некие мистеры, Сасман и Стил, спустя каких то 11 лет добавили это в свой язык Scheme... Ну что же. Посмотрим замыкания в Scheme... А именно откроем книгу Сасмана и соавторов "Структура и интерпретация компьютерных программ" или "SICP" Первый раз (исключая содержание) слово "замыкание" встречается в главе 2 "Поcтроение абстракций c помощью данных"


Важная идея в работе с составными данными — понятие замыкания (closure): клей для
сочетания объектов данных должен позволять нам склеивать не только элементарные
объекты данных, но и составные.


Но, как выяснилось, это не те замыкания... Это замыкание в том смысле, что структуру одного типа (в данном случае пары) могут ссылаться на самих себя. И это то как термин "замыкание" понимают математики. Далее в SICP никаких других определений замыканий нет (несмотря на то, что само их использование конечно же есть) Ну ничего не остаётся как посмотреть что же сегодня называют замыканиями в языке Scheme... Гугл по этому запросу выдал первой ссылкой: https://www.cs.utexas.edu/ftp/garbage/cs345/schint....
Цитирую:

Procedures are Closures
Scheme procedure's aren't really just pieces of code you can execute; they're closures.

A closure is a procedure that records what environment it was created in. When you call it, that environment is restored before the actual code is executed. This ensures that when a procedure executes, it sees the exact same variable bindings that were visible when it was created--it doesn't just remember variable names in its code, it remembers what storage each name referred to when it was created.

Since variable bindings are allocated on the heap, not on a stack, this allows procedures to remember binding environments even after the expressions that created those environments have been evaluated. For example, a closure created by a lambda inside a let will remember the let's variable bindings even after we've exited the let. As long as we have a pointer to the procedure (closure), the bindings it refers to are guaranteed to exist. (The garbage collector will not reclaim the procedure's storage, or the storage for the let bindings.)

Here's an example that may clarify this, and show one way of taking advantage of it.

Suppose we type the following expression at the Scheme prompt, to be interpreted in a top-level environment:

Scheme> (let ((count 0))
           (lambda ()
              (set! count (+ count 1))
              count)))

##

Evaluating this let expression first creates a binding environment with a binding for count. The initial value of this binding is 0. In this environment, the lambda expression creates a closure. When executed, this procedure will increment the count, and then return its value. (Note that the procedure is not executed yet, however--it's just created.) This procedure, returned by the lambda expression, is also returned as the value of the let expression, because a let returns the value of its last body expression. The read-eval-print loop therefore prints a representation of the (anonymous) procedure.


Что имеем ? В Scheme - вообще любая функция - это замыкание! Т к она всегда захватывает контекст, даже если там пусто!. Что видим в коде ? Ну с точки зрения Scheme тут конечно вложенная функция, т к let - это на самом деле lambda, но так происходит потому, что в Scheme нет переменных в понимании js. В js же можно считать, что глобальный контекст - результат действия нечто, что эквивалентно let. Захватывает ли любая функция в js этот контекст ? Не знаю. Но точно знаю, что в вашем примере это определенно так - х часть контекста, который захватывается функцией. Кто хочет поспорить - идите спорьте с создателями Scheme (скажите, когда попкорн покупать, я посмотрю как вас пороть будут. Набежало тут вайтишников =)) Кто то может сказать "а вот у нас в js замыкания определяются не так" На что я отвечу: знаете, товарищи, а идите ка вы лесом! Если есть однажды созданный и определенный термин, нужны весьма веские причины что бы менять его определение. Желания левой ноги очередного вайтишника тут не достаточно.
Ответ написан
snaiper04ek
@snaiper04ek
Не стреляйте в эникея, он админит как умеет
парень. Всё равно ты будешь использовать ту терминологию, которую используют на работе. Если там под замыканием подразумевается замыкание с инкапсуляцией, то после того как тебе сказали что "твой код - говно", было два варианта: 1)поговорить о терминах либо со ссылкой на официальную документацию, либо вместо с собеседником вывести определение исходя из смысла понятия, не прибегая к авторитетам вообще. 2) Сказать о том, что прочитал такое определение у %авторитет%, и сказать, что готов использовать то, которым пользуетесь вы на работе.

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

Есть второй вариант: собеседник тебе говорит: "Ахтунг! Замыкание используется не просто для того, чтобы спасти переменную от удаления! Это ещё и способ сокрытия данных: замыканием можно использовать локальную глобальную переменную, вместо того чтобы использовать просто глобальную переменную, или же городить отдельный класс."
В этом случае всё твое определение идёт в пешее эротическое, и ты соглашаешься, что для этого придётся обернуть функцию в функцию, чтобы у тебя была функция с локальными переменными, которые будут глобальными для этой функции в функции.
Ответ написан
@lazarevBoris
Тред просто огонь!:)
Дело в том, что такой же спор (из 8 страниц, на эту же тему) был не так давно на другом ресурсе, где мудрый дядька под ником "javascript" объяснял окружающим (которые тоже уперлись рогом, как и многие здесь), почему в js по факту всё является замыканием.
Поэтому было крайне интересно, чем же и здесь дело закончится.
И в очередной раз было доказано, что где бы ни была объявлена функция (а начиная с es6 и let - и блок кода) - она образует замыкание с цепочкой внешних лексических сред (или хотя бы одной LE), что полностью соответствует определению, предысторию которого хорошо описал Александр.
Что наглядно изобразил Alexandroppolus (надеюсь, вы не будете против, если я достану ваш пример из jsfiddle и размещу здесь, чтобы он не пропал с течением времени):

description:
функция, созданная внутри iframe, замкнула внутрифреймовую глобальную переменную, и продолжает работать, даже когда фрейм удалили. Кнопкой frameValue можно убедиться, что в window той переменной нет.

html:
<script>
  var globalValue = 1000;
  var globalCounter = function() {
    return globalValue++;
  };
</script>
<div>
  <button>iframe counter</button>
  <button>globalCounter</button><br >
  <button>frameValue</button>
  <button>globalValue</button>
</div>

JS:
const iframeHTML = 'iframe...<' + `script>
  var frameValue = 1;
  var counter = function() {
    // debugger;
    return frameValue++;
  };
  
  window.parent.iframeCounter = counter;
<` + '/script>';

function createTempFrame() {
	const blob = new Blob([iframeHTML], {type: "text/html"});
  const frm = document.createElement('iframe');
  frm.src = URL.createObjectURL(blob);
  
  document.body.appendChild(frm);
  
  setTimeout(() => {
  	document.body.removeChild(frm);
  }, 900);
}

window.onload = function () {
  const buttons = document.querySelectorAll('button');
  
  buttons[0].onclick = function() {
    alert(iframeCounter());
  };

  buttons[1].onclick = function() {
    alert(globalCounter());
  };
  
  buttons[2].onclick = function() {
    alert(window.frameValue);
  };
  
  buttons[3].onclick = function() {
    alert(window.globalValue);
  };
 
  createTempFrame();
};


Так что да, автор треда, который ходил на собеседование, был прав, замыкание в его примере есть.
Ответ написан
@Vitsliputsli
Абсолютно понимаю автора, он цитирует определение замыкания, а ему в ответ - определения не нужны, они лишь путают. Он спрашивает, а как тогда? Ему кто про мусорщик, кто про стек, и у него создается ощущение, что сами не могут договориться. Но кроме этого, автор прав, его функция - замыкание.
Замыкания везде работают одинаково. Если функция содержит ссылки на переменные объявленные вне тела этой функции, и которые не являются ее параметрами - это и есть замыкание. Что значит фраза - все функции в javascript - замыкания? Дело в том, что в других языках область видимости может просто не позволить обращаться к внешним переменным, в таких языках функция не будет замыканием, но может быть возможность превратить функцию в замыкание через специальный синтаксис. В javascript таких манипуляций не нужно, поэтому в нем любая функция - замыкание.
Т.е. замыкание это возможность в функции создать ссылки на внешние переменные. А здесь уже как следствие, работа мусорщика, если есть рабочая ссылка, то объект не уничтожается, а в приведенных примерах с 2 функциями она рабочая, так как можно получить доступ из корневого объекта, что удовлетворяет требованиям алгоритма mark-and-sweap. Но это следствие, а не принцип работы замыкания.
Поэтому автор абсолютно прав - его функция это замыкание. Потому что ни в одном определении замыкания нет никаких упоминаний о мусорщике, а значит разницы нет на какие внешние данные ссылаться.
Другое дело, все хотят видеть не замыкание, а его хитрое использование, а именно сохранение ссылки объявленной в замыкании при уничтожении ссылки во внешней функции. Не надо считать собеседующего бездарем, если бы вы ему объяснили свою точку зрения, он вполне мог бы и согласиться, хотя и не факт, многие собеседования проходят в виде допроса, что говорит о неадекватности или о слабой квалификации собеседующего, в такие конторы не стоит идти.
Ответ написан
@Barrakuda74
Нет, не так. Ваш ответ был верным.
Любая функция вкупе с лексическим окружением образует замыкание.
Когда создаётся глобальное окружение (иначе говоря = начинается выполнение скрипта) - javascript создаёт лексическое окружение, в котором и будут храниться переменные и функции глобального контекста. Когда вы создаёте свою функцию в глобальном контексте, она замыкается на этом же лексическом окружении - лексическом окружении глобального контекста. Как видите, всё ОЧЕНЬ просто.

Держитесь подальше от таких контор, где собеседуют такие "специалисты".
Есть, конечно, вероятность, что вас HR собеседовала или техничка, но маловероятно.
Всегда нужно стремиться в те компании, где вы будете расти как программист, а не деградировать.
В галерах, где тимлид не знает о том, что такое замыкание, никакого роста, как вы понимаете, ожидать не приходится.

p.s.: смотрю у многих ответы далеки от реальности, но это скорее всего из-за того, что ваш вопрос под тегом "Простой", поэтому получили набег джунов, только что вылупившихся из яйца.
Ответ написан
dollar
@dollar
Делай добро и бросай его в воду.
Очевидно, что любая функция может использовать внешние переменные. Причём, это верно практически для любого ЯП.

Также локальные переменные внутри функции живут только в интервале времени, пока функция выполняется. И как только она завершает работу ("}" или "return"), локальные переменные (на стеке) уничтожаются.

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

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

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

Так что в вашем примере и правда нет замыкания.
Ответ написан
Alexandroppolus
@Alexandroppolus
кодир
По версии Ильи Кантора, глобальная функция при создании записывает себе лексическим окружением (Scope) глобальный объект, а в момент выполнения обращается к Scope.
globalFunc.[[Scope]] = window;

то есть формально да, это замыкание.

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

upd: заметим, что глобальных объектов может быть несколько, например, ифреймы на странице, или контекст в нодовском модуле VM. Тогда замыкание глобальных переменных (как в примере из вопроса) таки имеет смысл.
Ответ написан
wapster92
@wapster92 Куратор тега JavaScript
Kyle Simpson дал хорошее определение замыкания, вообще советую его книги для прочтения.
Замыкание — это когда функция умеет запоминать и имеет доступ к лексической области видимости даже тогда, когда эта функция выполняется вне своей лексической области видимости.
Ответ написан
insighter
@insighter
-First time? - Huh? (C#, React, JS)
Очень упрощенно - замыкание это захват локальных переменных другой функции, это блокирует освобождение памяти, которую они занимают.
У тебя в примере просто глобальная функция.
Ответ написан
Adamos
@Adamos
Высосанные из пальца определения для учебников только запутывают, если не подкреплять их теорию практикой.
А замыкание - это просто трюк: сохранение переменных из того контекста, в котором определена функция, до момента ее вызова, хотя по стандарту языка они к этому времени уже должны быть уничтожены.
Ответ написан
@Carburn
Полностью введение в википедии прочитай
Замыкание — это особый вид функции. Она определена в теле другой функции и создаётся каждый раз во время её выполнения. Синтаксически это выглядит как функция, находящаяся целиком в теле другой функции. При этом вложенная внутренняя функция содержит ссылки на локальные переменные внешней функции. Каждый раз при выполнении внешней функции происходит создание нового экземпляра внутренней функции, с новыми ссылками на переменные внешней функции.

В общем замыкания это не все функции, а функции, в которых есть ссылки на внешние переменные
https://learn.javascript.ru/closures#itogo-zamykaniya
Замыкание – это функция вместе со всеми внешними переменными, которые ей доступны.

Таково стандартное определение, которое есть в Wikipedia и большинстве серьёзных источников по программированию. То есть, замыкание – это функция + внешние переменные.
Тем не менее, в JavaScript есть небольшая терминологическая особенность.
Обычно, говоря «замыкание функции», подразумевают не саму эту функцию, а именно внешние переменные.
Иногда говорят «переменная берётся из замыкания». Это означает – из внешнего объекта переменных.

Что это такое – «понимать замыкания?»
Иногда говорят «Вася молодец, понимает замыкания!». Что это такое – «понимать замыкания», какой смысл обычно вкладывают в эти слова?

«Понимать замыкания» в JavaScript означает понимать следующие вещи:

Все переменные и параметры функций являются свойствами объекта переменных LexicalEnvironment. Каждый запуск функции создаёт новый такой объект. На верхнем уровне им является «глобальный объект», в браузере – window.
При создании функция получает системное свойство [[Scope]], которое ссылается на LexicalEnvironment, в котором она была создана.
При вызове функции, куда бы её ни передали в коде – она будет искать переменные сначала у себя, а затем во внешних LexicalEnvironment с места своего «рождения».
Ответ написан
Пригласить эксперта
Ответы на вопрос 2
greenkey
@greenkey
программист
Не стоит ругаться и обвинять друг друга в некомпетентности ;-) не знать чего-либо совершенно нормально, даже если ты лет двадцать без устали кодишь. Настоящего профессионала отличает не то, что он знает вообще все, а то, что он прекрасно осознает, что много чего не знает, и его это нисколько не смущает - мы учимся постоянно. Для меня, например, некоторые моменты связанные с closures оказались новыми, и я решил, что надо поглубже погрузиться в данный вопрос.
А он не так прост, как может показаться на первый взгляд, и содержит в себе целый ряд нюансов, поэтому чрезвычайно важно 1) оперировать одинаково понимаемыми терминами, 2) пойти в документацию, а именно в ecmascript, и, как правильно заметил dollar , проверить, как это было реализовано уже в реальной реализации javascript, в движке, например V8.
Вопрос, хоть и теоретический, но очень важный, потому что проливает свет на понимание работы современных языков "изнутри", что безусловно важно хорошему программисту.
Предлагаю всем, кого вопрос также заинтересовал, немного погрузиться в теорию, для начала вот это:
https://262.ecma-international.org/12.0/#sec-abstr...
более развернуто в статье, хоть и старой, но принцип closure не поменялся
dmitrysoshnikov.com/ecmascript/chapter-6-closures
Оно же на русском:
dmitrysoshnikov.com/ecmascript/ru-chapter-6-closures
Ответ написан
Комментировать
AgentSmith
@AgentSmith
Это мой правильный ответ на твой вопрос
тут нет замыкания. Это обычный вызов функции. Ты прав.
Тебе нужно понять простую вещь - на собеседования часто приходят мудаки, которые не понимают ничего в программировании. Как в теории, так и в практике
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы