← JavaScript/Устаревший var#102 из 383← ПредыдущийСледующий →+15 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Устаревший var

Открываешь легаси-код jQuery-плагина 2012 года или старую кодовую базу без транспилера — везде var. Код работает, но ведёт себя неожиданно: переменная вдруг видна за пределами блока, счётчик цикла в setTimeout всегда равен максимуму. Понять var — значит уметь читать и фиксить старый код.

Какую проблему решает (точнее — создаёт)

До ES6 (2015) var был единственным способом объявить переменную. Его особенности — функциональная область видимости и поднятие — создавали трудноуловимые баги. Именно поэтому в ES6 появились let и const с предсказуемым поведением.

На основе предыдущих уроков

  • Переменные — let, const, var
  • Циклы — for и счётчики
  • Замыкания — замыкания в setTimeout
  • setTimeout — асинхронность и колбэки
  • Область видимости: функция, а не блок

    Главное отличие var от let/const — область видимости. let и const ограничены блоком {}, а var — функцией:

    function checkStatus(isAdmin) {
      if (isAdmin) {
        var role = 'администратор'  // "утекает" за пределы if!
        let title = 'Супер-пользователь'  // только внутри блока
      }
      console.log(role)   // 'администратор' — var виден!
      console.log(title)  // ReferenceError: title is not defined
    }

    Поднятие (Hoisting)

    Объявления var «поднимаются» в начало функции. Переменная существует с самого начала, но значение undefined до строки присваивания:

    function greetUser() {
      console.log(name)  // undefined — НЕ ошибка!
      var name = 'Иван'
      console.log(name)  // 'Иван'
    }
    
    // Движок интерпретирует код как:
    // var name         // поднято в начало функции
    // console.log(name) // undefined
    // name = 'Иван'
    // console.log(name) // 'Иван'

    let и const тоже поднимаются, но находятся в «временной мёртвой зоне» (Temporal Dead Zone) до момента объявления — обращение вызывает ReferenceError.

    Повторное объявление — нет ошибки

    var count = 1
    var count = 2  // не ошибка — тихо перезаписывает!
    console.log(count)  // 2
    
    let value = 1
    let value = 2  // SyntaxError: Identifier 'value' has already been declared

    Классическая ловушка: var в цикле с setTimeout

    Самый коварный баг — все замыкания разделяют одну переменную i:

    // var — все колбэки видят ОДНО И ТО ЖЕ i (финальное значение)
    for (var i = 0; i < 3; i++) {
      setTimeout(() => console.log('var:', i), 100)
    }
    // var: 3, var: 3, var: 3 — НЕ 0, 1, 2!
    
    // let — каждая итерация создаёт НОВУЮ переменную
    for (let j = 0; j < 3; j++) {
      setTimeout(() => console.log('let:', j), 100)
    }
    // let: 0, let: 1, let: 2 — как ожидалось

    "use strict" и var

    Директива 'use strict' не устраняет проблемы var — поднятие и функциональная область видимости остаются. Она лишь запрещает использование необъявленных переменных (без var/let/const).

    Когда встречается var

  • Легаси-код до 2015 года (jQuery плагины, AngularJS 1.x)
  • Устаревшие туториалы и книги (до 2016)
  • Минифицированные старые библиотеки
  • Код сгенерированный старыми транспиляторами
  • В современном коде используй только `let` и `const`: const по умолчанию, let когда нужно переприсвоение.

    Типичные ошибки

    Ошибка 1: var в цикле с асинхронным кодом

    // Классический баг jQuery-обработчиков: все кнопки показывают один номер
    const buttons = ['Кнопка 1', 'Кнопка 2', 'Кнопка 3']
    for (var i = 0; i < buttons.length; i++) {
      // После цикла i === 3 — все обработчики видят одно значение
      setTimeout(() => console.log(`Нажата кнопка ${i + 1}`), 0)
    }
    // 'Нажата кнопка 4', 'Нажата кнопка 4', 'Нажата кнопка 4'
    
    // Решение 1: let
    for (let i = 0; i < buttons.length; i++) { ... }
    
    // Решение 2 (для старого кода): IIFE
    for (var i = 0; i < buttons.length; i++) {
      ;(function(idx) {
        setTimeout(() => console.log(`Нажата кнопка ${idx + 1}`), 0)
      })(i)
    }

    Ошибка 2: hoisting создаёт undefined вместо ошибки

    // var — тихий баг, переменная существует но undefined
    function calcDiscount(price) {
      console.log(discount)  // undefined — не ошибка!
      if (price > 1000) {
        var discount = 0.1
      }
      return price * (1 - discount)  // NaN если price <= 1000!
    }
    
    // let — явная ошибка, легко поймать
    function calcDiscount(price) {
      // console.log(discount)  // ReferenceError — сразу видно проблему
      if (price > 1000) {
        let discount = 0.1
        return price * (1 - discount)
      }
      return price  // нет скидки
    }

    В реальных проектах

  • Code review: автоматические линтеры (ESLint) с правилом no-var запрещают var
  • Рефакторинг: замена var на let/const — один из первых шагов modernизации кода
  • Debugging: понимание hoisting помогает читать stack trace в легаси-коде
  • Примеры

    Демонстрация проблем var: hoisting, утечка из блока, ловушка в цикле

    // 1. Утечка var из блока if — var видна снаружи блока
    function processOrder(isPremium) {
      if (isPremium) {
        var discount = 0.2   // var "утекает" за пределы if!
        let badge = 'PREMIUM' // let — только внутри блока
      }
      console.log(discount)  // 0.2 — var видна здесь
      // console.log(badge)  // ReferenceError — let не видна здесь
    }
    processOrder(true)
    
    // 2. Hoisting ловушка — переменная существует до объявления
    function initCart() {
      console.log('Корзина:', cartName)  // undefined — НЕ ошибка!
      var cartName = 'Моя корзина'
      console.log('Корзина:', cartName)  // 'Моя корзина'
    }
    initCart()
    
    // 3. var в цикле с setTimeout — классический баг
    console.log('--- var в цикле (проблема):')
    for (var i = 0; i < 3; i++) {
      setTimeout(() => console.log('var i:', i), 0)
    }
    // Выведет: var i: 3, var i: 3, var i: 3
    // (все замыкания ссылаются на одну переменную i, которая после цикла = 3)
    
    console.log('--- let в цикле (правильно):')
    for (let j = 0; j < 3; j++) {
      setTimeout(() => console.log('let j:', j), 0)
    }
    // Выведет: let j: 0, let j: 1, let j: 2
    
    // 4. Повторное объявление — тихая перезапись
    var userName = 'Иван'
    var userName = 'Мария'  // не ошибка — молча перезаписывает
    console.log(userName)   // 'Мария'

    Устаревший var

    Открываешь легаси-код jQuery-плагина 2012 года или старую кодовую базу без транспилера — везде var. Код работает, но ведёт себя неожиданно: переменная вдруг видна за пределами блока, счётчик цикла в setTimeout всегда равен максимуму. Понять var — значит уметь читать и фиксить старый код.

    Какую проблему решает (точнее — создаёт)

    До ES6 (2015) var был единственным способом объявить переменную. Его особенности — функциональная область видимости и поднятие — создавали трудноуловимые баги. Именно поэтому в ES6 появились let и const с предсказуемым поведением.

    На основе предыдущих уроков

  • Переменные — let, const, var
  • Циклы — for и счётчики
  • Замыкания — замыкания в setTimeout
  • setTimeout — асинхронность и колбэки
  • Область видимости: функция, а не блок

    Главное отличие var от let/const — область видимости. let и const ограничены блоком {}, а var — функцией:

    function checkStatus(isAdmin) {
      if (isAdmin) {
        var role = 'администратор'  // "утекает" за пределы if!
        let title = 'Супер-пользователь'  // только внутри блока
      }
      console.log(role)   // 'администратор' — var виден!
      console.log(title)  // ReferenceError: title is not defined
    }

    Поднятие (Hoisting)

    Объявления var «поднимаются» в начало функции. Переменная существует с самого начала, но значение undefined до строки присваивания:

    function greetUser() {
      console.log(name)  // undefined — НЕ ошибка!
      var name = 'Иван'
      console.log(name)  // 'Иван'
    }
    
    // Движок интерпретирует код как:
    // var name         // поднято в начало функции
    // console.log(name) // undefined
    // name = 'Иван'
    // console.log(name) // 'Иван'

    let и const тоже поднимаются, но находятся в «временной мёртвой зоне» (Temporal Dead Zone) до момента объявления — обращение вызывает ReferenceError.

    Повторное объявление — нет ошибки

    var count = 1
    var count = 2  // не ошибка — тихо перезаписывает!
    console.log(count)  // 2
    
    let value = 1
    let value = 2  // SyntaxError: Identifier 'value' has already been declared

    Классическая ловушка: var в цикле с setTimeout

    Самый коварный баг — все замыкания разделяют одну переменную i:

    // var — все колбэки видят ОДНО И ТО ЖЕ i (финальное значение)
    for (var i = 0; i < 3; i++) {
      setTimeout(() => console.log('var:', i), 100)
    }
    // var: 3, var: 3, var: 3 — НЕ 0, 1, 2!
    
    // let — каждая итерация создаёт НОВУЮ переменную
    for (let j = 0; j < 3; j++) {
      setTimeout(() => console.log('let:', j), 100)
    }
    // let: 0, let: 1, let: 2 — как ожидалось

    "use strict" и var

    Директива 'use strict' не устраняет проблемы var — поднятие и функциональная область видимости остаются. Она лишь запрещает использование необъявленных переменных (без var/let/const).

    Когда встречается var

  • Легаси-код до 2015 года (jQuery плагины, AngularJS 1.x)
  • Устаревшие туториалы и книги (до 2016)
  • Минифицированные старые библиотеки
  • Код сгенерированный старыми транспиляторами
  • В современном коде используй только `let` и `const`: const по умолчанию, let когда нужно переприсвоение.

    Типичные ошибки

    Ошибка 1: var в цикле с асинхронным кодом

    // Классический баг jQuery-обработчиков: все кнопки показывают один номер
    const buttons = ['Кнопка 1', 'Кнопка 2', 'Кнопка 3']
    for (var i = 0; i < buttons.length; i++) {
      // После цикла i === 3 — все обработчики видят одно значение
      setTimeout(() => console.log(`Нажата кнопка ${i + 1}`), 0)
    }
    // 'Нажата кнопка 4', 'Нажата кнопка 4', 'Нажата кнопка 4'
    
    // Решение 1: let
    for (let i = 0; i < buttons.length; i++) { ... }
    
    // Решение 2 (для старого кода): IIFE
    for (var i = 0; i < buttons.length; i++) {
      ;(function(idx) {
        setTimeout(() => console.log(`Нажата кнопка ${idx + 1}`), 0)
      })(i)
    }

    Ошибка 2: hoisting создаёт undefined вместо ошибки

    // var — тихий баг, переменная существует но undefined
    function calcDiscount(price) {
      console.log(discount)  // undefined — не ошибка!
      if (price > 1000) {
        var discount = 0.1
      }
      return price * (1 - discount)  // NaN если price <= 1000!
    }
    
    // let — явная ошибка, легко поймать
    function calcDiscount(price) {
      // console.log(discount)  // ReferenceError — сразу видно проблему
      if (price > 1000) {
        let discount = 0.1
        return price * (1 - discount)
      }
      return price  // нет скидки
    }

    В реальных проектах

  • Code review: автоматические линтеры (ESLint) с правилом no-var запрещают var
  • Рефакторинг: замена var на let/const — один из первых шагов modernизации кода
  • Debugging: понимание hoisting помогает читать stack trace в легаси-коде
  • Примеры

    Демонстрация проблем var: hoisting, утечка из блока, ловушка в цикле

    // 1. Утечка var из блока if — var видна снаружи блока
    function processOrder(isPremium) {
      if (isPremium) {
        var discount = 0.2   // var "утекает" за пределы if!
        let badge = 'PREMIUM' // let — только внутри блока
      }
      console.log(discount)  // 0.2 — var видна здесь
      // console.log(badge)  // ReferenceError — let не видна здесь
    }
    processOrder(true)
    
    // 2. Hoisting ловушка — переменная существует до объявления
    function initCart() {
      console.log('Корзина:', cartName)  // undefined — НЕ ошибка!
      var cartName = 'Моя корзина'
      console.log('Корзина:', cartName)  // 'Моя корзина'
    }
    initCart()
    
    // 3. var в цикле с setTimeout — классический баг
    console.log('--- var в цикле (проблема):')
    for (var i = 0; i < 3; i++) {
      setTimeout(() => console.log('var i:', i), 0)
    }
    // Выведет: var i: 3, var i: 3, var i: 3
    // (все замыкания ссылаются на одну переменную i, которая после цикла = 3)
    
    console.log('--- let в цикле (правильно):')
    for (let j = 0; j < 3; j++) {
      setTimeout(() => console.log('let j:', j), 0)
    }
    // Выведет: let j: 0, let j: 1, let j: 2
    
    // 4. Повторное объявление — тихая перезапись
    var userName = 'Иван'
    var userName = 'Мария'  // не ошибка — молча перезаписывает
    console.log(userName)   // 'Мария'

    Задание

    Перепиши функцию showDelayed с var на let/const, чтобы она корректно выводила индексы 0, 1, 2. Объясни через комментарии почему оригинальный код работает неправильно и как let это исправляет. Также исправь функцию initApp, где hoisting создаёт баг.

    Подсказка

    Замени var на let в цикле for — это создаст новую переменную для каждой итерации, поэтому замыкание захватит правильное значение. const нельзя объявить без значения и нельзя переприсвоить — это сразу покажет ошибку при попытке использовать до объявления.

    Загружаем среду выполнения...
    Загружаем AI-помощника...