let и const в JavaScript: чем отличаются и что выбрать

Прозрачные вложенные контейнеры на столе разработчика рядом с ноутбуком: переменная видна только внутри своего блока Программирование и разработка

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

Оба ключевых слова появились в ECMAScript 2015 (ES6), и актуальные браузеры поддерживают их полностью. До них переменные создавали только через var. Все примеры запускались в Node.js 24.14, вывод скопирован из консоли и дан комментариями в конце каждого примера. В Chrome тот же движок V8, поэтому тексты ошибок там совпадают; Firefox формулирует их иначе.

Отличие let, const и var в одной таблице

Что проверяем let const var
Область видимости блок { } блок { } функция или весь скрипт
Новое значение через = можно TypeError можно
Объявить без значения можно, будет undefined SyntaxError можно, будет undefined
Повторное объявление в той же области SyntaxError SyntaxError можно
Обращение до строки объявления ReferenceError ReferenceError undefined
Свойство globalThis в обычном скрипте нет нет да
Отдельная переменная на каждой итерации for да только в for...of и for...in нет

Объявление переменных в JavaScript

Переменная хранит значение под именем, по которому к нему потом обращаются. Объявить переменную можно тремя способами: с помощью ключевого слова let, const или устаревшего var. Объявление создаёт переменную, а инициализация записывает в неё первое значение через оператор присваивания =. Обычно оба действия делают в одной строке:

let age = 25;
const name = "Аня";
let isAdmin = false;
let result = null;
console.log(typeof age, typeof name, typeof isAdmin, typeof result);

age = "двадцать шесть";
console.log(typeof age);

// number string boolean object
// string

Ни let, ни const не задают тип данных. Тип определяет значение, поэтому переменная age сначала хранила число (number), а после присваивания стала строкой (string). typeof null возвращает "object": так сложилось с первых версий языка, и ради совместимости это не исправляют.

Переменные let: значение можно менять

let подходит для счётчиков, накопителей и всего, что по ходу работы программы получает новое значение. Если объявить переменную без значения, в ней будет undefined:

let score;
console.log(score);

score = 10;
score = score + 5;
console.log(score);

// undefined
// 15

Переменная живёт внутри своего блока, то есть между ближайшими фигурными скобками: тело if, цикла, функции или просто пара { }.

Константы const: переприсвоить нельзя

Объявление const создаёт константу: переменная навсегда связана с тем значением, которое ей дали в момент создания. Попытка переприсвоить её роняет программу:

const MAX_USERS = 100;
console.log(MAX_USERS);

MAX_USERS = 200;
100
...\02-const.js:4
MAX_USERS = 200;
          ^

TypeError: Assignment to constant variable.
    at Object.<anonymous> (...\02-const.js:4:11)

Node.js v24.14.1

Путь к файлу и стек вызовов здесь сокращены. Число 100 успело вывестись: ошибка TypeError возникает во время выполнения, когда программа дошла до присваивания. По спецификации запись в const бросает это исключение и в строгом режиме, и без него.

Читайте также:  Async/await в JavaScript: как работает и где ошибаются

Объявить константу «на потом» тоже не выйдет. Такой код не запустится вообще:

const limit;
// SyntaxError: Missing initializer in const declaration

const с объектами и массивами

const запрещает переприсвоить переменную, но не делает объект неизменяемым. Константа хранит ссылку на объект, и эта ссылка не меняется. Свойства объекта и элементы массива менять можно:

const user = { name: "Аня", tags: ["frontend"] };

user.name = "Оля";
user.tags.push("backend");
console.log(user);

try {
  user = { name: "Ира" };
} catch (e) {
  console.log(e.name + ": " + e.message);
}

// { name: 'Оля', tags: [ 'frontend', 'backend' ] }
// TypeError: Assignment to constant variable.

Изменение свойства user.name и вызов push прошли. Запись нового объекта в user закончилась ошибкой. Если объект нужно запретить менять, используют Object.freeze(). Но заморозка поверхностная, и в нестрогом режиме нарушение проходит молча:

const config = Object.freeze({ port: 3000, db: { host: "localhost" } });

config.port = 8080;
config.db.host = "example.com";
console.log(config.port, config.db.host);

// Результат в файле .js (CommonJS, нестрогий режим):
// 3000 example.com

Значение port осталось прежним, а вложенный объект db изменился: freeze его не тронул. Тот же код в файле .mjs или с директивой "use strict" в начале падает на первой же записи:

TypeError: Cannot assign to read only property 'port' of object '#<Object>'

