Галоўная / Артыкулы

Семь распаўсюджаных ідіом JavaScript, які таямніча паводзяць да будучых багоў

Пасвячаецца таму, як будзь-дзеньныя шаблоны JavaScript, такія як пераканальніе перагляды, неабяжлівая ланцоўка і синтаксіс распрасцелення, маскуюць прыязванні, якія тыха паслабляюцься праз развіцце коду.

Выдавец: REACTAPP.TOP14 хв чытання3286 слоў

Крыніцы: 7 JavaScript Anti-Patterns That Cause Future Bugs

Запрос на перэўтварэнне скараціў дванаццаць ліній коду на JavaScript да чатыро. Апдэйтаваная версія выкарыстоўвае неабязковыя ланцюгі, пераканальную перапалню, розпашчэнне об’екта і аднародную табліцу пошуку, ў результате чаго перагляд коду праходзіць гладка. Код стаў болей структураваным, лёгкім для чытання і правільна функцыонуе з усімі дадзеннямі, якія зараз прыймае аплікацыя.

Пазней, чэрез калькі месцаў, значэнне 0 пачынае прыймаліся як легітыма значэнне налашчання. Адпаведны адказ API стаў прыходзіць без поля, якое раней завжды было прысутнае. Фонавая задача, якая раней обрабоўвала дзесяткі записаў, раптам стала вынужаная обрабоўваць тыясячы. Вкладзены об’ект пачаў викорыстоўваліся ў месцах, дзе прыпускалася, што ён вже быў скопіраваны. Ніякі з гэтых пазнейшых змяненняў не выглядае пов’язаным з пачатковым перэўтварэнням коду, пры тым кожна з іх адкрывае прыпусак, што компактны код містіў у сабе без прымет.

Гэта сама прычына, чаму дзеяння на адрысе JavaScript ёсць рызыкавымі. Часта яны не выглядаюць некоректнымі на момент ўжыцтва, а кваліфікаваныя разработчыкі вжываюць іх самэўсёлкавана таму, што яны зручныя, выразныя і точныя пад сучаснымі вакументамі. Проблемы пачынаюцца тады, калі такая зручнасць таямніча злучае калькольныя станы ў аднае поведэнне, або калі синтаксіс, які здаецца гарантыяю безпекі, на самай працэ ёсць толькі скрацаннем.

Таму справжняя запытка пад час адзінакоўкі — не тое, чы рэштка коду ёсць хітрым або стислым. Гэта тое, чы код дастаткова ясна адкрывае своія прыпускі, каб будучыя змены спрычынілі явныя адказы, а не таямнічна некоректныя рэзультаты.

1. Перакананні ў правдзівасці спыяюць злучэнню разных значэнняў у аднае рашэнне

Уявіце настройку конфігурацыі, якая керуе тым, сколькі разоў дозволена практыка перапрыбутку.

function getRetryCount(config) {
  if (!config.retries) {
    return 3;
  }

  return config.retries;
}

Гэта логіка працюе нормальна, калі retries ўжо є паазытывным числам або ж не задана. Але калі хтось дадае можласць абсолютна выключыць павторныя спробы, задаўшы значэнне 0, функцыя тылу надае пераканальнае значэнне і замест таго вяртае 3. Правіла аднаковасці ў JavaScript выкананы правільна — проста гэтыя правіла больш не адпавядаюць таму, калі на самай працы патрэбна функцыя.

У JavaScript калькі значэнняя, кроме false, таксама лічыцца як фальшывыя: 0, "", null, undefined і NaN — усе яны падпадаюць під гэта правіло. Таму умова на кшталт if (!value) на самай працы не пераканальвае, чыгонькі чагосці не ёсць — яна пераканальвае, чыгонькі JavaScript зможа прыроўнаць гэта значэнне да false, што ўзмацненыя пытанне, чым проста „чыгонькі гэта не ёсць?“

Точнейшая версія залежыць ад таго, як выглядае фактычны контракт.

