Большинство людей начинают изучение JavaScript с создания чего-то практического. Вы привязываете действие к кнопке, загружаете какие-то данные и наблюдаете, как меняется DOM. А затем абстракции начинают «протекать». Всплывают ошибки, которые кажутся бессмысленными: переменные существуют до их объявления, функции помнят переменные, к которым не должны иметь доступа, а ключевое слово this указывает на window, на кнопку или вовсе ни на что. Обычно именно в этот момент приходит понимание, что нужно заглянуть «под капот» и разобраться, что на самом деле делает движок.
Контекст выполнения: двухфазная настройка
Когда движок JavaScript в вашем браузере или Node.js встречает скрипт, он не просто читает файл сверху вниз, как человек, сканирующий страницу. Вместо этого он создает контекст выполнения — контейнер, который содержит всё необходимое для запуска определенного фрагмента кода. Каждый контекст выполнения проходит через две отчетливые фазы.
Фаза создания памяти. Во время этого начального прохода движок сканирует всю область видимости и выделяет память для каждого найденного объявления переменной или функции. Если он видит var, он резервирует место и сохраняет undefined в качестве заполнителя. Если он видит объявление функции, он сохраняет всё тело функции целиком. Именно поэтому функцию, объявленную с помощью традиционного ключевого слова function, можно вызвать из строк, которые находятся выше в той же области видимости. Движок уже знает о ней до того, как начнет выполнение.
Фаза выполнения кода. Теперь движок выполняет ваш код строка за строкой. Здесь происходят присваивания. Здесь вычисляются выражения. Здесь вызываются функции. Если вы написали var name = "Alice";, заполнитель из первой фазы наконец-то заменяется строкой. Понимание этого двухэтапного поведения проясняет удивительно много моментов. Движок не работает небрежно; он следует строгому алгоритму настройки.
Переменные и временная мертвая зона
Выбор между var, let и const — это не просто вопрос стиля. var имеет область видимости на уровне функции, что означает, что он полностью игнорирует фигурные скобки. Объявите его внутри блока if, и он «вытечет» наружу. Такое поведение могло иметь смысл в ранних версиях JavaScript, но в современных приложениях оно создает серьезные проблемы при поддержке кода. let и const имеют блочную область видимости. Они уважают фигурные скобки и исчезают, когда блок заканчивается.
Существует также тонкое, но критически важное различие в том, как хоистинг (поднятие) обрабатывает их. Объявления var поднимаются и сразу инициализируются значением undefined. let и const технически тоже поднимаются; движок знает об их существовании еще до того, как дойдет до строки объявления. Но они не инициализируются. Они находятся в своего рода «лимбе», называемом временной мертвой зоной (temporal dead zone). Если вы попытаетесь прочитать их до того, как выполнится строка объявления, вы получите жесткую ошибку ReferenceError вместо незаметного undefined. Этот сбой на самом деле полезен: он не позволяет логике продолжаться на основе неинициализированных данных.
Лексическая область видимости и замыкания
Область видимости отвечает на простой вопрос: где я могу получить доступ к этой переменной? JavaScript использует лексическую область видимости, что означает: права доступа функции определяются тем, где она была физически написана в исходном коде, а не тем, где она вызывается. Если вы определяете функцию внутри другой функции, внутренняя функция может «дотянуться» до родительской, чтобы прочитать её переменные. Внешняя функция не может заглянуть внутрь. Эти отношения статичны. Вы можете передать эту внутреннюю функцию между модулями, сохранить её в глобальной переменной и вызвать из совершенно другого файла. Она всё равно будет помнить переменные из той области видимости, в которой она была «рождена».
Такое поведение естественным образом порождает замыкания. Замыкание формируется, когда внутренняя функция сохраняет ссылку на переменную из своей внешней области видимости. Даже после того, как внешняя функция завершит выполнение и её локальные переменные должны быть собраны сборщиком мусора, JavaScript сохраняет их в памяти, потому что внутренняя функция всё еще нуждается в них. Внутренняя функция несет свое окружение с собой.
Это не просто академическая деталь. Замыкания дают практический способ создания приватного состояния в языке, где отсутствуют явные модификаторы доступа.
function makeCounter() {
let count = 0;
return function() {
count = count + 1;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
Здесь count скрыта. Ничто за пределами возвращаемой функции не может сбросить её или прочитать напрямую. Это приватная переменная, созданная исключительно за счет механики областей видимости.
Хоистинг на практике
It is common to hear that JavaScript "moves declarations to the top." That is a useful mental model, but the code does not actually get rewritten. During the memory creation phase, the engine simply registers declarations before execution begins. A statement like var x = 5; behaves as if the declaration and initialization are decoupled. The declaration var x; is processed early and initialized to undefined. The assignment x = 5; stays exactly where you wrote it and runs during the execution phase.
Because of this, var can lead to surprising results. A variable used near the top of a function might hold undefined even though a giant assignment sits at the bottom. Using let and const eliminates that particular foot-gun because the temporal dead zone forces you to keep your declarations above your usage.
How this Gets Its Value
If execution context and scope determine where variables live, this determines which object is currently in charge. Unlike lexical variables, this is not decided by where a function is written. It is decided entirely by how the function is called.
Default binding occurs when you invoke a plain, standalone function. In non-strict mode, this falls back to the global object. In a browser, that is window. Call a function without any context, and you might accidentally be touching global state without realizing it.
Implicit binding happens when you call a function as a method on an object. If you write user.sayName(), the dot quietly tells the engine to set this to user for the duration of that call. The site of the call matters more than the site of the definition.
Explicit binding lets you override everything manually. call() and apply() invoke a function immediately while forcing this to be a specific object you provide. The only difference between them is how arguments are passed: call takes a comma-separated list, while apply takes an array. bind() works differently. It does not invoke the function right away. Instead, it returns a new function with this permanently locked to the value you supplied. This is invaluable for callbacks that might otherwise lose their context when passed around.
New binding comes into play when you use the new keyword in front of a function call. The engine constructs a brand-new empty object, sets its prototype linkage, and points this inside the constructor to that fresh instance.
Writing Code That Lasts
Understanding mechanics is only half the craft. The other half is writing code that humans can read six months from now.
DRY (Don't Repeat Yourself) sounds obvious, but it is violated constantly. If you find yourself writing the same validation logic or API call pattern in three different files, extract it. Write one function. One source of truth means one place to update when requirements change, and that saves time in ways that are difficult to overstate.
KISS (Keep It Simple, Stupid) is a defense against ego. Nested ternaries and one-liner closures feel clever, but they cost hours during debugging. The closure pattern I showed earlier is powerful, yet nesting five levels deep just because you can is a mistake. Simple code survives team turnover, production incidents, and those middle-of-the-night alerts when something breaks and nobody remembers why.
The Real Payoff
Studying execution
