Код читают гораздо чаще, чем пишут: свой собственный — через месяц, чужой — каждый раз на код-ревью (проверке изменений другим разработчиком перед тем, как они попадут в общую ветку). И большая часть этого чтения сводится к чтению имён. Имя переменной, функции или константы отвечает на вопрос «что здесь лежит» быстрее любого комментария — если оно выбрано осмысленно.

Приёмы давно описаны — в Clean Code Роберта Мартина и Code Complete Стива Макконнелла, — и почти все сводятся к паре вопросов, которые стоит задать себе до того, как нажата первая буква. Разберём их на примерах JavaScript: что имя обязано рассказать, как различать похожие сущности, как называть данные разных типов, когда имя начинает откровенно врать и какие конвенции за вас проверит линтер. В конце — про перегибы: попытка назвать всё «идеально» портит код не хуже односимвольных имён.

Что имя обязано рассказать

Первая проверка простая: если рядом с объявлением хочется поставить комментарий, объясняющий, что это такое, — имя выбрано неудачно. Комментарий в такой ситуации не дополняет имя, а компенсирует его.

let d; // время, прошедшее с создания, в днях

Буква d не сообщает ничего: ни единицу измерения, ни точку отсчёта. Причём вариантов замены сразу несколько, и это само по себе показательно:

let elapsedDays;
let daysSinceCreation;
let daysSinceLastEdit;
let fileAgeInDays;

Все четыре имени — не синонимы: каждое отвечает ещё и на вопрос «от какого момента считаем». Пока переменная называлась одной буквой, этот вопрос не был задан вообще.

Тот же эффект на масштабе целой функции — сложных конструкций нет, а понять происходящее невозможно:

function getThem() {
  const list1 = [];
  for (const x of theList) {
    if (x[0] === 4) list1.push(x);
  }
  return list1;
}

Неизвестно, что такое theList, почему сравнивается нулевой элемент с четвёркой и что в итоге вернётся. Лечится это без единой правки логики, только именами:

const STATUS = 0;
const FLAGGED = 4;

function getFlaggedCells(gameBoard) {
  const flaggedCells = [];
  for (const cell of gameBoard) {
    if (cell[STATUS] === FLAGGED) flaggedCells.push(cell);
  }
  return flaggedCells;
}

Строк стало даже больше, но угадывать нечего: это игровое поле сапёра, у клетки есть статус, и нужны помеченные флажком. Читаемость держится на именах, а не на форме записи — при переходе к декларативному стилю она никуда не девается:

const getFlaggedCells = (gameBoard) =>
  gameBoard.filter((cell) => cell[STATUS] === FLAGGED);

Сколько букв достаточно

Слишком длинное имя читается не лучше слишком короткого:

let numberOfPeopleOnTheNationalOlympicTeam; // многовато
let n;                                      // маловато
let np;                                     // непонятно

let teamMemberCount;                        // в самый раз

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

Похожие имена: как их различать

Когда в одном месте нужны две сущности одного вида, рука тянется добавить цифру:

function copyChars(a1, a2) {
  for (let i = 0; i < a1.length; i++) {
    a2[i] = a1[i];
  }
}

Цифра различает имена, но не различает смысл: чтобы понять, откуда и куда копируем, приходится читать тело функции, а порядок аргументов при вызове ниоткуда не следует. Имена, отражающие роль, снимают вопрос целиком:

function copyChars(source, target) {
  for (let i = 0; i < source.length; i++) {
    target[i] = source[i];
  }
}

Для противоположностей есть устоявшиеся пары. Держаться их выгодно: читатель, увидев одну половину пары, автоматически знает вторую.

Пара Где применяется
begin / end границы диапазона, отрезок времени
first / last крайние элементы коллекции
min / max пределы допустимых значений
next / previous обход списка, пагинация
source / target копирование, перемещение, синхронизация
open / close соединения, модальные окна, файлы
show / hide видимость элементов интерфейса
old / new значения до и после изменения

Один термин на одну сущность

Привычка, которая экономит больше времени, чем кажется: в пределах проекта одну и ту же операцию называть одним и тем же глаголом, а одну и ту же сущность — одним и тем же существительным. Если в одном модуле fetchUser, в соседнем getCustomer, а в третьем loadAccountData, читатель каждый раз тратит секунду на вопрос «это три разных механизма или один?». Обычно — один.

