javascript
March 26

Все, что вы стеснялись спросить про переменные в JavaScript

В этой статье мы разберем не только базовую теорию про переменные, но и связанные с ними нюансы, погружаясь в тему глубже, чем это делают курсы для начинающих. Уверен, даже опытные разработчики смогут найти в этой статье что-то интересное для себя

Вы узнаете про правила именования, когда использовать let, а когда const, что такое TDZ, ссылочный тип и как эволюционировали переменные в JavaScript. Звучит интригующе? Поехали!


var

Давайте сразу определимся - на момент написания статьи var 10 лет как устарел. Не будем долго задерживаться здесь:

  • У var функциональная область видимости
  • У var есть Hoisting
  • В браузере (но не в Node.js) при объявлении переменной через var вне функции, переменная добавляется в объект window, то есть становится глобальной
  • Повторное объявление одноименной переменной через var не вызовет ошибку

Резюмируя - используем var только если мы пишем под какой-то древний движок, не поддерживающий ES6 (ECMAScript 2015)

Про Hoisting и Области Видимости я уже рассказывал в соответствующих статьях:


let и const

Коротко об основных особенностях let и const:

  • Блочная область видимости
  • Нельзя объявить повторно
  • Недоступны до инициализации, так как попадают в TDZ
  • Объявление без значения
    • let - Да
    • const - Нет
  • Изменяемое значение
    • let - Да
    • const - Нет

Давайте разбираться

Переменные let и const недоступны за пределами конструкций, отделенных фигурными скобками как, например, здесь:

{
  const test = 0;
  console.log(test); // 0
}

// ❌ Error: test is not defined
console.log(test);

А вкупе с отсутствием Hoisting это решает проблему со значением перменной на каждой итерации цикла:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // ❌ 3, 3, 3
}

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 100); // ✅ 0, 1, 2
}

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

Переменную нельзя определить повторно в той же области видимости

let name = 'Tester';

// ❌ SyntaxError: Identifier 'name' has already been declared
let name = 'Another Tester';

let можно объявить без значения, а const - нет

// ✅ Здесь все ок, в 'a' будет undefined
let a;

// ❌ SyntaxError: Missing initializer in const declaration
const b;

Значение let можно полностью перезаписать, а значение const - нет

const c = 50;

// ❌ TypeError: Assignment to constant variable
c = 60; 

Так когда использовать let, а когда const?

Сейчас вы наверное подумаете, что я буду в очередной раз рассказывать, что let нужно использовать когда мы хотим изменять переменную, а const для констант? В этом конечно есть доля смысла, но в JavaScript есть более правильный подход

Старайтесь не использовать let вообще. Нет, я не шучу. Всегда объявляйте переменные через const, кроме случаев, когда без let совсем никак не обойтись - например, в обычных циклах for как в примере выше

Еще можно использовать let в случаях, когда нужно чтобы на переменную ссылался и try и catch. Вот упрощенный пример из настоящего кода из продакшна:

let transaction;

try {
  const { data } = req.body; 

  if (!data.requiredField) {
    throw new BadRequestError('Some error message');
  }

  transaction = await db.sequelize.transaction();

  /**
   * Здесь делаем что-то с базой данных
   */

  await transaction.commit();
  return result;
} catch (error) {
  await transaction?.rollback();
  return serializeRequestError(error);
}

Здесь мы унифицируем обработку ошибок, при этом начинаем транзакцию уже после валидации, чтобы занять подключение к базе данных только тогда, когда мы точно готовы делать запросы. Конечно, это не самый простой пример, но зато это самое частое плюс минус оправданное использование let. Кроме циклов 😀

Неизменяемые Переменные VS Константы

Объявляя переменные через const, мы не всегда хотим определить константу в привычном понимании - мы объявляем переменную, значение которой не изменится по ходу выполнения. А вот если мы действительно хотим объявить константу, выделяем ее неймингом не в camelCase, а в MACRO_CASE:

// Константы
const API_URL = 'https://example.com';
const MAX_CONNECTIONS = 10;
const DEFAULT_TIMEOUT_MS = 5000;

// Неизменяемое значение
const adminAccount = getAdminAccount();

Константы - это определенные заранее статические значения. С точки зрения движка JavaScript, константа это и const c = 50, и const MAX_CONNECTIONS = 10, но вот с точки зрения разработчика - это разные сущности

