Перейти к основному содержимому

[Medium] 📄 var, let, const

Обзор (Overview)

В JavaScript есть три ключевых слова для объявления переменных: var, let и const. Все три объявляют переменные, но различаются по области видимости, требованиям инициализации, поведению при повторном объявлении, правилам переприсваивания и времени доступа.

Ключевые различия (Key Differences)

Поведениеvarletconst
Область видимости (Scope)Функциональная или глобальнаяБлочнаяБлочная
ИнициализацияНеобязательнаНеобязательнаОбязательна
Повторное объявлениеРазрешеноНе разрешеноНе разрешено
ПереприсваиваниеРазрешеноРазрешеноНе разрешено
Доступ до объявленияВозвращает undefinedВыбрасывает ReferenceErrorВыбрасывает ReferenceError

Подробное объяснение (Detailed Explanation)

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

var имеет функциональную (или глобальную) область видимости, тогда как let и const имеют блочную область видимости (включая блоки функций, блоки if-else и циклы for).

function scopeExample() {
var varVariable = 'var';
let letVariable = 'let';
const constVariable = 'const';

console.log(varVariable); // 'var'
console.log(letVariable); // 'let'
console.log(constVariable); // 'const'
}

scopeExample();

console.log(varVariable); // ReferenceError: varVariable is not defined
console.log(letVariable); // ReferenceError: letVariable is not defined
console.log(constVariable); // ReferenceError: constVariable is not defined

if (true) {
var varInBlock = 'var in block';
let letInBlock = 'let in block';
const constInBlock = 'const in block';
}

console.log(varInBlock); // 'var in block'
console.log(letInBlock); // ReferenceError: letInBlock is not defined
console.log(constInBlock); // ReferenceError: constInBlock is not defined

Инициализация (Initialization)

var и let могут быть объявлены без инициализации, но const должен быть инициализирован при объявлении.

var varVariable; // допустимо
let letVariable; // допустимо
const constVariable; // SyntaxError: Missing initializer in const declaration

Повторное объявление (Redeclaration)

В одной области видимости var позволяет повторно объявить ту же переменную, а let и const — нет.

var x = 1;
var x = 2; // допустимо, x теперь 2

let y = 1;
let y = 2; // SyntaxError: Identifier 'y' has already been declared

const z = 1;
const z = 2; // SyntaxError: Identifier 'z' has already been declared

Переприсваивание (Reassignment)

Переменные, объявленные с var и let, могут быть переприсвоены, а переменные const — нет.

var x = 1;
x = 2; // допустимо

let y = 1;
y = 2; // допустимо

const z = 1;
z = 2; // TypeError: Assignment to a constant variable

Примечание: хотя переменная const не может быть переприсвоена, содержимое объекта/массива всё ещё может быть изменено.

const obj = { key: 'value' };
obj.key = 'new value'; // допустимо
console.log(obj); // { key: 'new value' }

const arr = [1, 2, 3];
arr.push(4); // допустимо
console.log(arr); // [1, 2, 3, 4]

Доступ до объявления (Temporal Dead Zone)

Переменные, объявленные с var, поднимаются и инициализируются как undefined. let и const тоже поднимаются, но не инициализируются до объявления, поэтому ранний доступ выбрасывает ReferenceError.

console.log(x); // undefined
var x = 5;

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

console.log(z); // ReferenceError: Cannot access 'z' before initialization
const z = 5;

Вопрос для собеседования (Interview Question)

Вопрос: классическая ловушка setTimeout + var

Предскажите вывод этого кода:

for (var i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}

Распространённый неправильный ответ

Многие думают, что выведется: 1 2 3 4 5

Фактический вывод

6
6
6
6
6

Почему?

Этот вопрос затрагивает три основные концепции:

1. Функциональная область видимости var

// `var` не создаёт блочную область видимости в циклах
for (var i = 1; i <= 5; i++) {
// `i` находится во внешней области видимости; все итерации используют один и тот же `i`
}
console.log(i); // 6 (после завершения цикла)

// концептуальный эквивалент `var`
{
var i;
i = 1;
i = 2;
i = 3;
i = 4;
// цикл завершён
}

