Перейти к основному содержимому

Рекомендации по разработке на JavaScript

Разработчику Архитектору

Рекомендации по разработке на JavaScript​

См. также: Тестирование — Vitest и Testing Library.

Так, представим, что вы уже изучили основы языка. Как правильно разрабатывать на JavaScript?


Общие принципы​

Чистый код и качество​

Качество кода определяется количеством вопросов, которые возникают при его чтении. Чем меньше вопросов, тем выше качество. Основная цель разработки — создание понятного, поддерживаемого и расширяемого кода.

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


Один оператор на строку​

Каждый оператор заканчивается точкой с запятой. Автоматическая точка с запятой не используется. Это правило обеспечивает предсказуемость выполнения кода и избегает неожиданных ошибок.


Использование современных возможностей языка​

Приоритет отдается современным возможностям JavaScript. Код пишется с использованием актуальных версий языка и стандартов. Устаревшие конструкции заменяются современными аналогами.


Декларативный подход​

Декларативный стиль программирования предпочтительнее императивного, где это упрощает понимание. Декларативный код описывает, что нужно сделать, а не как это сделать.


Требования к именованию​

Стили именования​

Различные элементы кода используют разные стили именования:

  • Переменные, параметры, функции, методы, свойства — camelCase
  • Классы, конструкторы, интерфейсы — PascalCase
  • Константы — UPPER_CASE или camelCase внутри функций
  • Обработчики событий — префикс on или суффикс Handler

Названия переменных​

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

Значения именуются существительными — user, product, config. Массивы именуются во множественном числе — users, products, items. Функции именуются глаголами или глаголами с существительными — getUser, updateProduct, validateForm.


Названия функций и методов​

Функции и методы именуются глаголами, которые описывают выполняемое действие. Имена должны быть описательными и передавать суть функции. Аргументы функций также именуются осмысленно.

Примеры корректных имен:

function notifyUser(emailAddress) { }
function calculateTotalPrice(items) { }
function validateFormData(data) { }

Избегание избыточности​

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

Плохо:

const userUserName = "John";
const productProductPrice = 100;

Хорошо:

const userName = "John";
const productPrice = 100;

Константы и магические числа​

Магические числа и строки выносятся в константы. Литеральные значения не используются в коде, кроме случаев, когда их смысл ясен из контекста.

const MAX_USERS = 100;
const DEFAULT_TIMEOUT = 5000;
const API_BASE_URL = "https://api.example.com";

Требования по оформлению​

Отступы и пробелы​

Отступы составляют четыре пробела. Табуляция не используется. Операторы окружены пробелами. После запятых ставится пробел.

let x = a + b;
let items = ["item1", "item2", "item3"];

Фигурные скобки​

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

Код ITЗагрузка примера кода…


Длина строк​

Строки ограничиваются 65 символами для повышения читаемости. Длинные инструкции разбиваются на несколько строк. Точки переноса размещаются перед операторами.


Пустые строки​

Пустые строки разделяют логические блоки кода:

  • Между методами класса
  • Между многострочными выражениями
  • После закрытия парных скобок
  • Между несвязанными блоками кода

Кавычки​

Для строк используются одинарные кавычки. Двойные кавычки применяются в HTML-атрибутах. Шаблонные литералы оформляются обратными кавычками.

const message = 'Hello, world!';
const html = "<div class='container'></div>";
const greeting = `Hello, ${name}!`;

Объектные литералы​

Объектные литералы представляют собой либо структуры с ключами без кавычек, либо словари с ключами в кавычках. Эти типы ключей не смешиваются в одном объекте.

// Структура
const config = {
width: 42,
height: 100,
enabled: true
};

// Словарь
const translations = {
'hello': 'Привет',
'goodbye': 'До свидания'
};

Требования к переменным​

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

Все локальные переменные объявляются с помощью const или let. Ключевое слово const используется по умолчанию. let применяется, когда переменная нуждается в переназначении. Ключевое слово var не используется.

const PI = 3.14159;
const users = [];

let counter = 0;
counter = counter + 1;
const <имя> = <значение>;
let <имя> = <значение>;

Одна переменная на объявление​

Каждая декларация объявляет только одну переменную. Совместное объявление нескольких переменных не используется.

// Плохо
let a = 1, b = 2, c = 3;

// Хорошо
const a = 1;
const b = 2;
const c = 3;

