Головна / Статті / Десять повсякденних правил JavaScript, які порушують вашу когнітивну модель.

Десять повсякденних правил JavaScript, які порушують вашу когнітивну модель.

Розглядає десять тонких особливостей роботи JavaScript — від змінності констант до клоузур та асинхронного оброблення помилок — які тихо спричиняють баги в коді досвідчених розробників.

4076 слів

JavaScript перестає здаватися страшним, як тільки ви звикнете до його синтаксису. Ви більше не спотикаєтесь через відсутність крапок з комою, асинхронні обробники подій стають звичайною справою, а різниця між let та const — чимось само собою зрозумілим. Після реалізації достатньої кількості проектів ця мова починає здаватися старим другом. Ви одразу помічаєте поширені помилки, маєте чітке уявлення про те, як вирішуються обіцянки, і знаєте, що null та undefined — це не одне й те саме, незалежно від того, наскільки неуважно деякі API їх змішують.

Але знайомість не усуває всіх сюрпризів, вона лише змінює їхню форму. Плутанина на початковому рівні замінюється більш тонкими, більш небезпечними припущеннями. Ви знаєте, що об’єкти передаються за посиланням, проте все одно забуваєте, що поверхнева копія залишає взаємно доступними вкладені дані. Ви знаєте, що обіцянки ґрунтуються на мікрозавданнях, проте все одно неправильно оцінюєте порядок виконання, коли кілька черг починають взаємодіяти. Ви знаєте про існування примусу, проте все одно не помічаєте єдиного тихого перетворення, прихованого всередині на перший погляд невинної порівняння, виклику сортування чи пошуку властивості.

Найскладніші аспекти JavaScript рідко є екзотичними винятковими ситуаціями чи дрібницями мови. Це звичайні правила, які поєднуються непередбачуваними способами. Кожен окремий рядок виглядає нормально, легко читається та пройшов би поверхневу перевірку. Сюрприз виникає через те, як система виконання поєднує ці рядки, у спосіб, який не відповідає уявленню, яке у вас було під час читання коду.

Десять описаних нижче поведінок продовжують створювати проблеми у досвідчених командах саме тому, що вони приховані всередині коду, який виглядає абсолютно нормальним.

1. const захищає зв’язок, а не об’єкт

Поширеним скороченням для пояснення значення const є твердження, що воно „створює значення, яке не може змінитися“. Це гарне спрощення для примітивних типів, але воно не діє у випадку масивів та об’єктів. Насправді const гарантує лише те, що змінна не може бути перенаправлена на інший об’єкт. Воно нічого не каже про те, чи може об’єкт, на який вона посилається, зазнати змін.

Об’єкт user, оголошений за допомогою const, все ще може отримувати нові властивості. Його внутрішній об’єкт налаштувань також може бути оновлений. Масив, оголошений за допомогою const, все ще можна поповнювати елементами, сортувати чи спорожнювати. З точки зору JavaScript усе це не впливає на прив’язку змінної – вона все одно посилається на той самий об’єкт, як і раніше.

Ця різниця між очікуваннями та реальністю все ще спричиняє справжні проблеми в кодових базах, які сильно спираються на ідею незмінності. Об’єкт конфігурації імпортується як константа, потім один з модулів тихо її змінює, і раптово всі інші модулі, які використовують цей посилання, бачать зміни. Масив, який зберігається у стані додатку, сортується на місці, що змінює як „текуще“ значення, так і старіше значення, про яке інша частина коду вважала замороженим історичним знімком. Тест змінює спільний об’єкт-фікстуру, і абсолютно непов’язаний тест починає припиняти працювати, але лише тоді, коли виконувач тестів випадково запускає їх у іншому порядку.

Ключове слово const створює оманливе відчуття безпеки — здається, що це захисна оболонка навколо значення, але час виконання захищає лише вказівник на змінну, а не саму структуру, на яку вона вказує.