function getRetryCount(config) {
  if (config.retries === undefined) {
    return 3;
  }

  return config.retries;
}

Калі і null, і undefined павінны сигналізаваць пра тое, што «нічога не было задана», то метод злучэння значэнняў типу null можа чыстае выразіць такую намеранню.

const retries = config.retries ?? 3;

Этае разлік мае вельмі важлівое значэнь для полоў формы, параметраў сторанавача, цэнаў, лімітаваń, флагоў функцый, лічыльнікаў і іншых числовых налаштаванняў. Флаг пад назвай enabled можа правільна мець значэнне false. Поле для пошуку можа правільна мець порожняя строчка. Офсэт можа правільна мець значэнне 0. Адносленне кожнага лжывага значэння да «не заданага» прыводзіць да втраты інфармацыі зарады стислась.

Нічыя з гэтых прычын не робіць пераканальвання на існуючасць значэння абсалютна неправильным. Яны добра працуюць, калі тэрмінавакладка справды стосуецца саме існуючасці значэння — напрыклад, калі трэба выводзіць необавязковы рэчэнак толькі тады, калі ён не пусты. Але яны стаюць прычыной проблем, калі домэн мае больш заўсёды разныя станы, чым можа запам’ятаваць механізм прымусовага пераканальвання логічных значэнняў у JavaScript.

Пытанне, якое варта задаць пад час аналізу, ёсць простае: чы рэальна потрэба пераканальваты на незіснуючасць значэння, чы існуючасць значэння выкорыстоўваецца як замена для болей конкрэтнага бізнес-правіла?

2. Апшыраная ланцоўка для необавязковых элементаў можа ператварыць зламаныя данні на „тыхо“ відсутныя данні

Апшыраная ланцоўка для необавязковых элементаў атлежыць да найценнейшых дадаўненняў у JavaScript, таму што яна значным чынам спрыяе легкаму навігацыі праз структуры, дзе элементы ў значнай меры необавязковы.

const avatarUrl = user.profile?.avatar?.url;

Калі profile або avatar сапраўды можа не існаваць, самэ гэта і є ситуацыя, для якой створены апцыйнальны ланцюгавы вызыв. У гэтым случае отрыманне значэння undefined ёсць значымым, адколі ўжо сама відсутнасць — гэта нормальны, прыемлівы результат.

Проблемы пачынаюцца, калі апцыйнальны ланцюгавы вызыв застаўляецца для дадзеных, якія, за прыняццем, завжды должны быць наявны.

function getShippingCountry(order) {
  return order.customer?.address?.country;
}

Падазроўваючы, што кантракт системы гарантуе, што кожны замовленне, якое можа быць адправлена, містіць кліента і адресу для адправкі. Якщо які-небудзь баг у вышэйшых частках коду спрычыніць замовленне без аднаго з гэтых элементаў, апцыйнальны ланцюгавы вызыв таямніча ператварае тое, што павінна быць наяўнасцю правалявання кантракту, у звычайнае значэння undefined. Гэтая проблема можа не праказацца даўно, а выявіцца у вачынку некорактныя расчыткі податкаў, відсутнасці етикеткі для адправкі або плутанню ў якой-небудзь частцы коду, якая не адносіцца да гэтага пытання.

У такім случае кращым падходам можа быць негайна паўтарка, адразу ў тым месцы, дзе заўважэння насправды мае значэнне.

function getShippingCountry(order) {
  if (!order.customer?.address) {
    throw new Error(
      `Order ${order.id} is missing a required shipping address`
    );
  }

  return order.customer.address.country;
}

Гэты варыянт ўсё бол працоўны, але яго можна лепей зрозумець. Ён паведамляе тых, хто чытае яго пазней, што адреса — гэта не проста дадзеныя, якія можна маты або не маты; ўсуненне іх означае, што об’ект ужо зовсім не павінен вважацца валідным для гэтай операцыі.

