Статическая цепочка областей действия JavaScript и «динамическая» цепочка закрытия

внешний интерфейс JavaScript
Статическая цепочка областей действия JavaScript и «динамическая» цепочка закрытия

Самая важная заключительная статья в Восточном полушарии, запечатанная ssh [съесть дыню].

Прочитав эту статью, вы получите ответы на следующие вопросы:

  • Разница между статической цепочкой областей видимости и динамической цепочкой областей видимости
  • Почему есть закрытия
  • Когда было создано замыкание?
  • Что такое атрибут [[scopes]]
  • Что хранит замыкание
  • Где хранятся замыкания
  • Почему eval производительность плохая
  • Когда eval создает замыкание

текст

В JavaScript функции, блоки и модули могут образовывать область видимости (независимое пространство для хранения переменных).Они могут быть вложены друг в друга, и между областями будет сформирована ссылочная связь.Эта цепочка называется цепочкой области видимости.

Что такое цепочка областей видимости?

цепочка статических областей видимости

такой как этот код

  function func() {
    const guang = 'guang';
    function func2() {
      const ssh = 'ssh';
      {
        function func3 () {
          const suzhe = 'suzhe';
        }
      }
    }
  }

Среди них 3 переменные guang, ssh, suzhe, 3 функции func, func2, func3 и блок, цепочку областей видимости между ними можно проверить с помощью babel.

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const code = `
  function func() {
    const guang = 'guang';
    function func2() {
      const ssh = 'ssh';
      {
        function func3 () {
          const suzhe = 'suzhe';
        }
      }
    }
  }
`;

const ast = parser.parse(code);

traverse(ast, {
  FunctionDeclaration (path) {
    if (path.get('id.name').node === 'func3') {
      console.log(path.scope.dump());
    }
  }
})

оказаться

Визуализируйте это так

image.png

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

Почему его называют «статическим»?

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

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

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

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

function func () {
  const a = 1;
  return function () {
    console.log(a);
  }
}
const f2 = func();

Это приводит к проблеме: изначально хорошо создавать и вызывать уровни функций последовательно, и создавать и уничтожать области действия последовательно — хорошо, но если внутренняя функция возвращается или открывается через другие, то внешняя функция уничтожается. и внутренняя функция уничтожается.Функция слоя не уничтожается.Как быть с областью видимости в это время, и родительская область не уничтожается? (Например, следует ли уничтожать область действия, когда здесь заканчивается вызов func)

Вызовы и закрытия функций не по порядку

Например, измените приведенный выше код, верните внутреннюю функцию, а затем вызовите ее снаружи:

function func() {
  const guang = 'guang';
  function func2() {
    const ssh = 'ssh';
    function func3 () {
      const suzhe = 'suzhe';
    }
    return func3;
  }
  return func2;
}

const func2 = func();

Когда вызывается функция func2, функция func1 уже была выполнена, будет ли пин в это время уничтожен? Так JavaScript разработал механизм замыканий.

Как проектировать затворы?

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

Прежде всего, следует ли уничтожить родительскую область? Можно ли не уничтожать родительскую область?

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

Как сделать подфункцию пакета и забрать?

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

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

Давайте поэкспериментируем со свойствами замыканий:

image.png

Нужно ли этой func3 что-то упаковывать? Будут ли закрытия?

image.png

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

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

image.png

Взгляните еще раз на [[Scopes]] (упакованная среда закрытия):

image.png

В это время есть два закрытия, почему? куда делся Суже?

В первую очередь нам нужно упаковать только то, чего нет в окружении, то есть замыкание сохраняет только внешние ссылки. Затем он сохраняется в атрибуте функции при создании функции, и созданная функция будет упакована в функцию, когда она вернется, но как движок JS узнает, какие внешние ссылки он использует? Механизмы JS будут выполнять ленивый синтаксический анализ, когда вы перейдете к функции синтаксического анализа в это время, вы также можете узнать, какие внешние ссылки она использует, а затем упаковать эти внешние использования в замыкания Closure и добавить их в [[scopes]].

