Поскольку JavaScript становится все более популярным и широко используемым, люди используют его возможности для создания как серверных, так и клиентских приложений, которые работают быстро и используют меньше ресурсов машины.
В этом посте мы поговорим о движке JS, его контексте выполнения, его фазах и о том, как все работает внутри движка Javascript.
Прежде чем перейти к терминологии, давайте посмотрим, почему получение глубокой информации о внутренней работе JS является более важным. Если мы говорим о Javascript, то без понимания внутренней работы движка javascript и времени выполнения мы можем получить от программы результат, которого не ожидали. Это отличает javascript от других языков и делает его более сложным для понимания некоторыми программистами.
Теперь давайте начнем с введения в движок Javascript.
Что такое движок Javascript?
«Javascript Engine — это не что иное, как программа, которая берет код, написанный на javascript, и выполняет его».
Внутри программы при выполнении кода происходит несколько этапов, но здесь нам просто нужно знать, что движок javascript берет высокоуровневый код, преобразует его в машинный код, выделяет память и выполняет нашу программу.
В настоящее время в интернет-пространстве существует несколько движков Javascript, но самое главное — все движки следуют стандарту, который известен как «ECMA Script». ECMA Script указывает каждой реализации движка, как будет выполняться Javascript или, можно сказать, как он будет себя вести, поэтому на каждом движке Javascript работает практически одинаково.
Наиболее популярными JS движками являются Google V8, Spider Monkey, Javascript Core, Chakra и др.
Где находится этот движок Javascript?
Теперь вы задаетесь вопросом, хорошо, я понял, что делает JS Engine, но где находится этот JS Engine? Является ли этот движок встроенным в мою машину или он находится где-то еще?
Вот ответ на этот вопрос. Движок Javascript находится внутри JS runtime (о JS runtime мы поговорим в следующей части), и этот JS Runtime предварительно встроен в наш браузер. Поэтому, когда мы устанавливаем любой браузер, мы получаем этот движок Javascript, встроенный в него, и с помощью этого движка браузер может обрабатывать все веб-сайты/веб-приложения, использующие javascript.
Компоненты JS Engine:
Теперь начинается основная часть JS Engine, JS Engine состоит из двух основных компонентов,
Куча памяти и стек вызовов.
1. Куча памяти: Это место, где всем примитивам, т.е. функциям и объектам, отводится пространство памяти.
2. Стек вызовов: Как следует из названия, это структура данных, которая представляет собой стек и поддерживается Javascript Engine для отслеживания потока выполнения нашей javascript-программы. Стек вызовов следит за контекстом выполнения, о котором мы поговорим чуть позже. Каждый раз, когда вызывается функция, создается новый контекст исполнения, и Javascript Engine помещает этот контекст исполнения в эту структуру данных стека. Как мы знаем, стек использует свойство LIFO, т.е. Last In First Out, поэтому всякий раз, когда контекст исполнения завершает свое выполнение, он просто выгружается из стека.
Как происходит внутреннее выполнение кода?
Теперь, как мы знаем, код javascript выполняется движком JS Engine, но как он выполняется, давайте разберемся.
Когда мы выполняем определенный фрагмент кода, он попадает внутрь движка Javascript Engine, который создает специальную среду для выполнения кода, известную как «Контекст выполнения».
Теперь возникает вопрос, когда именно создается этот контекст выполнения? Чтобы понять это, нам нужно знать, сколько существует типов контекстов выполнения.
Итак, в основном существует два типа контекста выполнения. Первый — это GEC, т.е. глобальный контекст выполнения, а второй — FEC, т.е. контекст выполнения функции.
Теперь, я знаю, что вы можете быть ошеломлены всей этой информацией, но просто потерпите меня, и все станет понятно через некоторое время.
Глобальный контекст исполнения:
Глобальный контекст выполнения создается каждый раз, когда ваш файл сценария загружается в Javascript Engine, другими словами, когда вы пытаетесь выполнить файл javascript, прежде всего, создается глобальный контекст выполнения. Внутри него выполняется весь код javascript. Для каждого файла javascript существует только один GEC.
Контекст исполнения функции:
Как следует из названия, контекст выполнения функции создается всякий раз, когда вы пытаетесь вызвать/запустить функцию. Поскольку контекст выполнения функции создается отдельно для каждой функции, в процессе выполнения кода может существовать несколько FEC.
Оба контекста выполнения GEC и FEC похожи, есть только одно различие — время создания.
Теперь, далее, этот контекст выполнения делится на две части:
Память; Нить выполнения / Код.
1. Память: Это место, где всем примитивам, т.е. функциям и объектам, отводится пространство памяти.
2. Код: Поток исполнения или, можно сказать, часть кода — это место, где наша программа фактически выполняется.
Обе эти части контекста выполнения имеют свои собственные фазы для обработки программы.
-
Фаза создания памяти
-
Фаза выполнения кода
В фазе создания памяти все переменные и функции хранятся в нашей куче, т.е. области памяти. Всем переменным и функциям выделяется память «сразу», т.е. выделение памяти начинается сверху и продолжается до конца сценария. Изначально переменным присваивается значение по умолчанию «undefined», а функции хранятся так, как они написаны в коде. Из-за этого процесса возникает концепция, называемая Hoisting. Также на этапе создания добавляется еще одна часть памяти, известная как «Lexical Environment», но мы не будем говорить о лексической среде и hoisting здесь, оставим это на потом.
На фазе выполнения кода переменным, которые были присвоены неопределенными ранее на фазе создания, теперь присваивается фактическое значение, определенное внутри программы, а если мы говорим о функции, если она вызывается, т.е. вызывается где-то в программе, она создает свой собственный контекст выполнения функции внутри своего родительского контекста выполнения, который по умолчанию является GEC, т.е. глобальным контекстом выполнения.
Теперь давайте разберемся во всем, что мы только что увидели, на примере того, как выполняется наш код. Допустим, у нас есть функция, которая принимает число и возвращает квадрат данного числа.
var number = 2;
function getSquare(num) {
var answer = num * num;
return answer;
}
var square2 = getSquare(number);
var square4 = getSquare(4);
Теперь в качестве первого шага Javascript Engine создаст глобальный контекст выполнения, как мы обсуждали ранее в этом разделе. Этот контекст выполнения будет помещен в стек вызовов для отслеживания потока выполнения.
Теперь начинается наша первая фаза, т.е. «фаза создания». Здесь, в нашей программе, мы видим три переменные на первом уровне (вне любой функции) number
, square2
и square4
. Всем этим трем переменным выделяется память в нашем пространстве памяти и присваивается undefined
. Функция getSquare()
хранится как есть в нашей области памяти.
После распределения памяти начинается вторая фаза, т.е. построчное выполнение кода. В начале выполнения встретится 1-я строка, а именно
var number = 2;
Теперь, поскольку переменная number уже хранится в памяти, Javascript Engine просто выделит ее значение 2
и обновит его в области памяти.
Теперь выполнение нашего кода дойдет до определения функции, но здесь ничего не произойдет, потому что функции уже выделена память на этапе создания. Помните, что код функции выполняется только тогда, когда она вызывается.
Теперь на экране появится следующая строка:
var square2 = getSquare(2)
Здесь мы вызываем функцию, передавая ей аргумент и сохраняя ее возвращаемое значение в переменной square2.
При вызове функции создается новый контекст выполнения функции внутри нашего глобального контекста выполнения (GEC), который имеет свою собственную память и часть выполнения кода, а также наш новый контекст выполнения заталкивается в стек вызовов.
Теперь, поскольку стек имеет свойство Last In First Out, наш новый контекст выполнения начнет выполняться первым.
function getSquare(num) {
var answer = num * num;
return answer;
}
Для этого нового контекста выполнения начнется фаза создания. Мы видим, что здесь есть две переменные, которым нужна память для хранения, это answer и num. В контексте выполнения функции, если у нас есть некоторые параметры, то эти параметры также рассматриваются как переменные и им также выделяется память.
Теперь, после первой фазы, начинается вторая фаза, т.е. фаза выполнения кода.
Она встретит первую строку функции "function getSquare(num)"
. Значение, которое мы передали при вызове функции, присваивается num, поэтому num будет присвоено значение 2.
Теперь начнет выполняться строка 2, а именно var answer = num*num;
. Результат num*num будет сохранен внутри ответа.
Наконец, будет выполнен оператор return. Когда весь код будет выполнен, контекст выполнения функции будет извлечен из стека вызовов, а значение, которое мы вернули, будет присвоено квадрату square2. Оператор return указывает выполнению функции перейти к предыдущему контексту выполнения, чтобы снова начать выполнение GEC.
Теперь этот же процесс выполняется для квадрата4, и ему присваивается значение 16. После выполнения обоих вызовов функции наша программа завершается, и глобальный контекст выполнения также выгружается из стека вызовов, а память очищается.
Вот как выполняется программа javascript под капотом.
Поскольку Javascript Engine выполняет одну строку кода за раз, поэтому javascript является «однопоточным».
Совет:
Вы можете увидеть, как все эти вещи работают в реальном времени в вашем браузере.
Заключение:
JavaScript Engine и Execution Context — это основа для правильного понимания многих других фундаментальных концепций.
Контекст выполнения (GEC и FEC) и стек вызовов — это процессы, которые выполняются внутри JS Engine для выполнения нашего кода javascript.
Надеюсь, теперь вы лучше понимаете, что такое JS Engine, в каком порядке запускается программа/код и как JavaScript Engine выделяет им память.
Если вам понравился этот материал, пожалуйста, поделитесь им со своими друзьями, коллегами, друзьями-кодировщиками.
Хорошего дня