Опцыйнае лануванне таксама мае тэндэнцію распространяцца, калі ў типе або у контракце API з’яўляецца нэвядомасць. Адна невялікая нэвядомая польцоўкі ператвараецца на foo?.bar, якая заўтра стае foo?.bar?.baz, і незабоў жа кожны вызывальнік тыха тольеруе відсутныя дадзеныя, замест таго каб спытацца, чаму польцо, якое апэндавалася быць обав’язковым, продовжае знікати. Синтаксіс зменшуе колькасць выкліканых абэктаў, але ён таксама можа легка падвышыць загальную неяснасць.

Тое, чы розглядваць неабяжлівую ланцюгавую аперацыю як правильны інструмент, павінна вылічвацца на адповіднасці з модэллю домэны, а не на практычнасці. Якща значэнне справды є неабяжлівым, неабяжліва ланцюгавая аперацыя є чыстым спосабом яго выражэння. Якщо ж яго наявнасць є обавязковай, дазволенне undefined працэваць даўжэй у програме можа значна ускладніць выявленне первіннага месца виникнення бага.

3. Сынтаксі распрасцвання выглядае як цэлакрава копія, але вёсканыя об’екты застаюцца зі своей первінной ідэнтычнасцю

Распрасцванне об’екта ў новы ёсць стислым спосабам напісання апдэйтаў, якія выглядаюць незменнымі.

const updatedUser = {
  ...user,
  name: "Umar",
};

На верхнім роўні гэта справды стварае новы об’ект з тымі значэннямі, якія вы чакаеце. Гэты успех часта прыводзіць разработчыкаў да таго, што яны спрыямаюць { ...object } як "склоніць гэты об’ект", хоча на самай працы гэтае аперацыя толькі дуплікуе власнасці, якія знаходзяцца безпасуль на гэтым об’екте.

Усе, што знаходзіцца на аднам рэвэле глыбей, не патрацьце такога адгульнення.

const original = {
  name: "Umar",
  preferences: {
    theme: "light",
  },
};

const copy = { ...original };

copy.preferences.theme = "dark";

console.log(original.preferences.theme);

// "dark"

У гэтым прыкладзе copy дзейсна ёсць адзінокы зовнішні об’ект, але copy.preferences як і раней вяртаецца да таго ж самога об’екта, што і original.preferences. Функцыя spread скопіювала толькі ссылку, а не структуру, яка ўтвараецца на базе гэтай ссылкі.

Гэта шыбкаа стварае рызык у кантролі стану, адколькі рэзультатывае код застаецца з візуальным выглядам незменнай актуалізацыі.

const nextState = { ...state };
nextState.user.preferences.theme = "dark";

Зовнішній об’ект є новым, але user і preferences можаць быть тымі ж самымі об’ектамі з пярэднега стану. Любы код, які вырашае, чы рэендараваць чы перасчыляваць данні на адпаведнасць рэферэнсаў, можа пабачыць гібрыдны стан: часткі, якія былі створеныя занова, і часткі, якія былі змененыя на месцы. У той жа час любы іншы фрагмент коду, які ўсё ўтримвае рэферэнс да старога стану, таксама будзе бачыць гэтыя змены.

Якщо для змены патрэбна толькі адна галузь вялікага об’екта, спецыяльна скопіюйте тую галузь, а не пакладайцеся на адну толькі операцию распрасовання на верхнім рэвэле.

const nextState = {
  ...state,
  user: {
    ...state.user,
    preferences: {
      ...state.user.preferences,
      theme: "dark",
    },
  },
};

Це трэба большэй напісаць, але кожны ранг, які змянюецца, атрымлее новую ідэнтычнасць, тады як часткі, якія вы не зменялі, продовжуюць безпечнае саўтарэння. Такія інструменты, як Immer, могуць спрыяць усуненню гэтага патерна, калі дрэвы стану становяцца глыбокімі та вкладчатымі, але яны не усуваюць основнай патрабованасці: вам яшчэ трэба знать, якія рэферэнсы застаюцца пасля апдэйта, а якія — ні.