Переменные, объявленные по такому принципу, принесут намного больше девидендов - логика становится более линейной и предсказуемой, отладка проще, а код чище: вы продумываете код исходя из того, что значение переменной после инициализации изменить нельзя ни намеренно, ни случайно

Кроме того, JavaScript оптимизирует const лучше, так как движок знает, что значение не поменяется, но это тема для отдельной статьи

Чтобы вы убедились в эффективности такого подхода, вот пример:

const getShippingCost = (weight, isExpress) => {
  let baseCost = 5;

  if (weight > 10) {
    baseCost = 10;
  }

  let finalCost = baseCost;

  if (isExpress) {
    finalCost += 15;
  }

  return finalCost;
}

Путем нехитрых преобразований получаем...

const getShippingCost = (weight, isExpress) => {
  const baseCost = weight > 10 ? 10 : 5;

  return isExpress
    ? baseCost + 15
    : baseCost;
}

Если такая разница получается на примере простейшей функции, легко представить, что происходит со сложной и ветвистой логикой


Правила и ограничения имен переменных

Ограничения

В именах переменных допускаются только буквы (a-z, A-Z), цифры (0-9), символы _ (подчеркивание) и $ (знак доллара)

Регистр имеет значение - userName и username – это разные переменные

Имя переменной не должно начинаться с цифры!

// ❌ SyntaxError: Invalid or unexpected token
const 1test = 'test';

А еще именем переменной не могут быть ключевые слова JavaScript - function, class, NaN, undefined, null, Infinity и т. д.

Интересный факт

Символ доллара в именах переменных сейчас редко можно встретить, но во времена, когда jQuery был королем, разработчики записывали объекты jQuery в переменные, начинающиеся с доллара

Наскальные рисунки, за 1000 лет до появления фронтенд фреймворков:

var $hiddenBox = $('#banner-message');

Дело в том, что алиасом для функции jQuery как раз был символ доллара и так можно было легко отличить где обычные данные, а где ссылка на элемент 😃

Символ подчеркивания можно часто встретить в именах методов или свойств класса, что лишь по соглашениям маркирует метод или свойство как protected (Не путать с private)

class User {
  constructor(id, name, role) {
    // public свойство
    this.name = name;
    // protected свойство (по соглашению)
    this._id = id;
    // protected свойство (по соглашению)
    this._role = role;
  }

  // public метод
  get displayName() {
    return `User: ${this.name}`;
  }

  // protected метод (по соглашению)
  _checkPermissions() {
    return this._role === 'admin';
  }
}

const user = new User(1, 'Alice', 'user');

// ✅ Можно
console.log(user.name);

// ✅ Можно
console.log(user.displayName);

// ⚠️ По соглашению не стоит трогать
console.log(user._id);

// ⚠️ По соглашению не стоит вызывать
console.log(user._checkPermissions());

Обращаю ваше внимание на то, что символ подчеркивания не применяет какую-то магическую особенность языка и все такие методы и свойства остаются доступными извне! Такой подход к именованию лишь рекомендует разработчикам использовать метод или свойство только внутри класса

Лучшие практики именования

Придерживайтесь этих правил, когда выбираете имена для своих переменных и функций - тогда ваш код будет легче прочитать, понять и поддерживать

Помните, что лучше иногда потратить время на то, чтобы придумать правильное имя для переменной, чем потом искать ошибку в сотнях, а то и тысячах строк кода

=> Говорящие имена

  • const age = 25; лучше, чем const a = 25;
  • const isLoggedIn = true; лучше, чем const x = true;

=> camelCase – стандарт для JavaScript

  • Например: const myVariable = 'hello';
  • const user_name = 'Alice' - это не про JavaScript, но иногда в Node.js snake_case все же встречается

=> Константы (статические значения) — заглавными буквами (MACRO_CASE)

=> Бойтесь аббревиатур и сокращений

  • const usrNm = 'John';ОЧЕНЬ плохо, лучше const userName = 'John';

=> Логичное именование булевых переменных

  • Используйте is, has, can, should:
const isActive = true;
const hasPermission = false;
const canEdit = true;

=> Используйте существительные для данных, глаголы для функций

  • const users = []; (не const getUser = [];)
  • function fetchUsers() {} (не function users() {})

=> Не перебарщивайте с длиной

  • let counter = 0; лучше, чем let numberOfAttemptsBeforeTimeout = 0;

Что хранится в переменных?

Что за вопрос - конечно же, данные! Но вот в какой форме?

