Ответ 1
Во-первых, вы делаете неверное предположение: компилируется современный JavaScript. Двигатели типа V8, SpiderMonkey и Nitro компилируют JS-источник в native машинный код хост-платформы.
Даже в более старых версиях JavaScript не интерпретируется. Они преобразуют исходный код в bytecode, который выполняется с помощью виртуальной машины.
На самом деле, как работают на языках Java и .NET: когда вы "компилируете" ваше приложение, вы фактически преобразуете исходный код в байт-код платформы, Java bytecode и CIL соответственно. Затем во время выполнения компилятор JIT компилирует байт-код в машинный код.
Только очень старые и упрощенные JS-движки фактически interpret исходный код JavaScript, потому что интерпретация очень медленная.
Итак, как работает компиляция JS? На первом этапе исходный текст преобразуется в абстрактное синтаксическое дерево (AST), структуру данных, которая представляет ваш код в формате, который могут обрабатывать машины. Концептуально это очень похоже на то, как текст HTML преобразуется в представление DOM, с которым работает ваш код.
Чтобы генерировать AST, движок должен иметь дело с вводом необработанных байтов. Обычно это делается с помощью лексического анализатора. Лексер действительно не читает файл "по очереди"; скорее он читает байты за байтом, используя правила синтаксиса языка для преобразования исходного текста в токены. Затем лексер передает поток токенов в парсер , который фактически создает АСТ. Парсер проверяет, что токены образуют допустимую последовательность.
Теперь вы должны ясно видеть, почему синтаксическая ошибка мешает вашему коду работать вообще. Если в исходном тексте появляются неожиданные символы, двигатель не может сгенерировать полный AST, и он не может перейти к следующей фазе.
Как только двигатель имеет AST:
- Интерпретатор может просто начать выполнять инструкции непосредственно из АСТ. Это очень медленно.
- Реализация JS VM использует AST для генерации байт-кода, затем начинает выполнение байт-кода.
- Компилятор использует AST для генерировать машинный код, который выполняет процессор.
Итак, теперь вы должны видеть, что, как минимум, выполнение JS происходит в два этапа.
Однако фазы выполнения действительно не влияют на то, почему ваш пример работает. Он работает из-за правил , которые определяют, как программы JavaScript должны оцениваться и выполняться. Правила можно было бы так же легко записать таким образом, чтобы ваш пример не работал, не влияя на то, как сам движок интерпретирует/компилирует исходный код.
В частности, JavaScript имеет функцию, известную как подъем. Чтобы понять подъем, вы должны понимать разницу между объявлением функции и выражением функции.
Просто объявление функции - это когда вы объявляете новую функцию, которая будет вызываться в другом месте:
function foo() {
}
Выражение функции - это когда вы используете ключевое слово function
в любом месте, которое ожидает выражение, например, назначение переменной или аргумент:
var foo = function() { };
$.get('/something', function() { /* callback */ });
JavaScript-мандаты, чтобы объявления функций (первый тип) были назначены именам переменных в начале контекста выполнения, независимо, где объявление появляется в исходном тексте (контекста). Контекст выполнения примерно равнозначен области – в простом выражении, код внутри функции или самый верх вашего script, если не внутри функции.
Это может привести к очень любопытным поведением:
var foo = function() { console.log('bar'); };
function foo() { console.log('baz'); }
foo();
Что вы ожидаете от входа в консоль? Если вы просто читаете код линейно, вы можете подумать baz
. Тем не менее, он будет фактически регистрировать bar
, потому что объявление foo
поднимается над выражением, которое присваивает foo
.
Итак, заключаем:
- Исходный код JS никогда не "читается" по очереди.
- Исходный код JS фактически скомпилирован (в истинном смысле слова) в современных браузерах.
- Двигатели компилируют код в несколько проходов.
- Поведение вашего примера является побочным продуктом правил языка JavaScript, а не тем, как оно компилируется или интерпретируется.