Контекст выполнения и стек вызовов в JavaScript | by Nikita | WebbDEV
Published in·
7 min read·
Nov 14, 2018Если вы — JavaScript-разработчик или хотите им стать, это значит, что вам нужно разбираться во внутренних механизмах выполнения JS-кода. В частности, понимание того, что такое контекст выполнения и стек вызовов, совершенно необходимо для освоения других концепций JavaScript, таких, как поднятие переменных, области видимости, замыкания. Материал посвящён контексту выполнения и стеку вызовов в JavaScript.
Контекст выполнения (execution context) — это, если говорить упрощённо, концепция, описывающая окружение, в котором производится выполнение кода на JavaScript. Код всегда выполняется внутри некоего контекста.
В JavaScript существует три типа контекстов выполнения:
- Глобальный контекст выполнения. Это базовый, используемый по умолчанию контекст выполнения. Если некий код находится не внутри какой-нибудь функции, значит этот код принадлежит глобальному контексту. Глобальный контекст характеризуется наличием глобального объекта, которым, в случае с браузером, является объект
window
, и тем, что ключевое словоthis
указывает на этот глобальный объект. В программе может быть лишь один глобальный контекст. - Контекст выполнения функции. Каждый раз, когда вызывается функция, для неё создаётся новый контекст. Каждая функция имеет собственный контекст выполнения. В программе может одновременно присутствовать множество контекстов выполнения функций. При создании нового контекста выполнения функции он проходит через определённую последовательность шагов, о которой мы поговорим ниже.
- Контекст выполнения функции
eval
. Код, выполняемый внутри функцииeval
, также имеет собственный контекст выполнения. Однако функциейeval
пользуются очень редко, поэтому здесь мы об этом контексте выполнения говорить не будем.
Стек выполнения (execution stack), который ещё называют стеком вызовов (call stack), это LIFO-стек, который используется для хранения контекстов выполнения, создаваемых в ходе работы кода.
Когда JS-движок начинает обрабатывать скрипт, движок создаёт глобальный контекст выполнения и помещает его в текущий стек. При обнаружении команды вызова функции движок создаёт новый контекст выполнения для этой функции и помещает его в верхнюю часть стека.
Движок выполняет функцию, контекст выполнения которой находится в верхней части стека. Когда работа функции завершается, её контекст извлекается из стека и управление передаётся тому контексту, который находится в предыдущем элементе стека.
Изучим эту идею с помощью следующего примера:
Вот как будет меняться стек вызовов при выполнении этого кода.
Состояние стека вызововКогда вышеприведённый код загружается в браузер, JavaScript-движок создаёт глобальный контекст выполнения и помещает его в текущий стек вызовов. При выполнении вызова функции first()
движок создаёт для этой функции новый контекст и помещает его в верхнюю часть стека.
При вызове функции second()
из функции first()
для этой функции создаётся новый контекст выполнения и так же помещается в стек. После того, как функция second()
завершает работу, её контекст извлекается из стека и управление передаётся контексту выполнения, находящемуся в стеке под ним, то есть, контексту функции first()
.
Когда функция first()
завершает работу, её контекст извлекается из стека и управление передаётся глобальному контексту. После того, как весь код оказывается выполненным, движок извлекает глобальный контекст выполнения из текущего стека.
До сих пор мы говорили о том, как JS-движок управляет контекстами выполнения. Теперь поговорим о том, как контексты выполнения создаются, и о том, что с ними происходит после создания. В частности, речь идёт о стадии создания контекста выполнения и о стадии выполнения кода.
Стадия создания контекста выполнения
Перед выполнением JavaScript-кода создаётся контекст выполнения. В процессе его создания выполняются три действия:
- Определяется значение
this
и осуществляется привязкаthis
(this binding). - Создаётся компонент
LexicalEnvironment
(лексическое окружение). - Создаётся компонент
VariableEnvironment
(окружение переменных).
Концептуально контекст выполнения можно представить так:
Привязка this
В глобальном контексте выполнения this
содержит ссылку на глобальный объект (как уже было сказано, в браузере это объект window
).
В контексте выполнения функции значение this
зависит от того, как именно была вызвана функция. Если она вызвана в виде метода объекта, тогда значение this
привязано к этому объекту. В других случаях this
привязывается к глобальному объекту или устанавливается в undefined
(в строгом режиме). Рассмотрим пример:
Лексическое окружение
В соответствии со спецификацией ES6, лексическое окружение (Lexical Environment) — это термин, который используется для определения связи между идентификаторами и отдельными переменными и функциями на основе структуры лексической вложенности ECMAScript-кода. Лексическое окружение состоит из записи окружения (Environment Record) и ссылки на внешнее лексическое окружение, которая может принимать значение null
.
Проще говоря, лексическое окружение — это структура, которая хранит сведения о соответствии идентификаторов и переменных. Под «идентификатором» здесь понимается имя переменной или функции, а под «переменной» — ссылка на конкретный объект (в том числе — на функцию) или примитивное значение.
В лексическом окружении имеется два компонента:
- Запись окружения. Это место, где хранятся объявления переменных и функций.
- Ссылка на внешнее окружение. Наличие такой ссылки говорит о том, что у лексического окружения есть доступ к родительскому лексическому окружению (области видимости).
Существует два типа лексических окружений:
- Глобальное окружение (или глобальный контекст выполнения) — это лексическое окружение, у которого нет внешнего окружения. Ссылка глобального окружения на внешнее окружение представлена значением
null
. В глобальном окружении (в записи окружения) доступны встроенные сущности языка (такие, какObject
,Array
, и так далее), которые связаны с глобальным объектом, там же находятся и глобальные переменные, определённые пользователем. Значениеthis
в этом окружении указывает на глобальный объект. - Окружение функции, в котором, в записи окружения, хранятся переменные, объявленные пользователем. Ссылка на внешнее окружение может указывать как на глобальный объект, так и на внешнюю по отношении к рассматриваемой функции функцию.
Существует два типа записей окружения:
- Декларативная запись окружения, которая хранит переменные, функции и параметры.
- Объектная запись окружения, которая используется для хранения сведений о переменных и функциях в глобальном контексте.
В результате, в глобальном окружении запись окружения представлена объектной записью окружения, а в окружении функции — декларативной записью окружения.
Обратите внимание на то, что в окружении функции декларативная запись окружения, кроме того, содержит объект arguments
, который хранит соответствия между индексами и значениями аргументов, переданных функции, и сведения о количестве таких аргументов.
Лексическое окружение можно представить в виде следующего псевдокода:
Окружение переменных
Окружение переменных (Variable Environment) — это тоже лексическое окружение, запись окружения которого хранит привязки, созданные посредством команд объявления переменных (VariableStatement
) в текущем контексте выполнения.
Так как окружение переменных также является лексическим окружением, оно обладает всеми вышеописанными свойствами лексического окружения.
В ES6 существует одно различие между компонентами LexicalEnvironment
и VariableEnvironment
. Оно заключается в том, что первое используется для хранения объявлений функций и переменных, объявленных с помощью ключевых слов let
и const
, а второе — только для хранения привязок переменных, объявленных с использованием ключевого слова var
.
Рассмотрим примеры, иллюстрирующие то, что мы только что обсудили:
Схематичное представление контекста выполнения для этого кода будет выглядеть так:
Как вы, вероятно, заметили, переменные и константы, объявленные с помощью ключевых слов let
и const
, не имеют связанных с ними значений, а переменным, объявленным с помощью ключевого слова var
, назначено значение undefined
.
Это так из-за того, что во время создания контекста в коде осуществляется поиск объявлений переменных и функций, при этом объявления функций целиком хранятся в окружении. Значения переменных, при использовании var
, устанавливаются в undefined
, а при использовании let
или const
остаются неинициализированными.
Именно поэтому можно получить доступ к переменным, объявленным с помощью var
, до их объявления (хотя они и будут иметь значение undefined
), но, при попытке доступа к переменным или константам, объявленным с помощью let
и const
, выполняемой до их объявления, возникает ошибка.
То что мы только что описали, называется «поднятием переменных» (Hoisting). Объявления переменных «поднимаются» в верхнюю часть их лексической области видимости до выполнения операций присвоения им каких-либо значений.
Стадия выполнения кода
Это, пожалуй, самая простая часть данного материала. На этой стадии выполняется присвоение значений переменным и осуществляется выполнение кода.
Обратите внимание на то, что если в процессе выполнения кода JS-движок не сможет найти в месте объявления значение переменной, объявленной с помощью ключевого слова let
, он присвоит этой переменной значение undefined
.
Только что мы обсудили внутренние механизмы выполнения JavaScript-кода. Хотя для того, чтобы быть очень хорошим JS-разработчиком, знать всё это и не обязательно, если у вас имеется некоторое понимание вышеописанных концепций, это поможет вам лучше и глубже разобраться с другими механизмами языка, с такими, как поднятие переменных, области видимости, замыкания.
Перевод статьи Understanding Execution Context and Execution Stack in Javascript
Просмотр стека вызовов в отладчике — Visual Studio (Windows)
- Статья
Область применения:Visual StudioVisual Studio для Mac Visual Studio Code
С помощью окна Стек вызовов можно просматривать вызовы функций и процедур, которые в данный момент находятся в стеке. В окне Стек вызовов показан порядок вызова методов и функций. Стек вызовов хорошо подходит для изучения и анализа потока выполнения приложения.
Если символы отладки недоступны для части стека вызовов, в окне Стек вызовов может не получиться отобразить правильные сведения об этой части стека вызовов.
[Frames below may be incorrect and/or missing, no symbols loaded for name.dll]
Примечание
Отображаемые диалоговые окна и команды меню могут отличаться от описанных здесь в зависимости от текущих параметров или выпуска. Чтобы изменить параметры, выберите в меню Сервис пункт Импорт и экспорт параметров. См. раздел Сброс параметров. Окно Стек вызовов аналогично перспективе «Отладка» в некоторых интегрированных средах разработки, например Eclipse.
Просмотр стека вызовов в отладчике
Во время отладки в меню Отладка выберите Окна > Стек вызовов или нажмите клавиши ctrl
+alt
+C
.
Стрелка идентифицирует кадр стека, в котором в данный момент находится указатель выполнения. По умолчанию это кадр стека, сведения которого отображаются в окнах: исходного кода, Локальные, Контрольные значения, Видимые и Дизассемблированный код. Чтобы изменить контекст отладчика на другой кадр стека, переключитесь на другой кадр стека.
Желтая стрелка указывает на кадр стека, в котором находится указатель выполнения. По умолчанию это кадр стека, сведения которого отображаются в окнах: исходного кода, Локальные, Контрольные значения, Видимые и Дизассемблированный код. Чтобы изменить контекст отладчика на другой кадр стека, переключитесь на другой кадр стека.
Вы также можете просматривать кадры стека исключений в стеке вызовов во время отладки. Дополнительные сведения см. в разделе Просмотр стека вызовов во вспомогательной службе исключений.
Отображение непользовательского кода в окне «Стек вызовов»
Чтобы отобразить внешний или непользовательский код, щелкните правой кнопкой мыши элемент Показать внешний код в окне Стек вызовов и выберите Показать внешний код.
Чтобы отобразить внешний или непользовательский код, переключите кнопку Показать внешний код на панели инструментов стека вызовов или щелкните правой кнопкой мыши окно Стек вызовов и выберите пункт Показать внешний код.
Непользовательский код — это любой код, который не отображается при включении режима Только мой код. В управляемом коде кадры непользовательского кода скрыты по умолчанию. Вместо кадров непользовательского кода отображается следующая запись.
[<External Code>]
Переключение на другой кадр стека (изменение контекста отладчика)
В окне Стек вызовов щелкните правой кнопкой кадр стека, код и данные которого нужно просмотреть.
Или можно дважды щелкнуть кадр в окне Стек вызовов, чтобы переключиться на этот кадр.
Выберите пункт Перейти к кадру.
Рядом с выбранным кадром стека появится зеленая стрелка с фигурным концом. Указатель выполнения остается в исходном кадре, который по-прежнему отмечен желтой стрелкой. При выборе команд Шаг или Продолжить в меню Отладка выполнение продолжится с исходного, а не с выбранного кадра.
Нажмите кнопку Просмотреть все потоки , чтобы просмотреть все связанные потоки в окне Параллельный стек.
Поиск стека вызовов
Вы можете искать соответствующие кадры стека вызовов, введя соответствующие условия поиска в поле поиска, расположенном в левом верхнем углу окна стека вызовов. Будут выделены соответствующие кадры стека вызовов.
Просмотр исходного кода функции в стеке вызовов
В окне Стек вызовов щелкните правой кнопкой мыши функцию, исходный код которой нужно увидеть, и выберите пункт К исходному коду.
Выполнение кода до определенной функции из окна «Стек вызовов»
В окне Стек вызовов выберите функцию, щелкните ее правой кнопкой мыши и выберите команду Выполнить до курсора.
Установка точки останова в точке выхода вызова функции
См. раздел Установка точки останова в функции стека вызовов.
Отображение вызовов в другой поток или из него
Щелкните правой кнопкой мыши в окне Стек вызовов и выберите пункт Включить вызовы между потоками.
Визуальная трассировка стека вызовов
В Visual Studio Enterprise (только) можно просматривать карты кода для стека вызовов во время отладки.
В окне Стек вызовов откройте контекстное меню. Выберите Показать стек вызовов на карте кода (CTRL + SHIFT + ` ).
Дополнительные сведения см. в статье Создание визуальной карты стека вызовов при отладке.
Просмотр дизассемблированного кода функции в стеке вызовов (C#, C++, Visual Basic, F#)
В окне Стек вызовов щелкните правой кнопкой мыши функцию, дизассемблированный код которой нужно увидеть, и выберите пункт К дизассемблированному коду.
Щелкните правой кнопкой мыши в окне Стек вызовов и установите или снимите флажок Показывать <нужные сведения> .
Загрузка символов для модуля (C#, C++, Visual Basic, F#)
В окне Стек вызовов можно загружать отладочные символы для кода, для которого символы сейчас не загружены. Это могут быть символы .NET или системные символы, скачанные с общедоступных серверов Майкрософт, либо символы из каталога на компьютере, где идет отладка.
См. статью Указание файлов символов (.pdb) и файлов с исходным кодом в отладчике Visual Studio.
Чтобы загрузить символы
В окне Стек вызовов щелкните правой кнопкой какой-либо кадр стека, для которого не загружены символы. Кадр затеняется.
Укажите на параметр Загрузить символы, а затем выберите Серверы символов (Майкрософт) (если доступно) или перейдите по пути к символам.
Установка пути к символам
В окне Стек вызовов выберите пункт Параметры символов из контекстного меню.
Появится диалоговое окно Параметры, открытое на странице Символы.
Выберите
В диалоговом окне Параметры щелкните значок «Папка».
В поле Места размещения файлов символов (.pdb) появится курсор.
Введите путь к каталогу с символами на компьютере, на котором производится отладка. При локальной и удаленной отладке это путь на локальном компьютере.
Нажмите кнопку OK, чтобы закрыть диалоговое окно Параметры.
См. также
- Смешанный код и отсутствующие данные в окне стека вызовов
- Указание файлов символов (PDB) и файлов с исходным кодом
- Использование точек останова
Стеки вызовов JavaScript: Введение
Я хорошо знаком со стеками… блинов — короткими стеками, высокими стеками, всеми стеками. Так что блины — это, естественно, первое, что приходит на ум, когда я слышу слово «стопка». К счастью, это подходящая визуальная метафора для программного стека.
Что такое стек?
Программные стеки — это тип данных, аналогичный массиву, который ведет себя как набор элементов. В этом визуальном примере каждый блин представляет собой какой-то элемент, который мы добавили на вершину стека; мы можем удалить каждый элемент только сверху. Из-за этой линейной структуры стеки также называются LIFO или последним пришел, первым вышел.
Стеки выполняют две основные операции: вталкивание и извлечение. Если вы знакомы с массивами, вам будет приятно узнать, что эти методы — именно то, что вы думаете: Push добавляет элемент на вершину стека, а pop удаляет из вершины последний добавленный элемент. В отличие от массива (или стопки блинов), невозможно получить доступ к элементу снизу или посередине, не удалив сначала те, что находятся сверху.
Что такое стек вызовов JS?
Стек вызовов JavaScript является примером структуры данных стека. В стеке вызовов JS элементами являются вызовы функций. Каждый раз, когда вы вызываете функцию, она добавляется в стек. Если у него есть вложенная функция, эта вложенная функция также будет добавлена на вершину стека и так далее. Вы можете думать о стеке вызовов как о своего рода списке дел для JavaScript.
Дополнительные разъяснения от JuliaCreate Приложение React и TypeScript — краткое руководство
Стеки и стек вызовов JS
Стек вызовов JavaScript является примером структуры данных стека. В стеке вызовов JS элементами являются вызовы функций. Каждый раз, когда вы вызываете функцию, она добавляется в стек. Если у него есть вложенная функция, эта вложенная функция также будет добавлена на вершину стека и так далее. Вы можете думать о стеке вызовов как о своего рода списке дел для JavasScript.
Важно помнить, что JavaScript в основном представляет собой однопоточный процесс, то есть JavaScript обрабатывает одну команду за раз. Асинхронные действия обрабатываются по-другому, и я настоятельно рекомендую это видео, если вы хотите узнать об этом больше.
Как только вы помещаете все функции, которые должны выполняться в заданном скрипте, на вершину стека в порядке вызова, JavaScript начинает обрабатывать их в порядке сверху вниз, извлекая их из стека.
В этом видео вы можете видеть, что функция printSquare
вызывается внизу экрана. Функция printSquare
добавляется в стек вызовов, но она не может быть разрешена, пока не получит результат своей вложенной функции с именем Square
. Квадратная функция теперь добавлена в стек вызовов, но она не может быть разрешена, пока не получит возврат своей вложенной функции , называемой , умноженной на
. Функция умножить
помещается на вершину стека, и, поскольку у нее есть оператор return, который может быть возвращен сразу же, она продолжается, завершается и выталкивается из вершины стека. Стек вызовов продолжается таким же образом, извлекая функции в обратном порядке, в котором они были вызваны.
Работа над этой статьей была чем-то вроде управления моим собственным стеком вызовов. Когда я начинал одно направление исследований, оно неизбежно поднимало вложенный вопрос, который мне нужно было добавить в начало моего собственного списка дел, чтобы понять, прежде чем продолжить; во время исследования этого второго вопроса я наткнулся на другую тему, которую я добавил в начало своего списка, прежде чем продолжить предыдущую тему, и так далее, несколько раз. Решение самого последнего вопроса, который я добавил, позволило мне удалить его из верхней части списка и продолжить со следующим вопросом внизу, пока я не вернусь на первый уровень, готовый написать этот блог!
Получите больше от разработчиков встроенного программного обеспеченияЧто такое символ @ в Python и как его использовать?
Практика работы со стеками
Браузер Chrome имеет действительно надежную панель инструментов разработчика. Одна из вещей, которые вы можете сделать с панелью, — это пройтись по коду строка за строкой и увидеть, как он помещается в стек. Чтобы открыть его, щелкните правой кнопкой мыши (или введите Cmd + option + j
) на любой веб-странице в Chrome, выберите параметр «Проверка» и вкладку «Источники» вверху. Я сделал небольшое видео с демонстрацией вызова стека с использованием инструментов точки останова и пошагового выполнения.
Я добавляю точку останова к вызову функции aDayInTheLife
, которая приостанавливает код и позволяет нам пройти через каждую каскадную функцию. Как видите, функции добавляются в стек снизу вверх, а затем разрешаются сверху вниз по мере того, как функции получают свои возвращаемые значения и разрешаются. В конечном счете, строка «Сделал шину ровной за секунды» передается обратно в исходную функцию aDayInTheLife
и возвращается в консоль в самом конце после очистки стека.
Произошла ошибка.
Невозможно выполнить JavaScript. Попробуйте посмотреть это видео на сайте www.youtube.com или включите JavaScript, если он отключен в вашем браузере.
Стек вызовов JavaScript, объяснениеЧто такое переполнение стека?
Основная предпосылка заключается в том, что память компьютера конечна, и если в стек помещается больше элементов, чем есть свободного места, стек переполняется. Наиболее распространенной причиной ситуации переполнения стека является бесконечная или очень глубокая рекурсия. Рекурсивная функция — это функция, которая вызывает сама себя — или представьте две функции, которые вызывают друг друга. Первая функция добавляет вторую функцию в стек вызовов, вторая функция добавляет первую функцию в стек вызовов и так далее. Поскольку ничего не возвращается и не разрешается, стек переполнится.
Две функции, вызывающие друг друга, приведут к переполнению стека.
Как видите, инструмент разработчика Chrome имеет для этого обработку ошибок и выдает ошибку после 16 000 кадров (элементов стека вызовов).
The Beatles прощаются навсегда — обработка ошибок не требуется.
Я оставлю вас с забавным фактом: Stack Overflow , веб-сайт , получил свое название в результате опроса Coding Horror 2008 года, проведенного создателями, которые искали помощи в названии своего веб-сайта. Среди других вариантов были humbledeveloper.com и writeoncereadmany.com. 25 процентов из почти 7000 голосов достались stackoverflow.com, что сделало его победителем, и я, например, думаю, что они выбрали лучший.
Итак… кто голоден?
Объяснение стека вызовов JavaScript.
Введение в вызов JavaScript… | Эллен ПаркВведение в стек вызовов, цикл событий и очередь событий в JavaScript Келли Борк на Unsplash
Звонок stack — это механизм, с помощью которого интерпретатор (например, интерпретатор JavaScript в веб-браузере) отслеживает свое место в сценарии, вызывающем несколько функций — какая функция выполняется в данный момент, какие функции вызываются из этой функции и т. д. — Веб-документы MDN
Стек вызовов отслеживает выполняемые функции. Когда мы вызываем функцию, она добавляется, или перемещает на вершину стека вызовов. Когда функция возвращается, она удаляется, или извлекает из стека вызовов. Любые асинхронные функции (
, setTimeout
, async
и т. д.) перемещаются в очередь событий (подробнее об этом позже).
Скорее всего, вы видели стек вызовов в своей консоли, когда возникает ошибка.
На изображении выше видно текущее состояние стека вызовов (a, b, c) и время возникновения ошибки.
Возможно, вы слышали, что JavaScript является однопоточным. Это означает, что у него есть только один стек вызовов, и он может обрабатывать только один оператор за раз. Стек вызовов следует принципу LIFO (последний вошел, первый обслужен), что означает, что он всегда будет сначала обрабатывать вызов на вершине стека.
При вызове функции она добавляется в стек. Когда функция вызывает еще , она добавляется поверх вызывающей функции.
Учитывая приведенный выше блок кода, мы можем утверждать, что:
- Сначала вызывается
sayHi
, а добавляет в стек вызовов (sayHi
еще не разрешен). -
sayHi
затем вызываетsayBye
и добавляетsayBye
в стек вызовов (обе функции все еще не разрешены). - На данный момент состояние стека вызовов в настоящее время:
4.
sayBye
теперь находится на вершине стека вызовов. Он выполнит и выведет на консоль «Пока». Затем он удаляется из вершины стека вызовов. 5. Теперь sayHi
находится на вершине стека вызовов. Он выведет «Привет» на консоль, а затем будет удален из стека вызовов. Стек вызовов теперь пуст.
6. Окончательный вывод:
Когда вызывается асинхронная функция, она не попадает в стек вызовов. Вместо этого он перенаправляется на очередь событий . Соединение между стеком вызовов и очередью событий обеспечивается циклом событий . Цикл событий постоянно проверяет стек вызовов. Если стек вызовов пуст, он добавляет первую функцию в очереди событий в стек вызовов для выполнения. В противном случае он продолжает обработку кода в стеке вызовов.
Очередь событий — это место, где асинхронный код ожидает выполнения. В отличие от стека вызовов, очередь событий следует принципу FIFO (первым пришел, первым обслужен), что означает, что она всегда будет обрабатывать вызовы в том же порядке, в котором они были добавлены в очередь.
Важно отметить, что очередь событий начнет выполняться только тогда, когда стек вызовов пуст. Если стек вызовов уже обрабатывает код, цикл событий не добавит никаких функций из очереди событий. Цикл обработки событий не возобновится, пока стек вызовов не будет очищен.
Взгляните на следующий код.
Как и ожидалось, приведенный выше код немедленно выведет в консоль «foo»
. Затем, через 1 секунду, он выведет баров
.
Как насчет приведенного ниже кода?
Здесь setTimeout
устанавливается на 0
. Из-за этого вы можете подумать, что bar()
выполнится немедленно, в результате чего «bar»
будет напечатано до «foo»
. Но это не так. Оба фрагмента кода приведут к следующему результату:
Хотя
имеет значение 0
, он все равно перенаправляется в очередь событий. Это означает, что он не может выполняться до тех пор, пока стек вызовов не будет полностью очищен.