Дзесяць частаў прыменення 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 або ў будзь-якай системе, якае выклікае паведамлення пра змяны шляхом порэвану пасылаў, – такі тип мутацыі на месцы застаецца абсалютна непазірным для яе. Сам пасыл ніколі не зменшаецца, таму не відбываецца перерэндары, ніхто з падпісчыкаў не паведамляецца, і вы застаецеся ў стані падбору UI, якое таёмніча адмовляецца апошнавацца.
function addItem(cart, item) {
return { ...cart, items: [...cart.items, item] };
}
Стварэнне абсалютна новага об’екта чы рэйту заместа модыфікаціі перваснага викорыстоўвае трохі больш памяці. У зворатнай адзынке вы отрымваеце прагнозаваныя змяны стану, што ўсё ж такі є вартай альтернатываю, якую трэба выкарыстоўваць значна частае, чым здаецца.
3. Неадрабатаванне адхіленняях Promise
Функцыя 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. Прыхованыя глобальныя зменнікі
Якщо працяваць без режыму strict і пракінуць заявленне const, let або var, 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 — «гэта спецыяльна было задана як нічога». З гэтага можна выкарыстоўваць аператар злучэння значэнняў, якія ўзьмуцца за ноль, каб адразу пераглядаць оба варыянты, не пісаючы два разы тое жа порэванне:
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, якія таямніча зламваюць код — Апавясненне тонкіх прынцыпаў JavaScript і TypeScript, такіх як параболі з NaN, асінхронны таймінг і прымусовая канвертаванне типаў, якія вызываюць багі, нават калі код выглядае правільна.
- Функцыі Node.js 26, якія таямніча заменяюць гады варыянтных рашэнняў — Огляд API Temporal у Node.js 26, выконання TypeScript на роўні ядра, дапаможных прыбораў для кэшу і іншых дадаткаў, якія усунулі давга існуючыя варыянтные рашэнняў.