All streams
Search
Write a publication
Pull to refresh

Comments 23

1) Я попробовал написать статью с терминами спецификации: чтобы самому не путаться в "областях видимости", "скоупах", "блоках".

2) Я хотел максимально её визуализировать, чтобы самому что-то понять: вышло чёткое разделение Environmental Records и Lexical/Variable-Environment.

3) В документации learn.javascript.ru есть несколько "допущений", которые расходятся с EcmaScript и путают меня. Ведь V8 реализует EcmaScript, а не документацию learn.javascript.ru или блог MDN.

Примеры из документации learn.javascript.ru:

Устаревшее ключевое слово "var".

К сожалению, не нашёл слов depricated или прочих указаний, что это слово устарело =(

у каждой … функции, блока кода {...} … есть связанный с ними …скрытый объект - LexicalEnvironment. Он состоит из Environment Record и ссылки outer.

В начале статьи есть описание "блочной видимости", но после введения термина "LexicalEnvironment" такого примера уже нет. Остаётся неясным переход LexicalEnvironment от "объекта" функции к объекту блока.

4) Как и в пункте 1 я тоже путался. Что вы имеете в виду под _scope и OuterEnv?

[[OuterEnv]] я взял как поле, которое ссылается на родительскую запись окружения. 9.1 Environment Records

[[OuterEnv]] я взял как поле, которое ссылается на родительскую запись окружения. 9.1 Environment Records

Короче я погуглил, раньше называлось [[Scope]]

  • [[Scope]] (old) = whole chain of lexical environments stored with the function.

  • [[OuterEnv]] (new) = each environment points to its parent environment; a function just stores its immediate environment ([[Environment]]), and the chain is followed through [[OuterEnv]].

Но тут тоже надо отметить, что теперь это Environment, а в нем OuterEnv

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

Круто! Спасибо за справку.

[[Environment]] - это поле у объекта функции, оно ссылается на запись окружения.

[[OuterEnv]] - это поле у записи окружения, оно ссылается на родительскую запись окружения.

Это как я понял.

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

Scope оно и есть. Просто в контексте спецификации оно обозначается как окружение. OuterEnv - это просто ссылка на область видимости выше по стеку.

Раньше просто был [[Scope]] и ссылались через него. Так и в стандарте обзывали)

Видимо это уже сахар от девтулзов) надо обновлять знания))

Так да!

Scope, closure, block - это в хром девтулзах, а полез в спеку - таких слов вообще нет.

Отсутствие документации по V8 - боль!

кликбейтных заголовков не существует.

Народные мифы или ECMAScript

такие себе мифы.

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

что в общем-то никак не противоречит утверждению, что это функция, вернувшая функцию. Ну и не любой функции, а функции содержащей то самое определение. если есть функция foo() которая никак не связывается с bar(), которая в свою очередь содержит замыкание, то при создании foo замыкание в bar не будет создаваться.

цепочке записей окружения,

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

Замыкание — это особый объект с переменными

> Это не объект,

В JS буквально почти всё объект. В том числе и функции. У не объекта не должно быть никаких полей или методов.

Демонстрируется на любом обращении к внешней переменной

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

Environment Record = запись окружения = окружение

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

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

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

FunctionDeclaration (Декларативное объявление функции)

что значит "декларативное"? вы сделали тавтологию на ровном месте. Определение/объявление (англ. Declaration) функции оно и в африке им остаётся.

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

учитывая, что вся цепочка видимости, как и различные окружения - абстракция спеки, о чем вам написали парой абзацев выше, почему вы зовёте это РЕАЛИЗАЦИЕЙ?

Она дополнительно управляет this.

ничем она не управляет. this - это указатель объекта на себя и конструируется как часть объекта функции.

Этот простой рекурсивный обход

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

И дальше по статье ещё кучка таких вот отсебятин.

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

Остальное, ответы на комментарии:

что в общем-то никак не противоречит утверждению, что это функция, вернувшая функцию. Ну и не любой функции, а функции содержащей то самое определение. если есть функция foo() которая никак не связывается с bar(), которая в свою очередь содержит замыкание, то при создании foo замыкание в bar не будет создаваться.

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

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

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