ES-модули всегда работают в строгом режиме, поэтому в проектах на import вы увидите именно эту ошибку, а в старом скрипте без "use strict" запись просто потеряется. Чтобы заморозить вложенные объекты, Object.freeze вызывают для каждого уровня.

Блочная область видимости

Область видимости определяет, из какой части кода можно обратиться к переменной, а где доступа к ней нет. У let и const это блок, у var функция. Хорошо видно на if:

if (true) {
  var a = "var";
  let b = "let";
  const c = "const";
}

console.log(a);
console.log(typeof b, typeof c);
console.log(b);

// var
// undefined undefined
// ReferenceError: b is not defined

Переменная a вытекла из блока и доступна ниже. Переменные b и c за пределами фигурных скобок не существуют. typeof для необъявленной переменной возвращает "undefined", а прямое обращение к b даёт ReferenceError.

Блочная область видимости защищает от случайной перезаписи: let i во вложенном цикле не испортит i внешнего цикла, а временная переменная внутри if не заденет одноимённую снаружи.

Поднятие (hoisting) и временная мёртвая зона

Поднятие означает, что движок узнаёт обо всех объявлениях блока или функции до того, как начнёт выполнять код. С var переменная при этом сразу получает значение undefined. С let и const переменная тоже создаётся заранее, но остаётся неинициализированной до строки объявления. Отрезок от начала блока до этой строки называют временной мёртвой зоной (temporal dead zone, TDZ):

console.log(oldStyle);
var oldStyle = 1;

console.log(modern);
let modern = 2;

// undefined
// ReferenceError: Cannot access 'modern' before initialization

Часто пишут, что let и const не поднимаются. Мы считаем эту формулировку неточной:

let price = 100;

{
  console.log(price);
  let price = 200;
}

// ReferenceError: Cannot access 'price' before initialization

Если бы внутренний price не существовал до своего объявления, console.log взял бы внешнюю переменную и вывел 100. Но внутренний price уже создан, просто не инициализирован, и читать его нельзя.

С typeof то же самое. Для переменной, которой нигде нет, он безопасен, а для переменной в мёртвой зоне падает:

console.log(typeof notDeclared);

{
  console.log(typeof later);
  let later = 1;
}

// undefined
// ReferenceError: Cannot access 'later' before initialization

Зона называется временной, потому что считается по порядку выполнения кода. Функция может ссылаться на константу, объявленную ниже, если её вызывают после объявления:

function showTotal() {
  console.log(total);
}

const total = 42;
showTotal();

// 42

Вызов showTotal() до объявления const total дал бы ту же ошибку Cannot access 'total' before initialization.

let в цикле for и замыкания

Колбэк setTimeout выполняется после того, как цикл закончился:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log("var:", i), 0);
}

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log("let:", j), 0);
}

// var: 3
// var: 3
// var: 3
// let: 0
// let: 1
// let: 2

С var на весь цикл одна переменная i, и к моменту вызова колбэков её значение уже 3. С let в заголовке for JavaScript создаёт новую переменную на каждой итерации и копирует в неё текущее значение, поэтому каждая стрелочная функция запоминает своё число.

Читайте также:  Язык программирования C++: история, стандарты и первые программы

const в заголовке работает в for...of и for...in: там на каждом шаге создаётся новая константа. В классическом for счётчик меняется через k++, и это присваивание:

for (const item of ["a", "b", "c"]) {
  console.log(item);
}

for (const k = 0; k < 3; k++) {
  console.log(k);
}

// a
// b
// c
// 0
// TypeError: Assignment to constant variable.

Первая итерация прошла и напечатала 0, ошибка возникла на k++. Для счётчика в обычном for нужен let.

Почему var устарел и где он ещё встречается

var нужен, чтобы читать старый код и библиотеки, написанные до 2015 года. Три свойства делают его источником ошибок: функциональная область видимости (переменная вытекает из if и циклов), undefined вместо ошибки при обращении до объявления и тихое повторное объявление.

var x = 1;
var x = 2;
console.log(x);

let y = 1;
let y = 2;

// SyntaxError: Identifier 'y' has already been declared

Здесь console.log(x) не вывел даже 2. Повторное let ловится при разборе файла, до выполнения первой команды, и не запускается ничего. Для var повтор разрешён, и в длинном файле так легко затереть чужую переменную.

Поведение var на верхнем уровне зависит от того, где запущен код. В обычном скрипте браузера (тег <script> без type="module") переменная var на верхнем уровне становится свойством глобального объекта, а let и const нет. В файле Node.js и в модулях верхний уровень принадлежит модулю, и глобальным свойством не становится никто:

var a = 1;
let b = 2;
console.log(globalThis.a, globalThis.b);

