Введение в JavaScript
Давайте посмотрим, что такого особенного в JavaScript, чего можно достичь с его помощью и какие другие технологии хорошо с ним работают.
Что такое JavaScript?
Изначально JavaScript был создан, чтобы «сделать веб-страницы живыми».
Программы на этом языке называются скриптами. Они могут встраиваться в HTML и выполняться автоматически при загрузке веб-страницы.
Скрипты распространяются и выполняются, как простой текст. Им не нужна специальная подготовка или компиляция для запуска.
Это отличает JavaScript от другого языка – Java.
Когда JavaScript создавался, у него было другое имя – «LiveScript». Однако, язык Java был очень популярен в то время, и было решено, что позиционирование JavaScript как «младшего брата» Java будет полезно.
Со временем JavaScript стал полностью независимым языком со своей собственной спецификацией, называющейся ECMAScript, и сейчас не имеет никакого отношения к Java.
Сегодня JavaScript может выполняться не только в браузере, но и на сервере или на любом другом устройстве, которое имеет специальную программу, называющуюся «движком» JavaScript.
У браузера есть собственный движок, который иногда называют «виртуальная машина JavaScript».
Разные движки имеют разные «кодовые имена». Например:
-
– в Chrome, Opera и Edge. – в Firefox.
- …Ещё есть «Chakra» для IE, «JavaScriptCore», «Nitro» и «SquirrelFish» для Safari и т.д.
Эти названия полезно знать, так как они часто используются в статьях для разработчиков. Мы тоже будем их использовать. Например, если «функциональность X поддерживается V8», тогда «Х», скорее всего, работает в Chrome, Opera и Edge.
Движки сложны. Но основы понять легко.
- Движок (встроенный, если это браузер) читает («парсит») текст скрипта.
- Затем он преобразует («компилирует») скрипт в машинный язык.
- После этого машинный код запускается и работает достаточно быстро.
Движок применяет оптимизации на каждом этапе. Он даже просматривает скомпилированный скрипт во время его работы, анализируя проходящие через него данные, и применяет оптимизации к машинному коду, полагаясь на полученные знания. В результате скрипты работают очень быстро.
Что может JavaScript в браузере?
Современный JavaScript – это «безопасный» язык программирования. Он не предоставляет низкоуровневый доступ к памяти или процессору, потому что изначально был создан для браузеров, не требующих этого.
Возможности JavaScript сильно зависят от окружения, в котором он работает. Например, Node.JS поддерживает функции чтения/записи произвольных файлов, выполнения сетевых запросов и т.д.
В браузере для JavaScript доступно всё, что связано с манипулированием веб-страницами, взаимодействием с пользователем и веб-сервером.
Например, в браузере JavaScript может:
- Добавлять новый HTML-код на страницу, изменять существующее содержимое, модифицировать стили.
- Реагировать на действия пользователя, щелчки мыши, перемещения указателя, нажатия клавиш.
- Отправлять сетевые запросы на удалённые сервера, скачивать и загружать файлы (технологии AJAX и COMET).
- Получать и устанавливать куки, задавать вопросы посетителю, показывать сообщения.
- Запоминать данные на стороне клиента («local storage»).
Чего НЕ может JavaScript в браузере?
Возможности JavaScript в браузере ограничены ради безопасности пользователя. Цель заключается в предотвращении доступа недобросовестной веб-страницы к личной информации или нанесения ущерба данным пользователя.
Примеры таких ограничений включают в себя:
JavaScript на веб-странице не может читать/записывать произвольные файлы на жёстком диске, копировать их или запускать программы. Он не имеет прямого доступа к системным функциям ОС.
Современные браузеры позволяют ему работать с файлами, но с ограниченным доступом, и предоставляют его, только если пользователь выполняет определённые действия, такие как «перетаскивание» файла в окно браузера или его выбор с помощью тега <input> .
Существуют способы взаимодействия с камерой/микрофоном и другими устройствами, но они требуют явного разрешения пользователя. Таким образом, страница с поддержкой JavaScript не может незаметно включить веб-камеру, наблюдать за происходящим и отправлять информацию в ФСБ.
Различные окна/вкладки не знают друг о друге. Иногда одно окно, используя JavaScript, открывает другое окно. Но даже в этом случае JavaScript с одной страницы не имеет доступа к другой, если они пришли с разных сайтов (с другого домена, протокола или порта).
Это называется «Политика одинакового источника» (Same Origin Policy). Чтобы обойти это ограничение, обе страницы должны согласиться с этим и содержать JavaScript-код, который специальным образом обменивается данными.
Это ограничение необходимо, опять же, для безопасности пользователя. Страница https://anysite.com , которую открыл пользователь, не должна иметь доступ к другой вкладке браузера с URL https://gmail.com и воровать информацию оттуда.
JavaScript может легко взаимодействовать с сервером, с которого пришла текущая страница. Но его способность получать данные с других сайтов/доменов ограничена. Хотя это возможно в принципе, для чего требуется явное согласие (выраженное в заголовках HTTP) с удалённой стороной. Опять же, это ограничение безопасности.
Подобные ограничения не действуют, если JavaScript используется вне браузера, например — на сервере. Современные браузеры предоставляют плагины/расширения, с помощью которых можно запрашивать дополнительные разрешения.
Что делает JavaScript особенным?
Как минимум, три сильные стороны JavaScript:
- Полная интеграция с HTML/CSS.
- Простые вещи делаются просто.
- Поддерживается всеми основными браузерами и включён по умолчанию.
JavaScript – это единственная браузерная технология, сочетающая в себе все эти три вещи.
Вот что делает JavaScript особенным. Вот почему это самый распространённый инструмент для создания интерфейсов в браузере.
Хотя, конечно, JavaScript позволяет делать приложения не только в браузерах, но и на сервере, на мобильных устройствах и т.п.
Языки «над» JavaScript
Синтаксис JavaScript подходит не под все нужды. Разные люди хотят иметь разные возможности.
Это естественно, потому что проекты разные и требования к ним тоже разные.
Так, в последнее время появилось много новых языков, которые транспилируются (конвертируются) в JavaScript, прежде чем запустятся в браузере.
Современные инструменты делают транспиляцию очень быстрой и прозрачной, фактически позволяя разработчикам писать код на другом языке, автоматически преобразуя его в JavaScript «под капотом».
Примеры таких языков:
-
добавляет «синтаксический сахар» для JavaScript. Он вводит более короткий синтаксис, который позволяет писать чистый и лаконичный код. Обычно такое нравится Ruby-программистам. концентрируется на добавлении «строгой типизации» для упрощения разработки и поддержки больших и сложных систем. Разработан Microsoft. тоже добавляет типизацию, но иначе. Разработан Facebook. стоит особняком, потому что имеет собственный движок, работающий вне браузера (например, в мобильных приложениях). Первоначально был предложен Google, как замена JavaScript, но на данный момент необходима его транспиляция для запуска так же, как для вышеперечисленных языков. транспилирует Python в JavaScript, что позволяет писать приложения на чистом Python без JavaScript.
Есть и другие. Но даже если мы используем один из этих языков, мы должны знать JavaScript, чтобы действительно понимать, что мы делаем.
Как JavaScript и движок JavaScript работает в браузере и на узле?