Якщо вам справді потрібна незмінність, доведеться створити її самостійно. Це може означати повернення абсолютно нових об’єктів замість редагування існуючих, застосування Object.freeze до конкретних значень, які мають значення, використання бібліотеки, створеної для роботи з незмінним станом, або проектування функцій так, щоб вони з самого початку не передавали посилання на внутрішні змінні дані. Навіть Object.freeze фіксує лише верхній рівень; якщо не заморозити кожен вкладений об’єкт, внутрішні шари залишаються такими ж змінними, як і раніше.

Більшість досвідчених розробників можуть запам’ятати це правило, проте здивування з’являється щоразу, коли візуальний стиль коду натякає на сильніші гарантії, ніж насправді пропонує JavaScript.

2. Об’єктне розповсюдження копіює менше, ніж здається

Розповсюдження об’єкта за допомогою { ...obj } стало одним із поширених ідіом у JavaScript. Цей підхід зручний для поєднання значень за замовчуванням та їх перевизначення, а також дозволяє створити модифіковану версію значення, не торкаючись оригіналу.

Візуально це виглядає як повна, незалежна копія. Насправді ж копія існує лише на одному рівні глибини.

const original = {
  profile: {
    name: "Umar",
    skills: ["JavaScript", "Node.js"],
  },
};

const copy = { ...original };
copy.profile.skills.push("TypeScript");

Після виконання цього фрагмента ново додана навичка також з’являється в original.profile.skills. Об’єкт верхнього рівня справді є новим, але об’єкт profile, розташований всередині нього, та масив skills, розташований усередині цього об’єкта, залишаються точно такими самими об’єктами, на які вказував оригінал.

Саме це робить цю проблему настільки постійною: перший рівень поводиться саме так, як можна було очікувати. Перевірка copy !== original повертає значення true, що здається доказом наявності чистого розділення. Проблема спільних змін проявляється лише тоді, коли переходиш на наступний рівень.

Механізм поширення також створює друге несподівання під час об’єднання об’єктів конфігурації. Використання { ...defaults, ...options } замінює всі вкладені об’єкти, замість того щоб об’єднати їх окремі ключі. Якщо options перевизначає яке-небудь налаштування всередині вкладеного об’єкта, усі суміжні налаштування, які знаходилися разом із ним у defaults, зникають разом із ним, хоча викликаючий код зовсім не мав наміру їх торкатися.

Рішенням не є автоматичне створення „глибокої копії всього“. Повна глибока копія може бути дорогою, призвести до втрати ідентичності об’єктів, від яких ви насправді залежите в інших частинах коду, а також до дублювання даних, які мали залишатися спільними. Насправді важливо з’ясувати, які саме вкладені шляхи потребують окремих копій. Обробляйте їх явно, використовуйте відповідний інструмент для незмінних оновлень або переструктуруйте глибоко вкладені дані так, щоб межі власності були видимі одразу.

Функція розповсюдження об’єктів працює саме так, як описано, коли вам справді потрібна поверхнева копія. Вона стає проблемою в ту мить, коли її скорочена синтаксис сприймається як повна глибока копія.

3. Звичайна рівність може приховувати кілька процедур перетворення

Найбільш досвідчені розробники зазвичай використовують ===, щоб уникнути проблем, пов’язаних із примусовою конвертацією, яка виникає під час використання ==. Ця звичка справді корисна, але вона не захищає вас від усіх прихованих конвертацій, які виконує JavaScript – багато з них трапляються у місцях, що не мають нічого спільного з оператором рівності.

Ключі властивостей об’єктів є гарним прикладом. За винятком символів, кожен ключ об’єкта насправді є рядком. Тож як призначення значення object[1], так і подальше зчитування object[“1”] впливають на одну й ту саму властивість. Код, який у своїх міркуваннях розглядає числові та рядкові ідентифікатори як два окремі типи, може потрапити у скрутне становище в таких ситуаціях.

