Контекст выполнения и область видимости в JavaScript

Контекст выполнения (execution context)

Читайте также JavaScript: Ядро

Контекст выполнения и стек вызовов в JavaScript

Контекст выполнения (execution context) — это абстрактная концепция, в рамках которой спецификация языка Javascript, известная как ECMAScript, объясняет модель выполнения кода после запуска.

Виды контекста в JavaScript:

  1. глобальный контекст (с которого начинается исполнение скрипта);
  2. контекст выполнения вызова (начинается с момента входа в тело функции).

Контекст выполнения вызова

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

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

Один вызов функции имеет ровно один контекст выполнения, связанный с ним. При этом в  каждый момент времени в Javascript активен только один контекст выполнения.

Поэтому Javascript называют “однопоточным”, имея ввиду, что только одна инструкция исполняется в каждый момент времени (подробнее о работе движка JavaScript).

Браузеры отслеживают контекст выполнения с помощью стека.

Стек выполнения (стек вызовов, call stack) - это структура данных, которая используется для хранения контекстов выполнения, создаваемых в ходе работы кода. Стек выполнения действует по принципу “первый вошедший уходит последним” (Last In First Out, LIFO), то есть последний объект, добавленный на стек, окажется на его вершине и будет предоставлен первым при извлечении объекта. Добавлять объекты можно только на вершину стека, и удалять их можно только с вершины.

Активный контекст выполнения находится на вершине стека. Он снимается со стека, когда выполнение кода активного контекста завершается, и выполнение продолжается в коде предыдущего контекста, который перемещается на вершину стека.

ВАЖНО! Если в данный момент исполняется код из некоторого контекста выполнения, это ещё не значит, что он должен завершиться до запуска другого потока выполнения. Есть временные интервалы, на которых активный контекст засыпает, и активным становится другой контекст выполнения. Заснувший контекст выполнения может позднее проснуться на той точке, в которой он ранее заснул. 

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

Подробнее

  • Состояние вычисления (code evaluation state): всё состояние контекста, необходимое для выполнения, приостановки и возобновления оценки кода, связанного с этим контекстом выполнения.
  • Функция (function):
    1. объект функции, с вызовом которой связан контекст выполнения, либо
    2. null, если контекст выполнения связан со скриптом или модулем.
  • Область (realm): набор внутренних объектов, глобальное окружение ECMAScript и связанные с ним ресурсы, а также весь код на ECMAScript, который загружается в области видимости глобального окружения.
  • Скрипт или модуль (ScriptOrModule) - запись модуля или запись скрипта, из которой исходит связанный код. Если исходного скрипта или модуля нет, значение равно нулю.

[свернуть]

В процессе создания контекста выполняются три действия:

  1. Определяется значение this и осуществляется привязка this (this binding).
  2. Создаётся компонент LexicalEnvironment (лексическое окружение), которое содержит переменные и функции и используется для сопоставления идентификаторов (ссылок) внутри контекста выполнения (т.е. идентификаторы в лексическом окружении — это имена сущностей, таких как переменные и функции). 
  3. Создаётся компонент VariableEnvironment (окружение переменных) - таблица, связанная с лексическим окружением, в которой в качестве ключей занесены все имена переменных, используемые в инструкциях объявления переменных.

Замечание: технически и таблица переменных, и лексическое окружение нужны для реализации замыкания. 

Подробнее о лексическом окружении

Лексическое окружение (Lexical Environment)

Лексическое окружение — это область спецификации, описывающая связывание идентификаторов с переменными и функциями, основанная на лексической вложенности кода, написанного на ECMAScript (JavaScript). Лексическое окружение состоит из таблицы символов и ссылки на внешнее лексическое окружение (нулевой для глобального окружения, т.к. глобальное окружение не имеет внешнего окружения).

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

  • глобальное окружение, или
  • окружение модуля (содержащее привязки имён к символам, объявленным в этом модуле), или
  • окружение функции (созданное в процессе её вызова).

Обычно лексическое окружение связано с синтаксической конструкцией в коде на ECMAScript, такой как объявление функции, блок кода (окружённый фигурными скобками), или блок catch в try-инструкции.

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

[свернуть]

Контекст выполнения функций

