Главная / Статьи / Десять распространённых привычек в JavaScript, которые тайно подрывают ваш кодовый базис

Десять распространённых привычек в JavaScript, которые тайно подрывают ваш кодовый базис

Объясняются десять распространённых проблем в JavaScript и TypeScript, начиная с слабого равенства и заканчивая изменением состояния, а также показываются более безопасные подходы для их решения.

1748 слов

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

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

1. Использование == вместо ===

Оператор слабого равенства в JavaScript приводит типы к общему виду перед сравнением, и результаты такого сравнения крайне трудно предсказать:

0 == "0"        // true
0 == ""         // true
"" == "0"       // false
null == undefined // true

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

if (userInput === "0") { ... }  // clear, predictable

2. Прямое изменение состояния

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

function addItem(cart, item) {
  cart.items.push(item); // mutates the original array
  return cart;
}

Если объект cart отслеживается где-то ещё — в состоянии React, хранилище Redux или любой системе, которая обнаруживает изменения путём сравнения ссылок, — такого рода мутация «на месте» остаётся совершенно незаметной для неё. Сама ссылка никогда не меняется, поэтому не происходит перерисовки, никто из подписчиков не уведомляется, и в итоге вы пытаетесь работать с интерфейсом, который таинственным образом отказывается обновляться.

function addItem(cart, item) {
  return { ...cart, items: [...cart.items, item] };
}

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

3. Неправильная обработка отклонений обещаний

Функция async, которая выбрасывает исключение без оборачивания в блоки try/catch, или цепочка методов .then() без соответствующего .catch(), как правило, работает без видимых ошибок. В браузере это может сопровождаться полным отсутствием видимых действий; в Node же может появиться предупреждение о неразобранном отклонении, которое легко упустить среди остальных записей в журнале.

async function getUser(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}
getUser(42); // if this fails, where does the error go?
async function getUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`Request failed: ${res.status}`);
    return await res.json();
  } catch (err) {
    logger.error("Failed to fetch user", { id, err });
    throw err;
  }
}

Любая функция async, способная дать сбой, требует четкой стратегии обработки таких случаев. Оставление ситуации без решения на самом деле не является стратегией — это просто ошибка, готовая проявиться позже.

4. Глубоко вложенные обратные вызовы

Никто не планирует намеренно создавать «ад обратных вызовов». Это происходит постепенно, с каждым новым асинхронным шагом, пока код не станет вложенным на шесть уровней, а отступы не напоминают лестницу:

getUser(id, (user) => {
  getOrders(user.id, (orders) => {
    getShipping(orders[0].id, (shipping) => {
      updateUI(shipping); // and it keeps going
    });
  });
});

async/await были введены именно для того, чтобы устранить подобную вложенность:

async function loadShippingInfo(id) {
  const user = await getUser(id);
  const orders = await getOrders(user.id);
  const shipping = await getShipping(orders[0].id);
  updateUI(shipping);
}

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

5. Тайком появляющиеся глобальные переменные

Если пропустить объявление const, let или var вне режима strict mode, JavaScript тихо привяжет эту переменную к глобальному объекту, вместо того чтобы выдать ошибку:

function calculateTotal() {
  total = 0; // no declaration — this is now global
  for (const item of items) total += item.price;
  return total;
}

Теперь переменная total находится вне области видимости функции, что позволяет ей сталкиваться с любой другой переменной с таким же именем в кодовой базе: либо перезаписывать другие значения, либо сама быть перезаписанной в зависимости от порядка выполнения. Добавление строки "use strict" в начало файла превращает это в немедленную, заметную ошибку вместо скрытой, возникающей позже. Современная синтаксис модулей с использованием import/export автоматически включает строгий режим, поэтому такие ошибки становятся гораздо реже при работе с ES-модулями.

6. Сравнение объектов и массивов с помощью ===

Эта ошибка часто встречается у тех, кто слишком строго следует правилу №1. Оператор === сравнивает объекты и массивы по ссылке, а не по содержимому:

{ a: 1 } === { a: 1 }       // false
[1, 2, 3] === [1, 2, 3]     // false