Порівняння типу “відносне” виконують власні конвертації залежно від того, що порівнюється. Два рядки порівнюються лексикографічно (за символом до символа), тоді як порівняння числа з числовим рядком може призвести до виконання числового порівняння. Саме тому "20" < "100" має значення false, тоді як 20 < "100" має значення true. Якщо змінити джерело значення, логіка сортування чи перевірки може змінитися, навіть якщо відображувані значення виглядають ідентично.

Оператор + є особливо складним, оскільки виконує як числове додавання, так і об’єднання рядків, причому JavaScript вирішує, який з цих варіантів використати, залежно від контексту. Один-єдиний рядок, який з’являється на початку ланцюжка операцій додавання, може змінити інтерпретацію всього, що йде після нього. Оскільки значення, отримані з полів форми, параметрів запиту URL та інших джерел HTML, зазвичай надходять у вигляді рядків, вираз, який ідеально працював із внутрішніми числовими значеннями, може мовчки перейти на об’єднання рядків у момент підключення до вхідних даних користувача.

Досвідчені команди уникають цих пасток, нормалізуючи значення вже на самому початку, замість того щоб покладатися на оператори для правильної обробки необробленого вхідного даних у реальному часі. Ідентифікатор з самого початку визначається як рядок або число. Грошова сума перетворюється на перевірений числовий тип. Дата аналізується на відповідний часовий тип або фіксований ISO-рядок ще до будь-яких порівнянь.

Сам процес примусового перетворення не є справжньою проблемою — правила JavaScript у цьому аспекті добре визначені та послідовні. Справжня проблема полягає у тому, що ці правила постійно застосовуються там, де в коді немає жодних ознак того, що відбувається перетворення.

4. Array.prototype.sort — переставляє елементи на місці та за замовчуванням порівнює текст

Сортування здається однією з найпростіших операцій, які можна виконати з масивом, але насправді воно поєднує два механізми, які часто створюють плутанину.

Першим сюрпризом є те, що сортування змінює масив. Виклик методу .sort() переупорядковує масив, на якому він був викликаний, та повертає посилання саме на цей масив. Легко зберегти значення, отримане у результаті, у новій змінній та припустити, що оригінальний масив все ще зберігає свій попередній порядок, але потім з’ясувати, що обидві змінні вказують на один і той самий, переупорядкований список.

Другим сюрпризом є те, як працюють порівняння, коли не надано функції-компаратора. У такому випадку JavaScript перетворює кожен елемент на рядок та сортує їх лексикографічно. Якщо виконати це з масивом чисел, можна отримати щось на кшталт 1, 100, 20, 3 замість порядку, що зростає за числовими значеннями.

Ця комбінація створює небезпеку під час керування станом фронтенду. Уявіть компонент, який сортує масив безпосередньо перед відображенням, не усвідомлюючи, що цей масив є спільним посиланням на дані з кешу чи від сервера. В результаті він мутує це спільне джерело. Компонент-брат, який читає ті самі базові дані, також бачить новий порядок, навіть не викликаючи функцію сортування. Логіка виявлення змін та мемоїзація також можуть дати хибні результати, оскільки ідентичність масиву (його посилання) ніколи не змінюється, хоча його вміст змінюється — тому поверхневе порівняння не виявить жодних відмінностей.

Сучасний JavaScript пропонує альтернативи без зміни даних: у середовищах, які їх підтримують, доступний метод toSorted(), а клонування масива перед сортуванням залишається стандартним рішенням у тих випадках, коли цей метод відсутній. Для числового сортування все одно потрібно передати методу .sort() явний компаратор, який відображає саме ту логіку порівняння, яка вам потрібна.

Основний висновок полягає у тому, що назва методу не розкриває всіх його характеристик. Слово „Сортувати“ описує кінцевий результат, але не говорить про те, чи змінює метод дані на місці, як він перетворює значення для порівняння, які гарантії стабільності він надає чи який специфічний порядок може знадобитися. Навіть досвідчені розробники потрапляють у скрутну ситуацію, коли метод здається настільки стандартним, що вони навіть не замислюються над тим, що він насправді робить у своїй основі.