Деструктуризация​

Деструктуризация используется для извлечения значений из объектов и массивов. Это упрощает доступ к вложенным свойствам и делает код чище.

const { inputIndex, readOnly, elementConfig: { linkedCollectionName, linkedCollectionGuid } } = this.props;

const [first, second, third] = items;
const { <свойство1>, <свойство2> } = <объект>;
const [ <элемент1>, <элемент2> ] = <массив>;

Неиспользуемый код​

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


Отладочные команды​

Команда debugger не остается в итоговом коде. Отладочные точки удаляются перед коммитом изменений.


Требования к функциям​

Размер и сложность функций​

Функции должны решать одну задачу. Идеальная функция содержит не более двух аргументов. Функции с большим количеством параметров декомпозируются или принимают объект с настройками.

// Плохо
function getUsers(fields, fromDate, toDate, sortBy, limit) { }

// Хорошо
function getUsers({ fields, fromDate, toDate, sortBy, limit }) { }

Аргументы по умолчанию​

Аргументы по умолчанию предпочтительнее условных конструкций внутри функции. Это делает сигнатуру функции более понятной.

// Плохо
function createShape(type) {
const shapeType = type || "cube";
}

// Хорошо
function createShape(type = "cube") { }
function <имя>(<параметр1>, <параметр2> = <значение_по_умолчанию>) {
// тело функции
}

Возврат значений​

Функции возвращают значения явно. Неявное возвращение используется только для коротких стрелочных функций. Функции не возвращают null для коллекций — вместо этого возвращается пустой массив или объект.

function getItems() {
if (!hasItems) {
return []; // Вместо null
}
return items;
}

Чистые функции​

Предпочтение отдается чистым функциям — функциям без побочных эффектов. Чистые функции проще тестировать и понимать. Они всегда возвращают одинаковый результат для одинаковых входных данных.


Стрелочные функции​

Стрелочные функции используются для коротких колбэков и обработчиков. Для методов класса и конструкторов используются обычные функции.

const doubled = numbers.map(num => num * 2);

const filtered = items.filter(item => item.active);
const <имя> = (<параметр>) => <выражение>;
const <имя> = (<параметр>) => {
// тело функции
};

Функции высшего порядка​

Функции высшего порядка предпочтительнее традиционных циклов. Методы map, filter, reduce, find, some, every делают код более декларативным.

// Плохо
let sum = 0;
for (let num of numbers) {
sum += num;
}

// Хорошо
const sum = numbers.reduce((total, num) => total + num, 0);

Требования к массивам​

Создание массивов​

Для создания массивов используется литеральная нотация. Конструктор Array не применяется.

const items = [];
const users = ["John", "Jane", "Bob"];
const <имя> = [];
const <имя> = [<значение1>, <значение2>];

Добавление элементов​

Для добавления элементов в массив используется метод push. Прямое присваивание по индексу не применяется.

items.push("newItem");

Копирование массивов​

Для копирования массивов используется оператор расширения .... Для глубокого копирования применяется комбинация JSON.parse и JSON.stringify или специализированные библиотеки.

const itemsCopy = [...items];
const deepCopy = JSON.parse(JSON.stringify(items));

Итерация по массивам​

Итерация по массивам выполняется с помощью методов высшего порядка. Циклы for-in и for-of используются только в специальных случаях.

items.forEach(item => {
console.log(item);
});

const activeItems = items.filter(item => item.active);

const names = items.map(item => item.name);

Требования к объектам​

Создание объектов​

Для создания объектов используется литеральная нотация. Конструктор Object не применяется.

const user = {
name: "John",
age: 30,
email: "john@example.com"
};

Копирование объектов​

Для копирования объектов используется метод Object.assign или оператор расширения. Глубокое копирование выполняется рекурсивно или с помощью специализированных библиотек.

const userCopy = { ...user };
const merged = { ...defaults, ...config };

Установка свойств по умолчанию​

Свойства объектов по умолчанию устанавливаются с помощью Object.assign. Это делает код более читаемым и предсказуемым.

function createShape(config) {
config = Object.assign({
type: "cube",
width: 250,
height: 250
}, config);
}

Доступ к свойствам​

Для доступа к свойствам объекта используется точечная нотация. Квадратные скобки применяются только для динамических ключей.

const name = user.name;
const key = "email";
const value = user[key];