Прежде чем мы заглянем в анатомию движка JavaScript и посмотрим, как все сочетается друг с другом, давайте сначала разберемся с некоторыми основными концепциями JavaScript и небольшой историей его возникновения.
Вкратце о JavaScript
JavaScript — это интерпретируемый язык. Это означает, что нам не нужно компилировать исходный код JavaScript перед его отправкой в браузер. Интерпретатор может взять необработанный код JavaScript и запустить его за вас.
JavaScript также является языком с динамической типизацией, в отличие от C и C ++. Это означает, что переменные, объявленные с использованием var , могут хранить любой тип данных, например int , string , boolean , а также сложные типы данных, такие как object и array .
Отсутствие системы типов — это то, что замедляет работу JavaScript. Статически типизированный язык может создавать очень эффективный машинный код благодаря информации, которую он имеет о данных, такой как тип и размер.
Поэтому всякий раз, когда вы думаете, что статически типизированный язык, такой как C или C ++, без всякой причины превращает вашу жизнь в ад, подумайте о производительности.
История JavaScript
Вы можете спросить, почему JavaScript был разработан таким образом, если он настолько плох с точки зрения скорости? Для этого нам нужно понять его историю.
В первые дни Интернета веб-браузеры использовались для отображения статических страниц. Обычно эти страницы не интерактивны. Чтобы добавить взаимодействия, новый язык был введен в браузере Netscape еще в 1995 Бренданом Эйхом. Этим новым языком был JavaScript (ранее назывался LiveScript), и на его разработку у него ушло 10 дней.
Из 10 дней ничего хорошего не получится, но для 10 дней усилий JavaScript был настоящим чудом. Появились другие языки и плагины, такие как ActionScript, Silverlight и Flash, но JavaScript выиграл битву.
JavaScript не был разработан с учетом производительности. Он должен был просто работать внутри браузера и предоставлять API для работы с DOM. Но поскольку многие браузеры пытались адаптировать его по-своему, его пришлось стандартизировать.
Ecma International — это организация по стандартизации, которая стандартизирует JavaScript, и Технический комитет 39 (TC39) управляет этим стандартом. Этот стандарт известен как EcmaScript, а фраза EcmaScript также используется как синонимы JavaScript, поскольку торговая марка JavaScript принадлежит Oracle Corporation ( источник ).
Анатомия движка JavaScript
Спецификация EcmaScript сообщает, как JavaScript должен быть реализован браузером, чтобы программа JavaScript выполнялась точно так же во всех браузерах, но не сообщает, как JavaScript должен работать в этих браузерах. Решение остается за производителем браузера.
Каждый браузер предоставляет движок JavaScript, запускающий код JavaScript. Браузер Netscape использовал движок JavaScript SpiderMonkey. Этот движок представлял собой рудиментарный интерпретатор без каких-либо оптимизаций. Выполнение кода JavaScript с помощью этого движка было медленным, но оно работало.