5. Об’єкт Date моделює окрему мить — більшість вхідних даних не можна однозначно прив’язати до однієї

Проблеми з часовими поясами в JavaScript насправді не пов’язані з тим, що часові пояси є концептуально складними. Вони виникають через те, що короткий рядок містить набагато більше неявних припущень, ніж здається.

По суті, об’єкт Date — це часовий слід: точна мить, виміряна за UTC. Але більшість дат, з якими люди насправді працюють — день народження, термін виконання завдання, цикл оплати, запланована зустріч — є календарними поняттями, а не фіксованими точками універсального часу, і кожне з них по-різному пов’язане з часовими поясами.

Якщо ви парсуєте лише рядок з датою, а потім відображаєте його за місцевим часом, дата в календарі, яку бачить користувач, може змінюватися залежно від його місцезнаходження. Значення, призначене для позначення „3 серпня“, може бути інтерпретоване як північ, UTC, що при місцевому відображенні в іншому регіоні буде показано як 2 серпня. Так само часовий слід, створений без чіткого вказання зсуву UTC, може тлумачитися по-різному залежно від точного формату рядка та способу його обробки під час виконання.

Літній час створює ще одну складність. Додавання фіксованої кількості мілісекунд до об’єкта Date не гарантує того, що це буде еквівалентно додаванню одного календарного дня за місцевим часом — у деякі дні в зонах, де діє зміна часу, тривалість дня насправді становить двадцять три або двадцять п’ять годин.

Досвідчені розробники все ще стикаються з цією проблемою, оскільки їхній код використовує один тип Date для представлення кількох незалежних понять одночасно. У системі типів немає жодних вказівок щодо того, чи має дане значення бути точною миттю, простою датою календаря чи місцевим часом, призначеним для інтерпретації в певному регіоні.

Надійні системи вирішують цю проблему, роблячи намір явним, а не неявним. Миттєвості містять зсув UTC або виражаються безпосередньо у форматі UTC. Значення, що стосуються лише календаря, залишаються такими, не перетворюючись без потреби на повні часові позначки. Розклади, специфічні для регіону, зберігають контекст часової зони, у якій вони були створені. Парсинг та форматування відбуваються у чітко визначених межах системи, а не там, де випадково відображається чи зчитується дата.

Отже, сюрприз зазвичай полягає не у тому, що задіяні часові зони — а у тому, що якась на перший погляд невинна операція конвертації вже прийняла непомітне рішення щодо тієї чи іншої зони.

6. Обіцянку можна виконати ще до того, як її функція-відповідь насправді виконається

Обіцянка, яка була вирішена, здається завершеною справою, але функція, приєднана до неї за допомогою .then — або код, що знаходиться після await — не виконується прямо посеред поточно виконуваного синхронного коду. Натомість вона потрапляє у чергу мікрозавдань та виконується пізніше.

Це чергування створює певний порядок виконання, який все одно може здивувати досвідчених розробників, коли починають змішуватися обіцянки, таймери, обробники подій та звичайний синхронний код. Обіцянка, яка вже реалізована, планує своє продовження на пізніший час, тоді як поточний синхронний код продовжує працювати без перерв. Це чергове продовження зазвичай виконується раніше, ніж настане час виконання будь-якого калебу з таймера, запланованого для наступного завдання — навіть setTimeout із затримкою у нуль мілісекунд.

Справжньою практичною небезпекою не є вгадування порядку виведення даних у консолі під час тестування. Небезпека полягає у нерозумінні того, що фрази „обіцянка виконана“ та „її обробник дійсно запущений“ позначають два різні моменти часу. Оновлення стану, яке відбувається через обробник обіцянки, ще може не бути видимим для коду, який виконується пізніше в тій самій синхронній стекуванні. Тест може перевіряти значення до того, як його очікувані мікрозавдання встигнуть бути виконані. Крім того, достатньо довга послідовність мікрозавдань може відтермінувати роботу таймерів та процес відображення даних довше, ніж очікується, адже движок виконання завжди спочатку обробляє всю чергу мікрозавдань, перш ніж переходити до чогось іншого.

