Немного о теории компиляторов

Код, перед выполнением проходит 3 стадии обработки, которые приближенно обьединяются термином компиляция:

  1. Лексический анализ / разбиение на токены ( token ) разбиение последовательности символов на осмысленные (с точки зрения языка) фрагменты, называемые токенами. Для примера возьмем программу var a = 2;. Скорее всего, эта программа будет разбита на следующие токены: var, a, =, 2 и ;. Пропуски могут сохраняться в виде токенов, а могут и не со- храняться в зависимости от того, имеет это смысл или нет.
  2. Разбор ( parsing) преобразование потока (массива) токенов в дерево вложенных элементов, которые в совокупности представляют грамматическую структуру программы. Это дерево называется «абстрактным деревом синтаксиса», или AST(Abstract Syntax Tree). Скажем, дерево для var a = 2; может начинаться с узла верхнего уровня VariableDeclaration, который содержит дочерний узел Identifier (со значением a) и другой дочерний узел AssignmentExpression, у которого есть свой дочерний узел с именем NumericLiteral (его значение равно 2).
  3. Генерирование кода — процесс преобразования AST в исполняемый код. Эта часть сильно зависит от языка, целевой платформы и т. д.

Движок JavaScript не ограничивается только этими 3 этапами . Это общая картина.

Область видимости

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

Например,

let a = 2
  1. Когда компилятор обнаружил let a , он обращается к области видимости , чтобы узнать, существует ли переменная A в наборе этой конкретной области видимости .
    • Если переменная существует: компилятор игнорирует объявление и двигается дальше.
    • Если переменная не существует: компилятор обращается к области видимости для объявления новой переменной с именем a , в наборе этой области видимости.
  2. компилятор генерирует код для последующего выполнения . Движок обрабатывает присваивание a=2. Код спрашивает у области видимости доступна ли переменная с именем A в наборе текущей области видимости,
    • Если переменная доступна, то движокJS использует эту переменную.
    • Если нет, то движок ищет переменную в другом месте.
  3. Если движокJS находит переменную , он присваивает ей значение 2. А если нет, то сообщает об ошибке.

Когда Движок выполняет код, сгенерированный Компилятором на шаге 2, он должен провести поиск переменной a и определить, была ли она объявлена; этот поиск называется проверкой Области видимости. Однако тип проверки, выполняемой Движком, влияет на результат поиска.

В нашем примере Движок будет выполнять LHS-поиск переменной a. Другая разновидность поиска называется RHS-поиск. Сокращения означают «LeftHand Side» (левосторонний) и «RightHand Side» (правосторонний).

«Левая/правая сторона присваивания» в обозначениях LHS и RHS не обязательно буквально означает «левая/правая сторона оператора присваивания =». Присваивание также может выполняться другими способами, поэтому лучше концептуально рассматривать их как «приемник присваивания» (LHS) и «источник присваивания» (RHS).

Возьмем следующую программу, в которой задействованы как LHS-, так и RHS-ссылки:

function foo(a) {
	console.log( a ); // 2
}
 
foo( 2 );

Последняя строка с вызовом функции foo(..) также требует RHS-ссылки на foo, которая означает «Найти значение foo и предоставить его мне». Более того, (..) означает, что значение foo должно быть выполнено, а значит, это должна быть функция!

Упражнение

  1. Найдите все LHS-поиски (всего 3).
  2. Найдите все RHS-поиски (всего 4).
function foo(a) {
	var b = a;
	return a + b;
}
 
var c = foo( 2 );

Вложенная область видимости

Чтобы наглядно представить процесс разрешения вложенных областей видимости, вообразите высокое здание.

Это здание изображает набор правил вложенных областей видимости. Первый этаж — текущая область видимости, какой бы она ни была, а верхний этаж — глобальная область видимости.

Чтобы разрешить LHS- или RHS-ссылку, система начинает поиск с текущего этажа. Если переменная не будет найдена, поиск поднимается на следующий этаж, ищет там, потом на следующем и т. д. Добравшись до верхнего этажа (глобальной области видимости), вы либо находите искомое, либо не находите. В любом случае здесь придется остановиться.

Почему важно отличать LHS от RHS?

Потому что эти два типа поиска по-разному ведут себя в ситуации, когда переменная еще не была объявлена (не найдена ни в одной из просмотренных областей видимости).

Пример:

function foo(a) {
	console.log( a + b );
	b = a;
}
 
foo( 2 );

Когда RHS-поиск для b происходит впервые, он завершается неудачей. Переменная, не найденная в области видимости, считается «необъявленной». Если RHS-поиск не находит переменную ни в одной из вложенных областей видимости, движок выдает ошибку ReferenceError. Важно заметить, что ошибка относится именно к типу ReferenceError.

С другой стороны, если движок выполняет LHS-поиск и прибывает на верхний этаж (глобальная область видимости), так и не обнаружив искомое, если программа не выполняется в строгом режиме strict, в глобальной области видимости создается новая переменная с указанным именем, которая передается движку.

«Нет, такой переменной не было, но я хочу помочь и создать ее для тебя».

Режим strict, добавленный в ES5, во многих отношениях отличается от обычного/нестрогого режима.

Одно из отличий заключается в том, что он запрещает автоматическое/неявное создание глобальных переменных. В этом случае LHS-поиск не вернет переменную с глобальной областью видимости, и движок выдаст ошибку ReferenceError по аналогии со случаем RHS.

Если переменная будет найдена для RHS-поиска, но вы пытаетесь сделать с ее значением нечто невозможное (например, попытка выполнить как функцию значение, которое функцией не является, или обращение к свойству для значения null или undefined), движок выдаст другую разновидность ошибки — TypeError.

Ошибка ReferenceError относится к проблемам при разрешении области видимости, а ошибка TypeError подразумевает, что разрешение области видимости прошло успешно, но была сделана попытка выполнить с результатом недопустимую/невозможную операцию.