Два объекта, кажущихся идентичными, по-прежнему являются отдельными сущностями в памяти, поэтому оператор строгого сравнения считает их не равными. Чтобы сравнить реальное содержимое, необходим подход глубокого сравнения: помощник вроде isEqual из lodash, JSON.stringify для простых случаев или пользовательская рутина сравнения. Использование === здесь не является синтаксической ошибкой, оно просто отвечает на другой вопрос, чем тот, на который вы на самом деле хотели получить ответ.

7. Неудаление обработчиков событий и таймеров

Каждый вызов addEventListener, setInterval или любой другой функции подписки представляет собой обещание того, что кто-то в конечном итоге удалил бы эти элементы. Если игнорировать это обещание, возникает утечка памяти, которую легко упустить во время разработки, но которая становится серьезной проблемой при работе продукта:

useEffect(() => {
  window.addEventListener("resize", handleResize);
  // no cleanup — this listener never goes away
}, []);
useEffect(() => {
  window.addEventListener("resize", handleResize);
  return () => window.removeEventListener("resize", handleResize);
}, []);

8. Чрезмерное использование any в TypeScript

any на самом деле не является типом, скорее это способ избежать определения типа, и чрезмерное использование такого подхода постепенно превращает строго типизированную базу кода обратно в нетипизированную, при этом никто не принимает такого решения осознанно:

function processPayment(data: any) {
  return data.amount * data.rate; // no safety net at all
}

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

type PaymentData = { amount: number; rate: number };

function processPayment(data: PaymentData) {
  return data.amount * data.rate;
}

Если вы действительно пока не знаете структуру какого-либо объекта, unknown является более честным аналогом any. Он требует от вас уточнения типа перед его использованием, вместо того чтобы позволять действовать на основе непроверенных предположений.

9. Игнорирование разницы между null и undefined

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

if (value === null || value === undefined) { ... }

Такой подход обычно свидетельствует о том, что никто так и не договорился о едином стандарте. Более аккуратным решением является присвоение каждому значению четкого значения: undefined означает «это никогда не устанавливалось», а null — «это намеренно установлено в значение «ничего»». С этим оператор слияния значений nullish позволяет проверять оба значения одновременно, без необходимости писать сравнения дважды:

const displayName = user.nickname ?? "Anonymous";

?? активируется только тогда, когда значение слева равно null или undefined. Это отличается от оператора ||, который также использует в качестве альтернативы значения 0, "" или false — значения, которые часто являются допустимыми и не должны считаться отсутствующими.

10. Написание кода для компьютера, а не для других людей

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

const r = a.filter(x=>x.a).map(x=>x.b).reduce((a,b)=>a+b,0);

Этот код работает. Но тот, кто будет его читать позже, включая вас через несколько месяцев, должен будет разобраться, что на самом деле означают a, x и остальные элементы этой цепочки, прежде чем вносить какие-либо изменения.

const activeUserBalances = users
  .filter((user) => user.isActive)
  .map((user) => user.balance);

const totalActiveBalance = activeUserBalances.reduce((sum, balance) => sum + balance, 0);

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

Общая схема всех десяти принципов

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

Связанная литература

  • Девять привычек на уровне кода, которые делают работу старших инженеров более надежной — рассматриваются девять конкретных практик программирования — от защитных клозул до строгого моделирования данных — которые делают код более устойчивым, читаемым и удобным для отладки в условиях давления.
  • Десять архитектурных привычек, которые позволяют кодовым базам фронтенда оставаться поддерживаемыми годами — объясняются структурные привычки, такие как оптимизация с точки зрения возможности удаления элементов, четкое моделирование потоков данных и изоляция бизнес-логики, которые помогают кодовым базам оставаться поддерживаемыми на протяжении многих лет изменений.
  • Археология программного обеспечения: практический метод анализа устаревшего кода — Узнайте пошаговый подход к безопасному исследованию недокументированных устаревших кодовых баз, от анализа истории коммитов до рефакторинга без нарушения работоспособности системы.
  • Десять повседневных правил JavaScript, разрушающих вашу когнитивную модель — Рассматриваются десять тонких особенностей поведения JavaScript — от изменяемости констант до замыканий и обработки ошибок асинхронных операций — которые тайно приводят к ошибкам в коде опытных разработчиков.
  • Семь распространённых идиом JavaScript, которые тихо приводят к будущим ошибкам — объясняет, как такие повседневные паттерны JavaScript, как проверки на истинность, опциональное цепление и синтаксис распространения, скрывают предположения, которые молча нарушаются по мере развития кода.