Код стає крихким, коли залежить від цього випадкового порядку, а не від чітко визначеної послідовності. Саме тому досвідчені розробники воліють повертати та очікувати обіцянки, коли один крок справді мусить слідувати за іншим, користуються будь-якими механізмами життєвого циклу, які надає їхня фреймворк-система, та уникають використання таймерів з нульовою затримкою як надійного способу синхронізації з іншими завданнями.

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

7. Огортання виклику async у блоки try/catch не гарантує виявлення будь-яких проблем

Блок try/catch, який оточує виклик функції, здається, має захищати цей виклик від невдач. Однак у випадку асинхронних функцій ефективність такого захисту залежить виключно від того, чи очікуєте ви на повернене обіцяння.

try {
  saveAuditLog(record);
} catch (error) {
  reportError(error);
}

Якщо saveAuditLog кидає помилку синхронно ще до того, як поверне обіцяння, блок catch успішно її обробить. Але якщо saveAuditLog є асинхронною функцією, яка зазнає невдачі пізніше, до моменту відхилення вона вже повернула обіцяння, і виконання вже покинуло блок try. Тепер це відхилення існує у тому обіцянні — воно не має нічого спільного з синхронним викликом, який вже завершився.

Додавання await знову пов’язує відмову із контекстним блоком try/catch, але це працює лише тоді, коли сама функція, яку ви пишете, дозволяє очікувати на щось. Передача обіцянки в іншу ланцюгову структуру також може зберегти цей зв’язок між помилкою та кодом обробки. Просте викликання асинхронної функції зсередини коду обробки помилок без очікування чи повернення результату нічого не досягає.

Ця помилка постійно зустрічається у обробниках подій, функціях-колбеках масивів та інтеграціях з бібліотеками, де навколишнє API часто не знає, що робити з обіцянкою, поверненою асинхронним колбеком — і часто просто ігнорує її. Колбек все одно правильно спричиняє відмову, але ніхто цього не спостерігає. Залежно від середовища виконання це може проявитися як попередження про непроцесовану відмову, записаний у журнал помилка, зупинка процесу чи робота, яка мовчки ніколи не отримує підтвердження.

Розробники, які постраждали від цих проблем через використання об’єктів типу promise замість правильного відступу коду. Вони запитують, який обсяг насправді очікує асинхронної роботи, де відмова перетворюється на щось дійове, та чи існує у будь-яких навмисних викликах типу fire-and-forget реальний шлях для повідомлення про помилку.

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

8. Значення за замовчуванням під час деструктуризації застосовуються лише до undefined

Значення за замовчуванням під час деструктуризації є зручним захистом від відсутності вхідних даних.

const { timeout = 5000 } = options;

Це значення за замовчуванням застосовується лише тоді, коли timeout дорівнює undefined або взагалі відсутній у об’єкті. Воно не буде застосовуватися до null, 0, порожньої стрічки чи будь-якого іншого значення, яке було явно вказано.

Часто саме така поведінка і є бажаною. Значення „нуль“ може бути навмисним способом відключення затримки, а null може мати власне окреме значення. Проблеми починаються тоді, коли хтось вважає, що значення за замовчуванням підходить для всього, що є „непридатним“. API може надіслати null, а код, що працює далі, намагатиметься виконувати з ним математичні операції. Поле форми може надсилати порожній рядок, тому значення за замовчуванням ніколи не активується. Завантажувач конфігурації може ретельно розрізняти ситуацію „змінна не встановлена“ та ситуацію „змінна явно встановлена у порожнє значення“, тоді як код, що використовує цю конфігурацію, ставиться до обох випадків однаково та переходить на значення за замовчуванням.

