Hoisting: поднятие переменных и функций
Что такое hoisting
Hoisting («поднятие») — поведение JavaScript, при котором объявления переменных и функций обрабатываются до выполнения кода в этом скоупе.
Это не физическое перемещение строк — это следствие того, что JavaScript парсит весь код скоупа перед выполнением и заранее регистрирует объявления в Environment Record.
Поднятие функций — полное
Объявления функций поднимаются полностью: имя и тело функции доступны до строки объявления.
// Вызов ДО объявления — работает!
sayHello() // "Привет!"
function sayHello() {
console.log('Привет!')
}
// Что происходит под капотом:
// 1. JavaScript парсит весь файл
// 2. Находит function sayHello — сразу добавляет в Environment Record
// 3. Начинает выполнение — sayHello уже существует
Это позволяет организовывать код в удобном порядке: сначала высокоуровневый код, потом детали реализации.
// Читаемая организация: главное — сверху
function main() {
const data = fetchData()
const result = processData(data)
renderResult(result)
}
// Детали — снизу, хотя вызываются из main выше
function fetchData() { /* ... */ }
function processData(data) { /* ... */ }
function renderResult(result) { /* ... */ }
function name() {}). Функциональные выражения (const fn = function() {}, const fn = () => {}) не поднимаются — они следуют правилам var/let/const.Поднятие var — частичное
var поднимается частично: имя переменной регистрируется в Environment Record, но значение — нет. До строки присваивания переменная равна undefined.
console.log(x) // undefined (не ReferenceError!)
var x = 5
console.log(x) // 5
// Как это видит JavaScript:
// var x; ← поднято (объявление)
// console.log(x) // undefined
// x = 5; ← остаётся на месте (присваивание)
// console.log(x) // 5
Это поведение создаёт неочевидные баги:
function processUser(user) {
console.log(role) // undefined — не ошибка! Запутывает.
if (user.isAdmin) {
var role = 'admin'
}
console.log(role) // 'admin' или undefined
}
// var поднимается в функцию, блок if не создаёт скоуп для var
var в функциональном скоупе
var поднимается до ближайшей функции (или глобального скоупа). Блоки if, for, while для var прозрачны.
function demo() {
for (var i = 0; i < 3; i++) {
var temp = i * 2
}
console.log(i) // 3 — i пробила for-блок!
console.log(temp) // 4 — temp пробила for-блок!
}
// Эквивалентно:
function demo() {
var i // поднято
var temp // поднято
for (i = 0; i < 3; i++) {
temp = i * 2
}
console.log(i) // 3
console.log(temp) // 4
}
Порядок поднятия при конфликтах
Если функция и переменная var имеют одинаковое имя, функция «побеждает»:
console.log(typeof foo) // "function"
var foo = 'переменная'
function foo() {} // поднимается первой
// Но после присваивания:
console.log(typeof foo) // "string"
- Сначала обрабатываются все
functionобъявления — добавляются в Environment Record с их телами - Затем обрабатываются
var— добавляются какundefined(если имя уже занято функцией — не перезаписывают) - Затем начинается выполнение кода построчно
Для let и const имена тоже регистрируются в Environment Record при парсинге, но остаются в TDZ (Temporal Dead Zone) до строки инициализации — об этом в следующей главе.
Итог: сравнение поведения
// Функция — полное поднятие
hello() // "Hello" ✓
function hello() { console.log('Hello') }
// var — частичное поднятие (undefined)
console.log(a) // undefined ✓ (не ошибка)
var a = 1
// let — TDZ (ошибка до объявления)
// console.log(b) // ReferenceError ✗
let b = 2
// const — TDZ (ошибка до объявления)
// console.log(c) // ReferenceError ✗
const c = 3
// Функциональное выражение — как переменная
// fn() // TypeError: fn is not a function ✗
var fn = function() {}