Стек вызовов и лексическое окруж
1. Контекст выполнения и стек вызовов
При вызове функции внутри ее тела могут вызываться другие функции, а в них другие и т. д. JavaScript - однопоточный (в основном) язык, то есть в одну единицу времени может выполняться только одна операция.
Это значит, что уже вызванные функции, которые не закончили свое выполнение, должны ждать выполнения функций, вызванных внутри себя для того, чтобы продолжить свое выполнение. Соответственно, необходим механизм хранения списка функций, которые были вызваны, но еще не закончили свое выполнение и механизм управления порядком выполнения этих функций - и именно за это отвечает стек контекстов выполнения.
Стек — структура данных, которая работает по принципу последним пришёл — первым вышел (LIFO - Last In, First Out)
. Последнее, что вы добавили в стек, будет удалено из него первым, то есть можно добавить или удалить элементы только из верхушки стека.
Представьте стек как массив у которого есть только методы pop
и push
, то есть мы можем добавить или удалить только элемент в конце коллекции.
Контекст выполнения (execution context) - внутренняя конструкция языка для отслеживания выполнения функции, содержит метаинформацию о ее вызове.
Глобальный контекст выполнения (global execution context) - это контекст есть по умолчанию, сам файл скрипта - это функция, которая запускается на выполнение.
Контекст выполнения функции (functional execution context) - создается каждый раз при вызове функции.
Стек вызовов (Execution Context stack, call stack) - внутренняя конструкция движка, которая содержит все контексты выполнения.
Stack frame (кадр стека, запись стека) - структура которая добавляется на стек при вызове функции. Хранит некоторую метаинформацию: имя функции, аргументы, которые были переданы во время вызова и номер строки, в которой произошел вызов.
Глобальный контекст есть в стеке по умолчанию, создается на старте выполнения скрипта, обычно называется main
или global
и находится в его нижней части, то есть помещается туда первым. Если при выполнении кода происходит вызов функции, для нее создается новый контекст и добавляется на верх стека контекстов.
JS-движок исполняет функцию, контекст выполнения которой находится на самом верху стека. После того, как произойдет выход из функции (return
), ее контекст выполнения снимается со стека. Далее выполняться функция, контекст которой лежит следующим.
При выполнении этого кода сначала вызывается foo()
, затем внутри foo()
сначала вызывается bar()
, а затем baz()
. Вызовы console.log()
так же учитываются, ведь это функция. На иллюстрации ниже пошагово изображен стек вызовов примера.
2. Лексическое окружение
Лексическое окружение (lexical environment) — внутренняя, закрытая от прямого доступа структура движка для хранения в памяти таблицы (Environment Record
) идентификаторов переменных и их значений, а так же значение this
и механизм для извлечения этих значений при обращении по имени, а так же ссылки на родительское лексическое окружение (Parent
).
Каждый раз, когда вызывается функция и создается ее контекст выполнения, создается лексическое окружение привязанное к этому вызову. Если при вызове функции внутри нее объявляется еще одна функция, то в ее скрытое свойство (internal property или internal slot) [[Environment]]
записывается ссылка на это окружение.
Таким образом, каждая функция запоминает лексическое окружение того контекста исполнения в котором она была объявлена. Это позволяет поддерживать цепочку лексических окружений и поэтому работает цепочка областей видимости.
Лексическое окружение можно представить как словарь - набор пар ключ:значение
. Зная ключ можно получить значение. Точно так же, как и при работе с обычным бумажным словарем, где ключ — это слово, а значение — определение слова.
Все переменные внутри функции после объявления записываются в словарь.
В словаре хранится ссылка на внешнее лексическое окружение, то, в котором функция была объявлена.
Для глобального окружения ссылки на внешнее окружение нет, там
null
.Так же здесь хранится ссылка на контекст исполнения, но об этом расскажем позже.
В процессе выполнения функции, значения переменных могут меняться, что сразу же отражается в лексическом окружении. В конце выполнения функции её лексическое окружение уничтожается, а занятая им память высвобождается.
Поиск значения идентификатора начинается с локального окружения и если в нём не найден нужный идентификатор, то поиск идет дальше по цепочке окружений, вплоть до глобального.
В примере ниже комментариями показано состояние словаря перед выполнением каждой строки кода. Не забывайте, что создание и наполнение словаря происходит при вызове функции, а не при ее определении.
Last updated