Стандартні параметри функцій працюють за тією самою логікою: якщо викликати функцію без аргументів, застосовується значення за замовчуванням, але якщо явно передати null, це не спрацює. Ця різниця має велике значення під час передачі даних через JSON-пакети, рядки баз даних, форми та API сторонніх сервісів — у всіх цих випадках null зустрічається дуже часто.

Розробники, які покладаються на цю модель, зазвичай розділяють питання значень за замовчуванням та перевірки. Значення за замовчуванням відповідає на запитання „що відбувається, якщо нічого не було передано?“, а перевірка — на запитання „чи є передані дані справді прийнятними?“. Об’єднання обох завдань у один синтаксичний елемент може створити ілюзію, що некоректні дані насправді були належним чином ініціалізовані.

Правило мови в цьому випадку є точним та послідовним. Плутанина виникає виключно через слово „default“, яке натякає на ширшу сферу застосування, ніж фактично забезпечує сувора поведінка, яка дозволяє лише значення undefined.

9. Відсутня властивість, видалена властивість та undefined — це три різні речі

JavaScript дозволяє властивості об’єкта мати значення undefined або взагалі не існувати в об’єкті. Будь-який з цих варіантів при прямому доступі повертає undefined, тож вони здаються взаємозамінними — але це не так.

Інструменти на кшталт оператора in, методів Object.hasOwn та Object.keys, синтаксису розповсюдження, механізмів ітерації, валідаторів схем та серіалізації JSON допомагають визначити цю різницю. Наприклад, функція JSON.stringify повністю видаляє властивості з значенням undefined, тоді як елемент масиву, що містить undefined, обробляється інакше. Об’єднання об’єктів також може перезаписати наявне коректне значення на undefined, навіть якщо автор операції об’єднання мав намір залишити це поле недоторканим.

Ця прогалина є постійним джерелом тонких помилок під час оновлень. Пакет даних типу PATCH у бекенді може сприймати пропущене поле як сигнал „не торкатися цього“, а поле, яке явно встановлено у значення null, — як сигнал „видалити це“. У той же час фронтенд-форма може генерувати значення undefined для будь-якого поля, до якого користувач ніколи не торкався, але при передачі цих даних у об’єкт оновлення все одно вставляються ці властивості типу undefined, які потім замінюють існуючі значення під час об’єднання.

Щоб уникнути цього, необхідно свідомо визначати семантику оновлень, а не допускати це випадково. Пропуски, значення undefined, null та законно порожні значення не повинні автоматично набувати певного значення лише через механізм серіалізації чи об’єднання об’єктів.

Це має велике значення особливо у TypeScript, де опційна властивість та обов’язкова властивість, тип якої випадково включає undefined, виражають дві структурно різні наміри — навіть якщо код програми, що їх оточує, згодом буде ставитися до них однаково. Суворіші налаштування компілятора можуть забезпечити це розрізнення на рівні типу, але фактичні дані, що передаються через програму під час виконання, все одно потребують власної перевірки.

Два значення, які здаються ідентичними під час їх читання, все одно можуть представляти дуже різні контракти після того, як вони передаються через межу мережі або під час операції оновлення.

10. Замикання зберігає змінну, а не кадр у часі

Замикання є одним із найпотужніших інструментів, які надає JavaScript. Функція зберігає доступ до змінних із області видимості, де вона була визначена, і саме це дозволяє калебекам, фабричним функціям, обробникам подій та шаблонам модулів працювати настільки природно, як це відбувається.

Поширеним скороченням, яке використовують люди, є твердження, що замикання „пам’ятає значення“. Більш точний опис полягає у тому, що воно зберігає доступ до прив’язки змінної. Якщо значення цієї змінної змінюється до того, як замикання насправді виконується, замикання буде бачити саме нове значення — а не те, яке існувало на момент його створення.