// разнобой: три глагола на одну операцию, три слова на одну сущность
fetchUser(id);
getCustomer(id);
loadAccountData(id);

// договорённость: get — достать из того, что уже есть,
// fetch — сходить по сети; сущность всюду называется user
getUser(id);   // из локального кеша
fetchUser(id); // запрос к API

Такой словарь не обязан быть где-то записан. Достаточно перед тем, как придумывать новое имя, заглянуть в соседний файл и взять слово оттуда.

Имя живёт вне редактора

У имени есть две жизни за пределами строки, где оно объявлено: его произносят вслух и его ищут поиском по проекту.

Произносимость

Сокращение из первых букв понятно ровно одному человеку — тому, кто его придумал:

let genymdhms; // generation year, month, day, hour, minute, second
let modymdhms;
let pszqint = "102";

Такое имя невозможно обсудить: на созвоне придётся либо диктовать по буквам, либо изобретать прозвище, у каждого своё. Развёрнутые слова стоят нескольких лишних символов:

let generationTimestamp;
let modificationTimestamp;
let recordId = "102";

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

Поиск по проекту

Второе испытание — глобальный поиск. Вот фрагмент, который его не проходит:

let s = 0;
for (let j = 0; j < 34; j++) {
  s += (t[j] * 4) / 5;
}

Одиночную s искать бессмысленно — она встретится в каждом слове. Числа 4 и 5 дадут сотни совпадений, из которых нужные не отличить. А 34 не связано ни с чем: если завтра задач станет тридцать пять, найти это место получится только перебором.

const REAL_DAYS_PER_IDEAL_DAY = 4;
const WORK_DAYS_PER_WEEK = 5;

let totalWeeks = 0;

for (let taskIndex = 0; taskIndex < taskEstimates.length; taskIndex++) {
  const realDays = taskEstimates[taskIndex] * REAL_DAYS_PER_IDEAL_DAY;
  totalWeeks += realDays / WORK_DAYS_PER_WEEK;
}

Разница видна не в редакторе, а в панели поиска: запрос по константе находит ровно те места, где величина используется по смыслу, запрос по числу/букве — всё подряд.

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

Панель глобального поиска в редакторе: запрос по числу 5 даёт сотни результатов

Отсюда же граница для односимвольных имён: они уместны там, где поиск не нужен, потому что всё использование помещается в несколько строк. Классический счётчик цикла — как раз такой случай.

Имена под тип данных

У разных видов значений сложились свои правила. Они не про эстетику: каждое экономит читателю отдельный поход в код.

Счётчики и индексы

Короткий индекс в коротком цикле не мешает никому:

for (let i = 0; i < scores.length; i++) {
  scores[i] = 0;
}

Но как только циклов становится два, буквы перестают работать — перепутанные местами i и j дают код, который выглядит правильным и молча считает не то:

for (let teamIndex = 0; teamIndex < teamCount; teamIndex++) {
  for (let eventIndex = 0; eventIndex < eventCount[teamIndex]; eventIndex++) {
    scores[teamIndex][eventIndex] = 0;
  }
}

С осмысленными именами перестановка scores[eventIndex][teamIndex] бросается в глаза сразу. То же правило действует, когда счётчик нужен после закрывающей скобки: у такого значения должно быть имя, а не буква.

Булевые значения

Для переменных, которые хранят только true или false, правил три: имя-утверждение вместо отрицания, префикс-вопрос вместо абстрактного «флага» и конкретика вместо слова flag.

// было
if (flag) { /* … */ }
if (statusFlag === 0x0F) { /* … */ }
if (!notFound) { /* … */ }

// стало
if (isDataReady) { /* … */ }
if (charType === PRINTABLE_CHAR) { /* … */ }
if (isFound) { /* … */ }

Хуже всего отрицательные имена: в условии они почти всегда оказываются под оператором !, и читателю приходится разворачивать двойное отрицание в голове. А префиксы is, has, can попутно сообщают, что это булевое значение, а не объект и не число.

Константы

Константа называется по смыслу, а не по значению. Имя, повторяющее число, придётся менять вместе с ним — и однажды это забудут сделать:

const FIVE = 5;          // после правки значения имя начнёт врать
const CYCLES_NEEDED = 5; // значение можно менять сколько угодно

Само по себе ключевое слово const запрещает переприсваивание, но не делает объект неизменяемым — разница разобрана в статье про var, let и const.