Как видно из диаграммы выше, задача первого движка JavaScript заключалась в том, чтобы взять исходный код JavaScript и скомпилировать его в двоичные инструкции (машинный код), понятные процессору.
Элементарный движок JavaScript содержит базовый компилятор, задачей которого является компиляция исходного кода JavaScript в промежуточное представление (IR), которое также называется bytecode и передает этот байт-код интерпретатору.
Интерпретатор принимает этот байт-код и преобразует его в машинный код, который в конечном итоге запускается на аппаратном обеспечении машины (ЦП).
Это похоже на то, как работает Java, но генерация байт-кода выполняется программистом, а байт-код используется повсеместно, а не исходный код.
Задача базового компилятора — скомпилировать код как можно быстрее и сгенерировать менее оптимизированный байт-код (или машинный код в других случаях). Поскольку интерпретатор имеет неоптимизированный байт-код для работы, скорость приложения будет медленной, однако время начальной загрузки приложения будет намного меньше.
SpiderMoney. JavaScript превратился в часть сложного механизма для создания высокооптимизированного машинного кода и в настоящее время используется в браузере Firefox. Вы можете ознакомиться с исходным кодом этой документации.
Когда дело доходит до высокодинамичного и интерактивного веб-приложения, эта модель выполнения JavaScript оставляет желать лучшего. С этой проблемой столкнулся браузер Google Chrome при отображении Карт Google в Интернете. Чтобы повысить производительность JavaScript в Интернете, им пришлось придумать лучший подход.
Google Chrome с самого начала использует движок JavaScript V8. Вначале, чтобы улучшить производительность JavaScript, они добавили две части в конвейер своего движка JavaScript, как показано ниже.

В версии 2010 движка V8 JavaScript было два основных механизма, которые выполняли основную работу по движку. full-codegen был базовым компилятором, задача которого заключалась в том, чтобы как можно быстрее выдавать неоптимизированный машинный код для более быстрой загрузки приложения.
Во время работы приложения компилятор коленчатого вала включился и оптимизировал исходный код, а также заменил части машинного кода, сгенерированные базовым компилятором. Эта оптимизация приведет к повышению производительности приложения, поскольку генерируется все более качественный машинный код.
Однако этот процесс требует больших затрат ресурсов процессора и памяти. Следовательно, V8 должен предложить другую модель.
Вышеупомянутая версия движка JavaScript не содержит интерпретатора. Это модель компиляции JIT (Just-In-Time), поскольку код компилируется на машинный уровень на лету, а затем оптимизируется также для машинного кода.
Как оптимизирован JavaScript?
Существуют различные критерии оптимизации кода JavaScript. Прежде чем код JavaScript будет передан интерпретатору или базовому компилятору, он должен сначала быть проанализирован в абстрактное синтаксическое дерево (AST), которое представляет собой древовидную структуру кода.
Когда мы запускаем приложение JavaScript, нам не нужен весь код во время запуска приложения. Например, если у нас есть функция, которая вызывается при действии пользователя, например, при нажатии кнопки, этот код можно проанализировать позже.
Выявление вещей, которые необходимо немедленно проанализировать, и генерация машинного кода — лучшая стратегия для более быстрой загрузки приложения.
Иногда код JavaScript содержит ненужную сложную логику, которую можно упростить. Например, цикл for для увеличения целого числа может быть встроен с использованием + операций n количество раз. Этот процесс называется Разворачивание цикла. Аналогичную оптимизацию можно произвести с помощью встраивания функций.
Отсутствие системы типов в JavaScript — это то, что заставляет движок JavaScript создавать менее оптимизированный машинный код. Следовательно, на основе уже определенных значений движок JavaScript может угадывать типы данных переменных и генерировать лучший машинный код.
Весь этот процесс задокументирован и очень хорошо объяснен Полом Райаном в его сообщении в блоге о движке V8 на alligator.io. Вам обязательно стоит прочитать эту статью, чтобы глубже понять эти концепции.
Между тем, механизм JavaScript также может собирать данные профилирования выполнения кода и искать код, который работает медленнее. Этот код называется «Горячим», возможно, потому, что он сжигает ЦП. Этот код можно дополнительно оптимизировать и заменить оптимизированным машинным кодом.
Принимая во внимание эти вещи и другие проблемы, вызванные полным кодированием и коленчатым валом, команда V8 создала новую версию V8 двигатель с нуля. Эта новая версия движка JavaScript была выпущена в 2017.