В JS буквально почти всё объект. В том числе и функции. У не объекта не должно быть никаких полей или методов.

Функция – это объект, но "замыкание" - это механизм работы JS. Я это хотел подчеркнуть.

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

исправил ✅

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

Я сократил название, чтобы было легче им оперировать. Да, окружения наследуются лишь через поле [[OuterEnv]]. "Environment Record is … used to define the association of Identifiers to… functions"

Какое окружение имеете в виду вы?

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

Не понял комментария. Да, самого слова "замыкание" нет в спеке. Поэтому и глагол "захватывает" можно использовать условно.

Захват контекста в функции зависит от записей окружения

Это я тоже не понял. Что значит контекст? Он отличается от записей окружения?

что значит "декларативное"? вы сделали тавтологию на ровном месте. Определение/объявление (англ. Declaration) функции оно и в африке им остаётся.

Простое объявление функции идёт в запись окружения через компонент VariableEnvironment, а FunctionExpression созданное через const идёт через LexicalEnvironment.

учитывая, что вся цепочка видимости, как и различные окружения - абстракция спеки, о чем вам написали парой абзацев выше, почему вы зовёте это РЕАЛИЗАЦИЕЙ?

исправил ✅

ничем она не управляет. this - это указатель объекта на себя и конструируется как часть объекта функции.

исправил ✅

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

исправил ✅.

Ещё раз спасибо. Я могу ошибаться.

без примера не могу понять связь этих функций.

function foo(args) {
/* body */
}
function bar(args) {
  return function(33, args..) { /* */}
}

Вызов foo никак не повлияет на вызов function(33,args), то есть не любой вызов.

"Замыкание", как механизм поиска идентификаторов (переменных внутри этой функции) есть всегда.

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

В контексте спецификации оно и не должно существовать. Также как не существует таких вещей как функтор, типаж или монада или даже ООП с полиморфизмом и наследованием в той же спецификации. Это не значит, что их не может существовать в JS.

"замыкание" может быть и у блока

ещё раз - окружение и замыкание не одно и то же. каждый блок кода на стеке фактически имеет три постоянных ссылки - ссылку на глобальное окружение (GlobalEnv) , ссылку на окружение уровнем выше (OuterEnv) и ссылку на собственную область видимости (LexicalEnv). если на стек попадает какой-то новый объект, то ей LexicalEnv предыдущего блока попадает в OuterEnv текущего и создаётся новый LexicalEnv. Такая штука как VariableEnv на самом деле всегда существует в пределах GlobalEnv.

Кстати из-за непонимания вот этих вещей у вас код на баше и на js неравноценный, т.к. аналогом local в bash будет тот же let в js, хотя общие принципы работы области видимости у них примерно совпадают.

Да, окружения наследуются

наследование это маленько другое и работает через то что в спеке называется ObjectEnv, который содержит поля объектов класса/прототипа. OuterEnv это просто ссылка на LexicalEnv который был на стеке, либо GlobalEnv, если ничего другого не осталось.

Не понял комментария. Да, самого слова "замыкание" нет в спеке. Поэтому и глагол "захватывает" можно использовать условно.

На примере покажу