Перечисления

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

const REPORT_TYPE = Object.freeze({
  daily: "daily",
  weekly: "weekly",
  annual: "annual",
});

if (report.type === REPORT_TYPE.annual) { /* … */ }

Метод Object.freeze запрещает добавлять, менять и удалять свойства — дописать в перечисление вариант из другого модуля не получится. Строковые значения удобнее числовых: в логах видно "annual", а не двойку, которую надо расшифровывать.

Временные переменные

Слово temp сообщает ровно одно: автор не стал придумывать имя. Между тем у большинства промежуточных величин имя есть — просто его надо вспомнить:

const temp = Math.sqrt(b ** 2 - 4 * a * c);
root[0] = (-b + temp) / (2 * a);
root[1] = (-b - temp) / (2 * a);

// у величины есть название — дискриминант
const discriminant = Math.sqrt(b ** 2 - 4 * a * c);
root[0] = (-b + discriminant) / (2 * a);
root[1] = (-b - discriminant) / (2 * a);

Заодно ловушка, на которую регулярно наступают при переносе формул из учебника: возведение в степень в JavaScript записывается как **. Знак ^ — это побитовое исключающее ИЛИ, и выражение b ^ 2 молча посчитает совсем другое, не выдав ни ошибки, ни предупреждения.

Переименование на границе с чужим кодом

Иногда плохое имя приходит извне — из ответа API или из чужого модуля. Менять его на той стороне нельзя, а вот имя, под которым значение живёт у нас, вполне можно назначить своё. Для этого в JavaScript есть переименование при распаковке:

// ответ сервера: { d: 12, st: 1, usr: { nm: "Мария" } }
const { d: elapsedDays, st: statusCode, usr: user } = response;

console.log(elapsedDays); // 12

Тот же приём работает на импортах — когда библиотека экспортирует что-нибудь вроде fmt:

import { fmt as formatPrice } from "./utils/price.js";

Граница с внешним кодом — последнее место, где плохое имя ещё можно остановить. Пропустили его здесь — оно расползётся по всему модулю. Подробнее про синтаксис распаковки объектов и массивов — в статье про деструктуризацию.

Когда имя врёт

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

Тип, которого нет. Имя accountList обещает список. Если под ним лежит Map или обычный объект, читатель напишет цикл, который не заработает. Нейтральное множественное число врать не умеет:

const accountList = new Map(); // обещали список, отдали Map
const accounts = new Map();    // просто «счета», и это правда

Символы, похожие на цифры. Строчная l в большинстве шрифтов неотличима от единицы, прописная O — от нуля:

let a = l;
if (O === l) a = O1;
else l = 01;

Разобрать такой фрагмент без подсветки синтаксиса практически невозможно. Последняя строка вдобавок в современном коде не запустится: запись вида 01 в строгом режиме считается устаревшим восьмеричным литералом и даёт синтаксическую ошибку — про сам режим есть отдельный разбор.

Имя, которое отстало от кода. Функция называлась validateEmail, потом в неё дописали проверку телефона, потом нормализацию пробелов. Имя осталось прежним и описывает треть происходящего. Это самый частый вид лжи в именах: он возникает сам собой, решения соврать никто не принимал. Лечение — на ревью смотреть не только на изменённые строки, но и на имена вокруг них.

Конвенции и то, что их проверяет

Конвенция именования — не про красоту, а про предсказуемость. Увидев имя капсом через подчёркивания, читатель сразу знает: значение задано один раз и меняться не будет. Ему не нужно искать объявление.

Стиль Что им называют Пример
camelCase переменные, функции, методы, свойства userProfile, getUserProfile
PascalCase классы, конструкторы, компоненты OrderService, UserCard
SCREAMING_SNAKE_CASE константы уровня модуля MAX_RETRY_COUNT
kebab-case имена файлов, CSS-классы user-profile.js
#name приватные поля и методы класса #total, #recalculate()

Отдельная история — подчёркивание в начале имени. Долгие годы приватность в JavaScript была договорной: _total означало «снаружи не трогать», но технически поле оставалось обычным — его можно было прочитать, перезаписать и увидеть в списке ключей объекта. Сейчас у языка есть настоящие приватные поля с решёткой:

class Order {
  #total = 0;

  add(price) {
    this.#total += price;
  }

  get total() {
    return this.#total;
  }
}