Требования к классам​

Объявление классов​

Классы объявляются с помощью ключевого слова class. Функциональные конструкторы не используются в современном коде.

class User {
constructor(name, email) {
this.name = name;
this.email = email;
}

greet() {
return `Hello, ${this.name}!`;
}
}

Код ITЗагрузка примера кода…


Наследование​

Наследование реализуется с помощью ключевого слова extends. Множественное наследование не поддерживается — для этого используются миксины или композиция.

class Admin extends User {
constructor(name, email, permissions) {
super(name, email);
this.permissions = permissions;
}
}
class <ДочернийКласс> extends <РодительскийКласс> {
constructor(<параметры>) {
super(<параметры>);
// дополнительная инициализация
}
}

Статические методы​

Статические методы объявляются с помощью ключевого слова static. Они вызываются на классе, а не на экземпляре.

class MathUtils {
static add(a, b) {
return a + b;
}
}

const result = MathUtils.add(2, 3);

Приватные поля​

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

class Counter {
#count = 0;

increment() {
this.#count++;
}

getCount() {
return this.#count;
}
}

Требования к асинхронному программированию​

Промисы​

Асинхронные операции реализуются с помощью промисов. Коллбэки используются только в специальных случаях.

function fetchData(url) {
return fetch(url)
.then(response => response.json())
.then(data => data)
.catch(error => {
console.error("Error:", error);
throw error;
});
}
<функцияВозвращающаяПромис>()
.then(<результат> => { /* обработка */ })
.catch(<ошибка> => { /* обработка ошибки */ });

Async/Await​

Синтаксис async/await предпочтительнее цепочек промисов. Он делает асинхронный код более читаемым и похожим на синхронный.

async function getUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to fetch user data:", error);
throw error;
}
}
async function <имя>(<параметры>) {
try {
const <результат> = await <промис>();
return <результат>;
} catch (<ошибка>) {
// обработка ошибки
}
}

Обработка ошибок​

Ошибки в асинхронном коде обрабатываются с помощью try/catch. Неперехваченные ошибки логируются. Промисы всегда завершаются обработчиком ошибок.

async function loadData() {
try {
const data = await fetchData();
process(data);
} catch (error) {
logger.error("Failed to load data:", error);
showErrorMessage("Не удалось загрузить данные");
}
}

Требования к обработке ошибок​

Строгие сравнения​

Операторы === и !== используются вместо == и !=. Это предотвращает неожиданное приведение типов.

if (value === 5) { }
if (name !== null) { }

Проверка аргументов​

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

function divide(a, b) {
if (typeof a !== "number" || typeof b !== "number") {
throw new TypeError("Arguments must be numbers");
}
if (b === 0) {
throw new Error("Division by zero");
}
return a / b;
}
if (typeof <аргумент> !== '<тип>') {
throw new TypeError('<сообщение>');
}
if (<некорректное_условие>) {
throw new Error('<сообщение>');
}

Исключения для исключительных ситуаций​

Исключения используются только для исключительных ситуаций. Управление потоком выполнения не осуществляется с помощью исключений. Проверки предпочтительнее перехвата исключений.

// Плохо
try {
const value = obj.property.value;
} catch (error) {
// Обработка ошибки
}

// Хорошо
if (obj && obj.property) {
const value = obj.property.value;
}

Логирование ошибок​

Ошибки логируются с полной информацией. Используется метод toString() для получения стека вызовов и внутренних исключений.

try {
// Код
} catch (error) {
logger.error(`Operation failed: ${error.toString()}`);
// Или
logger.error(`Operation failed: ${error}`);
}

Требования к комментариям​

Общие правила​

Комментарии добавляются для документирования кода. Комментарии размещаются на отдельной строке, а не в конце строки кода. Текст комментария начинается с заглавной буквы и заканчивается точкой.

// Получение данных пользователя
const userData = getUserData(userId);

// Валидация входных параметров
validateInput(params);

JSDoc​

Для документирования функций, классов и методов используется формат JSDoc. Комментарии содержат описание, параметры, возвращаемые значения и возможные исключения.

/**
* Создает нового пользователя
* @param {string} name - Имя пользователя
* @param {string} email - Email пользователя
* @param {number} age - Возраст пользователя
* @returns {User} Созданный пользователь
* @throws {Error} Если email уже существует
*/
function createUser(name, email, age) {
// Реализация
}

