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 была неточность.

★★★

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

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

если в скопе 100 переменных, то это массив из 100 адресов этих переменных. и чтобы нормально «чистить ненужное», они аллокированы на куче. доступ к переменной идет по ее индексу в массиве. тогда O(1).

А номер адреса откуда? Вообще, если мы уже получили и храним номер адреса, то почему не хранить сам адрес?

Тут одно из двух. Или как в интерпретаторе адрес получается по имени переменной в данном скоупе, тогда действительно хэш или двоичное дерево, хотя для небольшого количества имён переменных список будет быстрее. Или просто доступ по адресу. Тогда O(1) и никаких дополнительных действий.

monk ★★★★★
()

какой-то шизо тред,

Environment Records are purely specification mechanisms…

PS чекните на деалокацию с помощью FinalizationRegistry

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

Поэтому это обычные функции, а не замыкания.

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

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

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

А следовательно если объединить а и б - получается ты опять несешь бред, а потом еще и оправдываешься каким-то контрпримерами.

я тебе выше структуру скопа уже нарисовал.

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

А номер адреса откуда? Вообще, если мы уже получили и храним номер адреса, то почему не хранить сам адрес?

ты не понял. массив такой { addr0, addr1, addr2 …}то есть сами адреса

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

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

А что будет, если в замыкание передаётся свойство this.x, потом удаляется через delete это свойство и вызывается функция?

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

я тебе выше структуру скопа уже нарисовал.

В художника играешь?

Давай еще раз, вот твои слова «они могут удаляться, если только там списочная структура»

Ты все еще это утверждаешь? Или соглашаешься что ошибся?

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

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

int x = 5;

int f(int y) { return x+y; }

Здесь f - замыкание?

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

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

Так если при компиляции известно, что x имеет номер 3, то при компиляции можно и адрес сразу подставить. Если же есть только имя, то сначала надо по строке «x» найти номер в этом массиве.

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

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

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

В общем ты в очередной раз «все понял»

Вот тебе ссылка на исходники V8 по части скоупа, (не могу гарантировать 100% что это именно реализация environment records, но очень похоже на то)

https://github.com/v8/v8/blob/master/src/ast/scopes.cc

И мы видим что там хэштаблица,

«они могут удаляться, если только там списочная структура» - Это сразу нобелевская по computer science

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

Так если при компиляции известно, что x имеет номер 3, то при компиляции можно и адрес сразу подставить. Если же есть только имя, то сначала надо по строке «x» найти номер в этом массиве.

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

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

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

а переменная x определена не в глобальной области видимости, а лексической

Так вот это я и написал. «никаких замыканий не будет (если for не внутри функции и f не вернуть наружу)» = «(если f определена в глобальной области видимости)».

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

Я тебе скриншот зачем прикрепил в ответном сообщении?

В js у каждого модуля СВОЯ лексическая область видимости. Она не глобальная. Она локальная в пределах модуля.

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

Вот тебе ссылка на исходники V8 по части скоупа, (не могу гарантировать 100% что это именно реализация environment records, но очень похоже на то)

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

при интерпретации все вообще тухло и медленно.

я ж говорю, для случая если эту кухню можно хоть как-то компилировать. а компиляторы js есть.

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

Вот тебе квест. В приведенном ниже коде, есть замыкания или нет?

let x = 0

globalThis.f = []

for (globalThis.i = 0; globalThis.i < 1; globalThis.i++){
    globalThis.f.push(n => x)
}
javascript
()
Ответ на: комментарий от javascript

А что в вопросе непонятного?
Ты внутри функции используешь this.x, а после её объявления делаешь delete this.x и вызываешь функцию.

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

А что в вопросе непонятного?

Непонятно, что у тебя вообще вызывает вопрос.

Тебе рассказать, что будет если обратиться к несуществующему свойству объекта? Вернется undefined.

Или рассказать про позднее связывание, и что this, может быть динамическим?

Или что ты хочешь? Что за вопросы уровня - а что если к двум прибавить три. А если к двум прибавить семь?

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

В js у каждого модуля СВОЯ лексическая область видимости. Она не глобальная. Она локальная в пределах модуля.

ОК. В пределах модуля.

static int x = 5;

int f(int y) { return x+y; }

Теперь x локальная в пределах модуля. f стало замыканием?

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

Но this.x должно быть доступно через замыкание, ведь в момент объявления свойство есть. Допустим делается delete this.x и потом новое присваивание this.x = 5. Тогда будет считаться это свойство тем же или все равно ошибка будет?

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

есть замыкания или нет?

Зависит от места определения x. Если внутри функции, то есть. Если вне, то нет.

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

С ЧЕГО ВДРУГ?

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

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

Но this.x должно быть доступно через замыкание, ведь в момент объявления свойство есть.

У тебя лютая каша в голове. Пообщайся с кем-нибудь еще.

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

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

Просто вы понимаете замыкание, как любой доступ функции к внешней переменной.
А в книгах про замыкание пишут, как про способ получения доступа к переменным, которые были доступны в момент объявления функции, но во время выполнения доступные именно благодаря наличию замыкания, а не тому, что переменная просто имеется во внешнем scope во время выполнения.

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

так как она в глобальной области жизни.

