Открываешь легаси-код jQuery-плагина 2012 года или старую кодовую базу без транспилера — везде var. Код работает, но ведёт себя неожиданно: переменная вдруг видна за пределами блока, счётчик цикла в setTimeout всегда равен максимуму. Понять var — значит уметь читать и фиксить старый код.
До ES6 (2015) var был единственным способом объявить переменную. Его особенности — функциональная область видимости и поднятие — создавали трудноуловимые баги. Именно поэтому в ES6 появились let и const с предсказуемым поведением.
Главное отличие 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
}Объявления 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Самый коварный баг — все замыкания разделяют одну переменную 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 — поднятие и функциональная область видимости остаются. Она лишь запрещает использование необъявленных переменных (без var/let/const).
В современном коде используй только `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 // нет скидки
}no-var запрещают varvar на let/const — один из первых шагов modernизации кодаДемонстрация проблем 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) // 'Мария'Открываешь легаси-код jQuery-плагина 2012 года или старую кодовую базу без транспилера — везде var. Код работает, но ведёт себя неожиданно: переменная вдруг видна за пределами блока, счётчик цикла в setTimeout всегда равен максимуму. Понять var — значит уметь читать и фиксить старый код.
До ES6 (2015) var был единственным способом объявить переменную. Его особенности — функциональная область видимости и поднятие — создавали трудноуловимые баги. Именно поэтому в ES6 появились let и const с предсказуемым поведением.
Главное отличие 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
}Объявления 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Самый коварный баг — все замыкания разделяют одну переменную 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 — поднятие и функциональная область видимости остаются. Она лишь запрещает использование необъявленных переменных (без var/let/const).
В современном коде используй только `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 // нет скидки
}no-var запрещают varvar на let/const — один из первых шагов modernизации кодаДемонстрация проблем 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 нельзя объявить без значения и нельзя переприсвоить — это сразу покажет ошибку при попытке использовать до объявления.