const order = new Order();
order.add(120);

console.log(order.total);  // 120
console.log(order.#total); // SyntaxError — из-за этой строки не запустится весь файл

Разница принципиальная: подчёркивание — просьба, решётка — запрет на уровне синтаксиса. Обращение к приватному полю извне не проходит даже разбор кода, до выполнения дело не доходит. Подчёркивание никуда не делось: оно осталось в старом коде и в библиотеках, которым важна совместимость, — но в новом коде выбирать между ними уже не нужно.

Поддержка браузерами
chrome
Chrome
74
firefox
Firefox
90
edge
Edge
79
safari
Safari
14.1
opera
Opera
62

Что из этого проверяет линтер

Ловить нарушения конвенций глазами на ревью — трата времени рецензента. ESLint (линтер, то есть инструмент статической проверки кода без его запуска) умеет следить за именами сам:

  • camelcase — требует camelCase у переменных и свойств;
  • id-length — задаёт минимальную и максимальную длину идентификаторов со списком исключений;
  • id-denylist — чёрный список имён: сюда обычно отправляют data, temp, foo;
  • id-match — проверка имени по регулярному выражению, если в команде свои требования.

Важный момент для тех, кто настраивает линтер по старым инструкциям: файлов .eslintrc больше нет. Старую систему конфигурации окончательно убрали в ESLint 10, остался только плоский конфиг — обычный JS-модуль eslint.config.js:

// eslint.config.js
export default [
  {
    rules: {
      camelcase: ["error", { properties: "never" }],
      "id-length": ["error", { min: 2, exceptions: ["i", "j", "x", "y", "_"] }],
      "id-denylist": ["error", "data", "temp", "foo", "bar"],
    },
  },
];

Сами правила про имена помечены в документации как frozen — новых возможностей у них не появится, но работать они продолжают. Если проект на TypeScript, вместо них обычно берут правило naming-convention из typescript-eslint: оно гибче и позволяет задать разные требования отдельно для переменных, параметров, типов, приватных полей и булевых значений.

Перегибы: когда хорошее имя делает хуже

Именование — та область, где легко перестараться. Несколько типичных перегибов.

Тип в имени. Венгерская нотация — приём, при котором к имени спереди клеится обозначение типа: strName, arrItems, oUser. Она появилась там, где редактор не умел подсказывать типы; сегодня он их показывает сам, а префикс начинает врать первым: массив, ставший множеством, остаётся arrItems ровно до того, как кто-нибудь напишет по нему цикл по индексу.

Суффиксы-пустышки. Слова Data, Info, Object, Manager чаще всего не добавляют ничего: userData и user различаются только длиной. Проверка: убрать суффикс и посмотреть, изменился ли смысл. Не изменился — суффикс лишний.

Длинное имя в коротком скоупе. Идентификатор currentlyProcessedOrderItemIndex в цикле на четыре строки не помогает, а мешает: строка перестаёт помещаться на экран, а объявление всё равно видно сверху.

Имя вместо декомпозиции. Если функции требуется имя из шести слов, проблема не в имени:

// имя честно описывает три обязанности сразу
function calculateAndSendMonthlyReportToAdmins(orders) { /* … */ }

// обязанности разделены — и каждому имени хватило двух слов
function buildMonthlyReport(orders) { /* … */ }
function sendReport(report, recipients) { /* … */ }

Длинное составное имя — это диагноз, а не решение. Союз «и» внутри имени функции почти всегда означает, что функцию пора разбить.

Появился и новый повод относиться к именам внимательнее: ассистенты, дописывающие код, читают окружающий файл как подсказку. По коду из data, temp и item угадать замысел нельзя ни человеку, ни модели. Про остальные риски сгенерированного кода — в разборе паттернов AI-долга.

Итог

Короткий список вопросов, которых хватает в большинстве случаев:

  • Нужен ли рядом комментарий, объясняющий имя? Нужен — имя слабое.
  • Понятно ли из имени, от чего считается величина и в чём измеряется?
  • Соответствует ли длина имени размеру области видимости?
  • Различаются ли похожие имена смыслом, а не цифрой в конце?
  • Можно ли произнести имя вслух и найти его поиском по проекту?
  • Не обещает ли имя тип, структуру или поведение, которых на самом деле нет?
  • Не пытается ли одно имя описать несколько обязанностей сразу?

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