Когда функция производит вызов вложенной функции, происходит следующее:

  1. выполнение текущей функции приостанавливается;
  2. контекст выполнения, связанный с ней, запоминается в специальной структуре данных – стеке контекстов выполнения.
  3. выполняются вложенные вызовы, для каждого из которых создаётся свой контекст выполнения;
  4. после завершения вложенных функций предыдущий контекст достаётся из стека, и выполнение внешней функции возобновляется с того места, где она была остановлена.
Рисунок "Контексты выполнения функций"

Контексты выполнения функций

Когда функция createGenerator() производит вызов вложенной функции genNewID(), происходит следующее:

  1. выполнение текущей функции createGenerator() приостанавливается;
  2. контекст выполнения, связанный с ней, запоминается в специальной структуре данных – стеке контекстов выполнения.
  3. выполняются вложенные вызовы функции genNewID(), для каждого из них создаётся свой контекст выполнения;
  4. после завершения вложенной функции предыдущий контекст достаётся из стека, и выполнение внешней функции возобновляется с того места, где она была остановлена.

[свернуть]

Область видимости (Scope)

Область видимости (Scope) - это текущий контекст выполнения, в котором значения и выражения являются "видимыми" или на которые можно ссылаться; это объединённое множество идентификаторов, доступ к которым имеет текущий контекст. 

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

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

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

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

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

Подробнее о динамической области видимости

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

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

[свернуть]

Поиск в области видимости прекращается как только движок находит первое совпадение с идентификатором. Одно и то же имя идентификатора может быть указано в нескольких слоях вложенных областей видимости, что называется "затенение (shadowing)" (внутренний идентификатор "затеняет (shadows)" внешний). Независимо от затенения, поиск в области видимости всегда начинается с самой внутренней области видимости, исполняющейся в данный момент и работает таким путем по направлению наружу/вверх пока не найдется первое совпадение и тогда останавливается (подробнее...).

Глобальные переменные также автоматически являются свойствами глобального объекта (window в браузерах и т.п.), поэтому можно ссылаться на глобальную переменную не прямо по ее лексическому имени, а вместо этого косвенно использовать ссылку на свойство глобального объекта.

Процесс поиска в лексической области видимости применяется только к идентификаторам первого класса, таким как a:

Если у вас есть ссылка на foo.bar.baz в строке кода, как только будет найдена переменная foo, на смену приходят правила доступа к свойствам объекта, чтобы разрешить имена свойств bar и baz, соответственно.

ВАЖНО! Два механизма в JavaScript могут "обмануть" лексическую область видимости: eval(..) и with. Первый может менять существующую лексическую область видимости (во время выполнения) исполняя строку "кода", в которой есть одно или несколько объявлений. Второй по сути создает целую новую лексическую область видимости (снова во время выполнения), интерпретируя ссылку на объект как "область видимости", а свойства этого объекта как идентификаторы этой области. Недостаток этих механизмов в том, что код будет выполняться медленнее в результате использования любой из этих возможностей. Не используйте их! (подробнее...)

Функции как области видимости

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

Область видимости функции в JavaScript:

  • функция, в которой она определена;
  • вся программа, если функция объявлена на верхнем (глобальном) уровне.

Например:

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

Примеры видимости переменных внутри функции

[свернуть]
Примеры

Первый вариант записи кода не очень идеален:

  1. мы должны объявить именованную функцию foo(), которая означает, что само имя идентификатора foo "засоряет" окружающую область видимости (в данном случае - глобальную);
  2. мы должны явно вызвать функцию по имени ( foo() ) для того чтобы обернутый код выполнился.

Решением проблемы является вариант 2:

  • оператор окружающей функции начинается с (function... в противоположность простому function.... , т.е. функция трактуется как функциональное выражение (function expression);
  • имя foo не связано с окружающей областью видимости, а связано только со своей собственной функцией.

Иными словами, (function foo(){ .. }) как выражение означает, что идентификатор foo может быть найден только в области видимости, которая обозначена { .. }, но не во внешней области видимости. Сокрытие имени foo внутри себя означает, что оно не будет неоправданно загрязнять окружающую область видимости.

Примечание: Самый легкий путь отличить объявление от выражения — позиция слова "function" в операторе (не только строка, но и отдельный оператор). Если "function" — самое первое, что стоит в операторе, то это объявление функции. Иначе, это функциональное выражение.

[свернуть]

Добавить комментарий

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