Сама синтаксіс распрасцоўвання не ўключае бяглых дзеянь. Баг выходзіць з припуску, што паверхневая копія дае глыбокую ізоляцыю.

Кожны раз, калі вы распрасцоўваеце об’ект, каб захаваць яго ад пазнейшых змяненняў ў іншым месцы, зупініцеся та пераканайцеся, якія вкладчатыя рэферэнсы ўсё ж такі саўтарэнуюцца пасля гэтага. Гэтыя перакананні маюць набліжна большое значэнне, чым тое, чы рэальна синтаксіс викорыстоўвае ....

4. Перадача через JSON.parse(JSON.stringify(...)) не ўсё роўна клонаванню

Адаптаваны спосаб працэвання з проблемамі паверхневага копіювання — гэты просты код:

const copy = JSON.parse(JSON.stringify(original));

Здаецца, ён рашыяе проблему вложаных абязаў, калі ўжо падчас парсавання строкі JSON ствараюцца абсалютна новыя об’екты. Для дадзеных, якія вже маюць формат чыстага JSON, ён можа нават дастаць самэ тое, чаго вы спадзяваліся.

Проблема заключаецца ў тым, што гэты результат спрыявае спачатку вважаць яго загальнай гарантіяй глыбокага клонавання.

JSON разумее толькі абмежаную модель дадзеных. Як толькі ваш об’ект мае значэння, якія не падпадаюць пад гэтую модель, процес серыялізацыі можа таямна іх зменіць, адмаўліць чы ўзнемагчыць працэс.

Возьмім гэты об’ект як прыклад:

const value = {
  createdAt: new Date(),
  missing: undefined,
  settings: new Map([["theme", "dark"]]),
};

Пасля перадачы дадзеных у формате JSON аб’ект Date стае простаю строкай і втрачае всія спецыфічныя для яго асэнтавання. Атрыбут undefined пачынае існаваць цэлкам. Об’ект Map не застаецца у своем формате з усімі парамі ключ-значэнне, што ўтварае проблэму, якщо вызывач спакаваўся на адпаведны структурны клон.

Цыркулярныя звязкі ўсё бол прымусова паслабляюць работу системы:

const user = {
  name: "Umar",
};

user.self = user;

JSON.stringify(user);

// TypeError

Іншыя типы значэння таксама прыносяць свае проблемы. Аб’екты BigInt не можаць быць пераданы за дапамою звычайнага методу JSON.stringify без дадатковых мераў, атрыбуты з ключамі-сімволамі проста не ўрахоўваюцца, а экземпляры спецыяльных класаў втрачаюць протатып, які спачатку надаваў ім класовыя асэнтавання.

Нічыя з гэтых прычын не робіць серыяванне JSON ненадзеяным — яно выконвае самэ гэта, што і павінна робіць серыяванне, тэбто стискае значэння JavaScript у болей строгі формат, які падтрымвае JSON.

Калі вам дасканальна патрэбна структурная копія, structuredClone() часта яўляецца лепшым вбудованым выборам, за ўмовы, што типы, з якімі вы працуеце, ёй падтрымляюцца. Для даных, якія тычуцца безпосередна да вашай сферы, стварэнне яснай функціі копіювання можа быць ўсё бол надзеяным, адколькі яна чытка паказвае, якія атрыбуты трэба захаваць, замест таго каб прыпускати, што кожны атрыбут павінен быць клонаваны у загальны спосаб.

Таксама варта згадаць, што глыбокая копіювання часта ёсць абсалютна неправым інструментам. Якщо вам патрэбна толькі змяніць адзін внутрошняй розгалужэння об’екта, клонаванне всей структуры марнуюць зусілля і ускладнююць адгук, калі самэе змянілася якая частка.

