JavaScript
JavaScript — це мова, яку виконує кожен браузер, і та, яку більшість вчить із синтаксису й ніколи — із семантики. Майже все, що здається в ній довільним, випливає з одного факту: ваш код виконується в одному потоці з одним стеком викликів, а все асинхронне — це черга, що чекає, доки стек спорожніє. На цій сторінці — цикл подій, посилання, замикання, модулі й те, де обробка помилок в асинхронному коді тихо провалюється.
- Потоків
- Один на контекст
- Черг
- Мікро й макро
- Стандарт
- ECMAScript, щороку
Один потік, дві черги
JavaScript у вкладці браузера виконується в одному потоці. Стек викликів один, і поки на ньому є функція, у цій вкладці не виконується більше нічого: ні інший обробник, ні колбек таймера, ні відмальовка. Саме останнє й означає «заблокувати інтерфейс» буквально: браузер не може намалювати кадр, бо ваш код не повернувся.
Коли стек порожніє, керування бере цикл подій, і він поводиться з двома чергами по-різному. Він повністю вичерпує чергу мікрозадач — кожен колбек промісу й усі нові мікрозадачі, які ці колбеки додають під час вичерпування. Потім бере рівно одну макрозадачу: один колбек таймера, один обробник кліку, одну мережеву відповідь. Потім браузеру дозволено намалювати кадр — і оберт починається знову.
Практичний наслідок стосується довгої роботи, а не дрібниць. await нічого не переносить в інший потік: він призупиняє вашу функцію й дає циклу працювати далі, але коли очікуване значення приходить, продовження виконується в тому самому єдиному потоці. Цикл, що чотириста мілісекунд рахує, блокує сторінку на чотириста мілісекунд незалежно від того, написаний він з async чи ні. Справжня паралельність потребує Web Worker — окремого потоку без доступу до DOM.
Значення, посилання, рівність
Примітиви — числа, рядки, булеві, null, undefined, символи, bigint — копіюються при присвоєнні. Усе інше є об’єктом, а об’єкти передаються за посиланням: дві змінні можуть називати один об’єкт, і зміна через одну видима через іншу. Масиви й функції — це об’єкти, і тому передати масив у функцію й відсортувати його там означає змінити масив викликача.
Копіювання за замовчуванням поверхневе. Розкладання об’єкта копіює верхній рівень; вкладені об’єкти лишаються спільними, тож зміна copy.user.name змінює й оригінал. Для справжньої глибокої копії є вбудований structuredClone(), який працює з циклами, датами, мапами й множинами, — це те, заради чого раніше писали JSON.parse(JSON.stringify(x)), і, на відміну від того трюку, він не викидає мовчки функції, значення undefined і дати.
Щодо рівності правило коротке: використовуйте === і не думайте про це. Нестрога рівність застосовує таблицю приведення, що дає обґрунтовані, але несподівані результати, і жодна кодова база ніколи не виграла від опертя на неї. Два винятки, які варто знати: NaN === NaN — це false, тож беріть Number.isNaN(); і x == null — компактний ідіоматичний спосіб перевірити null та undefined разом.
Область видимості, замикання та `this`
Замикання — це функція, яка пам’ятає змінні області, де її створили, і тримає їх живими стільки, скільки живе сама. Це механізм, завдяки якому колбеки працюють пізніше, існує приватність у модулях і працює кожен хук у React. Це ж механізм класичної помилки з циклом: із var усі ітерації ділять одне зв’язування, тож десять створених у циклі колбеків бачать кінцеве значення. З let кожна ітерація отримує власне зв’язування, і той самий код робить те, чого від нього чекали.
Це й є практична причина, чому var більше не використовують: він має функційну область і піднімається, тож витікає з блоків і існує ще до рядка оголошення. let і const мають блокову область, і const має бути вашим типовим вибором: він забороняє перепризначення — саме ту помилку, яку варто попереджати, — але дозволяє змінювати об’єкт, на який вказує.
this визначається тим, як функцію викликали, а не тим, де її оголосили, — тому передача obj.method як колбека губить об’єкт. Стрілкові функції не мають власного this і беруть його з навколишньої області, і саме тому вони стали типовим вибором для колбеків. Правило, що знімає майже все це: беріть стрілкові функції для колбеків, а якщо ви пишете .bind(this) чи const self = this, ви обходите звичайну функцію, яка мала бути стрілковою.
Три способи написати те саме
Асинхронний код мав три покоління синтаксису, і всі три досі трапляються в реальних проєктах — часто в одному файлі. Під ними той самий механізм; різниця в тому, як подорожують помилки і наскільки читабельна послідовність.
| Стиль | Обробка помилок | Де досі доречний |
|---|---|---|
| Колбеки | За домовленістю — помилка першим аргументом, і це ніщо не забезпечує. | Обробники подій і старі API Node; ніколи для нової послідовної роботи. |
| Проміси | .catch() покриває весь ланцюг вище — один обробник на багато кроків. | Композиція паралельності: Promise.all, allSettled, race. |
| async / await | Звичайний try / catch і стектрейси, у яких названо вашу функцію. | Типовий вибір для всього послідовного — читається в порядку подій. |
Єдине, що await дозволяє легко зіпсувати, — це випадкова послідовність. Очікування трьох незалежних запитів один за одним триває стільки, скільки всі три разом; запуск усіх трьох і очікування через Promise.all триває стільки, скільки найповільніший. Якщо виклики не залежать один від одного, очікування їх поспіль — це баг продуктивності, написаний бездоганно чистим синтаксисом.
Модулі
ES-модулі — це стандарт: import і export, підтримувані нативно і браузерами, і Node. Їхні імпорти статичні — набір залежностей відомий до виконання будь-якого коду, — і саме це уможливлює tree-shaking, бо складальник може довести, що іменований експорт ніде не імпортують, і викинути його. CommonJS, старіший require у Node, розв’язується під час виконання, тож той самий аналіз надійно провести не можна.
Дві практичні нотатки. Модулі завжди в суворому режимі й мають власну область, тож нічого не витікає в глобальний об’єкт — це прибирає цілу епоху багів. І віддавайте перевагу іменованим експортам перед типовим: у типового немає фіксованого імені, тож різні файли імпортують те саме під різними іменами, і перейменування перестає бути механічним рефакторингом.
Як це виявляється в реальній поставці
Різниця між тими, хто швидкий у цій мові, і тими, хто ні, майже ніколи не в синтаксисі. Вона в тому, чи тримають вони в голові модель виконання: чи можуть, дивлячись на шматок коду, сказати, що зараз на стеку, що в черзі й коли браузер зможе намалювати кадр. До цього зводиться будь-яке питання продуктивності на фронтенді — і більшість того, що виглядає як магія фреймворку.
Про TypeScript чесно: це не інша мова, а перевіряч типів над цією, і на збірці він стирається повністю. Він ловить саме той клас помилок, який описує ця сторінка — можливо-невизначене значення, помилку в назві властивості, виклик функції не тієї форми, — до їх виконання. Він не змінює цикл подій, нічого не пришвидшує і не може перевірити дані, що приходять із мережі, поки ви не валідуєте їх самі.
Де це вироджується
- Очікування незалежних викликів поспіль перетворює 300-мілісекундну сторінку на 900-мілісекундну без видимої причини.
async-колбек, переданий уforEach, повертається негайно й нічого не очікує.- Проміси, створені й ніколи не очікувані та не спіймані, стають необробленими відхиленнями.
||для типових значень там, де мався на увазі??, — і чинний0чи порожній рядок підміняється.- Поверхневі копії, сприйняті як глибокі, — вкладена зміна досягає оригіналу.
- Довга синхронна робота в головному потоці морозить сторінку, хоч би як її написали.
varу новому коді: його функційна область і підняття дають помилки, які зletнеможливі.
Коли це застосовувати
Застосовуйте, коли
- Усе, що виконується в браузері: альтернатив немає, є лише мови, що компілюються в неї.
- Додавайте TypeScript, щойно в проєкті більше одного учасника або він переживає одну пам’ять.
- Беріть Web Worker, коли робота справді впирається в CPU й не має блокувати інтерфейс.
- Беріть
Promise.allщоразу, коли очікувані виклики не залежать один від одного.
Уникайте, коли
- Покладатися на нестрогу рівність чи «правдивість» там, де
0або порожнє значення є чинними. - Важкі обчислення в головному потоці, коли користувач має продовжувати взаємодію.
- Змішувати три покоління асинхронного синтаксису в одному потоці — шляхи помилок стають нечитабельними.
- Вважати, що TypeScript валідує дані під час виконання, — ні; розбирайте те, що перетинає межу.
Було корисно?
Поділіться з тим, хто працює над тією ж задачею.