Галоўная / Артыкулы / Дзесяць частаў прыменення 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 або ў будзь-якай системе, якае выклікае паведамлення пра змяны шляхом порэвану пасылаў, – такі тип мутацыі на месцы застаецца абсалютна непазірным для яе. Сам пасыл ніколі не зменшаецца, таму не відбываецца перерэндары, ніхто з падпісчыкаў не паведамляецца, і вы застаецеся ў стані падбору 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, якія наражаюць на пагубу ваш ментальны модэль — Рассматрываюцца дзесяць тонкіх аспектоў працы з JavaScript — ад можлівасці змены значэння зменных типу const да клаусураў і обробкі асінхронных памылак — якія таямна ствараюць багі ў коде апытных разработчыкаў.
  • Сэмь распаўсюджаных ідіумаў JavaScript, якія таямніча ўводзяць будучыя багі — Адказвае на пытанне, як такі ўжываныя ў кожны дзень патэрны JavaScript, як перакананні пра правдзівасці, неабавязковыя ланцюгі і синтаксі распрасцварэння, маскуюць заўсёды правільныя прыпускі, якія таямніча ламаюцца праз развіцце коду.