let vs var: война миров или эволюция JavaScript?
Вы когда-нибудь писали код на JavaScript и ловили себя на мысли: «А почему эта переменная ведёт себя так странно?» Или, что ещё хуже, натыкались на баг, который исчезает при замене var на let? Добро пожаловать в клуб. Вопрос «What is the difference between "let" and "var"?» на Stack Overflow собирает сотни тысяч просмотров не просто так — это одна из самых частых ловушек для новичков и даже опытных разработчиков.
Сегодня мы не просто перечислим различия. Мы заглянем под капот: разберёмся, как let и var влияют на производительность вашего кода, оптимизацию в современных движках (V8, SpiderMonkey) и почему выбор между ними — это вопрос не только стиля, но и эффективности. Приготовьтесь, будет жарко.
Коротко о главном: область видимости и поднятие (hoisting)
Прежде чем говорить о производительности, давайте вспомним фундамент. У var и let три ключевых отличия, которые меняют всё.
1. Область видимости (Scope)
var— имеет функциональную область видимости. Если переменная объявлена внутри функции, она видна во всей функции. Если вне — она глобальна.let— имеет блочную область видимости. Она живёт только внутри ближайших фигурных скобок{}(цикл, условие, блок).
// Пример с var
if (true) {
var x = 10;
}
console.log(x); // 10 — переменная "утекла" из блока
// Пример с let
if (true) {
let y = 20;
}
console.log(y); // ReferenceError: y is not defined
2. Hoisting (поднятие) и Temporal Dead Zone (TDZ)
Обе переменные «поднимаются» (hoisting), но ведут себя по-разному.
var: объявление поднимается наверх области видимости и инициализируется значениемundefined. Вы можете обратиться к переменной до её объявления (но получитеundefined).let: объявление поднимается, но не инициализируется. До момента фактического объявления переменная находится в «временной мёртвой зоне» (Temporal Dead Zone). Любое обращение к ней вызываетReferenceError.
// var — "безопасное" undefined
console.log(a); // undefined
var a = 5;
// let — ошибка в TDZ
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;
3. Повторное объявление
var— можно переобъявить сколько угодно раз в одной области видимости. Ошибки не будет.let— повторное объявление в той же области видимости вызоветSyntaxError.
var z = 1;
var z = 2; // Ок
let w = 1;
let w = 2; // SyntaxError: Identifier 'w' has already been declared
А теперь о главном: производительность и оптимизация
Многие думают: «Раз let — это новый стандарт, значит он медленнее var?» Или наоборот: «let круче, значит быстрее». Правда, как всегда, посередине и зависит от контекста.
Как движки оптимизируют переменные
Современные JavaScript-движки (V8 в Chrome/Node.js, SpiderMonkey в Firefox) компилируют код в машинный с помощью JIT-компиляторов. Они используют эвристики для оптимизации.
var: из-за функциональной области видимости и возможности случайного переопределения движку сложнее делать предположения о типе переменной. Он часто создаёт «тяжёлые» объектыVariableObject, которые хранят переменные в динамической памяти.let: блочная область видимости и запрет на повторное объявление дают движку больше информации. Он может разместить переменную в регистре процессора или на стеке, а не в куче. Это ускоряет доступ.
Однако на практике разница в производительности для 99% кода микроскопическая. Вы не заметите её, пока не начнёте выполнять миллиарды операций.
Где разница становится заметной: циклы
Классический пример — производительность в циклах for. Из-за разной области видимости, let в заголовке цикла создаёт новую привязку для каждой итерации. Это критично для замыканий.
// Проблема с var
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100); // Выведет: 5,5,5,5,5
}
// Решение с let
for (let j = 0; j < 5; j++) {
setTimeout(() => console.log(j), 100); // Выведет: 0,1,2,3,4
}
С точки зрения производительности, let в цикле может быть чуть медленнее из-за создания новой лексической среды на каждой итерации. Но это окупается предсказуемостью и отсутствием багов. В современных движках эта разница нивелируется оптимизациями.
Память и сборка мусора
let позволяет более точно контролировать время жизни переменной. Переменная, объявленная внутри блока, перестаёт существовать после выхода из него. Это помогает сборщику мусора быстрее освобождать память.
// var — живёт до конца функции
function process() {
if (condition) {
var bigData = new Array(1000000);
// ... работаем
}
// bigData всё ещё существует здесь, занимает память
}
// let — умирает после блока
function processOptimized() {
if (condition) {
let bigData = new Array(1000000);
// ... работаем
}
// bigData уже уничтожена, память свободна
}
В больших приложениях (React, Angular, серверный Node.js) такая экономия памяти может дать реальный прирост производительности и уменьшить фризы от сборки мусора.
Практические советы: когда что использовать?
На основе анализа производительности и лучших практик, вот простой гайд:
- Всегда используйте
constдля переменных, которые не переназначаются. Это даёт движку максимальную информацию для оптимизации. - Используйте
let, если значение переменной будет меняться (счётчики, флаги). Это безопаснее и понятнее. - Избегайте
varв новом коде. Исключение — если вы поддерживаете очень старый код (IE11 и ниже) или пишете скрип