LINUX.ORG.RU

Неточности в определении замыкания в javascript

 , , ,


0

1

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

Определение из mdn (на английском тоже самое)

Замыкание — это комбинация функции и лексического окружения, в котором эта функция была определена. Другими словами, замыкание даёт вам доступ к Scope (en-US) внешней функции из внутренней функции. В JavaScript замыкания создаются каждый раз при создании функции, во время её создания.

Два фрагмента кода

const fn = () => {
  let x = 5;
  if (true) {
    console.log(x); // Вот тут x не существует в текущем (if-овском) скоупе, переменная ищется (lookup?) в родительском (fn-овском) 
  }
}

Вопрос, откуда берется значение переменной x? Хочется сказать и думать что из замыкания, но замыкание работает с функциями, тут же блочный скоуп

Или вот например если у нас есть файл

let x = 6;
export const getX = () => x

в данном случае переменная x не находится в скоупе внешней функции, она находится в скоупе модуля

[UPD] Так по факту и не разобрались, но большинство склоняется к тому что второй пример является замыканием, а значит на mdn была неточность.

★★★

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

то есть если в плюсах написать

void ff(){
  int x=0;
  auto my_closure = [x]()->int {
    return x;
  }
  int y = my_closure();
}

это равносильно такой записи…

int hidden_func_kinda_closure (int fx){ return fx;}

void ff(){
  int x=0;
  int y = hidden_func_kinda_closure(x);
}

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

то есть в асмовом выхлопе вы даже не отличите первый вариант от второго.

alysnix ()
Последнее исправление: alysnix (всего исправлений: 2)

то есть «захват» это технически - скрытое пропихивание адресов или значений переменных, этой самой «лямбде» в момент ее вызова.

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

alysnix ()
const fn = () => {
  let x = 5;
  if (true) {
    console.log(x); // Вот тут x не существует в текущем (if-овском) скоупе, переменная ищется (lookup?) в родительском (fn-овском) 
  }
}

тут вообще нет никакого захвата внешних к лямбде переменных.

определена локальная переменная x в данной функции - она и будет подставлена в log(..)

alysnix ()
Ответ на: комментарий от alysnix

определена локальная переменная x

В javascript блок кода (то что между фигурных скобок) создает новый scope, мы можем скажем вторую переменную определить

const fn = () => {
  let x = 5;
  if (true) {
    let x = 7;
    console.log(x); будет 7, а не 5. 
  }
  console.log(x); будет 5, а не 7. 
}
abs ★★★ ()
Ответ на: комментарий от abs

В javascript блок кода (то что между фигурных скобок) создает новый scope, мы можем скажем вторую переменную определить

в сиподобии то же самое и что?

ищется ближайшая по скопам вверх(от текущего до глобального) переменная с таким именем. если ее нет - будет ругань.

у вас она есть. в чем вопрос-то?

alysnix ()
Ответ на: комментарий от abs

Здесь нет замыкания. Не надо это называть замыканием в своём ролике (дебилов и так хватает, нечего их плодить). Здесь речь идёт об области видимости. Замыкание - это про функции, которые видят объекты из внешнего скопа (который, как раз, должен находиться в области видимости).

anonymous ()
Ответ на: комментарий от alysnix

то есть «захват» это технически - скрытое пропихивание адресов или значений переменных, этой самой «лямбде» в момент ее вызова.

В JS «замыкаются» не конкретные значения переменных, а весь scope вместе со всеми переменными, причем внешняя функция после отработки не будет очищаться GC, до тех пор пока внутренняя функция которая замкнула переменные будет в «зоне доступа» кого-то (если ее скажем вернуть из внешней функции и куда-то сохранить).

в чистом виде синтаксический сахар

Не уверен, в JS у функций есть явное скрытое свойство (то ли [[ENVIROMENT]] толи [[SCOPE]] в разных местах по разному пишут) которое и сохраняет ссылки на скоупы

abs ★★★ ()
Ответ на: комментарий от abs

В JS «замыкаются» не конкретные значения переменных, а весь scope вместе со всеми переменными,…

что вы лоб, что по лбу. видимый внешний скоп в данном случае будет просто адресом фрейма на стеке локальных переменных внешней функции, который передадут этой лямбде скрытым параметром. так в modula-2 сделано еще в начале 80х.

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

в данном случае - это общий захват, но в плюсах делается не так. там то, что надо захватить обьявляется явно.

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

alysnix ()
Последнее исправление: alysnix (всего исправлений: 2)
Ответ на: комментарий от alysnix

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

если б такое было в плюсах, то можно было писать так