Хорашым прыкметам яўляецца называнне операцыі так, якая ёна ў рэальнасці. Калі вы хочаце серыялізаваць які-небудзь элемент, робіце гэта намеравана. Калі вы хочаце клонаваць які-небудзь элемент, выберыце спосаб клонавання, чыя дзейнасць абоўязкова падтрымлівае значэнняя, якія вы зберагаеце.

5. Promise.all() не обмежвае колькість запускаемых задач адразу

Уявіце сабе прыклад аплікацыі, якой патрэбны данні для групы корыстувачаў.

const users = await Promise.all(
  userIds.map((id) => fetchUser(id))
);

Для невялікай колькасці корыстувачаў гэты падход працюе добра і зазвычай є правильным рашэнням. Код запускае всі асінхронныя запыткі, чакае на ўсе яны разам і вяртае рэзультаты, упорядкованыя так, як і вхідны масэвы.

Пазней хтось знова викорыстоўвае тую ж функцію для набора дадзеных з 50 000 записаў.

const users = await Promise.all(
  userIds.map((id) => fetchUser(id))
);

Нічыга ў сынтаксе не змянілася, але масштабы зменіліся. Паколькі .map() вызывае fetchUser для кожнага элемента, а кожны такі вызов, як правило, негайна выканае запит да сеті або базы дадзеных, Promise.all() не накладае жадных обмежэнняў на тое, скількі з гэтых запытоў можа выканавацца адночасова.

Гэта можа прывести да перавантажэння службы, выкарання запасу з’ўязкаў, перакрыцча лімітавых значэнняяў, росту выкарыстоўвання памяці або до початку серіі перапрыбуткаў, калі пачнуць накаплявацца ады. Візуальна код все ўсё выглядае як аднойчасная операцыя з обробкай набора дадзеных, таму лёгка не звернуць увагі на рэальныя витраты на ўжоўтворэнне.

У Promise.all() ёсць яшчэ адна захаваная прыпуск: як толькі хоць адна з обявленняў падае, усё падае. Гэта правільна поведзенне, калі вы спрацоўвайце групу як адну елементарную еўрэю работы, але гэта не значыць, што іншыя запусканые операцыі будуць анульваны — яны продовжаюць выконвацца нават пасля таго, як выйдзе на ўважанне аблікаванне.

Калі задачы на самай працэ є незалежнымі, а вы хочаце паведамлення пра кожны рэзультат, незалежна ад індывідуальных аблікаванняў, Promise.allSettled() часта ёсць лепшым выборам для выражэння вашых цялей.

const results = await Promise.allSettled(
  userIds.map((id) => fetchUser(id))
);

Але якщо ваша справжня проблема — навантажэнне на іншую систему, пераключэнне между Promise.all() і Promise.allSettled() сама па сабе гэта не выправіць — жадны з іх не обмежвае канкурэнцію. Насамперадзе вам патрабуецца батчаванне, семафор, калейдж або якая-небудзь утвар для обмежэння канкурэнціі, якая дазволяе адчыненым заўсёды толькі пэўную колькасць операцый.

Гэты патэрн ўсё гаразд, калі набор вхідных дадзеных застаецца малым і прагнозаваным, а цэльная система можа працаваць пад паралельным навантажэнням. Проблема выявляецца пазней, калі логіка, напісаная з увагай да дзесяці записаў, прыкладываецца да дзесяці тысяч, і ніхто не зупіняецца, каб пераглядзець, чы гэта ўсё такі ж безпечна.

Канкурэнція не залежыць ад таго, насколькі стисла вы чакаеце на рэзультаты своіх прамісаў — яна залежыць ад таго, сколькі роботы вы запускаеце і што насправды можа адрабатваць базовая система.

6. Методы масэў, якія выглядаюць функцыональнымі, все ж можу зменяцца

Методы масэў у JavaScript дазваляюць ствараць компактныя, чытабельныя ланцюгі обробкі дадзэнняў, і большасцю часоў гэта ёсць справжнія перавага.

const activeUsers = users
  .filter((user) => user.active)
  .map((user) => user.name);