Сколько еще раз нужно написать, что глобальные переменные лишь те, что объявлены явно как свойства глобального объекта globalThis.someVar, либо если ты пишешь на легаси жс в unstrict script mode. Все обычные переменные локальны, как минимум в пределах модуля.

В общем, я умываю руки. Ты не пробиваем.

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

Внезапно, даже в википедии есть примеры замыканий на Си.

на си есть все. на с++ тоже. в том смысле, что ручками там можно сделать все что угодно. и многое шаблонами, на плюсах.

вон сам интерпретатор JS написан на плюсах…не на самом же JS его писать.

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

а не тому, что переменная просто имеется во внешнем scope во время выполнения.

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

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

Внезапно, даже в википедии есть примеры замыканий на Си.

В смысле, GClosure? Или дай ссылку, что имеешь в виду.

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

В то время как суть замыкания в том, что для его использования недостаточно только указателя на функцию. То есть, если f ссылается только на те объекты, которые в программе существуют в единственном экземпляре, то она не замыкание. А вот если f ссылается на внутреннюю переменную другой функции, то эта внутренняя переменная будет новая при каждом запуске той функции, и для того, чтобы использовать f необходимо иметь указатель на f и указатель на контекст, полученный при запуске функции, переменную которой f использует.

for(let ...) в JS раскрывается в рекурсивную функцию, поэтому внутри него можно создавать замыкания. Модуль или блок всегда выполняются ровно один раз, поэтому дополнительный указатель на экземпляр контекста модуля или блока не требуется.

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

for(let …) в JS раскрывается в рекурсивную функцию

Только в твоем воображении.

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

Это снова неправда. Модуль может быть загружен и выполнен множество раз.

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

В то время как суть замыкания в том,…

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

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

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

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

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

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

А в скорости: Haskell/LLVM. Сплошные замыкания по построению языка, скорость аналогична сишной.

И ещё раз: замыкание не обязано быть на куче. Если оно не выходит за пределы функции, в которой создано, то оно должно быть (при нормальном оптимизаторе) на стеке.

И в целом, замыкание можно сделать в любом языке со структурами и указателями на функцию. Основное отличие языков, которые их поддерживают на уровне языка, в том, что синтаксис вызова функции и замыкания идентичен. Также как в языках, поддерживающих продолжения, идентичен вызов функции и продолжения (в остальных продолжение можно изобразить ручным описанием контекста и безусловным переходом).

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

monk ★★★★★
()
Ответ на: комментарий от javascript
import './module.js?1'
import './module.js?2'

И из какой версии функции и переменные станут доступны? По-моему будет или конфликт имён или вторая версия затрёт первую.

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

Из обоих все будет доступно.

По-моему

Предлагаю разложить пасьянс.

Сразу бы так и написал, что ты не знаешь языка, о котором тут все разговаривают. Не было бы вопросов.

Счастливо оставаться.

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

там другая семантика, имхо, похоже но не тоже самое.

$ cat m.js 
export var x = 1;
export const inc = () => { x = x + 1 };
$ cat x.js 
import { x, inc } from './m.js?1';
import { x as x2} from './m.js?2';
console.log(x, inc(), x, x2);
$ node x.js 
1 undefined 2 1

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

То же самое. Модуль это суть то же, что IIFE, ровно как и блок.

function load() {
   return (x => () => x++)(1)
}

let x = load(), y = load();

console.log(x(), x(), x(), y()) // 1 2 3 1

Вот еще пример с множественным вызовом блока, а не модуля.

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

Проблема не в скорости, а в удалении этих объектов.

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

Если оно не выходит за пределы функции, в которой создано, то оно должно быть (при нормальном оптимизаторе) на стеке.

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

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

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

Ты не понял.

function gen(x) {
   return () => x++
}

function f() {
  // замыкания a1 и a2 на стеке, но никаких адресов в списке параметров нет
  a1 = gen(1);
  a2 = gen(2);
  a1(); a2();
  ...
  return 0;
}

let c = gen(3) // вот это в куче, хотя если javascript прав и модуль тоже "функция", то может быть и на стеке
monk ★★★★★
()
Ответ на: комментарий от monk

// замыкания a1 и a2 на стеке, но никаких адресов в списке параметров нет a1 = gen(1); a2 = gen(2);

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

тогда транслируя выражение a1 = gen(1); компилятор (например) создаст «отложенный вызов» вида gen(1), и свяжет его с именем a1, у этот вызов будет делать везде, где будет вызов a1().

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

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

тогда транслируя выражение a1 = gen(1); компилятор (например) создаст «отложенный вызов» вида gen(1), и свяжет его с именем a1

Константа лишь инициализатор. Можно сделать

function gen() {
   x = 0
   return () => x++
}

function f() {
  // замыкания a1 и a2 на стеке, но никаких адресов в списке параметров нет
  a1 = gen();
  a2 = gen();

транслируя выражение a1 = gen(1); компилятор (например) создаст «отложенный вызов» вида gen(1)

Превращается он во что-то вроде:

class Gen {
  int x;
  public: Gen(): x(0) {}
  int operator() () { return x++; }
};

auto f() {
  Gen a1, a2;
  a1(); a2();
  ...

Обрати внимание, что a1 и a2 на стеке.

monk ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.