Как 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-разработчик или хотите им стать, это значит, что вам нужно разбираться во внутренних механизмах выполнения 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-разработчикам?
Where should I put my JavaScript code?
I would like to use the code for the auto-complete. The code is here.
However, I cannot figure out where I should put this code. In head? In body?
![]()
5 Answers 5
When to put script in HEAD
Scripts to be executed when they are called, or when an event is triggered, are placed in functions. Put your functions in the head section, this way they are all in one place, and they do not interfere with page content.
When to put script in BODY
If you don’t want your script to be placed inside a function, or if your script should write page content, it should be placed in the body section.
So in your case. You can put the script in the body
You should probably put your code right at the end of the body tag.
If you have multiple script includes and need to convince yourself that they will load in the correct order for you, check out WebSiteOptimization.com’s Article on the Defer Attribute, where you can see the order your scripts execute.
Put it into external file and then link that file with HTML document using:
Как написать и запустить свою первую программу на Node.js
Node.js – это популярная открытая среда выполнения, которая может запускать JavaScript вне браузера при помощи механизма V8. Механизм V8 используется для обработки JavaScript в браузере Google Chrome. Среда Node обычно используется для разработки инструментов командной строки и веб-серверов.
Умея работать в Node.js, вы сможете писать код для фронтенда и бэкенда на одном языке. Так будет проще переключаться между контекстами. Кроме того, вы сможете использовать одни и те же библиотеки на фронтенде и бэкенде.
Благодаря поддержке асинхронного выполнения Node.js отлично справляется с задачами с высокой нагрузкой I/O, потому он отлично подходит для веб-разработки. Приложения реального времени (например, видео-стримы или приложения, которые непрерывно отправляют и получают данные) могут работать более эффективно, если написать их в Node.js.
В этом мануале вы узнаете, как написать свою первую программу в среде выполнения Node.js. Вы познакомитесь с некоторыми концепциями, специфичными для Node. Мы создадим программу, которая поможет пользователям проверять переменные среды в своей системе. Для этого мы научимся выводить строки на консоль, собирать пользовательский ввод и получать доступ к переменным среды.
Требования
- Установка Node.js (в данном мануале мы используем версию 10.16.0). Чтобы установить эту среду выполнения на macOS, обратитесь к мануалу Установка Node.js и настройка локальной среды разработки в macOS. Для Ubuntu выполните мануал Установка Node.js в Ubuntu 18.04 (рекомендуем устанавливать через PPA).
- Базовые навыки работы с JavaScript. Полезные руководства есть в нашем Информатории.
1: Вывод на консоль
Чтобы создать простейшую программу “Hello, World!”, откройте текстовый редактор (например, nano) и создайте новый файл:
В файл введите следующий код:
Объект console в Node.js предоставляет простые методы для записи в stdout, stderr или в любой другой поток Node.js (что в большинстве случаев является командной строкой). Метод log выводит в поток stdout, так что вы можете увидеть его в своей консоли.
В контексте Node.js потоки – это объекты, которые могут принимать (например поток stdout) или выводить данные (например сетевой сокет или файл). В случае потоков stdout и stderr любые отправленные им данные будут показаны в консоли. Одна из замечательных особенностей потоков заключается в том, что они легко перенаправляются, и вы можете, например, просто перенаправить вывод своей программы в файл.
Сохраните и закройте nano, нажав сочетание клавиш CTRL + X, при появлении запроса на сохранение файла нажмите Y. Теперь ваша программа готова к запуску.
2: Запуск программы
Чтобы запустить свою новую программу, используйте такую команду:
Программа hello.js запустится и выведет такой результат:
Интерпретатор Node.js прочитал файл и выполнил строку:
вызвав метод log глобального объекта console. Строка “Hello World” была передана в качестве аргумента функции log.
Кавычки необходимы в коде, чтобы определить границы строки, но в результате они не выводятся на экран.
Убедившись, что программа работает, мы можем сделать ее более интерактивной.
3: Получение пользовательского ввода с помощью аргументов командной строки
Каждый раз, когда вы запускаете программу Node.js «Hello, World!», она выдает один и тот же результат. Чтобы сделать программу более динамичной, давайте научим ее собирать информацию от пользователя и отображать ее на экране.
Инструменты командной строки обычно принимают различные аргументы, которые изменяют их поведение. Например, команда node с аргументом –version печатает установленную версию, а не запускает интерпретатор. На этом этапе мы научим код принимать пользовательский ввод с помощью аргументов командной строки.
Создайте новый файл arguments.js в nano:
Введите следующую строку:
Объект process – это глобальный объект Node.js, который содержит функции и данные, связанные с текущим запущенным процессом Node.js. Свойство argv – это массив строк, содержащий все аргументы командной строки, заданные программе.
Сохраните и закройте файл.
Теперь при запуске программы можно использовать аргументы:
node arguments.js hello world
Вы получите такой вывод:
[ ‘/usr/bin/node’,
‘/home/8host/first-program/arguments.js’,
‘hello’,
‘world’ ]
Первым аргументом в массиве process.argv всегда является местоположение двоичного файла Node.js, с помощью которого выполняется программа. Второй аргумент – это всегда местоположение запускаемого файла. Остальные аргументы – это то, что ввел пользователь, в данном случае hello и world.
Нас больше всего интересуют не те аргументы, которые по умолчанию предоставляет Node.js, а аргументы, введенные пользователем. Откройте файл arguments.js в редакторе:
Измените console.log (process.arg); таким образом:
Поскольку argv является массивом, вы можете использовать встроенный в JavaScript метод slice, который возвращает набор элементов. Используя функцию slice с аргументом 2, вы получаете все элементы argv, которые идут в массиве после второго элемента, то есть аргументы, введенные пользователем.
Перезапустите программу с помощью команды node и тех же аргументов, что и в прошлый раз:
node arguments.js hello world
Теперь вывод выглядит так:
Итак, программа может собирать пользовательские данные, давайте теперь научимся собирать данные из среды программы.
4: Доступ к переменным среды
Переменные среды – это данные типа «ключ-значение», хранящиеся вне программы и предоставляемые операционной системой. Как правило, они устанавливаются системой или пользователем и доступны всем запущенным процессам для конфигурации или определения состояния. Вы можете использовать объект process, чтобы получить доступ к переменным среды.
Используйте nano для создания нового файла environment.js:
Добавьте следующий код:
Объект env хранит все переменные среды, которые доступны, когда Node.js запускает программу.
Сохраните и закройте файл, а затем запустите файл environment.js с помощью команды node.
После запуска программы вы должны увидеть подобный вывод:
…
COLORTERM: ‘truecolor’,
SSH_AUTH_SOCK: ‘/run/user/1000/keyring/ssh’,
XMODIFIERS: ‘@im=ibus’,
DESKTOP_SESSION: ‘ubuntu’,
SSH_AGENT_PID: ‘1150’,
PWD: ‘/home/8host/first-program’,
LOGNAME: ‘8host’,
GPG_AGENT_INFO: ‘/run/user/1000/gnupg/S.gpg-agent:0:1’,
GJS_DEBUG_TOPICS: ‘JS ERROR;JS LOG’,
WINDOWPATH: ‘2’,
HOME: ‘/home/8host’,
USERNAME: ‘8host’,
IM_CONFIG_PHASE: ‘2’,
LANG: ‘en_US.UTF-8’,
VTE_VERSION: ‘5601’,
CLUTTER_IM_MODULE: ‘xim’,
GJS_DEBUG_OUTPUT: ‘stderr’,
LESSCLOSE: ‘/usr/bin/lesspipe %s %s’,
TERM: ‘xterm-256color’,
LESSOPEN: ‘| /usr/bin/lesspipe %s’,
USER: ‘8host’,
DISPLAY: ‘:0’,
SHLVL: ‘1’,
PATH:
‘/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin’,
DBUS_SESSION_BUS_ADDRESS: ‘unix:path=/run/user/1000/bus’,
_: ‘/usr/bin/node’,
OLDPWD: ‘/home/8host’ >
Имейте в виду, что многие переменные среды зависят от конфигурации и настроек вашей системы, и ваши выходные данные могут существенно отличаться от того, что вы видите здесь.
Вместо длинного списка переменных среды вам может потребоваться получить конкретную переменную. Давайте попробуем это сделать.
5: Доступ к конкретной переменной среды
На этом этапе мы научимся просматривать переменные среды с помощью глобального объекта process.env и выводить их значения на консоль.
Объект process.env выполняет простое сопоставление имен переменных среды и их значений, хранящихся в виде строк. Как и с другими объектами в JavaScript, вы получите доступ к отдельному свойству, ссылаясь на его имя в квадратных скобках.
Откройте файл environment.js для редактирования:
Измените console.log (process.env) так:
Сохраните файл и закройте файл. Теперь запустите программу environment.js:
Вывод теперь выглядит так:
Вместо того чтобы отображать весь объект, вы теперь выводите только свойство HOME для process.env, в котором хранится значение переменной $HOME.
Опять же, ваш вывод, вероятно, будет отличаться от того, что вы видите здесь, потому что он специфичен для вашей системы.
Теперь, когда вы можете извлекать конкретные переменные среды, вы можете улучшить свою программу: она может запрашивать у пользователя переменную, которую он хочет получить.
6: Извлечение аргумента в ответ на ввод пользователя
Давайте используем возможность чтения аргументов командной строки и переменных среды для создания утилиты, которая выводит значение переменной среды на экран.
С помощью nano создайте новый файл echo.js:
Добавьте следующий код:
const args = process.argv.slice(2);
console.log(process.env[args[0]]);
В первой строке echo.js в переменной args сохраняются все аргументы командной строки, предоставленные пользователем. Вторая строка отображает переменную среды, хранящуюся в первом элементе args, то есть первый аргумент командной строки, предоставленный пользователем.
Сохраните и закройте файл. Запустите программу:
node echo.js HOME
Аргумент HOME был сохранен в массиве args, который затем использовался для поиска значения в среде через объект process.env.
Теперь вы можете получить доступ к значению любой переменной среды в вашей системе. Чтобы убедиться в этом, попробуйте просмотреть следующие переменные: PWD, USER, PATH.
Получение отдельных переменных – это хорошо, но лучше бы позволить пользователям указывать количество переменных.
7: Просмотр нескольких переменных среды
В настоящее время приложение может выдавать только одну переменную среды за один раз. Было бы полезно научить программу принимать несколько аргументов командной строки и выводить соответствующие значения. Используйте nano, чтобы отредактировать echo.js:
Отредактируйте файл так:
const args = process.argv.slice(2);
args.forEach(arg => <
console.log(process.env[arg]);
>);
Метод forEach – это стандартный метод JavaScript для всех объектов массива. Он принимает функцию обратного вызова, которая используется при выполнении итерации по каждому элементу массива. Мы используем forEach для массива args, предоставляя ему функцию обратного вызова, которая выводит значение текущего аргумента в среде.
Сохраните и закройте файл. Теперь перезапустите программу с двумя аргументами:
node echo.js HOME PWD
Вы должны увидеть следующий вывод:
Функция forEach обеспечивает вывод каждого аргумента командной строки в массиве args.
Теперь программа может извлекать переменные, которые запрашивает пользователь. Осталось только разобраться с неверным пользовательским вводом.
8: Обработка неправильного пользовательского ввода
Попробуйте передать программе неправильный аргумент:
node echo.js HOME PWD NOT_DEFINED
Вывод будет выглядеть примерно так:
/home/8host
/home/8host/first-program
undefined
Первые две строки правильные, а последняя строка имеет значение undefined. В JavaScript неопределенное значение undefined означает, что переменной или свойству не было присвоено значение. Поскольку NOT_DEFINED не является допустимой переменной среды, ее значение отображается как undefined.
Лучше вместо этого показать пользователю сообщение об ошибке, если аргумент командной строки не найден в среде.
Отредактируйте код echo.js таким образом:
const args = process.argv.slice(2);
args.forEach(arg => <
let envVar = process.env[arg];
if (envVar === undefined) <
console.error(`Could not find «$
> else <
console.log(envVar);
>
>);
Мы изменили функцию обратного вызова для forEach, и теперь она делает следующие вещи:
- Получает значение аргумента командной строки в среде и сохраняет его в переменной envVar.
- Проверяет, не является ли значение envVar undefined.
- Если envVar undefined, функция выводит полезное сообщение о том, что значение не удалось найти.
- Если переменная среды была найдена, она выведет ее значение.
Примечание: Функция console.error выводит сообщение на экран через поток stderr, а console.log – через поток stdout. Когда вы запускаете эту программу через командную строку, вы не видите разницы между потоками stdout и stderr. Однако ошибки рекомендуется выводить через поток stderr, чтобы их было легче идентифицировать и обрабатывать другими программами, которые чувствуют эту разницу.
Теперь выполните следующую команду еще раз:
node echo.js HOME PWD NOT_DEFINED
На этот раз получится:
/home/8host
/home/8host/first-program
Could not find «NOT_DEFINED» in environment
Теперь, когда программа получает аргумент командной строки, который не соответствует ни одной переменной, она выводит четкое сообщение об ошибке.
Заключение
Начав с простой программы «Hello World», вы написали утилиту командной строки Node.js, которая считывает пользовательские аргументы и выводит переменные среды.
Если вы хотите продолжить работу, вы можете еще больше изменить поведение этой программы. Например, можно проверить аргументы командной строки перед выводом результатов. Если один из аргументов не определяется, вы можете вернуть ошибку, и пользователь получит вывод, только если все аргументы будут соответствовать переменным среды.