int a() {
  int x = 100;

  int b() { //вложенная 
    int c() { // вложенная во вложенной
      return x; /// тут вложенная видит x, бай дезайн 
    }
  }
}
alysnix ()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от abs

Не, тебе надо в теорию. Не scope, при выполнении что происходит? Копай сюды. Если по таким вопросам плаваешь, то рекомендую посмотреть, как делали англичене в Америкe: https://frontendmasters.com/teachers/will-sentance/

Добавишь к нему по книге «птичьего языка» и будет нормально. Лучше него по JS и Node пока нет.

Сам курс по названию можно надыбать на рутрекере.
Что же каается ЛОР - тут по таким знаниям сосут лапу.

B0B ()
Ответ на: комментарий от abs

В JS «замыкаются» не конкретные значения переменных, а весь scope вместе со всеми переменными, причем внешняя функция после отработки не будет очищаться GC, до тех пор пока внутренняя функция которая замкнула переменные будет в «зоне доступа» кого-то (если ее скажем вернуть из внешней функции и куда-то сохранить).

Вот точно рекомендую просмотреть.

B0B ()

Хочется сказать и думать что из замыкания

Этот фрагмент к замыканиям никак не относится.

данном случае переменная x не находится в скоупе внешней функции

И что? Для замыкания важен скоуп внешний по отношению к текущей функции. А что это за скоуп – дело десятое.

no-such-file ★★★★★ ()
Ответ на: комментарий от no-such-file

И что? Для замыкания важен скоуп внешний по отношению к текущей функции. А что это за скоуп – дело десятое.

Вот в этом и проблема, определение из mdn (Я понимаю что это не самый главный источник знаний, но один из важных) говорит что

замыкание даёт вам доступ к Scope внешней функции из внутренней функции

abs ★★★ ()
Ответ на: комментарий от B0B

А как происходит поиск переменных?

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

Еще у меня путаница между scope, environment, Environment Records Насколько я понимаю scope это просто концепция, (хотя при этом в chrome dev tools мы можем увидеть у функции спец свойство [[scope]] которое представляет из себя что-то похожее на массив всех scope от текущего и до прапра…отца), ER я так понял это то что содержит в себе scope плюс ссылку на родителя ER

abs ★★★ ()
Ответ на: комментарий от abs

замыкание даёт вам доступ к Scope внешней функции из внутренней функции

как в модула-2. то есть если внутри лямбды твоей есть видимая для данного кода переменная X, то она и берется. а если ее нет, то она ищется в скопе синтаксически внешней функции, если ее там тоже нет - то в том скопе, в котором находится эта внешняя. и так далее.

естественно если язык компилируемый, то это далется статически при компиляции. если интерпретируемый, то во время исполнения.

alysnix ()

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

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

А всё, что определено внутри функции, лежит на стеке, тут ничего сложного нет и ничего дополнительно передавать не надо. То, что там могут быть переменные с одним именем быть в разных scope-ах, это как раз ерунда, просто переименовываешь все переменные и всё, это как раз пример простого синтаксического сахара, который тривиально реализуется простым преобразованием кода.

Legioner ★★★★★ ()
Последнее исправление: Legioner (всего исправлений: 2)
Ответ на: комментарий от Legioner

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

Это простое и понятное определение которым я обычно и пользовался, но оно не совсем корретное. Повторюсь, я хочу сделать видеоролик на ютуб с детальным объяснением про замыкания.

Говоря выше мы имеет в виду лексически ту область в которой функция была создана, в теории оно может быть и ниже)

Также нужно помнить что замыкание может быть на несколько уровней.

и адрес таблицы с указателями на внешние переменные

В JS замыкаются не переменные, а весь scope целиком

abs ★★★ ()
Ответ на: комментарий от Legioner

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

в компилируемом языке никакой «таблицы» нет. если только в интерпретируемом.

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

alysnix ()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от alysnix

Ты не понимаешь, что такое замыкание и не понимаешь, как работает компилятор. Реализуй сам хотя бы простейший язык с замыканием и поймёшь о чём я говорю. Чтобы там работал кода вида

var f1 = function (x) { return function (y) { return x + y } }
var f2 = f1(3);
var n7 = f2(4); // 7
Legioner ★★★★★ ()
Последнее исправление: Legioner (всего исправлений: 2)
Ответ на: комментарий от X512

Из «X в каком-то одном случае компилируется в Y» не следует «X является синтаксическим сахаром для Y», и уж точно не «концепция позади X является синтаксическим сахаром для Y».

Ну или с вас двоих препроцессор, добавляющий в C замыкания.

