JavaScript - один из самых любимых и в то же время ненавистных языков в мире. Его любят, потому что он мощный. Вы можете создать полнофункциональное приложение, просто изучив JavaScript и ничего более. А также его ненавидят, потому что иногда он ведет себя совершенно неожиданным образом. И это может расстроить или даже заставить вас возненавидеть его.

В этой статье я подробно объясню, как JavaScript выполняет код в браузере, и мы изучим это с помощью гифок. Прочитав эту статью, вы станете на шаг ближе к статусу рок-звезды разработки.

Контекст выполнения

«Все в JavaScript происходит внутри контекста выполнения (Execution Context)»

Было бы круто, чтобы вы запомнили эту фразу, потому что она очень важна. Скажем, что этот контекст выполнения является большим контейнером, вызываемым, когда браузер хочет запустить какой-то код JavaScript.

В этом контейнере есть два компонента: 1. Компонент памяти. 2. Компонент кода.

Компонент памяти также известен как переменная среды. В этом компоненте памяти переменные и функции хранятся в виде пар ключ-значение.

Компонент кода - это место в контейнере, где код выполняется по одной строке за раз. У этого компонента кода тоже есть необычное название, а именно «Поток выполнения» (Thread of Execution).

JavaScript - это синхронный однопоточный язык. Все потому, что он может выполнять только одну команду за раз и в определенном порядке.

Выполнение кода

Возьмем простой пример:

var a = 2;
var b = 4;

var sum = a + b;

console.log(sum);

В этом простом примере мы инициализируем две переменные, a и b, и сохраняем 2 и 4 соответственно. Затем мы складываем значение a и b и сохраняем его в переменной суммы.

Посмотрим, как JavaScript выполнит код в браузере.

Браузер создает глобальный контекст выполнения с двумя компонентами, а именно с памятью и компонентами кода.

Браузер выполнит код JavaScript в два этапа.

  1. Фаза выделения памяти

  2. Этап выполнения кода

На этапе выделения памяти JavaScript сканирует весь код и выделяет память для всех переменных и функций в коде. Для переменных JavaScript будет хранить undefined на этапе выделения памяти, а для функций он сохранит весь код функции, который мы рассмотрим в следующем примере.

Теперь, на 2-м этапе, то есть при выполнении кода, он начинает проходить весь код построчно. Когда он встречает var a = 2, он присваивает значение 2 переменной 'a'. До сих пор значение «а» не было определено.

То же самое и с переменной b. Он присваивает 4 переменной «b». Затем он вычисляет и сохраняет значение суммы в памяти, равное 6. Теперь, на последнем шаге, он выводит значение суммы в консоль, а затем уничтожает глобальный контекст выполнения по мере завершения нашего кода.

Как вызываются функции в контексте выполнения?

Функции в JavaScript, если сравнивать их с другими языками программирования, работают по-другому.

Возьмем простой пример:

var n = 2;

function square(num) {
 var ans = num * num;
 return ans;
}

var square2 = square(n);
var square4 = square(4);

В приведенном выше примере есть функция, которая принимает в качестве аргумента число и возвращает его квадрат.

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.

Стек вызовов - это механизм, позволяющий отслеживать свое место в скрипте, вызывающем несколько функций.

Пример:

function a() {
    function insideA() {
        return true;
    }
    insideA();
}
a();

Мы создаем функцию «a», которая вызывает другую функцию «insideA», которая возвращает значение true. Я знаю, что код бессмысленный и ничего не делает, но он поможет нам понять, как JavaScript обрабатывает коллбеки (функции обратного вызова).

JavaScript создаст глобальный контекст выполнения. Глобальный контекст выполнения выделит память для функции 'a' и вызовет 'function a' на этапе выполнения кода. Контекст выполнения создается для функции a, которая размещается над глобальным контекстом выполнения в стеке вызовов.

Функция a назначит память и вызовет функцию insideA. Контекст выполнения создается для функции insideA и помещается над стеком вызовов 'function a'. Теперь эта функция insideA вернет true и будет удалена из стека вызовов. Поскольку внутри 'function a' нет кода, контекст выполнения будет удален из стека вызовов.

Наконец, глобальный контекст выполнения также удаляется из стека вызовов.

Спасибо за редактуру @Viistomin