Как видно из рисунка выше, команда V8 представила новый конвейер интерпретатора Ignition, задача которого заключалась в генерации байт-кода из исходного кода JavaScript с использованием базового компилятора и более поздних версий. интерпретировать этот байт-код с помощью интерпретатора.
Компилятор оптимизации TurboFan может оптимизировать этот байт-код в фоновом режиме (в отдельных потоках) во время работы приложения и генерировать очень оптимизированный машинный код, который в конечном итоге будет заменен.
Turbofan получает данные профилирования от интерпретатора Ignition и ищет код, который является горячим. Он может сделать предположения о том, как лучше оптимизировать код (угадывая типы данных), а также оптимизировать или деоптимизировать код.
А как насчет других движков JavaScript?
Мы рассмотрели широкий обзор того, как работает движок JavaScript V8. Аналогичной модели придерживаются и другие поставщики браузеров, такие как движок SpiderMonkey для браузера Firefox и движок Chakra для Internet Explorer .
Некоторые движки JavaScript могут выглядеть сложными, возможно, потому, что у них есть несколько базовых компиляторов и компиляторов оптимизации, но в двух словах они следуют одной и той же модели оптимизации.
V8 — один из самых популярных движков JavaScript, возможно, потому, что он разработан Google. Но двигатель V8 постоянно развивается и становится все быстрее. Помимо Google Chrome, проект Chromium, Electron.js и исполняющая среда серверного JavaScript Node.js используют Двигатель V8.
JavaScript во время выполнения
Есть много увлеченных разработчиков, работающих над интерфейсом или сервером, которые посвящают свою жизнь защите области JavaScript. JavaScript очень прост для понимания и является неотъемлемой частью интерфейсной разработки.
Но в отличие от других языков программирования, это однопоточный язык во время выполнения. Это означает, что выполнение кода будет выполняться по частям. Поскольку выполнение кода выполняется последовательно, любой код, выполнение которого занимает много времени, блокирует все, что необходимо выполнить после этого. Поэтому иногда вы видите под экраном при использовании Google Chrome.

Когда вы открываете веб-сайт в браузере, он использует один поток выполнения JavaScript. Этот поток отвечает за все, например за прокрутку веб-страницы, печать чего-либо на веб-странице, прослушивание событий DOM (например, когда пользователь нажимает кнопку) и выполнение других действий.
Но когда выполнение JavaScript заблокировано, браузер перестанет выполнять все эти действия, а это означает, что браузер просто зависнет и не будет ни на что реагировать, пока эта задача не будет завершена.
Вы можете увидеть это в действии, используя приведенный ниже вечный цикл while .
Любой код после приведенного выше оператора не будет выполняться, поскольку цикл while будет повторяться бесконечно, пока в системе не закончатся ресурсы. Это также может произойти при бесконечно рекурсивном вызове функции.
Благодаря современным браузерам, так как не все открытые вкладки браузера полагаются на один поток JavaScript. Вместо этого они используют отдельный поток JavaScript для каждой вкладки или для домена. В случае с Google Chrome вы можете открывать несколько вкладок с разными веб-сайтами и запускать вечный цикл while .
Это только заморозит текущую вкладку, на которой был выполнен этот код, но другие вкладки будут работать нормально. Любая вкладка, на которой открыта страница из того же домена / того же веб-сайта, также зависнет, поскольку Chrome реализует политику один процесс на сайт, а процесс использует тот же поток выполнения JavaScript.
Чтобы визуализировать, как JavaScript выполняет программу, нам нужно понять время выполнения JavaScript и различные компоненты, которые играют в ней роль. Итак, давайте напишем простую программу на JavaScript, чтобы это визуализировать.
Здесь у нас есть простая программа на JavaScript, которая имеет три функции, а именно. foo , bar и baz . Функция foo вызывает функцию bar , а затем функция bar вызывает функцию baz , которая записывает что-то в консоль, используя функцию console.log , предоставляемую средой выполнения.
Когда мы запускаем эту программу, сначала вызывается функция foo , а затем цепочка вызовов начинается до выполнения console.log() . Давайте визуализируем это с помощью диаграммы и исследуем различные компоненты среды выполнения.

Как и любой другой язык программирования, среда выполнения JavaScript имеет один стек и одно хранилище в куче. Куча — это свободная единица памяти, в которой вы можете хранить память в произвольном порядке. Данные, которые будут сохраняться в течение значительного времени, попадают в кучу. Куча управляется средой выполнения JavaScript и очищается сборщиком мусора. Я не буду подробно рассказывать о куче, вы можете прочитать ее здесь.
Нас интересует стек. Стек представляет собой хранилище данных LIFO (последний пришел — первым ушел), в котором хранится текущий контекст выполнения функции программы. В приведенном выше примере, когда наша программа загружается в память, она начинает выполнение с первого вызова функции, которая равна foo() .
Следовательно, первая запись стека — foo() . Поскольку foo функция вызывает bar функцию, вторая запись в стеке — bar() . Так как функция bar вызывает функцию baz , третья запись в стеке — baz() . И, наконец, baz функция вызывает console.log , четвертая запись в стеке — console.log(‘Hello from baz’) .
Пока функция что-то не вернет (пока функция выполняется), она не будет извлечена из стека. Стек будет выдавать записи одну за другой, как только эта запись (function) вернет какое-то значение, и продолжит выполнение отложенных функций.
Каждая запись в стеке называется кадром стека. Кадр стека содержит информацию о вызове функции, такую как аргументы вызова функции, локальные переменные функции, адрес возврата (где будет использовано возвращаемое значение) и другую информацию о функции.

Как видно из приведенного выше снимка экрана, когда мы добавляем точку останова при вызове функции console.log , DevTools Chrome отображает Стек вызовов (справа), который содержит кадры стека вверх. до выполнения текущей функции.
Если какой-либо вызов функции в данном кадре стека вызывает ошибку, JavaScript выводит трассировку стека, которая является не чем иным, как снимком выполнения кода до этого кадра стека.
В приведенной выше программе мы выдавали ошибку внутри функции baz . Поэтому, когда JavaScript обнаруживает ошибку, он распечатывает приведенную ниже трассировку стека, чтобы показать, что и где пошло не так.