В JavaScript существует восемь типов данных:

  1. Примитивные типы
    • Number – числа, например, 42, 3.14, NaN, Infinity
    • String – строки, например, "hello", 'world', `template literal`
    • Boolean – логический тип: true или false
    • null – специальное значение, обозначающее "Ничего"
    • undefined – переменная объявлена, но не имеет значения
    • BigInt – большие числа, занимающие 53 бита и более, например, 9007199254740991n
    • Symbol – уникальные идентификаторы
  2. Сложный тип
    • Objectмассивы, функции, объекты, Date, RegExp, Set, Map и другие структуры данных, по сути являющиеся объектами

Здесь упоминание про типы данных нужно лишь для контекста. Сами типы данных мы разберем в отдельной статье

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

const original = 42;
let copy = original;

copy += 8;

console.log(original); // 42
console.log(copy); // 50

Переменные со сложными типами хранят не сами данные, а ссылки на них. Поэтому сложный тип называют еще и Ссылочным Типом

Что дает нам это знание? Возвращаясь к const - мы ведь создаем неизменяемое значение. Тогда почему возможно изменить объект, который мы, казалось бы, записываем в const:

const user = {
  id: 123,
  name: 'Tester',
  age: 18,
};

user.age = 28;

// Здесь у пользователя будет age = 28
console.log(user);

Дело в том, что мы изменяем сам объект - в const хранится ссылка на него. А вот саму ссылку, мы действительно изменить не можем:

const user = {
  id: 123,
  name: 'Tester',
  age: 18,
};

// ❌ TypeError: Assignment to constant variable
user = {
  id: 123,
  name: 'Tester',
  age: 28,
};

Таким образом, const защищает от изменения ссылку, но не сам объект

Чтобы полностью запретить изменение объекта, можно использовать Object.freeze(user), но это выходит за рамки текущей темы

Мы разобрались, как примитивы и объекты хранятся в переменных. Но как они ведут себя при передаче в функции?

Примитвы передаются по значению:

const add = (num, amount) => {
  return num + amount;
};

const a = 5;
const b = 2;

console.log(add(a, b)); // 7
console.log(a); // 5
console.log(b); // 2

Объекты - по ссылке:

const doSomething = (user) => {
  user.name = 'Admin';
  return user;
}

const user = {
  id: 123,
  name: 'Tester',
  age: 18,
};

// Здесь у пользователя будет name = 'Admin'
console.log(doSomething(user));

Этот пример наглядно показывает, что при передаче объекта в функцию мы фактически работаем с тем же самым объектом в памяти. Поэтому любые изменения внутри функции повлияют на исходные данные


Temporal Dead Zone

Эта глава посвящена низкоуровневым особенностям языка и будет интересна разве что в качестве информации для общего развития. Если ваш мозг кипит от переизбытка информации - можете перейти сразу к подведению итогов

JavaScript ведёт себя довольно предсказуемо при объявлении переменных через let и const. Как я уже отмечал ранее, Hoisting есть только у var... Хотя на самом деле это лишь допущение для простоты восприятия

TDZ (Temporal Dead Zone) – это промежуток от начала выполнения блока кода до момента инициализации переменной, объявленной через let или const. В этот период доступ к переменной невозможен, даже если она уже объявлена в коде

// ❌ ReferenceError: Cannot access 'username' before initialization
console.log(username);

const username = 'Alice';
console.log(username); // ✅ Alice

Объявления через let и const действительно поднимаются и движок о них знает, но в отличие от var, их нельзя использовать до момента инициализации, поэтому они находятся в TDZ

TDZ может встретиться и при использовании let/const внутри блока {}:

function example() {
  // ❌ ReferenceError (TDZ)
  console.log(a);
  const a = 10;
}

Или с аргументами функции, в которых есть значение по умолчанию:

function greet(name = user) {
  console.log(`Hello, ${name}`);
}

const user = 'Alice';
// ❌ ReferenceError: Cannot access 'user' before initialization
greet();

Здесь параметр name пытается получить значение user, но user ещё находится в TDZ

Простыми словами, TDZ – это временная зона, в которой переменная существует, но ее использование недоступно


Заключение

Поздравляю самых упорных — мы дошли до финала! Возможно, читая эту статью, у вас возникало ощущение, что тема сложнее, чем казалось на первый взгляд. И это правда 😃

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

По-настоящему ощутить разницу в подходах или различия между let и const можно прочувствовать только после практики - пожалуйста, не забывайте об этом