Десять постійних звичок JavaScript, які тихо підривають ваш кодовий базис
Пояснює десять поширених проблем у JavaScript та TypeScript, від слабкої рівності до зміни стану, та пропонує безпечніші підходи для їх усунення.
Майже кожен проект на 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 — „це навмисно встановлено як ніщо“. З цього моменту оператор об’єднання значень типу null дозволяє перевіряти обидва варіанти одночасно, без необхідності писати порівняння двічі:
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 та TypeScript, які тихо ламають код — пояснення тонких нюансів цих мов, від порівнянь з NaN до проблем із асинхронним таймінгом та примусовою заміною типів, які спричиняють баги, незважаючи на зовнішню правильність коду.
- Функції Node.js 26, які тихо замінюють роки використання обхідних шляхів — огляд API Temporal у Node.js 26, можливостей безпосередньої обробки коду на TypeScript, інструментів кешування та інших нововведень, які усувають давно існуючі обхідні рішення.