// node file.js  ->  undefined undefined
// node -e "..." ->  1 undefined

Режим node -e выполняет код как обычный скрипт, поэтому здесь ведёт себя как браузер.

Что выбрать: let или const

Наше правило короткое: объявляйте через const, а на let переходите, когда переменной действительно нужно присвоить новое значение. var в новом коде не используйте.

const по умолчанию сообщает читателю, что переменная дальше не переназначается, и ему не нужно просматривать функцию до конца. Если кто-то позже попробует переприсвоить такую переменную, программа упадёт с TypeError и укажет на место ошибки. Линтер ESLint умеет следить за этим автоматически: правило prefer-const находит let, которые ни разу не переприсвоены, и исправляет их командой --fix, а no-var запрещает var.

Где без let не обойтись:

  • счётчик классического цикла for;
  • сумма или строка, которую накапливают в цикле;
  • переменная, значение которой выбирают в if или try и используют ниже;
  • флаг, который меняется по ходу работы, например let isLoading = true.

Имена переменных

Имя может состоять из букв, цифр, _ и $, но не начинаться с цифры. Регистр важен: user и User это разные переменные. Зарезервированные слова вроде const, class или return именами быть не могут. Пишут в стиле camelCase: userName, totalPrice.

Верхний регистр с подчёркиванием (MAX_USERS, API_URL) принят для значений, известных до запуска программы. Мы бы не советовали писать так каждую const: const user = await getUser() остаётся обычной переменной, просто без переприсваивания, и const USER здесь только сбивает с толку. Как работает сам await, разобрано в статье про async/await в JavaScript.

Читайте также:  Рекурсия C++: как работает, примеры и когда лучше цикл

Частые ошибки и что они означают

Сообщение в консоли Причина Как исправить
TypeError: Assignment to constant variable. присваивание переменной const, в том числе ++ и += объявить через let или завести новую переменную
SyntaxError: Missing initializer in const declaration const без значения дать значение в той же строке
ReferenceError: Cannot access 'x' before initialization обращение к let или const до объявления, в том числе через typeof перенести объявление выше или вызов функции ниже
SyntaxError: Identifier 'x' has already been declared в этой области уже есть такое имя, и одно из объявлений сделано через let, const или class переименовать или убрать второе объявление
ReferenceError: x is not defined переменная объявлена в другом блоке или не объявлена совсем объявить её там, где к ней обращаются, или во внешнем блоке
TypeError: Cannot assign to read only property запись в замороженный объект в строгом режиме создать копию: { ...config, port: 8080 }

Отдельная ловушка ждёт в switch. Все ветки case лежат в одном блоке, поэтому переменные let или const с одинаковым именем в двух ветках дают SyntaxError: Identifier 'message' has already been declared. Каждую ветку оборачивают в свои фигурные скобки:

const command = "start";

switch (command) {
  case "start": {
    const message = "Запуск";
    console.log(message);
    break;
  }
  case "stop": {
    const message = "Остановка";
    console.log(message);
    break;
  }
}

// Запуск

Ноутбук с открытым редактором кода и консолью, где выделена строка с ошибкой

Частые вопросы

Можно ли объявить несколько переменных в одной строке?

Можно, через запятую: let a = 1, b, c = 3; создаёт три переменные, и значение b будет undefined. С const значение нужно каждой: строка const X = 10, Y; не запустится с SyntaxError: Missing initializer in const declaration. Мы бы советовали по одному объявлению на строку, так проще читать дифф в Git.

Можно ли изменить const внутри блока?

Переприсвоить константу нельзя нигде. Но внутри вложенного блока можно объявить новую константу с тем же именем, она закроет внешнюю до конца блока. Если в const лежит объект или массив, его содержимое меняется свободно.

Поднимаются ли let и const?

Да, такие переменные создаются при входе в блок. Но до объявления они не инициализированы, и любое обращение к ним даёт ReferenceError. У var в этот момент уже есть значение undefined.

Почему в консоли Chrome можно дважды написать let с одним именем?

Консоль DevTools с версии Chrome 80 специально разрешает повторное let и class, если объявления введены отдельными командами: так удобнее экспериментировать. В файле скрипта и внутри одной команды консоли повторное объявление по-прежнему даёт SyntaxError.

Работают ли let и const в старых браузерах?

Актуальные версии Chrome, Firefox, Safari и Edge поддерживают их полностью, Internet Explorer 11 только частично. Если проект обязан работать в IE, код переводят в синтаксис ES5 сборщиком, например Babel, и let с const превращаются в var.

Оцените статью
bestprogrammer.ru
Добавить комментарий