Как видно из приведенного выше снимка экрана, DevTools Chrome не только отображает сообщение об ошибке, но также показывает дорожку стека вплоть до кадра стека, в котором возникла ошибка. Если baz функция вызывает другую функцию после возникновения ошибки, она не будет помещена в стек.
Если вы хотите узнать больше о отслеживании стека в JavaScript и о том, как получить от него больше, прочтите документацию this V8 по API трассировки стека.
Поскольку JavaScript является однопоточным, у него есть только один стек и одна куча на процесс. Следовательно, если какая-либо другая программа хочет что-то выполнить, она должна дождаться полного выполнения предыдущей программы. Этот поток обычно известен как основной поток или основной поток выполнения.
Итак, давайте придумаем один сценарий. Что делать, если браузер отправляет HTTP-запрос для загрузки некоторых данных по сети или для загрузки изображения для отображения на веб-странице. Зависнет ли браузер до тех пор, пока этот запрос не будет обработан? Если да, то это очень плохо для пользователя.
Браузер поставляется с механизмом JavaScript, который отвечает за выполнение любого JavaScript, содержащегося внутри веб-приложения (веб-страницы). Например, Google Chrome использует движок JavaScript V8.
Но знаете что, браузер использует больше, чем просто движок JavaScript. Так выглядит браузер под капотом.

Выглядит действительно сложно, но это очень, если вы понимаете по одной части за раз, и они работают вместе в гармонии. На самом деле среда выполнения JavaScript состоит из еще двух компонентов, а именно. цикл событий и очередь обратного вызова. Очередь обратного вызова также называется очередью сообщений или очередью задач.
Помимо движка JavaScript, браузер содержит различные приложения, которые могут выполнять различные операции, такие как отправка HTTP-запросов, прослушивание событий DOM, задержка выполнения с использованием setTimeout или setInterval , кеширование, хранение базы данных и многое другое. Эти функции браузера помогают нам создавать многофункциональные веб-приложения и улучшать взаимодействие с пользователем.
Но подумайте об этом: если бы браузеру пришлось использовать один и тот же поток JavaScript для выполнения этих задач, пользовательский опыт был бы ужасным. Например, если браузеру пришлось использовать один и тот же поток JavaScript для выполнения задачи при получении сетевого ответа HTTP, тогда веб-страница не отвечала бы в течение нескольких секунд или даже минут.
Следовательно, браузер реализует свою собственную логику для выполнения этих операций, таких как отправка HTTP-запросов и прослушивание их ответов. Эти операции не блокируют основной поток выполнения JavaScript, поскольку они создаются в разных потоках, управляемых браузером, и JavaScript не имеет об этом представления.
Браузер может использовать низкоуровневый язык, такой как C или C++ , для реализации этих функций для повышения производительности и предоставления нам чистого JavaScript API для выполнения этих операций из JavaScript. Например, API fetch предоставляется браузером для отправки HTTP-запросов. Эти API-интерфейсы известны как веб-API, поскольку они не являются частью спецификаций JavaScript.
Эти веб-API являются асинхронными. Это означает, что вы можете проинструктировать эти API-интерфейсы делать что-то в фоновом режиме и возвращать данные, когда это будет сделано, а мы можем продолжить дальнейшее выполнение кода JavaScript. Поручая этим API-интерфейсам делать что-то в фоновом режиме, мы должны предоставить функцию обратного вызова. Функция обратного вызова отвечает за выполнение некоторого кода JavaScript в основном потоке Javascript после того, как веб-API завершит свою работу. Давайте разберемся, как все части работают вместе.
Поэтому, когда вы вызываете функцию, она помещается в стек. Если эта функция содержит вызов веб-API, JavaScript делегирует управление этим веб-API с помощью функции обратного вызова и перейдет к следующим строкам, пока функция что-то не вернет. Теперь функция обратного вызова связана с веб-API, который выполняет свою операцию в отдельном потоке, отдельном от основного потока.
Как только функция попадает в оператор return, эта функция извлекается из стека и переходит к следующей записи стека. Между тем, веб-API выполняет свою работу в фоновом режиме и запоминает, какая функция обратного вызова связана с этим заданием. После выполнения задания веб-API связывает результат этого задания с функцией обратного вызова и публикует сообщение в очередь сообщений (AKA очередь обратного вызова ) с этой функцией обратного вызова.
Единственная задача цикла обработки событий — просмотреть очередь обратного вызова, и как только в очереди обратного вызова будет что-то незавершенное, отправить этот обратный вызов в стек. Цикл событий помещает в стек одну функцию обратного вызова за раз, когда стек пуст. Позже стек выполнит функцию обратного вызова.
Давайте посмотрим, как все работает, шаг за шагом, используя setTimeout Web API. setTimeout Веб-API в основном используется для выполнения чего-либо через несколько секунд (в любой период времени). Это выполнение происходит после того, как весь код в программе завершен (когда стек пуст). Синтаксис функции setTimeout приведен ниже.
callbackFunction — это функция обратного вызова, которая будет выполняться после timeInMilliseconds . Давайте изменим нашу предыдущую программу и воспользуемся этим API.
Единственное изменение, внесенное в программу, — мы отложили выполнение printHello функции на 3 секунды. В этом случае стек будет накапливаться как foo() => bar() => baz() . Как только baz начнет выполнение и выполнит setTimeout вызов API, JavaScript передаст функцию обратного вызова веб-API и перейдет к следующей строке.
Поскольку следующей строки нет, функция возвращается, и стек выдает baz , затем bar , а затем foo вызовы функций. Между тем, веб-API ожидает прохождения 3 секунды. По прошествии 3 секунд он отправит этот обратный вызов в очередь обратного вызова, и, поскольку стек пуст, цикл событий поместит этот обратный вызов обратно в стек, где произойдет выполнение этого обратного вызова.
Филип Роберс создал потрясающий онлайн-инструмент для визуализации того, как работает JavaScript. Наш пример выше доступен по этой ссылке.
Цикл событий и очередь обратных вызовов — это части одной головоломки. Они не являются частью движка Javascript, скорее они находятся вне движка JavaScript и обычно предоставляются средой выполнения, такой как веб-браузер или Node.js. Цикл событий использует API-интерфейсы движка JavaScript для связи с ним и предоставления функций обратного вызова для выполнения.
Четный цикл внутри Node.js
Когда дело доходит до Node.js, он должен делать больше, потому что Node обещает больше. В случае браузера мы ограничены тем, что можем делать в фоновом режиме. Но в node мы можем делать большинство вещей в фоновом режиме, даже если это простая программа на JavaScript. Но как это работает?
Node.js использует движок Google V8 для обеспечения среды выполнения JavaScript и использует собственный цикл обработки событий с использованием библиотеки libuv (написанной на c). Node следует тому же подходу обратного вызова, что и веб-API, и работает аналогично браузеру.