let a = 34;
var x = 33; 
// 
// GlobalEnv { 
//    vars : {                # наш VariableEnv
//      x : 33                # наш объявленный var x
//    }
// }
// 
// LexicalEnv [[ Scope 1]] {  # локальное окружение Scope1
//     OuterEnv -> GlobalEnv  # ссылка на внешнее окружение
//     x -> GlovalEnv.vars.x  # ссылается на VariableEnv
//     a : 34                 # 
// }  
//
function foo() { 
// объявление foo попадает в GlobalEnv
// новое состояние GlobalEnv
// GlobalEnv { 
//    vars : {              # наш VariableEnv для переменных
//      x : 33              # наш объявленный var x
//    }
//    funcs: {              # VariableEnv для function
//       foo : <...>        # объявленная функция
//    }
// }
// конструируется локальное окружение
// LexicalEnv [[ foo ]]{         # локальное окружение функции foo
//    this -> [[ foo ]]          # ссылка на собственный LexicalEnv
//    OuterEnv -> [[ Scope1 ]]   # ссылка на внешний контекст
//    const: [2]                 # локальные константы
//    a -> OuterEnv.a            # ссылается на OuterEnv
//    x -> OuterEnv.x            # по цепочке ссылается на VariableEnv в GlobalEnv
//}
  return a + x + 2; // обращение будет выглядеть как
  // [[ foo ]].a + [[ foo ]].x + [[ foo ]].const[0]          ==>
  // OuterEnv.a + OuterEnv.x + [[ foo ]].const[0]            ==> 
  // [[ Scope1 ]].a + GlobalEnv.vars.x + [[ foo ]].const[0]
}

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

Надеюсь моя нотация понятна? -> обозначает что левое ссылается на правое, а : соотвественно содержит значение, [[ asd ]] просто обозначение сущности по имени для удобства.

Это условная структура и не следует спеке досконально.

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

function outer() {
  let x = 10; // Захваченая переменная (captured variable)
  function inner() {
    console.log(x); // доступ к "верхней" переменной (closure)
  }
  inner()
}

outer(); // x

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

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

В рамках спеки там про сборщик мусора вроде вообще речи не идёт.

Спасибо за пример, так яснее.

Да спека - лишь рекомендация для движков. Ещё бы почитать доки по V8 со сборщиком мусора, хранением значений и т.д., но их нет =(

Соглашусь с @DmitryOlkhovoi, как я понял вы говорите, что переменные внутри функции уже в момент создания "знают" на что ссылаться. Хотя я увидел, другой механизм:

  • у объектов функций есть поле [[Environment]], оно ссылается на запись окружения в момент создания.

  • Потом при вызове функции, создаётся запись окружения этой функции, у которой поле [[OuterEnv]] будет ссылаться на запись из [[Environment]].

  • В итоге, лишь при выполнении, идя по цепочке записей окружения, мы найдём переменную. А не в момент создания.

Взял отсюда: 9.1.2.1 GetIdentifierReference ( env, name, strict ).

код на Bash взял из chatgpt, могу ошибаться =) хотел узнать какие языки решили проблему фунарга иначе.

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

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

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

у объектов функций есть поле [[Environment]]

Это тоже детали имплементации, причём конкретных девтулзов. В примере естестевенно пропущено куча полей, которые в действительности там будут - и всякие встроеные свойства (тот же prototype) и автосгенерённые ссылки на разное. И похоже все эти [[blabla]] мета-поля тоже недавно появились ибо я не знаю что в них обычно складируют. Ради оптимизаций все эти скоупы могут вполне быть где-то в общей линейной памяти с помеченные некоторым битовым маркером ради сокращений непрямых обращений после прохода JIT оптимизатора - естественно к языку это имеет довольно опосредованное отношение.

код на Bash взял из chatgpt, могу ошибаться =) хотел узнать какие языки решили проблему фунарга иначе.

ну, кстати, если хочется посомотреть немного разницы, то можно попробовать посравнивать с Lua. Правда у того спеки нет. Но во многом похож на JS/EcmaScript - прототипы и метатаблицы, оба гоняют на виртуальной машине. оба имеют упрощеную лексику для работы как с глобальным так и с локальным окружением (читай областью видимости), резолюция модулей и вот это вот все. Правда код Lua (который PUC Rio) несравнимо меньше хотя бы того же V8 и даже вроде quickjs, но последнее не точно.

Спасибо за предложения.

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

Я говорю, что "контекст" = "окружение" = Environment Record. И "замыкание" - это механизм нахождения переменной в цепочке Environment Record.

Другого "замыкания" не существует. Согласны?

Замыкание это не поиск переменных, а то, как внешняя переменная оказывается в локальном контексте.

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