так,Закрытие состоит в том, чтобы сканировать ссылку на идентификатор в функции при возврате функции, помещать переменные в используемую область видимости в пакет Closure и помещать их в [[Scopes]].

Таким образом, вышеприведенная функция будет сканировать идентификаторы в функции, когда func3 возвращается, сканировать guang и ssh, затем искать эти две переменные в цепочке областей видимости, фильтровать их и упаковывать в два замыкания (поскольку они принадлежат двум областям), поэтому генерируются два Closures), плюс самый внешний Global, установите атрибут [[scopes]] на функцию func3, чтобы ее можно было упаковать и забрать.

При вызове func3 движок JS возьмет упакованную цепочку Closure + Global в [[Scopes]] и установит ее как новую цепочку областей видимости. Это вся внешняя среда, используемая функцией. С внешней средой это возможно бежать.

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

image.png

Этот ход, к которому, очевидно, можно получить доступ, почему он не отображает информацию? Отладчик работает слишком плохо?

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

Так что нам нужно только посетить его, мы можем получить к нему доступ при отладке.

image.png

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

eval

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

Верно, eval действительно не может анализировать внешние ссылки, поэтому он не может упаковывать замыкания.Такая особая обработка, просто упаковать всю область видимости.

Подтвердите это:

image.png

Это, как упоминалось выше, упаковывает внешние ссылки в замыкания.

image.png

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

image.png

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

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

определить замыкание

Давайте определим замыкание, используя наш простой эксперимент:

Закрытие — это когда функция создается, функция упаковывается и удаляется, чтобы отфильтровать оставшуюся цепочку цепочки областей видимости в соответствии с внешней ссылкой внутри функции. Это подмножество цепочки областей видимости, созданной во время создания функции, и представляет собой упакованную внешнюю среду. Поскольку evel не может анализировать содержимое, прямой вызов упаковывает всю область видимости (поэтому старайтесь не использовать eval, так как в замыкании легко сохранить слишком много бесполезных переменных), а если не вызываться напрямую, замыкания не будет.

Правила фильтрации:

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

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

  3. Ссылочная область также отфильтровывает привязки (объявления переменных), на которые нет ссылок. Упаковывайте только те переменные, которые используются.

Недостатки закрытия

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

Эта конструкция решает проблему, но есть ли недостатки?

На самом деле проблема заключается в этом атрибуте [[Scopes]]

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

image.png

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

Итак, что произойдет, если подфункция вернется?

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

image.png

Когда родительская функция уничтожается, память, соответствующая кадру стека, немедленно освобождается, используемый ssh ​​Obj будет восстановлен gc, а возвращаемая функция отфильтрует используемые ссылки из цепочки областей видимости, чтобы сформировать цепочку закрытия и поместить их в куче. Это приводит к скрытой опасности: если на большой объект ссылается функция, он может быть уничтожен в конце вызова функции, но теперь ссылка сохраняется в кучу через замыкание и никогда не используется, то это куча памяти Это было непригодно для использования, даже если это серьезно в определенной степени, это утечка памяти. Так что не используйте замыкания без разбора и упаковывайте немного меньше вещей в память кучи.

Суммировать

Начали болтать со статическими скоупами, выясняя, что такое скоупы, статически анализировали скоупы через babel, узнавали про статические и динамические скоупы, а потом представили проблему, что дочерние функции уничтожаются раньше родительских, и подумали над этой схемой, а потом ввели концепцию замыкания, проанализировали процесс генерации замыкания и сохранили местоположение. Мы также использовали характеристики замыканий, чтобы проанализировать, почему иногда переменная информация не может быть просмотрена во время отладки, а затем проанализировали, почему eval не может точно генерировать замыкания, когда все области действия упакованы, когда замыкания не генерируются, и почему eval вызовет слишком много памяти. использование. После этого я проанализировал характеристики функций с замыканиями в памяти и объяснил, почему могут возникать утечки памяти.

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

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