Код читают гораздо чаще, чем пишут: свой собственный — через месяц, чужой — каждый раз на код-ревью (проверке изменений другим разработчиком перед тем, как они попадут в общую ветку). И большая часть этого чтения сводится к чтению имён. Имя переменной, функции или константы отвечает на вопрос «что здесь лежит» быстрее любого комментария — если оно выбрано осмысленно.
Приёмы давно описаны — в 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;
}
Разница видна не в редакторе, а в панели поиска: запрос по константе находит ровно те места, где величина используется по смыслу, запрос по числу/букве — всё подряд.


Отсюда же граница для односимвольных имён: они уместны там, где поиск не нужен, потому что всё использование помещается в несколько строк. Классический счётчик цикла — как раз такой случай.
Имена под тип данных
У разных видов значений сложились свои правила. Они не про эстетику: каждое экономит читателю отдельный поход в код.
Счётчики и индексы
Короткий индекс в коротком цикле не мешает никому:
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 — из-за этой строки не запустится весь файл
Разница принципиальная: подчёркивание — просьба, решётка — запрет на уровне синтаксиса. Обращение к приватному полю извне не проходит даже разбор кода, до выполнения дело не доходит. Подчёркивание никуда не делось: оно осталось в старом коде и в библиотеках, которым важна совместимость, — но в новом коде выбирать между ними уже не нужно.
Что из этого проверяет линтер
Ловить нарушения конвенций глазами на ревью — трата времени рецензента. 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-долга.
Итог
Короткий список вопросов, которых хватает в большинстве случаев:
- Нужен ли рядом комментарий, объясняющий имя? Нужен — имя слабое.
- Понятно ли из имени, от чего считается величина и в чём измеряется?
- Соответствует ли длина имени размеру области видимости?
- Различаются ли похожие имена смыслом, а не цифрой в конце?
- Можно ли произнести имя вслух и найти его поиском по проекту?
- Не обещает ли имя тип, структуру или поведение, которых на самом деле нет?
- Не пытается ли одно имя описать несколько обязанностей сразу?
Переименование — самый дешёвый вид рефакторинга: редактор делает его одной командой, риск сломать поведение близок к нулю, а читаемость меняется заметно. Из всех способов улучшить код за пять минут этот — лучший.
Комментарии (0)