Контекст выполнения и стек вызовов в JavaScript
Если вы — 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-разработчиком, знать всё это и не обязательно, если у вас имеется некоторое понимание вышеописанных концепций, это поможет вам лучше и глубже разобраться с другими механизмами языка, с такими, как поднятие переменных, области видимости, замыкания.
Уважаемые читатели! Как вы думаете, о чём ещё, помимо контекста выполнения и стека вызовов, полезно знать JavaScript-разработчикам?
Область видимости и контекст в JavaScript
Область видимости – это некоторая сущность JavaScript, которая определяет границы действия переменных.
Создаются области видимости во время выполнения программы . Самая первая область, которая создаётся и которая включает в себя все остальные называется глобальной .
Именно в этой области определены такие переменные как window в веб-браузере и global в Node.js.
Вы также можете определять переменные в этой области. Для этого достаточно просто объявить переменные вне блока, функции и модуля. В этом случае они будут находиться в глобальной области видимости:
Переменные объявленные в глобальной области видимости называются глобальными переменными. Такие переменные могут быть доступны в любой точке программы.
Кроме глобальной области видимости в JavaScript имеются ещё локальные . Они, создаются, когда интерпретатор, например, выполняет код блочной конструкции :
Причем такая локальная область видимости называется областью видимости блока .
Переменные, объявленные внутри блока с помощью let и const имеют область видимости ограниченную этим блоком. Т.е. они привязаны к нему и будет действовать только в его рамках. Переменные, объявленные в локальной области видимости называются локальными.
Под блоком в JavaScript понимается любой код, который расположен в фигурных скобках { . } . Блоки используются в конструкциях if , for , while и т.д. Даже тело функции является блоком, т.к. находится между фигурными скобками.
Кроме этого локальные области видимости также создаются вызовами функций и модулями. Они соответственно называются областью видимости функции и областью видимости модуля .
Пример, в котором создаётся две функциональные области видимости:
В этом примере в глобальной области видимости объявляется функция salute с помощью ключевого слова function . Затем эта функция вызывается два раза.
Область видимости функции создаётся для каждого вызова функции. Даже, когда мы вызываем одну и ту же функцию. При этом для каждого вызова создаётся своя отдельная область видимости.
В этом примере будут созданы две локальные области видимости уровня функции.
Цепочка областей видимости
При создании локальной области видимости она всегда сохраняет ссылку на внешнюю область видимости. Эта ссылка используется для поиска переменных.
В момент выполнения console.log(a) мы имеем следующую картину:
Начинается этот код с создания переменных a , b , c и fnA с соответствующими значениями в глобальной области видимости.
После этого вызывается функция fnA() . При её вызове создаётся область видимости функции, которая имеет ссылку на внешнюю область. В данном случае ей является глобальная область видимости.
Далее переменной a присваивается значение 7 .
Но перед тем, как присвоить ей значение, нам необходимо сначала её найти. Поиск переменной всегда начинается с текущей области видимости. Но переменной и параметра a в области видимости вызова функции fnA() нет. Поэтому мы переходим по ссылке, в данном случае ведущую в глобальную область видимости и ищем переменную там. В данном примере такая переменная здесь имеется, и мы присваиваем ей значение 7 . Таким образом, на этом шаге мы внутри функции fnA присвоили новое значение глобальной переменной a .
На следующей строчке мы присваиваем переменной b значение 10 :
На текущий момент у нас ещё нет объявленной переменной b b в текущей области видимости. Поэтому мы также переходим по ссылке в глобальную область видимости и находим эту переменную там. После этого задаём ей новое значение. На этом этапе выполнения кода у нас в глобальной области видимости переменные a и b имеют соответственно значения 7 и 10.
Затем мы объявляем переменную b в локальной области функции, созданной вызовом fnA() и в этом же выражении сразу же ей присваиваем число 11 :
Несмотря на то, что переменная b есть в глобальной области видимости, мы можем создавать переменные с таким же именем в локальных областях видимости. После этого действия переменная b , созданная в области видимости функции будет пересекаться с переменной b , объявленной в глобальной области видимости, т.к. они имеют одинаковые имена.
Теперь, если мы попытаемся в этой области видимости получить доступ к переменной b , то получим переменную, объявленную в этой локальной области видимости, но никак не переменную b из глобальной области видимости. Т.е. после объявления b в этой области видимости нам уже будет не доступна переменная b , объявленная в глобальной области видимости.
Таким образом, на следующей строчке будет использоваться переменная b , объявленная в текущей области видимости:
После этого объявляется функция fnB . Затем она вызывается fnB() и интерпретатор создаёт новую область видимости внутри fnA . Эта область видимости в свою очередь тоже содержит ссылку на внешнюю по отношению к ней область видимости. В данном случае, на ту, которая была создана ранее при вызове fnA() . В итоге у нас получается цепочка областей видимости.
Таким образом, цепочкой областей видимости (scope chain) можно назвать последовательность областей видимости, которые интерпретатор JavaScript использует для поиска переменных . При этом поиск всегда начинается с текущей области видимости и если только она не найдена в текущей, то происходит переход к следующей по цепочке и поиск переменной там и т.д.
На строчке console.log(a) для вывода значения указанной переменной в консоль, её сначала нужно получить. Поиск переменной, как мы уже отмечали выше, всегда начинается с текущей области видимости. Но так как этой переменной здесь нет, то выполняется переход по ссылке к следующей области, которая является по отношению к текущей внешней.
В этой области (в данном случае созданной в результате вызова fnA() ) переменной a тоже нет. Но есть ссылка на следующую область, которая в данном случае является глобальной. Переходим по ней и пытаемся найти переменную там. В ней эта переменная есть. А, следовательно, берём эту переменную и выводим её значение в консоль. В данном случае, число 7 .
Итак, поиск переменной интерпретатор JavaScript всегда начинает с текущей области видимости . Если она в ней имеется, то поиск прекращается и берётся эта переменная. В противном случае интерпретатор в поиске переменной переместится к следующей области , содержащейся в ссылке, и попробует отыскать её там. После этого действия повторяются , т.е. при отсутствии искомой переменной в просматриваемой области видимости, интерпретатор перемещается к следующей области посредством ссылки и пытается обнаружить её там.
В результате поиск всегда заканчивается одним из двух нижеприведённых сценариев:
- интерпретатор нашёл искомую переменную в какой-нибудь области; в этом случае он берёт эту переменную и останавливает её дальнейший поиск по цепочке в других областях видимости;
- интерпретатор в поиске переменной дошёл до глобальной области и не нашёл её там; в этом случае возникает ошибка, что переменной с указанным именем не существует.
Глобальная область видимости — это последнее звено в цепочке областей видимости Она не содержит ссылку на другую область, на ней всё заканчивается.
Рассмотрим ещё один очень интересный пример:
В этом примере в глобальной области видимости объявлены переменные num , fnA и fnB . При вызове функции fnB() у нас создаётся локальная область видимости, которая будет содержать ссылку на внешнюю, в данном случае на глобальную область видимости. В этой области у нас создаётся переменная с таким же именем num , а затем вызывается функция fnA() . При вызове функции fnA() у нас создаётся локальная область видимости, которая будет иметь в качестве ссылки глобальную область. Почему так? Потому что внешняя область определяется в зависимости от того, где объявлена функция, а не вызвана. А так как функция объявлена в глобальной области видимости, то не зависимого того где она вызвана, она будет содержать в качестве ссылки — ссылку на внешнюю область видимости, в зависимости от того где она объявлена.
Контекст и ключевое слово this
Кроме области видимости в JavaScript имеется ещё контекст (context). Контекст – это то, на что указывает this .
По сути this – это объект, которому «принадлежит» выполняемый в данный момент код.
1. В контексте глобального объекта (вне модулей и функций) this – это глобальный объект.
2. Внутри функции this зависит от того, как вызывается функция.
2.1. Если функция вызывается не как метод объекта, то this в не строгом режиме указывает на глобальный объект, а в строгом – undefined .
2.2. Когда функция вызывается как метод, this – это объект, который использовался для его вызова:
3. Внутри класса this указывает на новый объект, который будет создан с помощью new :
4. В модуле на верхнем уровне this – это undefined .
Стрелочные функции нет имеют собственного this . Если внутри стрелочной функции происходит обращение к this , она берёт его снаружи.
Указание контекста с помощью call или apply
В JavaScript при вызове функции можно установить нужный this , т.е. контекст, в котором она должна выполняться. Осуществляется это с помощью метода call или apply .
Синтаксис использования метода call :
Метод apply аналогичен call . Единственное отличие в том, что аргументы в apply передаются в виде массива.
Синтаксис метода apply :
В качестве thisArg методам call и apply , кроме объекта, можно также установить значение null или undefined . В не строгом режиме эти значения будут заменены ссылкой на глобальный объект.
Привязка контекста к функции
У функций имеется метод bind . С помощью него можно установить определённый this , в рамках которого она должна выполняться. В качестве результата bind() возвращает новую связанную с этим this функцию. Выполнение связанной функции приводит к вызову исходной функции, но в указанном this .
Синтаксис метода bind :
1. Использование bind для привязки функции:
Так как функция setTimeout является методом объекта window , то внутри неё this указывает на глобальный объект. Чтобы this внутри функции был person , его нужно привязать с помощью метода bind() . То есть, как это и сделано в примере.
2. Применение bind для использования методов другого объекта:
В этом примере показано как можно использовать метод одного объекта для другого без создания в нём его копии.
3. Создание функции с заранее заданными начальными аргументами:
4. Привязывания одной функции к разным объектам:
В этом примере мы с помощью bind привязали одну функцию getFee к разным объектам. Для объекта users[’m-1’] её следует использовать как getFeeM1 , а для users[’m-2’] – как getFeeM2 .
Ключевое слово var
Создавать переменные посредством var и функций посредством Function Declaration не рекомендуется . Но понимать, как работает код, в котором эти вещи используются необходимо.
1. Переменные, объявление с помощью ключевого слова var , имеют область видимости функции :
Т.е. var в отличие от let и const создаёт переменные, которые ограничены областью видимости функции, а не блоком.
2. Ключевое слово var в отличие от let и const , создаёт глобальные переменные, которые будут являться свойствами глобального объекта window в веб-браузере и global в Node.js:
Переменные, созданные в глобальной области видимости с помощью ключевых слов let , const и class , не являются свойствами глобального объекта.
3. Переменные, объявленные с использованием var поднимаются (hoisting) в начало текущего контекста. При этом поднимается только само объявление переменной:
Этот код интерпретатор воспринимает так:
Но в JavaScript поднимаются не только переменные, созданные с помощью var , но и функции, объявленные как function declaration :
В этом примере мы вызываем функцию greet() до её объявления.
Необъявленные переменные и строгий режим
При попытке присвоить значение переменной, которая раньше нигде не была объявлена, ошибки не будет:
Переменная myName в этом примере будет объявлена автоматически, причём это будет сделано в глобальной области видимости. Тем самым после завершения выполнения функции sayHello() , эта переменная будет нам доступна и мы можем вывести её значение в консоль.
Таким образом, когда мы пытаемся присвоить значение переменной, которая нигде не была найдена, такая переменная будет создана автоматически при этом в глобальной области видимости.
Но писать код без объявления переменных не рекомендуется, и чтобы избежать их автоматического создания можно использовать строгий режим (на английском strict mode).
Строгий режим – это просто инструкция интерпретатору JavaScript, которая предотвращает выполнение определенных действий и создает больше исключений.
Для того чтобы включить строгий режим достаточно просто добавить следующую строку:
Строгий режим можно включить ко всему сценарию или к отдельным функциям и модулям.
Чтобы применить строгий режим ко всему сценарию, просто добавьте в самом вверху кода перед первой строчкой ‘use strict’ .
После включения строго режима при выполнении примера, приведённого выше, вы получите ошибку:
Таким образом, строгий режим запрещает использование не объявленных переменных.
Что такое контекст? Более обширный взгляд
Первый параметр статического метода makeText класса Toast — это контекст. Но я могу передать this (если вызов метода находится не дальше одного блока кода) или MainActivity.this (если имя класса-активности MainActivity ), могу также передать getApplicationContext() .
- Так в чем разница?
- Почему в качестве контекста можно передать this ? Это же ссылка на класс
- Есть ли случаи когда надо передать именно getApplicationContext ?
- Почему контекст нужен везде, где происходит работа с интерфейсом?
- Почему любой виджет имеет конструктор, в который надо передать контекст?
- Что вообще такое контекст?
Последний вопрос я задал, так как по ходу написания остальных, я понял, что ничего о нем не знаю 🙂
класс Context содержит в себе всевозможную информацию о ресурсах системы, как уже было сказано в другом ответе. Конкретно в этом вопросе нас интересует, что он содержит, помимо прочего, и параметры темы (стилей) для отображения View
Почему в качестве контекста можно передать this? Это же ссылка на класс
Активити является наследником класса Context и несет в себе информацию о контексте для этой активити, поэтому мы можем использовать ссылку на именно этот класс в качестве контекста. С Fragment , к примеру, это уже не работает — он не наследуется от Context
Есть ли случаи когда надо передать именно getApplicationContext?
Тема (стиль) всего приложения и конкретной активити может отличаться (для активити в манифесте указан другой стиль). Тогда запрос контекста приложения и контекста активти вернет разное оформление View
Почему контекст нужен везде, где происходит работа с интерфейсом?
Потому что он содержит стиль для View
на остальные вопросы ответ тот же — в контексте содержится информация, как должен выглядеть View . Например, стиль кнопки темы Holo и темы AppCompat сильно отличается, в контексте и содержится эта информация.
Возможно в приложении могут существовать и какие то другие отличия в окружении, назначенном всему приложению и конкретной активити, тогда обращение к контексту приложения или активити тоже будет иметь значение, но мне такие отличия (кроме тем и стилей) припомнить не удалось.
UPD несколько важных замечаний по getApplicationContext() , не связанных с UI приложения из этой статьи
контекст приложения следует использовать везде, где контекст необходимо передать за пределы жизненного цикла передающего компонента (в объекты, которые будут жить дольше, чем создавшая/вызвавшая их активность, например) во избежании удержания ссылки на этот компонент при использовании его собственного контекста и утечек памяти.
во внешние библиотеки следует передавать контекст приложения по тем же причинам, что и п.1
контекст приложения не имеет информации по особенностям GUI отдельной активити, если они отличаются от параметров всего приложения, в таких случаях нельзя использовать контекст приложения при работе с GUI этой активити.
приложение (класс Application ) — синглтон и его контекст тоже синглтон, этот контекст может удерживать объекты с более коротким жизненным циклом и приводить к утечкам памяти, если не позаботиться о их корректной обработке GC