2. Асинхронное выполнение setTimeout

// `setTimeout` является асинхронным и выполняется после завершения текущего синхронного кода
for (var i = 1; i <= 5; i++) {
setTimeout(function () {
// Этот callback ставится в очередь event loop
console.log(i);
}, 0);
}
// Цикл завершается первым (`i` становится 6), затем выполняются callback

3. Ссылка через замыкание (Closure)

// Все callback ссылаются на один и тот же `i`
// К моменту выполнения `i` уже равен 6

Решения (Solutions)

Решение 1: использовать let (рекомендуется) ★

for (let i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}
// вывод: 1 2 3 4 5

// `let` концептуально ведёт себя так:
{
let i = 1; // первая итерация
}
{
let i = 2; // вторая итерация
}
{
let i = 3; // третья итерация
}

Почему это работает: let создаёт новую привязку с блочной областью на каждой итерации, поэтому каждый callback захватывает значение этой итерации.

// концептуальный эквивалент:
{
let i = 1;
setTimeout(function () {
console.log(i);
}, 0);
}
{
let i = 2;
setTimeout(function () {
console.log(i);
}, 0);
}
// ...и так далее

Решение 2: использовать IIFE (Immediately Invoked Function Expression)

for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 0);
})(i);
}
// вывод: 1 2 3 4 5

Почему это работает: каждая итерация создаёт новую функциональную область и передаёт текущее i как параметр j.

Решение 3: использовать третий параметр setTimeout

for (var i = 1; i <= 5; i++) {
setTimeout(
function (j) {
console.log(j);
},
0,
i
); // третий аргумент передаётся в callback
}
// вывод: 1 2 3 4 5

Почему это работает: параметры после задержки передаются в функцию callback.

Решение 4: использовать bind

for (var i = 1; i <= 5; i++) {
setTimeout(
function (j) {
console.log(j);
}.bind(null, i),
0
);
}
// вывод: 1 2 3 4 5

Почему это работает: bind создаёт новую функцию с текущим i, привязанным как аргумент.

Сравнение решений (Solution Comparison)

РешениеПлюсыМинусыРекомендация
letКраткий, современный, понятныйТребует ES6+5/5 настоятельно рекомендуется
IIFEХорошая совместимостьБолее многословный синтаксис3/5 приемлемо
аргумент setTimeoutПростой и прямойМенее известен многим разработчикам4/5 рекомендуется
bindФункциональный стильЧуть менее читаемый3/5 приемлемо

Дополнительные вопросы (Follow-up Questions)

В1: Что если изменить так?

for (var i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, i * 1000);
}

Ответ: выводит 6 раз в секунду, всего 5 раз (на 1с, 2с, 3с, 4с и 5с).

В2: Как вывести 1, 2, 3, 4, 5 по порядку, по одному в секунду?

for (let i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, i * 1000);
}
// выводит 1 после 1с
// выводит 2 после 2с
// выводит 3 после 3с
// выводит 4 после 4с
// выводит 5 после 5с

Фокус на собеседовании (Interview Focus Points)

Этот вопрос проверяет:

  1. Область видимости var: функциональная vs блочная область
  2. Event Loop: синхронное vs асинхронное выполнение
  3. Closure: как функции захватывают внешние переменные
  4. Решения: множество подходов и компромиссов

Рекомендуемый порядок ответа на собеседовании:

  • Сначала назовите правильный результат (6 6 6 6 6)
  • Объясните причину (область видимости var + асинхронный setTimeout)
  • Предложите исправления (предпочтите let, затем упомяните альтернативы)
  • Покажите понимание внутренней работы JavaScript

Лучшие практики (Best Practices)

  1. Предпочитайте const: если переменная не требует переприсваивания, const улучшает читаемость и сопровождаемость.
  2. Используйте let, когда требуется переприсваивание.
  3. Избегайте var в современном JavaScript: его поведение с областью видимости/hoisting часто вызывает неожиданные проблемы.
  4. Учитывайте совместимость браузеров: для старых браузеров используйте транспиляторы вроде Babel для преобразования let/const.