Паколькі filter() і map() абодва вяртаюць новыя масэў, ёсць спадчынае падумаць, што будзь-які ланцюг методаў масэў не парадзейвае первісную масэў. Гэта припуск рушаецца, калі да ланцюга прыўяжуцься sort(), reverse(), splice() чы іншыя методы, якія зменяюць масэў.

Уявіце сабе компонент React, які прымеў масэў users як параметр:

function UserList({ users }) {
  const sortedUsers = users.sort(
    (a, b) => a.name.localeCompare(b.name)
  );

  return sortedUsers.map((user) => (
    <User key={user.id} user={user} />
  ));
}

Здаецца, што гэты код стварае новы масіў sortedUsers, але функцыя sort() на самай працэ пераранжавае аднойчыны масіў на месцы і проста вяртае аб’ект-звязак да таго ж масію. Таму компонент, чыя ўжоўна задача — адрасаванне, як побачны эфект, змінюе даны, якія належаць яго родзічу.

Такія змены можа застацца непазначанымі праўяло, пакуль іншая частка аплікэйшнае програмы не пачне выкарыстоўваць первісны порядак. Тэсты для гэтага компонента продовжваюць праходзіць, таму што адрасаваны выхід выглядае правільна, тады як суседні компонент або запам’ятаваная значэнне ў іншым месцы тайкам працюе з пераранжаванымі даннымі, якіх ніколи не чакала.

Аднам з рашэнняў є клонаванне масію прытаму, перш чым застосаваць метод, які вялічыну масію.

const sortedUsers = [...users].sort(
  (a, b) => a.name.localeCompare(b.name)
);

Новэйшыя рантаймы JavaScript таксама адказваюць немутуючымі аналогамі, такімі як toSorted(), якщо ваша цэльовая среда іх падтрымляе.

Настоям урокам тут не ў тым, каб запамятаць, якія методы масэвых структураў є „безпечнымі“. Справа ў тым, каб разважыць, чыя операцыяі адпаведаюць таму, хто ўладае данымі.

Якщо функцыі передаюць значэнне, якое ёй не належыць, і ў тылу яе мутуюць, гэта прыводзіць да парадоксальнай поведэнкі, якая не мае нічынага з там, за што функцыя апошней павінна несаць адказнасць. Код становіцца набагато простэйшым для розумення, калі мутацыя або ўзагалі не выкананая, або выкананая явна, у там месцы, дзе насправды знаходзіцца значэнне.

7. Табліцы пошуку можаюць сховваць некоректны ввод замест таго, каб яго адхіліць

Часта заменяюць оператар switch на просты об’ект, які выступае як табліца дыспэтчування, адтолькі ён выглядае болей декларатыўна.

const handlers = {
  create: createUser,
  update: updateUser,
  delete: deleteUser,
};

handlers[action]?.(payload);

Гэта компактна, і дыспэтчуванне па пошуку ёсць справды хорашым патэрнам у багатьых случаях. Але лёгка занедбаць тое, што выканаецца, калі action не падходзіць ні да якога вядомага ключа.

Паколькі гэты прыклад выкарыстоўвае неабавязковае запэўненне, непазнаёмая дзеяння проста нічога не робіць, тыха.

action = "udpate"; // typo

handlers[action]?.(payload); // no handler runs

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

Явныя адмовы робяць кантракт набагато чыстасловнейшым.

const handler = handlers[action];

if (!handler) {
  throw new Error(`Unsupported action: ${action}`);
}

return handler(payload);

Тэблі з пошукам становяцца болей рызыкавымі, калі ключы выходзяць з вводу корыстніка або з джэрелаў з слабкай перакананнем. Звычныя об’екты таксама успадковуюць атрыбуты зі свайго протатыпа, якщо толькі вы не стараецеся гэтага запобiec, таму адносленне довільных строк як да безпечных пошукоў атрыбутаў выклікае большую ажытковасць, чым у разе малага, замкнутага набора внутршна вядомых каманд.

