Используйте let и const вместо var для объявления переменных
В устаревшем коде часто встречается объявление переменных с помощью ключевого слова var. Переменные, объявленные с помощью var, имеют функциональную область видимости. Хотя var всe ещe поддерживается, использование let и const предпочтительнее, поскольку они обеспечивают блочную область видимости, что делает код более предсказуемым и уменьшает вероятность неожиданных ошибок.
Пример с let – переменная j существует только внутри цикла:
for (let j = 1; j < 5; j++) {
console.log(j); // выводит числа от 1 до 4
}
console.log(j); // выбросит ошибку: Uncaught ReferenceError: j is not defined
Пример с var – переменная j существует вне цикла, что может привести к неожиданным ошибкам:
for (var j = 1; j < 5; j++) {
console.log(j); // выводит числа от 1 до 4
}
console.log(j); // выводит 5, так как `var` делает переменную доступной за пределами цикла
Классы вместо прототипов
Во многих старых кодовых базах или статьях про ООП в JavaScript можно встретить использование функции и прототипов для создания классов. Это был способ имитировать работу классов до их официального появления в языке.
Пример старого подхода с использованием прототипов:
function Person(name) {
this.name = name; // Задаем свойство в конструкторе
}
Person.prototype.getName = function () {
return this.name; // Определяем метод через прототип
}
const p = new Person('A');
console.log(p.getName()); // 'A'
Современный подход с использованием классов:
class Person {
constructor(name) {
this.name = name; // Свойство задается в конструкторе
}
getName() {
return this.name; // Метод определен прямо в классе
}
}
const p = new Person('A');
console.log(p.getName()); // 'A'
Используйте class
вместо старого подхода с прототипами. Это стандартный, современный и читаемый способ создания объектов в JavaScript: классы поддерживают наследование, статические методы и приватные поля.
Приватные поля класса
Раньше JavaScript-разработчики часто использовали символ подчеркивания _ для обозначения приватных свойств или методов в классах. Однако это было всего лишь договоренностью между программистами и не обеспечивало реальной приватности.
Здесь свойство _name
доступно из-за пределов класса, что делает его уязвимым к случайному или преднамеренному изменению:
class Person {
constructor(name) {
this._name = name; // По соглашению считается "приватным", но это не так
}
getName() {
return this._name;
}
}
const p = new Person('A');
console.log(p.getName()); // 'A'
console.log(p._name); // 'A' (доступно извне, приватность не обеспечена)
JavaScript теперь поддерживает настоящие приватные поля, которые обозначаются с помощью символа #. Такие поля доступны только внутри класса и недосягаемы извне:
class Person {
#name; // Объявление приватного поля
constructor(name) {
this.#name = name; // Инициализация приватного поля
}
getName() {
return this.#name; // Метод для доступа к приватному полю
}
}
const p = new Person('A');
console.log(p.getName()); // 'A'
console.log(p.#name); // Ошибка: Private field '#name' must be declared in an enclosing class
Используйте приватные поля с #, если вам нужно защитить данные внутри класса. Это делает код более безопасным, устойчивым и соответствует современным стандартам JavaScript.
Стрелочные функции
Стрелочные функции часто используются для создания компактных и читаемых анонимных функций или колбэков. Они особенно полезны при работе с функциями высшего порядка map, filter и reduce:
const numbers = [1, 2];
// Стрелочная функция
numbers.map(num => num * 2);
// Вместо более длинного варианта:
numbers.map(function (num) {
return num * 2;
});
Почему стоит использовать стрелочные функции:
- Краткость. Стрелочные функции убирают лишний шаблонный код (ключевые слова function, return и фигурные скобки).
- Автоматическая привязка контекста this. Стрелочные функции не создают собственный контекст this. Вместо этого они используют контекст из окружающей области, где они были объявлены (лексическая область видимости).
Здесь стрелочная функция гарантирует, что this будет ссылаться на экземпляр класса Person, независимо от того, где вызывается метод getName:
class Person {
name = 'A';
// Стрелочная функция сохраняет контекст 'this'
getName = () => this.name;
}
const getName = new Person().getName;
console.log(getName()); // 'A'
Используйте стрелочные функции, когда нужно создать короткий и понятный колбэк или анонимную функцию. Они не только сокращают код, но и решают многие проблемы с привязкой контекста this, что делает их незаменимыми в современном JavaScript.
Оператор нулевого слияния
Раньше JavaScript-разработчики часто использовали логический оператор || для задания значений по умолчанию в случае, если переменная имеет значение undefined или null. Однако у этого подхода есть побочный эффект: || воспринимает значения 0, false или пустая строка "", как ложные и заменяет их на значение по умолчанию:
const value = 0;
const result = value || 10;
console.log(result); // 10 (неожиданно, если 0 — это корректное значение)
Оператор нулевого слияния ?? появился для того, чтобы корректно обрабатывать подобные ситуации. Он проверяет только на null или undefined и не заменяет такие ложные значения, как 0, false или "":
const value = 0;
const result = value ?? 10;
console.log(result); // 0 (ожидаемо)
Используйте ??, если хотите задавать значения по умолчанию только для случаев null или undefined. Это делает код более предсказуемым и надежным.
Опциональная цепочка
При работе с глубоко вложенными объектами или массивами в JavaScript часто возникает необходимость проверять существование каждого уровня вложенности перед попыткой получить значение из следующего уровня. До появления оператора ?. (опциональной цепочки) для упрощенного доступа к вложенным свойствам объектов или массивов это приводило к громоздкому и повторяющемуся коду. Здесь нам приходится вручную проверять, существует ли свойство price, перед доступом к price.tax, чтобы избежать ошибок:
const product = {};
// Без опциональной цепочки
const tax = (product.price && product.price.tax) ?? undefined;
console.log(tax); // undefined
Оператор ?. автоматически проверяет, существует ли свойство или метод, прежде чем попытаться к нему обратиться. Если на каком-либо уровне цепочки встречается null или undefined, оператор возвращает undefined, не выбрасывая ошибку:
const product = {};
// С оциональной цепочкой
const tax = product?.price?.tax;
console.log(tax); // undefined
Оператор ?. нужно использовать потому, что он:
- Сокращает объем шаблонного кода и упрощает работу с глубоко вложенными структурами.
- Снижает риск появления ошибок, так как позволяет аккуратно обрабатывать значения null или undefined.
- Улучшает читаемость и удобство поддержки кода, особенно при работе с динамическими данными или сложными объектами.
async/await
В старом JavaScript для работы с асинхронными операциями часто использовались колбэки или цепочки промисов, что быстро превращало код в сложный и трудночитаемый. Например, использование .then() для цепочек промисов делало логику менее наглядной, особенно при большом количестве асинхронных операций:
function fetchData() {
return fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
});
}
Используйте async и await, чтобы асинхронный код выглядел как обычный синхронный. Это улучшает читаемость и упрощает обработку ошибок с помощью try...catch:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
Синтаксис async/await упрощает работу с асинхронным кодом, убирая необходимость в использовании цепочек .then() и .catch(). Это делает код:
- Читаемым – он становится похож на обычный последовательный код.
- Поддерживаемым – легче разобраться в логике, особенно при большом количестве асинхронных вызовов.
- Удобным для обработки ошибок– конструкция try...catch упрощает отладку и поддержку.
Взаимодействие с ключами и значениями объектов
В старом JavaScript для работы с ключами и значениями объектов часто использовались циклы, например, for...in или Object.keys(), с последующим доступом к значениям через скобочную или точечную нотацию. Такой подход может приводить к избыточному коду:
const obj = { a: 1, b: 2, c: 3 };
// Старый подход с Object.keys()
Object.keys(obj).forEach(key => {
console.log(key, obj[key]);
});
Используйте современные методы Object.entries(), Object.values() и Object.keys(), чтобы упростить работу с объектами. Эти методы возвращают удобные структуры данных (например, массивы), делая код более лаконичным и понятным:
const obj = { a: 1, b: 2, c: 3 };
// Используем Object.entries() для перебора пар ключ-значение
Object.entries(obj).forEach(([key, value]) => {
console.log(key, value);
});
// Используем Object.values() для работы только со значениями
Object.values(obj).forEach(value => {
console.log(value);
});
Методы Object.entries(), Object.values() и Object.keys() нужно использовать потому, что они:
- Сокращают объем шаблонного кода, необходимого для перебора объектов.
- Удобны при работе с сложными или динамическими структурами данных.
- Позволяют легко преобразовывать объекты в другие структуры данных (например, массивы).
Как проверить, является ли переменная массивом
Раньше для проверки того, является ли переменная массивом, разработчики использовали различные сложные методы. Например, проверяли конструктор или использовали instanceof. Однако такие подходы часто были ненадежны, особенно при работе в разных контекстах выполнения (например, между iframe):
const arr = [1, 2, 3];
// Старый подход
console.log(arr instanceof Array); // true, но не всегда надежно в разных контекстах
Используйте современный метод Array.isArray(), который обеспечивает простой и надежный способ проверить, является ли переменная массивом. Этот метод стабильно работает в любых средах и контекстах выполнения:
const arr = [1, 2, 3];
console.log(Array.isArray(arr)); // true
Map
Раньше в JavaScript для сопоставления ключей со значениями разработчики использовали обычные объекты {}. Однако у этого подхода есть ограничения, особенно если в качестве ключей нужно использовать не строки и не символы.
Дело в том, что обычные объекты могут использовать только строки или символы в качестве ключей. Если попытаться использовать объект или массив, он автоматически преобразуется в строку, что может привести к неожиданному поведению:
const obj = {};
const key = { id: 1 };
// Попытка использовать объект в качестве ключа
obj[key] = 'value';
console.log(obj);
// { '[object Object]': 'value' } - ключ преобразован в строку!
Используйте Map, если вам нужно хранить ключи любого типа (включая объекты и массивы) или если вам требуется более гибкая структура данных:
const map = new Map();
const key = { id: 1 };
// Используем объект как ключ в Map
map.set(key, 'value');
console.log(map.get(key)); // 'value'
Преимущества Map:
- Позволяет использовать любой тип данных в качестве ключа, включая объекты и массивы.
- Сохраняет порядок добавленных элементов, в отличие от обычных объектов.
- Обеспечивает быстрый поиск и доступ к данным, особенно в больших коллекциях.
Symbol для скрытых значений
В JavaScript объекты обычно используются для хранения пар «ключ-значение». Однако если вам нужно добавить «скрытые» или уникальные свойства в объект без риска конфликтов имен с другими свойствами, или если вы хотите, чтобы эти свойства не были доступны извне, можно использовать Symbol.
Что делает Symbol:
- Создает уникальные ключи, которые нельзя случайно перезаписать.
- Эти ключи не отображаются при переборе объекта (for...in, Object.keys()).
- Символы не превращаются в строки, поэтому их нельзя случайно перезаписать другим свойством.
const obj = { name: 'Alice' };
const hiddenKey = Symbol('hidden'); // Создаем уникальный ключ
obj[hiddenKey] = 'Secret Value';
console.log(obj.name); // 'Alice'
console.log(obj[hiddenKey]); // 'Secret Value'
При использовании Symbol скрытые свойства видны только через Object.getOwnPropertySymbols():
console.log(Object.keys(obj)); // ['name'] (Символьные ключи не отображаются)
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(hidden)] (доступны, если явно запрашивать)
Преимущества Symbol:
- Предотвращает конфликты имен – два символа никогда не будут равны, даже если имеют одинаковое описание.
- Инкапсуляция – скрытые свойства не мешают другим частям кода и не перезаписываются случайно.
- Полезно в библиотеках и фреймворках – можно хранить метаданные или внутреннее состояние объекта без влияния на основную логику.
Ознакомьтесь с возможностями Intl API перед использованием сторонних библиотек
Раньше разработчики часто использовали сторонние библиотеки для форматирования дат, чисел и валют в разных локалях. Хотя такие библиотеки обладают мощным функционалом, они:
- Увеличивают размер проекта.
- Дублируют возможности, уже встроенные в JavaScript.
Перед установкой библиотеки попробуйте встроенный API Intl, который поддерживает форматирование:
- Чисел
- Дат
- Валют
- Других локализованных данных
Примеры использования Intl
Форматирование валюты без сторонних библиотек:
const amount = 123456.78;
const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
console.log(formatter.format(amount)); // $123,456.78
Форматирование дат:
const date = new Date();
const dateFormatter = new Intl.DateTimeFormat('en-GB', { year: 'numeric', month: 'long', day: 'numeric' });
console.log(dateFormatter.format(date)); // "15 October 2024"
Используйте строгое равенство === по возможности
Одной из самых сложных и удивительных особенностей JavaScript является поведение оператора нестрогого равенства ==
. Этот оператор выполняет приведение типов, пытаясь привести операнды к одному типу перед сравнением. Это может приводить к странным и неожиданным результатам, например:
console.log([] == ![]); // true (Это действительно удивительно!)
Используйте строгое равенство === вместо нестрогого ==, если это возможно:
- Строгое равенство
===
не выполняет приведение типов. - Оно сравнивает и значение, и тип данных напрямую, что обеспечивает более предсказуемое поведение.
console.log([] === ![]); // false (как и ожидалось)
Вот еще пример различия между == и ===:
console.log(0 == ''); // true (потому что '' приводится к 0)
console.log(0 === ''); // false (тип и значение разные)
Почему важно использовать ===:
- Избежание неожиданных ошибок. Приведение типов в == может быть непредсказуемым, особенно при работе с разными типами данных (числа, строки, булевы значения).
- Более читаемый и надежный код. Использование === делает ваши сравнения предсказуемыми и уменьшает вероятность появления скрытых багов.
Явная обработка выражений в if-условиях
В JavaScript оператор if автоматически преобразует результат выражения в истинное (truthy) или ложное (falsy) значение. Это означает, что следующие значения считаются ложными (falsy):
- 0
""
(пустая строка)- null
- undefined
- false
- NaN
Все остальные значения считаются истинными (truthy), даже такие, как пустой массив [] или пустой объект {}. Неявное приведение типов может повлечь за собой неожиданные проблемы, например:
const value = 0;
if (value) {
console.log('Этот код не выполнится, так как 0 - falsy.');
}
Чтобы избежать неожиданных результатов, лучше явно указывать условия в if.
1. Проверка на конкретное значение:
const value = 0;
if (value !== 0) {
console.log('Этот код выполнится, только если value не равно 0.');
}
2. Проверка на null или undefined:
const name = null;
if (name != null) { // проверяем, что name не null и не undefined
console.log('Переменная name определена');
} else {
console.log('Переменная name равна null или undefined');
}
Явная проверка особенно важна, когда мы работаем со значениями, которые могут принимать 0, false, null или "", так как их автоматическое преобразование может привести к нежелательному поведению.
Не используйте встроенный Number для точных вычислений
В JavaScript встроенный тип Number представляет собой число с плавающей запятой в формате IEEE 754. Этот формат удобен и эффективен, но он может приводить к неточностям в вычислениях, особенно при работе с десятичными дробями. Здесь, например, значение не равно 0.3, как можно было бы ожидать, из-за особенностей двоичного представления чисел с плавающей запятой:
console.log(0.1 + 0.2); // 0.30000000000000004
Такие неточности могут быть критичными, особенно если вы работаете с:
- Платежами и финансовыми расчетами.
- Бухгалтерскими отчетами.
- Криптовалютами.
Даже небольшая ошибка в вычислениях может привести к значительным потерям или юридическим проблемам. Для точных расчетов необходимо использовать специализированные библиотеки decimal.js или big.js:
const Decimal = require('decimal.js');
const result = new Decimal(0.1).plus(0.2);
console.log(result.toString()); // '0.3'
Будьте осторожны с JSON и большими числами
В JavaScript есть ограничения на работу с очень большими числами. Максимальное безопасное целое число (Number.MAX_SAFE_INTEGER) – это 9007199254740991.
Числа больше этого значения могут терять точность, что может привести к ошибкам, особенно при взаимодействии с API или базами данных, где идентификаторы могут быть очень большими. Здесь, например, значение 9007199254740999 изменилось на 9007199254741000, потому что JavaScript не может точно представить числа больше MAX_SAFE_INTEGER:
console.log(
JSON.parse('{"id": 9007199254740999}')
);
// Вывод: { id: 9007199254741000 } (Ошибка точности!)
Решение 1: Использование reviver в JSON.parse()
Можно явно обрабатывать большие числа и хранить их как строки, чтобы избежать потери точности:
console.log(
JSON.parse(
'{"id": 9007199254740999}',
(key, value, ctx) => {
if (key === 'id') {
return ctx.source; // Сохраняем оригинальное значение как строку
}
return value;
}
)
);
// Вывод: { id: '9007199254740999' }
Решение 2: Использование BigInt
JavaScript поддерживает BigInt, который позволяет безопасно работать с числами больше MAX_SAFE_INTEGER:
const bigNum = 9007199254740999n;
console.log(bigNum + 1n); // 9007199254741000n
Но у BigInt есть недостаток – его нельзя сериализовать в JSON напрямую:
const data = { id: 9007199254740999n };
try {
JSON.stringify(data);
} catch (e) {
console.log(e.message); // 'Do not know how to serialize a BigInt'
}
Чтобы сериализовать BigInt, нужно конвертировать его в строку:
const data = { id: 9007199254740999n };
console.log(
JSON.stringify(data, (key, value) => {
if (typeof value === 'bigint') {
return value.toString() + 'n'; // Добавляем 'n' для обозначения BigInt
}
return value;
})
);
// Вывод: {"id":"9007199254740999n"}
⚠️ Важно: клиент и сервер должны согласовывать формат данных!
Если сервер отправляет id как строку ("9007199254740999"), клиент должен корректно обработать это значение. При передаче BigInt в JSON обе стороны (сервер и клиент) должны быть согласны, как именно передавать и интерпретировать большие числа.
Используйте JSDoc для удобства чтения и редактирования кода
В JavaScript функции и объекты часто не имеют документации, что затрудняет понимание кода. Это может быть проблемой как для других разработчиков, так и для вас в будущем. Вот пример функции без документации:
const printFullUserName = user =>
// Есть ли у user middleName или surName?
`${user.firstName} ${user.lastName}`;
Без пояснений неясно:
- Какие свойства есть у объекта user?
- Есть ли у него middleName?
- Следует ли использовать surName вместо lastName?
JSDoc позволяет явно описывать структуры объектов, параметры функций и их возвращаемые значения.
Перепишем показанный выше пример с JSDoc:
/**
* @typedef {Object} User
* @property {string} firstName
* @property {string} [middleName] // Необязательное поле
* @property {string} lastName
*/
/**
* Выводит полное имя пользователя.
* @param {User} user - Объект пользователя с данными о имени.
* @return {string} - Полное имя пользователя.
*/
const printFullUserName = user =>
`${user.firstName} ${user.middleName ? user.middleName + ' ' : ''}${user.lastName}`;
Теперь ясно, какие свойства ожидаются в user, какие обязательны, а какие нет.
Используйте тесты
С увеличением объема кодовой базы становится все сложнее проверять вручную функциональность и корректность всех изменений. Автоматизированные тесты гарантируют, что код работает правильно, и позволяют вносить изменения без риска сломать что-то важное.
В JavaScript существует множество инструментов для тестирования (Jest, Mocha и т. д.), но начиная с Node.js 20, внешний фреймворк не нужен – появился встроенный тестовый раннер.
Простой пример с node:test:
import { test } from 'node:test';
import { equal } from 'node:assert';
// Простая функция для тестирования
const sum = (a, b) => a + b;
// Напишем тест для sum()
test('sum', () => {
equal(sum(1, 1), 2); // Ожидаем, что 1 + 1 = 2
});
Чтобы запустить тесты, просто выполните:
node --test
Для тестирования пользовательских сценариев в браузере отлично подходит библиотека Playwright. С ее помощью можно автоматизировать взаимодействие с сайтом в разных браузерах (Chrome, Firefox, Safari).
Пример теста с Playwright:
import { test, expect } from '@playwright/test';
test('Проверка заголовка на странице', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveTitle(/Example Domain/);
});
Альтернативные JavaScript-окружения Bun и Deno также имеют встроенные тестовые фреймворки. Пример теста в Deno:
Deno.test('Простое сложение', () => {
console.assert(1 + 1 === 2);
});
Почему необходимо перейти на автоматизированное тестирование:
- Автоматические тесты экономят время – ошибки находятся на ранних стадиях, а не после релиза в продакшн.
- Уверенность в коде – можно смело вносить изменения, зная, что тесты проверят работоспособность.
- Современные инструменты упрощают тестирование – теперь писать тесты можно без сложной настройки.
В заключение
Следование лучшим практикам помогает избежать распространенных ошибок, повысить безопасность и производительность приложений. В 2025 году важно не просто знать язык, но и применять его возможности наилучшим образом, чтобы создавать надежные, масштабируемые и легко поддерживаемые проекты. Чтобы быть в курсе всех изменений, обязательно следите за обновлениями ECMAScript и рабочей группой TC39, которая предлагает и разрабатывает новые функции JavaScript.
Комментарии