Если вы сравните диаграмму браузера с приведенной выше диаграммой узлов, вы увидите сходство. Весь правый раздел выглядит как веб-API, но он также содержит очередь событий (очередь обратного вызова / очередь сообщений) и цикл событий. Но V8, очередь событий и цикл событий работают в одном потоке, в то время как рабочие потоки несут ответственность за обеспечение асинхронной операции ввода-вывода. Вот почему считается, что Node.js имеет неблокирующую архитектуру асинхронного ввода-вывода, управляемую событиями.
Все вышесказанное прекрасно объясняет Филип Робертс в 30-минутном видео.
Я написал еще одну статью о WebAssembly, в которой подробно разбирается, как работает движок JavaScript.
Помимо цикла событий и очереди обратного вызова, современный движок JavaScript также имеет очередь микрозадач, которая используется для обещаний. Вы также можете узнать об этом из статьи ниже.
Как работает JavaScript [Объясняю визуально]
JavaScript — один из самых любимых и в то же время ненавистных языков в мире. Его любят, потому что он мощный. Вы можете создать полнофункциональное приложение, просто изучив JavaScript и ничего более. А также его ненавидят, потому что иногда он ведет себя совершенно неожиданным образом. И это может расстроить или даже заставить вас возненавидеть его.
В этой статье я подробно объясню, как JavaScript выполняет код в браузере, и мы изучим это с помощью гифок. Прочитав эту статью, вы станете на шаг ближе к статусу рок-звезды разработки.
Контекст выполнения
«Все в JavaScript происходит внутри контекста выполнения (Execution Context)»
Было бы круто, чтобы вы запомнили эту фразу, потому что она очень важна. Скажем, что этот контекст выполнения является большим контейнером, вызываемым, когда браузер хочет запустить какой-то код JavaScript.
В этом контейнере есть два компонента: 1. Компонент памяти. 2. Компонент кода.
Компонент памяти также известен как переменная среды. В этом компоненте памяти переменные и функции хранятся в виде пар ключ-значение.
Компонент кода — это место в контейнере, где код выполняется по одной строке за раз. У этого компонента кода тоже есть необычное название, а именно «Поток выполнения» (Thread of Execution).

