← Курс/var, let, const — в чём разница? Что такое hoisting?#128 из 257+40 XP

var, let, const — в чём разница? Что такое hoisting?

Краткий ответ

Главные различия: **область видимости** и **hoisting**. var имеет функциональную область видимости и поднимается с инициализацией undefined. let и const имеют блочную область видимости и тоже поднимаются, но остаются в "Temporal Dead Zone" (TDZ) до строки объявления — обращение к ним до объявления выбрасывает ReferenceError. const запрещает переприсваивание, но не мутацию объектов. Современный стандарт: используй const по умолчанию, let когда нужно переприсваивание, var не используй.

Полный разбор

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

// var — ФУНКЦИОНАЛЬНАЯ область видимости
function example() {
  if (true) {
    var x = 10  // видна во всей функции, не только в if-блоке
  }
  console.log(x)  // 10 — работает!
}

// let/const — БЛОЧНАЯ область видимости
function example2() {
  if (true) {
    let y = 20   // видна только внутри {}
    const z = 30 // тоже только внутри {}
  }
  console.log(y)  // ReferenceError: y is not defined
}

Блок — это любые фигурные скобки: if, for, while, функция, просто {}.

Hoisting (Поднятие)

JavaScript **перед выполнением** сканирует код и "поднимает" объявления переменных и функций наверх их области видимости.

var — поднимается и инициализируется как undefined:

console.log(a)  // undefined (не ошибка!)
var a = 5
console.log(a)  // 5

// JS видит это так:
var a = undefined   // поднято наверх
console.log(a)      // undefined
a = 5               // присваивание остаётся на месте
console.log(a)      // 5

let/const — поднимаются, но остаются в TDZ (Temporal Dead Zone):

console.log(b)  // ReferenceError: Cannot access 'b' before initialization
let b = 5

// let/const ПОДНИМАЮТСЯ (движок о них знает), но к ним нельзя обращаться
// до строки объявления. Зона от начала блока до объявления = TDZ.

TDZ — почему это важно:

// Пример TDZ в действии
{
  // ← здесь начинается TDZ для 'value'
  console.log(value)  // ReferenceError! (TDZ)
  // ← здесь заканчивается TDZ
  let value = 42
  console.log(value)  // 42
}

Hoisting функций

Функции-объявления (function declaration) поднимаются **целиком** — их можно вызывать до объявления:

greet()  // "Привет!" — работает!

function greet() {
  console.log('Привет!')
}

// Но function expression (через var/let/const) — НЕ поднимается
hello()  // TypeError: hello is not a function
var hello = function() { console.log('Hello') }

const: запрет переприсваивания, не мутации

const num = 42
num = 100  // TypeError: Assignment to constant variable

const obj = { name: 'Alice' }
obj.name = 'Bob'     // OK! — мутация разрешена
obj.age = 25         // OK! — добавление свойства разрешено
obj = { name: 'Eve' } // TypeError — переприсваивание запрещено

const arr = [1, 2, 3]
arr.push(4)   // OK! — [1, 2, 3, 4]
arr[0] = 99   // OK! — [99, 2, 3, 4]
arr = []      // TypeError

Для полной неизменяемости объекта используй Object.freeze().

var в цикле — классический баг

// var: все итерации делят ОДНУ переменную i
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)  // 3, 3, 3
}

// let: каждая итерация имеет СВОЮ переменную i
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)  // 0, 1, 2
}

Когда что использовать

const — по умолчанию для всего
  ↓ только если нужно переприсваивание
let — для счётчиков циклов, аккумуляторов, флагов
  ↓ никогда
var — устарел, не использовать в современном коде

Правило: начинай с const, переходи на let только при необходимости.

Таблица сравнения

              var           let           const
Scope         function      block         block
Hoisting      да (undef)    да (TDZ)      да (TDZ)
Переприсв.    да            да            нет
Мутация       да            да            да
Повторное     да            нет           нет
объявление