Це класичне пояснення проблем з циклами, пов’язаних із використанням var, але досвідчені розробники часто стикаються з більш тонкими варіантами тієї самої проблеми у асинхронному коді та логіці інтерфейсу. Функція-колбек може читати об’єкт конфігурації, який змінився після початку виконання операції. Обробник подій може працювати зі станом, який вже застарів. Функція з затримкою може використовувати поточний стан змінного об’єкта, тоді як розробник припускав, що вона буде працювати з об’єктом саме таким, яким він був у момент запланування функції.

Фреймворки додають свої власні правила життєвого циклу поверх цього, що зазвичай робить ефект більш помітним. У React, наприклад, калебек, створений під час певного відображення, зберігає значення, які існували під час саме цього відображення. Це може спричинити прояви, схожі на поведінку застарілого стану, хоча основна механізм замикання в JavaScript працює саме так, як задумано. Здивування виникає через очікування, що калебек якимось чином автоматично отримає найновіший стан — але замикання не працюють саме так.

Правильне рішення залежить від того, що вам насправді потрібно. Іноді ви справді хочете отримати значення у тому вигляді, в якому воно існувало на момент початку операції, у такому разі правильним кроком буде створення стабільного знімка заздалегідь. Іноді ви хочете отримати найсвіжіше доступне значення, що вимагає використання поточного посилання, схожого на ref, або функціонального оновлення. А іноді правильною відповіддю є те, щоб змінювані залежності повністю перстворювали callback.

Корисне запитання, яке варто поставити, — чи має затримана робота відображати стан світу на момент її планування, чи стан світу на момент її фактичного виконання. Сам закритий об’єкт не має думки з цього приводу — він вірно зберігає будь-які зв’язки, які ви налаштували у своєму коді, навіть якщо це не були ті зв’язки, які ви хотіли створити.

JavaScript поводиться послідовно — наші ментальні скорочення — ні

Жодна з описаних тут поведінок не є довільною особливістю. const блокує сам зв’язок, а не значення всередині нього. Функція розповсюдження створює копії лише на одному рівні глибини. Стандартний сортування масивів спочатку перетворює елементи на рядки. Обробники обіцянок виконуються як мікрозавдання. Функція руйнування за замовчуванням реагує саме на undefined. Замикання зберігають лексичні зв’язки, а не фіксовані значення. Кожне з цих правил застосовується мовою з абсолютною послідовністю.

Несподіванки виникають тоді, коли розробники покладаються на умовні моделі, які є простішими за те, що насправді забезпечує движок виконання. Ми кажемо, що константа „не може змінитися“, розповсюдження „створює копію“, асинхронний виклик „знаходиться всередині try/catch“ чи замикання „пам’ятає значення“. Ці спрощення є корисними, поки якась нова вимога не залежить саме від деталей, які вони проігнорували.

Те, що захищає досвідчених розробників, — це не запам’ятовування постійно зростаючого списку дрібниць, а звичка чітко формулювати умови там, де дані перетинають межі. Це означає нормалізувати значення, що надходять ззовні, розглядати „відсутнє“ та „недійсне“ як окремі поняття, уникати випадкової зміни спільних посилань, чітко визначати, хто відповідає за обробку помилок асинхронної операції, зберігати первісний сенс часових міток та часових зон, а також заздалегідь вирішувати, чи має затриманий калебек діяти зі старим чи поточним станом.

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

JavaScript продовжує дивувати досвідчених розробників саме тому, що досвід породжує довіру до коду, який здається знайомим. Ризик полягає у тому, що синтаксис, який здається знайомим, все ще може приховувати рішення щодо посилань, перетворень типів, часу виконання та відповідальності за помилки — рішення, які стають видимими лише тоді, коли щось інше в системі навколо них змінюється.

Мова майже завжди робить саме те, що від неї було зажадано.

Сюрприз полягає у тому, щоб зрозуміти, що насправді ваш код наказав їй робити.

Пов’язана література

  • Десять поширених звичок використання JavaScript, які тихо підривають ваш кодовий базис — пояснює десять поширених проблем у JavaScript та TypeScript, від недостатньо строгого порівняння до зміни стану, та пропонує безпечніші підходи для їх усунення.