Комментарии (12)


  1. Alexandroppolus
    04.10.2021 16:12
    +6

    JavaScript - это синхронный однопоточный язык

    Не бывает "однопоточных" и "многопоточных" языков. Бывают соответствующие среды исполнения. В языке может быть синтаксическая обертка над вызовом функции запуска потока (или иных действий с потоками), но суть от этого не меняется.

    Для js основные среды исполнения (браузер и нода) уже давно умеют создавать самые обычные потоки-воркеры.


    1. chtulhu
      04.10.2021 17:11

      Вы про web worker (в браузере)?


      1. Alexandroppolus
        04.10.2021 17:33

        ага


        1. chtulhu
          04.10.2021 18:53
          +1

          А разве web worker это не костыль для узкого спектра задач? Если в golang я могу любые задачи разбить на горутины, то web worker выполняет какую-то работу для основного процесса. У них нет общего контекста исполнения(только через api).

          Разве я могу с помощью web worker распараллелить обработку DOM событий, рендеринг элементов или любые другие вычисления в контексте одного конкретного документа?

          Поправьте меня, если я не прав.


          1. Alexandroppolus
            04.10.2021 19:23

            Да, воркеры - сугубо числодробильня, они занимаются cpu-bound задачами, дабы не тормозить основной поток. С домом и отрисовкой они не помогают. Да и не совсем понятно, как могли бы помочь. Модифицировать дом-дерево? В оном не бывает такого большого количества изменений на единицу времени, кроме совсем уж "узкого спектра задач". Рефлоу/репаинт? Это делается в основном потоке на, условно, каждом витке событийного цикла, а не когда вздумается. Там больше ресурсов уйдет на синхронизацию этого дела между потоками. Совместный доступ к дому не нужен, имхо.

            А вот, допустим, забабахать какой-нибудь сложный фильтр для здоровенной картинки с канвы - самое то. Берутся попиксельные данные, отправляются в воркер (не копируются, а просто передаются), после обработки присылаются обратно. В роли синхронизатора - механизм сообщений между потоками.

            Если хочется компактно и сиюминутно запустить воркер, не создавая к нему отдельный js-файл, а прямо вот передать функцию, то можно код этой функции положить в блоб, взять от блоба URL.createObjectURL с ним создать воркер. При отсутствии в функции замыканий и ссылок на кастомные объекты, всё отработает как надо.


            1. chtulhu
              04.10.2021 19:38

              Берутся попиксельные данные, отправляются в воркер

              т.е. чисто теоретически, делегируем другому процессу. Если убрать тайминговые накладки, то то же самое я мог бы отправить на какой-то сервер, делегировав ему.

              cpu-bound задачами

              Можете из опыта набросать список таких задач, для расширения моего кругозора.

              Модифицировать дом-дерево?

              На глазок, setTimeout/interval, колбеки событий (например скрол, движения мыши). Да та же сортировка, фильтрация больших объемов данных, без лишних накладок в виде пересылки их куда-либо.


              1. dynamicult
                05.10.2021 01:50

                т.е. чисто теоретически, делегируем другому процессу. 

                Не процессу, а изоляту, работающему в параллельном потоке. Это не зеленые треды с каким-то своим шедулером, как в го, это обычные нативные потоки операционной системы, работающие в одном общем адресном пространстве.
                Вы можете пересылать между ними данные в буферах памяти без копирования, передачей владения. Вы можете использовать между ними общие буферы памяти и обращаться к одному и тому же буферу одновременно из множества потоков, устраивать ситуации гонки и разруливать их с помощью примитивов синхронизации.


          1. Fen1kz
            04.10.2021 19:46

            В вебе примерно как в андроиде — есть UI-тред, к нему много чего привязано и, конечно, можно сделать отдельные потоки для выполнения чего-либо, но как правило не очень нужно.


            Точно не знаю, но сомневаюсь, что в golang по-другому — если делать UI на golang, точно так же у вас будет UI-тред, от которого вы вряд ли уйдете далеко. Если в го реально можно повесить скролл интерфейса на другой поток, то отпишитесь плиз, я присмотрюсь, это прям интересно


    1. sshikov
      04.10.2021 17:16

      >Не бывает «однопоточных» и «многопоточных» языков.
      Я скорее согласен с вами, чем с автором. С одним уточнением — если в языке нет никаких (языковых) механизмов синхронизации потоков, при многопоточной среде исполнения писать на нем будет не так удобно. Можно конечно все через библиотеки, но разница все-таки будет, и временами довольно заметная. Ну т.е. есть языки, которые с рождения учитывают, что среда будет многопоточная, и есть такие, к которым ее прикрутили позже.


      1. Alexandroppolus
        04.10.2021 17:37

        Ну, например, когда-то я кодил на WinApi, на плюсах. Там, разумеется, были и потоки, и объекты синхронизации, причем всё в виде обычных функций (а не специального синтаксиса) из какого-то заголовочного файла, уже не помню какого. И ничего, вроде норм. было.

        В браузерном js есть 3 кейса: 1) обычные данные, которые не шарятся между потоками (копируются при передаче) и не требуют синхронизации, 2) бинарные массивы, которые передаются в поток, становясь недоступными для отправителя, 3) некие SharedArrayBuffer, которые совсем общие и доступ к ним надо синхронизировать (для чего имеется тоже какой-то механизм, но я пока не вкуривал).


        1. sshikov
          04.10.2021 17:45

          >И ничего, вроде норм. было.
          Ну, это уже вопрос вкуса. Я не говорю, что так нельзя. Я говорю, что с подержкой на уровне языка сильно удобнее. Да и структуры данных, например — взять тот же js, и скажем java. Во втором случае у вас есть структуры, которые готовы к многопоточной среде. В первом же — если она вдруг станет многопоточной, возможно что угодно. То есть, сломаться может любой кусок кода, не обернутый в вызовы нужных функций синхронизации. Когда вы заранее пишете под многопоточность — вы об этом думаете. Когда вы вводите ее потом — уже не всегда.


  1. alexshy
    04.10.2021 17:07
    -1

    И зачем только люди Douglas'a Crockford'a читают?

    *Наверное, затем, что если всех индусов читать, никакой жизни не хватит.*