Связанные уроки курса

  • Переменные — основы var/let/const
  • var и hoisting — подробный разбор
  • Как отвечать на собеседовании

    **Структурируй ответ** по трём критериям: область видимости → hoisting → переприсваивание.

    **Обязательно упомяни TDZ**: многие кандидаты говорят "let не поднимается" — это неточно. Правильно: "поднимается, но находится в TDZ — обращение до объявления выбрасывает ReferenceError, а не возвращает undefined как var".

    **Покажи практику**: назови конкретные последствия — баг с var в цикле, function declaration vs expression.

    **Время ответа**: 2-3 минуты.

    Красные флаги ответа

    1. **"let и const не поднимаются"** — технически неверно. Они поднимаются, но остаются в TDZ. Это путаница, которая выдаёт поверхностное знание.

    2. **"const делает объект неизменяемым"** — нет! const запрещает только переприсваивание. Свойства объекта можно менять. Для заморозки нужен Object.freeze().

    3. **Не знать про var в цикле** — если используешь var в современном коде или не знаешь о проблеме замыкания — это красный флаг для любого JS-разработчика.

    Примеры

    Демонстрация hoisting, TDZ, scope различий и поведения const с объектами

    // ===== 1. HOISTING =====
    console.log('=== Hoisting ===')
    
    // var: поднимается и инициализируется как undefined
    console.log('var до объявления:', typeof varVariable)  // "undefined" (не ошибка!)
    var varVariable = 'привет'
    console.log('var после объявления:', varVariable)  // "привет"
    
    // let: поднимается, но TDZ — нельзя читать до объявления
    try {
      console.log(letVariable)  // ReferenceError
    } catch(e) {
      console.log('let до объявления:', e.constructor.name + ': ' + e.message)
    }
    let letVariable = 'мир'
    console.log('let после объявления:', letVariable)  // "мир"
    
    // ===== 2. ОБЛАСТЬ ВИДИМОСТИ =====
    console.log('\n=== Scope ===')
    
    function scopeDemo() {
      var funcScoped = 'я в функции'
    
      if (true) {
        var funcScoped2 = 'я тоже в функции (var)'
        let blockScoped = 'я только в блоке (let)'
        const alsoBlock = 'я только в блоке (const)'
    
        console.log('Внутри блока, funcScoped:', funcScoped)    // работает
        console.log('Внутри блока, blockScoped:', blockScoped)  // работает
      }
    
      console.log('Вне блока, funcScoped:', funcScoped)   // работает (var)
      console.log('Вне блока, funcScoped2:', funcScoped2) // работает (var)
    
      try {
        console.log(blockScoped) // ReferenceError
      } catch(e) {
        console.log('let вне блока:', e.constructor.name)  // ReferenceError
      }
    }
    scopeDemo()
    
    // ===== 3. HOISTING ФУНКЦИЙ =====
    console.log('\n=== Function Hoisting ===')
    
    // Function declaration — поднимается целиком
    console.log('Вызов до объявления:', add(2, 3))  // 5 — работает!
    
    function add(a, b) {
      return a + b
    }
    
    // Function expression — НЕ поднимается как функция
    try {
      subtract(5, 2)  // TypeError или ReferenceError
    } catch(e) {
      console.log('Expression до объявления:', e.constructor.name)
    }
    
    const subtract = (a, b) => a - b
    
    // ===== 4. CONST И МУТАЦИЯ =====
    console.log('\n=== const и мутация ===')
    
    const config = {
      host: 'localhost',
      port: 3000,
      options: { debug: false }
    }
    
    // Мутация разрешена!
    config.port = 8080
    config.options.debug = true
    config.newField = 'добавили'
    console.log('config после мутации:', config)
    
    // Переприсваивание запрещено
    try {
      config = {}  // TypeError
    } catch(e) {
      console.log('Переприсваивание const:', e.constructor.name)  // TypeError
    }
    
    // Для настоящей заморозки
    const frozen = Object.freeze({ x: 1, y: 2 })
    frozen.x = 99  // молча проигнорируется (в strict mode — ошибка)
    console.log('frozen.x после попытки изменить:', frozen.x)  // 1
    
    // ===== 5. VAR В ЦИКЛЕ =====
    console.log('\n=== var vs let в цикле ===')
    
    const varCallbacks = []
    for (var i = 0; i < 3; i++) {
      varCallbacks.push(() => i)
    }
    console.log('var:', varCallbacks.map(f => f()))  // [3, 3, 3]
    
    const letCallbacks = []
    for (let j = 0; j < 3; j++) {
      letCallbacks.push(() => j)
    }
    console.log('let:', letCallbacks.map(f => f()))  // [0, 1, 2]