Блочные комментарии​

Блочные комментарии снабжены отступом на том же уровне, что и окружающий код. Многострочные комментарии начинаются с /* и заканчиваются */. Последующие строки начинаются с *, выровненного с предыдущей строкой.

/*
* Это многострочный
* комментарий
* с правильным форматированием
*/

Избегание TODO-комментариев​

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


Требования к работе с данными​

Валидация входных данных​

Все значения, поступающие извне, проходят валидацию. Валидация включает проверку типа, формата, диапазона и бизнес-правил.

function processUserData(userData) {
if (!userData || typeof userData !== "object") {
throw new Error("Invalid user data");
}
if (!userData.email || !isValidEmail(userData.email)) {
throw new Error("Invalid email format");
}
// Обработка данных
}
if (<объект> && <объект>.<свойство>) {
// безопасный доступ
}

Параметризованные запросы​

При работе с базами данных используются параметризованные запросы. Это предотвращает SQL-инъекции и другие уязвимости.

// Плохо
const query = `SELECT * FROM users WHERE id = ${userId}`;

// Хорошо
const query = "SELECT * FROM users WHERE id = ?";
const result = db.query(query, [userId]);

Ограничение запрашиваемых данных​

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

// Плохо
const allUsers = await db.query("SELECT * FROM users");

// Хорошо
const users = await db.query(
"SELECT id, name, email FROM users WHERE active = 1 LIMIT 100"
);

Требования к производительности​

Избегание частых аллокаций​

Частые аллокации объектов в критических участках кода избегаются. Объекты создаются за пределами циклов. Повторное использование объектов предпочтительнее создания новых.

// Плохо
for (let i = 0; i < items.length; i++) {
const temp = { value: items[i] };
process(temp);
}

// Хорошее
const temp = {};
for (let i = 0; i < items.length; i++) {
temp.value = items[i];
process(temp);
}

Оптимизация циклов​

Циклы оптимизируются для повышения производительности. Длина массива кэшируется. Вложенные циклы минимизируются.

for (let i = 0, len = items.length; i < len; i++) {
// Обработка
}

Отложенные вычисления​

Тяжелые вычисления выполняются по требованию. Кэширование результатов предотвращает повторные вычисления.

class DataProcessor {
#cachedResult = null;

getResult() {
if (this.#cachedResult === null) {
this.#cachedResult = this.#calculateExpensiveResult();
}
return this.#cachedResult;
}
}

Требования к безопасности​

Экранирование данных​

данные экранируются перед выводом в HTML. Это предотвращает XSS-атаки. Специальные символы заменяются соответствующими HTML-сущностями.

function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
const <безопаснаяСтрока> = <строка>.replace(/[&<>"']/g, <символ> => <карта>[<символ>]);

Валидация на стороне сервера​

Все данные валидируются на стороне сервера. Клиентская валидация дополняет, но не заменяет серверную.


Защита конфиденциальных данных​

Конфиденциальные данные не выводятся в логи и ошибки. API-ключи, пароли и персональные данные защищаются.

// Плохо
logger.error(`Failed to authenticate user ${username} with password ${password}`);

// Хорошо
logger.error(`Failed to authenticate user ${username}`);

Требования к тестированию​

Покрытие тестами​

Критически важные компоненты покрываются модульными и интеграционными тестами. Тесты пишутся до или одновременно с реализацией (TDD/BDD).


Изоляция тестов​

Тесты изолированы друг от друга. Каждый тест может выполняться независимо. Состояние между тестами не сохраняется.


Описательные названия тестов​

Названия тестов описывают проверяемое поведение. Тесты читаются как спецификация.

describe("UserService", () => {
it("should create a new user with valid data", () => {
// Тест
});

it("should throw an error when email is invalid", () => {
// Тест
});
});

Инструменты и настройки​

ESLint​

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


Prettier​

Prettier используется для автоматического форматирования кода. Настройки включают отступы, кавычки, точку с запятой и длину строк.


EditorConfig​

Файл .editorconfig обеспечивает единообразие настроек редактора между разработчиками. Настройки включают отступы, кодировку и окончания строк.

root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

Git Hooks​

Git hooks используются для автоматической проверки кода перед коммитом. Pre-commit хуки запускают линтер и тесты.


Содержание