t184256 ★★★★★ ()
Ответ на: комментарий от alysnix

Я как раз компилятор писал, а ты нет.

Что тебе статически известно? Откуда f2(4) получит статически доступ к x == 3 (из моего примера)? f1 уже закончила выполняться, её стек уничтожен, локальная переменная x не существует.

Legioner ★★★★★ ()
Последнее исправление: Legioner (всего исправлений: 2)
Ответ на: комментарий от Legioner
var f1 = function (x) { return function (y) { return x + y } }
var f2 = f1(3);
var n7 = f2(4); // 7

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

alysnix ()
Ответ на: комментарий от Legioner

Я как раз компилятор писал, а ты нет.

компилятор чего ты там писал?

я лично - компилируемые, сильно типизированные.

alysnix ()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от alysnix

это язык то какой хоть

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

в первой строке что написано?

Переменной f1 присваивается функциональное значение: функция, которая принимает один параметр x и возвращающее другое функциональное значение: функцию, которая принимает один параметр y и возвращающую сумму x + y.

внутренняя видит параметр x из внешней?

Да.

потом делаются две функции одна на внешнем параметре 3, другая на 4?

Переменная f2 это «внутренняя» функция, для которой x будет равен трём и которая будет возвращать 3 + (переданный аргумент).

Legioner ★★★★★ ()
Ответ на: комментарий от abs

насколько я понимаю поиск происходит итеративно, ищется в текущем скоупе, если нет берется родительский скоуп и повторяется, до тех пор пока не найдем

Совершенно прав.

путаница между scope, environment

А вот тут оно и поможет, что выше. Дело не в хранении ([[scope]]), дело в выполнении и как работает всё это дело.

B0B ()
Ответ на: комментарий от alysnix

компилятор чего ты там писал?

Компилятор лиспо-подобного языка в машинный код.

я лично - компилируемые, сильно типизированные.

В данном случае типизация значения не имеет.

Legioner ★★★★★ ()
Ответ на: комментарий от Legioner

Что тебе статически известно? Откуда f2(4) получит статически доступ к x == 3 (из моего примера)? f1 уже закончила выполняться, её стек уничтожен, локальная переменная x не существует.

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

там переменная может быть подставлена?

типа

var f2 = f1(X);
alysnix ()
Ответ на: комментарий от alysnix

а я модулы-2 и оберона-2 в машинный код.

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

что там за «лиспоподобный»…где стандарт его?

Его «стандарт» был у меня в голове. Это был внутренний язык для внутренних нужд. Можно считать его кастрированной схемой с javascript-подобным синтаксисом (чтобы не пугались окружающие).

Legioner ★★★★★ ()
Ответ на: комментарий от Legioner

Конечно может быть.

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

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

alysnix ()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от alysnix

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

В JS это происходит постоянно, а еще одна лямбда может поменять значение которое вторая будет читать

const fn = () => {
  let sharedValue = 5;
  return {
    getValue: () => { return sharedValue; }
    setValue: (newValue) => { sharedValue = newValue; }
  }
}

Две анонимные функции которые замыкают одну и туже переменную

abs ★★★ ()
Ответ на: комментарий от alysnix

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

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

А если ничего хранить не надо, это просто указатель на функцию.

Legioner ★★★★★ ()
Последнее исправление: Legioner (всего исправлений: 1)
Ответ на: комментарий от Legioner

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

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

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

я б не сказал, что они что-то особо решают или упрощают в этой жизни. вот ваш пример, скорее все путает, когда возвращается такая вот «параметризованная» лямбда. а если код еще и посложней будет..можно вообще долго гадать - как оно работает.

на надо захватывать просто так. передавайте явно через параметры. так оно читабельней

alysnix ()
Ответ на: комментарий от alysnix

я б не сказал, что они что-то особо решают или упрощают в этой жизни. вот ваш пример, скорее все путает, когда возвращается такая вот «параметризованная» лямбда. а если код еще и посложней будет..можно вообще долго гадать - как оно работает.

ООП с непривычки выглядит не лучше, ФП очень удобный когда к нему привыкаешь.

Вот вам пример понятного кода который возможен благодаря замыканию

const getSpecialRoleUsers = (users, role) => {
  return users.filter(user => user.role === role)
}
abs ★★★ ()
Ответ на: комментарий от Legioner

А если ничего хранить не надо, это просто указатель на функцию.

вот плюсовая лямбда с захватом:

void f(){
  int x = 100;
  int y = [x](){return x;}
}

что тут хранить?

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

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

alysnix ()
Последнее исправление: alysnix (всего исправлений: 1)