Іспользованне Map можа зробіць намер у стыле словніка яшчэ яснейшым:

const handlers = new Map([
  ["create", createUser],
  ["update", updateUser],
  ["delete", deleteUser],
]);

const handler = handlers.get(action);

if (!handler) {
  throw new Error(`Unsupported action: ${action}`);
}

return handler(payload);

У TypeScript адаптаванне дискримініраванага союзу з выключным switch можа прынесці ўсё большую корысть, адколі дадзенне новай дзеяння прымусвае кампіляр пазначыць усі месцы ў кодбэсе, якія патрэбуе адчынення.

Нічога з гэтага не значыць, што switch категорычна лепшы за табліцу пошуку. Табліцы дыспатчаў ёсць выгодныя, калі операцыі якраз базуюцца на дадзэнах, а поведзенне для невядомых ключоў ўжо праграматычна выявлена.

Проблемы пачынаюцца, калі індырэкцыя не дазволяе адразаваць „праграма намерна распрацавала гэта“ ад „нічога не выйшла“.

Прыродныя косты хітрага — гэта схованыя прыпускі

Ні адна з гэтых сям прычын не заслуговае на абсалютную заборону.

Праменнія пераказы працуюць добра, калі праменнасць справаўды адпавядае правілам вашай домэны. Альтынатывны ланцуг выглядае чудова для адносаў, якія справаўды ёсць необавязковымі. Распространэнне об’екта ідеальна падходзіць для поверхневых апдэйтаў. JSON — гэта натуральны формат для дадзеных у формacie JSON. Promise.all() ідеальна падходзіць для паралельнай працы з межамі. Метады для мутацыі массиваў ўсё гаразд, калі вы абоўсюды контролюеце массив. А таблицы пошуку можаць значна спроставаць логіку дыстрыбуцыі.

Рызык заключаецца ў тым, што люди спрыяюць сваім зручным стандартам як гарантыям, якіх синтаксіс на самай працэ ніколі не дае. !value не значыць „відсутняе“. ?. не значыць „безпечна і правільная“. { ...obj } не значыць „полна незалежная копія“. Серіялізацыя ў JSON і зворачная дэконстракцыя не значыць „звычайны клон“. А Promise.all() не значыць „контролюемая паралельнае відбыванне задач“.

Самэй тым найболей падтрымваная версія фрагмента JavaScript не завжды ёст тая, якая мінімальна. Калякія дадатковыя рядкі, якіе разлічаюць undefined ад 0, адхіляюць некоректную структуру об’екта, обмежуюць колькість паралельных запитоў або выклікаюць падзею при незнаёмай дзейнасці, можа зберагчы інфармацыю, якую будучыя адпаведальныя за падтрымку практычна былі бы змушаны шукати ў історыі змян.

Калі код здаецца вам нешта незвычайная гэтым крэатыўным, знайдзіце хвілінку, каб паглядзець, што было стиснута. Інодзе адпаведзь — тое, што было удалена, ўсё ж такі неабавязковая деталізацыя, а значэнне застала недрабаваным; гэта самэ ўсё, што павінна робіць хорашая абстракцыя. Іншыя разы стиснутае — гэта разліка, якой нараз чырпакі проста ўсё ж не патрабавалі.

Самэ гэты другі варыянт часта становіцца прычыной багоў наступнага дня.

Корыстным прыкметам пад час адзірвання коду є запытка: якія умовы павінны застацца правдзівымі, каб стислы шаблон продаваў працаваць, калі змянююцца чырпакі. Якщо гэтая умова мае значэнне, але не ўізначена нідзе ў кодзе, запісуйце яе чытра, пакуль логіка ўсё ж свежая і простая для разумення.

Будучыя адтрымавальнікі рэдка калі сталквуюцца з проблемамі, таму што JavaScript дазваляе ім пісаць занадта великі кольківя коду. Яны сталквуюцца з проблемамі тады, калі код перестае чытальна показваць, якія разліки на самай справе маюць значэння.