JavaScript — это синхронный однопоточный язык. Все потому, что он может выполнять только одну команду за раз и в определенном порядке.
Выполнение кода
Возьмем простой пример:
В этом простом примере мы инициализируем две переменные, a и b, и сохраняем 2 и 4 соответственно. Затем мы складываем значение a и b и сохраняем его в переменной суммы.
Посмотрим, как JavaScript выполнит код в браузере.
Браузер создает глобальный контекст выполнения с двумя компонентами, а именно с памятью и компонентами кода.
Браузер выполнит код JavaScript в два этапа.
Фаза выделения памяти
Этап выполнения кода
На этапе выделения памяти JavaScript сканирует весь код и выделяет память для всех переменных и функций в коде. Для переменных JavaScript будет хранить undefined на этапе выделения памяти, а для функций он сохранит весь код функции, который мы рассмотрим в следующем примере.
Теперь, на 2-м этапе, то есть при выполнении кода, он начинает проходить весь код построчно. Когда он встречает var a = 2, он присваивает значение 2 переменной ‘a’. До сих пор значение «а» не было определено.
То же самое и с переменной b. Он присваивает 4 переменной «b». Затем он вычисляет и сохраняет значение суммы в памяти, равное 6. Теперь, на последнем шаге, он выводит значение суммы в консоль, а затем уничтожает глобальный контекст выполнения по мере завершения нашего кода.
Как вызываются функции в контексте выполнения?
Функции в JavaScript, если сравнивать их с другими языками программирования, работают по-другому.
Возьмем простой пример:
В приведенном выше примере есть функция, которая принимает в качестве аргумента число и возвращает его квадрат.
JavaScript создаст глобальный контекст выполнения и выделит память для всех переменных и функций на первом этапе, когда мы запустим код, как показано ниже.
Что касается функций, он сохранит всю функцию в памяти.
А вот и самое интересное: когда JavaScript запускает функции, он создает контекст выполнения внутри глобального контекста выполнения.
Когда он встречает var a = 2, он присваивает значение 2 переменной ‘n’. Строка номер 2 — это функция, и поскольку функции была выделена память ранее, все сразу перейдет к строке номер 6.
Переменная square2 вызовет функцию square, а javascript создаст новый контекст выполнения.
Этот новый контекст выполнения для функции square выделит память всем переменным, присутствующим в функции на этапе выделения памяти.
После выделения памяти всем переменным внутри функции код будет выполняться построчно. Будет получено значение num, равное 2 для первой переменной, а затем вычислено ans. После вычисления ans возвратится значение, которое будет присвоено square2.
Как только функция вернет значение, она уничтожит свой контекст выполнения по завершении работы.
Теперь он будет следовать аналогичной процедуре для строки номер 7 или переменной square4, как показано ниже.
Как только весь код будет выполнен, глобальный контекст выполнения также будет уничтожен, и именно так JavaScript будет выполнять код.
Стек вызовов
Когда функция вызывается в JavaScript, JS создает контекст выполнения. Контекст выполнения будет усложняться, поскольку мы добавляем функции внутрь функции.

JavaScript управляет созданием и удалением контекста выполнения кода с помощью стека вызовов. Стек — это упорядоченный набор элементов, в котором добавление новых элементов и удаление существующих элементов всегда происходит «с одной стороны». Первый элемент, добавленный в стек, будет удален оттуда последним. Этот принцип называется FILO.
Стек вызовов — это механизм, позволяющий отслеживать свое место в скрипте, вызывающем несколько функций.
Мы создаем функцию «a», которая вызывает другую функцию «insideA», которая возвращает значение true. Я знаю, что код бессмысленный и ничего не делает, но он поможет нам понять, как JavaScript обрабатывает коллбеки (функции обратного вызова).
JavaScript создаст глобальный контекст выполнения. Глобальный контекст выполнения выделит память для функции ‘a’ и вызовет ‘function a’ на этапе выполнения кода. Контекст выполнения создается для функции a, которая размещается над глобальным контекстом выполнения в стеке вызовов.
Функция a назначит память и вызовет функцию insideA. Контекст выполнения создается для функции insideA и помещается над стеком вызовов ‘function a’. Теперь эта функция insideA вернет true и будет удалена из стека вызовов. Поскольку внутри ‘function a’ нет кода, контекст выполнения будет удален из стека вызовов.
Наконец, глобальный контекст выполнения также удаляется из стека вызовов.
Как работает JavaScript

Ранее JavaScript предназначался для использования в веб-браузерах, однако ситуация изменилась с развитием Node. Мы знаем, как, где и когда его использовать. Но известно ли, что происходит за этими сценариями?
Даже если вы знаете это, то все равно сможете извлечь полезную информацию из данной статьи.
JavaScript — это высокоуровневый ЯП, а компьютер понимает только единицы и нули. Каким образом компьютер понимает написанный код? В этой статье мы рассмотрим ответ на один единственный вопрос: как работает JavaScript?
Движок JavaScript
Это главный герой, который отвечает за понимание компьютером JS-кода. Движок JavaScript принимает код и преобразует его в машиночитаемый язык. Он выполняет работу переводчика, преобразующего JS-код на понятный компьютеру язык. Как и JS, каждый ЯП также обладает движком, делающий написанный код понятным для компьютера.
У JavaScript есть множество различных движков, доступных для использования. На этой странице Википедии можно найти их список. Они также называются движками ECMAScript (подробнее об этом ниже).
Попробуем заглянуть внутрь движка, чтобы узнать, как преобразуются файлы JavaScript.
Что скрывает движок JavaScript
Движок — это язык, который разбивает код и переводит его. А V8 — это один из самых популярных движков JavaScript, который используется в Chrome и NodeJS и написан на низкоуровневом языке C++. Написать движок может кто угодно, что может привести к путанице.
Для поддержания контроля за этими механизмами был создан стандарт ECMA, который предоставляет характеристики написания движка и всех функций JavaScript. По этой причине в ECMAScript 6, 7, 8 и т. д. реализованы новые функции JavaScript, а движки обновлены для поддержки этих новых функций. Следовательно, необходимо проверять доступность расширенной функции JS в браузерах во время разработки.
Для примера возьмем движок V8, однако основные концепции остаются неизменными во всех движках.
Теперь рассмотрим с более технической точки зрения.
Движок JavaScript V8
Так выглядит движок JS изнутри. Вводимый код проходит через следующие стадии:
- Парсер
- AST
- Интерпретатор выдает байт-код
- Профайлер
- Компилятор выдает оптимизированный код
Не волнуйтесь, подробности рассмотрим в течение нескольких минут.
Однако для начала разберем важное различие.
Интерпретатор и Компилятор
Есть два способа преобразования кода в машиночитаемый язык. Концепция, о которой мы поговорим, применима не только к JavaScript, но и к большинству ЯП, таких как Python, Java и т. д.
- Интерпретатор читает код построчно и сразу выполняет его.
- Компилятор читает весь код, выполняет оптимизации, а затем производит оптимизированный код.
Рассмотрим на примере.
В приведенном выше примере функция add , которая добавляет два числа и возвращает сумму, вызывается 1000 раз.
- При передаче этого файла интерпретатору, он читает его построчно и сразу выполняет функцию, пока цикл не закончится. Таким образом, он просто переводит код в то, что компьютер понимает на ходу.
- При передаче этого файла компилятору, он читает всю программу, выполняет анализ действий и производит оптимизированный код на языке, который понимает машина. Это как взять X (JS-файл) и создать Y (оптимизированный код, понятный машине). Если использовать интерпретатор для Y (оптимизированный код), результат будет таким же, как при интерпретации X (JS-код).