Замыкание - от внешнего к внутреннему - как значение переменной оказывается в месте его использования. Можете взять из глобального окружения, можете дать ссылку на в памяти из текущего контекста, можете скопировать или даже переместить её полностью - стратегий много, причем многие могут сосуществовать. В случае с ES спеки - через референсы в новое окружение.

Environment != Environment Record. Вы же не зовёте предложение/абзац/главу в книге книгой - так и тут. В моём примере ключ-значение это как раз Record. То есть x:34 или this -> [[foo]] будут такими записями. Коллекция таких записей уже зовётся окружением/контекстом (в общем смысле).

Спасибо, понял вашу логику.

Но я не увидел в спеке механизма "замыкание" из вашей логики. Я увидел лишь "поиск" - это описание взял из 9.1.2.1 GetIdentifierReference ( env, name, strict ).

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

Про Environment Record как список записей - согласен, звучит тупо... но как есть. Просто хотел в рамках спеки что-то понять и объяснить. А то мгновенно путаюсь.

Но я не увидел в спеке механизма "замыкание"

найдёте в спеке механизм абстрактных фабрик или интерфейсов? Конечно же нет. Это всё метапонятия из CS и не имеют отношения к непосредственно языку. Ровно по этой же причине в спеке не описываются функторы, монады, кариррование и прочее. Замыкания в этом же списке. Спецификация описывает возможности языка и как должно работать. Что из этого можно собрать никак не декларируется. Поэтому искать замыкания в спеке не имеет смысла.

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

откройте хотя бы ту же википедию и почитайте про замыкания. А пока у вас индукция с дедукцией путаются.

Про Environment Record как список записей 

ещё раз record - это одна запись, окружение - коллекция/список этих записей.

Спасибо за разъяснение. Понял, что для вас "замыкание" - это возможность языка, как и каррирование.

Но я всё равно не понимаю его реализацию в рамках спеки или V8. Каррирование можно разложить на части и их поведение найти в спеке. Вашу логику про "замыкание" отличное от "поиска" я не могу разложить. Не понимаю каким ещё образом можно найти переменную как не через "поиск".

Почитал определение замыкания в википедии. Оно очень размыто. Меня же интересовала реализация в JavaScript.

Разницу Environment и Environment Record я тоже не нашёл. 9.1 Environment Records описывает поведение так, будто Environment Record - это список переменных (bindings). Хоть, как я и говорил, это звучит тупо.

Не понимаю каким ещё образом можно найти переменную как не через "поиск".

писал же выше - можно скопировать её сразу в место использования, можно вставить ссылку в памяти, можно написать кучку вариантов поиска. Детали тут implementation defined.

Environment Record - это список переменных (bindings)

Складывается впечатление, что они там сами запутались, так что похоже вы правы. Либо у меня эффект Манделы отработал, ибо я был уверен, что в прошлый раз фразы типа `Every Environment Record has an [[OuterEnv]] field, ` была просто `Every Environment has`. Это не считая, что ещё есть host environment. Похоже тут вы правы - разницы действительно нет.

Отлично, что нашли что-то общее! Да, я увидел что от версии к версии спека может отличаться прилично. Смотрел на первую версию ради человеческих объяснений, потом только текущую версию смотрел.

"скопировать в место использования" - всё ещё не понимаю где абстракция, где спека. Я хотел разобраться, что за замыкание в JS, не узнать очередную интерпретацию спеки, а понять буквально её. Увидел только "поиск" переменной, без других вариантов нахождения переменных. Если вы про V8, то да, я не знаю как там JS работает.

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

 всё ещё не понимаю где абстракция, где спека

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

 Если вы про V8

Не только V8. Имплементаций на самом деле уже немало.

  • V8 - Node JS, Deno, Chromium based браузеры

  • Webkit - Safari и весь WebUI в яблочной экосистеме

  • Gecko - все деривативы Firefox

  • QuickJS

  • Bun.

Наверняка во всяких полиглот-виртуальных машинах тоже есть свой набор для компиляции JS.

Sign up to leave a comment.

Articles