На изображении выше показано, что байт-код — это просто промежуточный код, который необходимо интерпретировать для обработки компьютером. Как интерпретатор, так и компилятор, преобразуют исходный код в машинный код, единственное отличие состоит в том, как выполняется это преобразование.
- Интерпретатор построчно преобразует исходный код в эквивалентный машинный код.
- Компилятор сразу преобразует весь исходный код в машинный код.
Плюсы и минусы интерпретатора и компилятора
- Преимущество интерпретатора заключается в немедленном выполнении кода без необходимости компиляции, что может быть полезно для запуска JS-файлов в браузере. Однако процесс замедляется при необходимости преобразования большего количества JS-кода. Вспомните маленький фрагмент кода, в котором функция вызывалась 1000 раз. В этом случае вывод остается прежним, даже если функция add была вызвана 1000 раз. Такие ситуации замедляют работу интерпретатора.
- В таких случаях компилятор может выполнить некоторые оптимизации, заменив цикл одним числом 2 (1 + 1 добавлялось каждый раз), поскольку он остается неизменным для всех 1000 итераций. Окончательный код, который выдает компилятор, оптимизирован и выполняется намного быстрее.
Таким образом, интерпретатор сразу начинает выполнение кода, но не выполняет оптимизацию. Компилятору требуется время для компиляции кода, однако он выдает более оптимизированный код.
Теперь вернемся к основной схеме движка JS.
Итак, зная плюсы и минусы компилятора и интерпретатора, можно использовать преимущества каждого. Здесь и появляется компилятор JIT (Just In Time). Он представляет собой комбинацию интерпретатора и компилятора, и большинство браузеров теперь реализуют эту функцию для повышения эффективности. Движок V8 также использует эту функцию.
Движок V8 JavaScript
В этом процессе:
- Парсер идентифицирует, анализирует и классифицирует различные части программы. Например, устанавливает, является ли элемент функцией, переменной и т.д. с помощью ключевых слов JavaScript.
- Затем AST (абстрактные синтаксические деревья) создают древовидную структуру на основе классификации парсера. В AST Explorer можно узнать о том, как строится дерево.
- Затем дерево передается интерпретатору, который создает байт-код. Как мы узнали ранее, байт-код не является кодом самого низкого уровня, однако его можно интерпретировать. На этой стадии браузер работает с доступным байт-кодом с помощью движка V8, чтобы пользователю не приходилось ждать.
- В то же время профайлер ищет оптимизации кода, а затем передает входные данные компилятору. Компилятор выдает оптимизированный код, в то время как байт-код временно используется для рендеринга в браузере. Как только компилятор создает оптимизированный код, временный байт-код полностью заменяется новым оптимизированным кодом.
- Таким образом используются лучшие качества интерпретатора и компилятора. Интерпретатор выполняет код, в то время как профайлер ищет оптимизацию, а компилятор создает оптимизированный код. Затем байт-код заменяется оптимизированным кодом, который является кодом более низкого уровня, таким как машинный код.
Это означает, что производительность будет постепенно улучшаться, не блокируя время выполнения.
Примечание по байт-коду
Как и в случае с машинным кодом, не все компьютеры понимают байт-код. Чтобы интерпретировать его на машиночитаемый язык, необходимо промежуточное ПО, такое как виртуальная машина, или движок (например, Javascript V8). По этой причине браузеры могут выполнять этот байт-код из интерпретатора во время вышеупомянутых 5-ти стадий с помощью движков JavaScript.
В результате возникает следующий вопрос:
Является ли JavaScript интерпретируемым языком?
Да, но не совсем. На ранних этапах JavaScript Брендан Айк создал движок JavaScript ‘SpiderMonkey’. У движка был интерпретатор, который говорил браузеру, что нужно делать. Сейчас есть не только интерпретаторы, но и компиляторы, а код не только интерпретируется, но и компилируется для оптимизации. Технически все зависит от реализации.