08 · JavaScript/TypeScript для AQA
Змінні, типи даних і приведення типів
Зміст
Автотест — це код, а код працює з даними: тіло відповіді API, текст із локатора, число з бази, прапорець із конфігу. І майже кожен фальшивий результат, який доводилося ловити руками, зводиться до типів. Тест порівнює рядок "200" зі статусом 200 через == — і зелений там, де мав би бути червоний. Перевірка if (response.body.count) мовчки пропускає легітимне значення 0, бо нуль — «хибний». Один тест мутує спільний обʼєкт із тестовими даними, а падає інший — бо обʼєкти передаються за посиланням. Це не екзотика, це щоденний фон роботи AQA.
Тому ця глава — не про синтаксис заради синтаксису. Це про модель, за якою JavaScript зберігає, копіює й порівнює значення. Хто її тримає в голові, той відрізняє баг застосунку від бага у власному тесті за секунди, а не за годину. І саме тому це — одна з найчастіших тем на технічній співбесіді AQA: питання про typeof null, == проти === і передачу за посиланням ставлять майже завжди.
var, let, const і блокова область
JavaScript має три способи оголосити змінну, і різниця між ними — не стилістична, а поведінкова.
var — стара форма. Вона має функціональну область видимості (function scope): змінна видима в усій функції, де оголошена, незалежно від блоків if, for чи фігурних дужок усередині. До того ж оголошення var «спливає» вгору функції (hoisting) — звертання до змінної до рядка оголошення повертає undefined, а не помилку.
let і const (з ES2015) мають блокову область видимості (block scope): змінна живе лише всередині того блоку { }, де оголошена. Звертання до неї до оголошення кидає ReferenceError — цей проміжок між початком блоку й рядком оголошення називають «мертвою зоною» (temporal dead zone, TDZ). Це не примха, а страховка: код падає одразу й голосно, замість тихо працювати з undefined.
Головна пастка новачка — думати, що const робить значення незмінним. Ні. const забороняє переприсвоєння самого звʼязку (binding), а не зміну вмісту обʼєкта.
const user = { role: "admin" };
user.role = "guest"; // OK — мутуємо обʼєкт, звʼязок той самий
// user = {}; // TypeError — переприсвоєння забороняне
Практичне правило для тестового коду: за замовчуванням const, let — лише коли значення справді треба переприсвоїти, var — ніколи. Чому var небезпечний саме в тестах, найкраще видно на прикладі циклу з setTimeout і замиканням — це окрема класична пастка, яку детально розбирає глава «Функції, замикання та область видимості».
Примітиви проти обʼєктів
Усі значення в JavaScript діляться на дві категорії, і ця межа визначає, як вони копіюються та порівнюються.
Примітивів сім типів: string, number, bigint, boolean, undefined, symbol і null. Примітив — це проста, незмінна (immutable) величина: рядок "abc" не можна «відредагувати на місці», будь-яка операція повертає новий рядок. Примітиви зберігаються й передаються за значенням — копіюється сама величина.
Обʼєкти — усе інше: обʼєктні літерали {}, масиви [], функції, Date, Map, Set, регулярні вирази. Обʼєкт зберігається в купі (heap), а змінна тримає лише посилання (reference) на нього. Тому обʼєкти передаються й копіюються за посиланням — про наслідки цього нижче окремий розділ.
Примітивне значення лежить у змінній безпосередньо; змінна-обʼєкт зберігає лише стрілку на дані в купі. Звідси все інше: два різні обʼєкти з однаковим вмістом не рівні, бо стрілки вказують у різні місця, а count і його копія рівні, бо це два незалежні 42.
typeof і його сюрпризи
Оператор typeof повертає рядок із назвою типу. Значень, які він може повернути, рівно вісім: "undefined", "boolean", "number", "bigint", "string", "symbol", "object" і "function". Здебільшого він працює очікувано — і саме на винятках будують питання співбесід.
typeof "hello" // "string"
typeof 42 // "number"
typeof true // "boolean"
typeof undefined // "undefined"
typeof null // "object" ← сюрприз №1
typeof [1, 2, 3] // "object" ← сюрприз №2
typeof function(){} // "function" ← сюрприз №3
typeof NaN // "number" ← сюрприз №4
Три речі, які треба вміти пояснити:
typeof null === "object"— це історична вада самого JavaScript, зафіксована ще в першій версії й залишена назавжди заради сумісності.nullне є обʼєктом; простоtypeofбреше на ньому. Щоб надійно перевірити наnull, порівнюють напряму:value === null.typeof [] === "object"— масив технічно є обʼєктом, тожtypeofне відрізняє масив від звичайного{}. Для перевірки на масив існує окремий методArray.isArray(value).typeof function === "function"— функції теж обʼєкти, але для нихtypeofробить виняток і повертає окремий рядок.
Для AQA це прямий інструмент: коли з API приходить поле невідомої форми, typeof і Array.isArray — перша лінія перевірки, перш ніж звертатися до властивостей і ловити TypeError.
null проти undefined
Обидва означають «значення немає», але семантика різна, і плутанина між ними дає реальні баги в перевірках.
undefined — це «значення не задане системою»: змінна оголошена, але нічого не присвоєно; відсутня властивість обʼєкта; функція нічого не повернула; аргумент не передали. Це стан за замовчуванням.
null — це «значення явно відсутнє за рішенням розробника». Хтось свідомо написав null, щоб сказати: тут порожньо навмисно.
Різницю добре видно на серіалізації JSON — а це щоденний хліб API-тестування:
JSON.stringify({ a: undefined, b: null }); // '{"b":null}'
Поле зі значенням undefined при серіалізації зникає взагалі, а null лишається як null. Тому «поля немає в JSON» і «поле дорівнює null» — це два різні контракти, і перевіряти їх треба по-різному: у першому випадку "a" in body === false, у другому body.a === null.
Ще одна корисна асиметрія: typeof undefined === "undefined", але typeof null === "object". І при нестрогому порівнянні null == undefined дає true (вони «рівні» одне одному й більше нічому), а при строгому null === undefined — false, бо це різні типи.
== проти === і неявне приведення
Це серце теми й найчастіше питання співбесіди. У JavaScript два оператори рівності.
=== (строга рівність, strict equality) порівнює без приведення типів: якщо типи різні — одразу false, жодної магії.
== (нестрога рівність, loose equality) спершу намагається привести операнди до спільного типу за набором історичних правил, і аж потім порівнює. Саме це приведення (coercion) і породжує результати, які виглядають абсурдно:
| Вираз | Результат | Чому |
|---|---|---|
"200" == 200 | true | рядок приводиться до числа |
0 == "" | true | обидва приводяться до числа 0 |
0 == "0" | true | "0" → число 0 |
"" == "0" | false | обидва рядки, приведення немає, різні рядки |
null == undefined | true | спецправило: рівні лише одне одному |
null == 0 | false | null не приводиться до числа тут |
false == 0 | true | false → 0 |
[] == 0 | true | [] → "" → 0 |
NaN == NaN | false | NaN не рівний нічому, навіть собі |
Зверніть увагу на «ланцюжкову» непослідовність: "" == "0" дає false, хоча 0 == "" і 0 == "0" обидва true. Причина в тому, що приведення залежить від типів операндів, а не від «здорового глузду». Тримати всю таблицю правил у голові не треба — треба знати висновок.
Висновок для тестового коду: завжди ===. == у продакшн-тесті — це запрошення до фальшиво-зелених результатів. Класичний приклад: сервер повернув статус як рядок "404", а тест написав expect(status == 404) — приведення зробить це true, і баг серіалізації сховається. Зі === тест чесно впаде. У TypeScript строгий лінтер (eslint з правилом eqeqeq) забороняє == саме з цієї причини; єдиний виправданий виняток — навмисна перевірка value == null, яка одним порівнянням ловить і null, і undefined.
truthy і falsy
Коли значення потрапляє в булевий контекст — умову if, оператор &&, потрійний ? : — JavaScript приводить його до true або false. Значення, які приводяться до false, називають хибними (falsy), решту — істинними (truthy).
Хибних значень вісім, їх варто вивчити напамʼять:
false, 0, -0, 0n (нуль типу BigInt), "" (порожній рядок), null, undefined, NaN.
(Формально є ще девʼятий — застарілий браузерний document.all, — але в коді тестів він не трапляється; для співбесіди досить цих восьми.)
Усе інше — істинне. І тут ховається найпідступніше для тестів: істинними є "0" (рядок з нулем), "false" (рядок), [] (порожній масив) і {} (порожній обʼєкт). Тобто порожній масив у логічній перевірці — це true, хоча інтуїція підказує протилежне.
Головна пастка AQA — перевірка наявності через істинність:
// Погано: 0 і "" — легітимні значення, але вони falsy
if (response.body.count) { /* ... */ }
// Добре: явно питаємо про відсутність
if (response.body.count !== undefined) { /* ... */ }
Якщо поле count законно може дорівнювати 0 (нуль замовлень — теж валідна відповідь), перша перевірка помилково вважатиме його «відсутнім». Те саме з порожнім рядком "" як валідним значенням поля. Для акуратного «взяти значення, якщо воно є» у сучасному JS/TS використовують оператор ?? (nullish coalescing), який реагує лише на null і undefined, а не на всі хибні значення, — його разом із опціональним ланцюжком ?. розбирає глава «Обʼєкти, масиви та методи роботи з даними».
NaN і Number.isNaN
NaN (Not-a-Number) — це спеціальне значення типу number, яке позначає «результат числової операції, що не є числом»: Number("abc"), parseInt("xyz"), 0/0. Так, попри назву, typeof NaN === "number".
NaN має унікальну властивість, якої немає в жодного іншого значення мови: він не дорівнює сам собі.
NaN === NaN // false
NaN == NaN // false
Тому перевірити «чи це NaN» звичайним порівнянням неможливо. Для цього є функції — і між ними теж пастка:
isNaN(value)(глобальна) спершу приводить аргумент до числа, а потім перевіряє. Через це вона бреше:isNaN("abc")повертаєtrue, бо"abc"приводиться доNaN, хоча сам рядок — неNaN.Number.isNaN(value)(з ES2015) не приводить аргумент: повертаєtrue, лише якщо значення справді єNaN.Number.isNaN("abc")—false.
isNaN("abc") // true ← оманливо
Number.isNaN("abc") // false ← правильно
Number.isNaN(NaN) // true
Правило: у тестах завжди Number.isNaN. Де це спливає на практиці: тест дістає число з тексту на сторінці (Number(await locator.textContent())), і якщо в елементі був не той текст, отримаєте NaN. Перевірка Number.isNaN(parsed) перед асертом дає зрозумілу помилку замість загадкового провалу далі по коду.
Передача за значенням проти передачі за посиланням
Це друге найчастіше питання після == проти ===, і саме воно стоїть за цілим класом «плаваючих» падінь у тестах.
Правило прямо випливає з поділу на примітиви й обʼєкти:
- Примітиви передаються за значенням. Функція отримує копію; змінити її всередині — не зачепить оригінал.
- Обʼєкти передаються за посиланням. Точніше — за значенням копіюється саме посилання, тож і викликач, і функція дивляться на той самий обʼєкт у купі. Мутація всередині функції змінює оригінал.
function tweak(num: number, obj: { role: string }) {
num = 999; // локальна копія — оригінал не зміниться
obj.role = "guest"; // той самий обʼєкт — зміна видима зовні
}
let n = 1;
const u = { role: "admin" };
tweak(n, u);
// n === 1 ← примітив недоторканий
// u.role === "guest" ← обʼєкт змінено
Той самий механізм працює й при простому присвоєнні: const b = a для обʼєкта копіює посилання, а не обʼєкт. Після цього b і a — два імені одного обʼєкта, і b.x = 5 змінює й a.
Чому це болить у тестах. Уявіть спільний фікстурний обʼєкт із даними користувача, який один тест мутує «під себе»:
const baseUser = { email: "qa@example.com", role: "admin" };
// тест 1 псує спільний обʼєкт
baseUser.role = "guest";
// тест 2 очікує admin — і падає, хоча його код правильний
Симптом класичний: тест падає залежно від порядку запуску або «через раз» при паралельному прогоні — територія флакі-тестів (їхню таксономію розбирає розділ про стратегію автоматизації). Лікування — не мутувати спільні дані, а робити копію. Для плоских обʼєктів вистачає поверхневої копії через spread ({ ...baseUser }), для вкладених потрібна глибока (structuredClone) — про різницю між ними розповідає глава «Обʼєкти, масиви та методи роботи з даними».
Типові помилки
-
Виглядає як «поля немає», а насправді поле є зі значенням
0.if (body.count)вважає нуль хибним і пропускає його. Перевіряйте наявність через!== undefinedабо"count" in body, а не через істинність. -
Виглядає як «тест правильно порівняв статус», а насправді
==приховав різницю типів.status == 200дастьtrueі для числа200, і для рядка"200". Завжди===— щоб баг серіалізації не сховався за приведенням. -
Виглядає як «const робить дані незмінними», а насправді захищений лише звʼязок.
const config = {}не заважаєconfig.timeout = 5000. Незмінність вмісту — цеObject.freezeабо дисципліна, неconst. -
Виглядає як «скопіював обʼєкт із тестовими даними», а насправді скопіював посилання.
const b = aдля обʼєкта дає друге імʼя того самого обʼєкта. Мутаціяbпсуєa— і сусідній тест падає без видимої причини. -
Виглядає як «перевірив на NaN», а насправді
isNaNзбрехала. ГлобальнаisNaNприводить аргумент, тожisNaN("2 items")—true. Для чесної перевірки —Number.isNaN. -
Виглядає як «звернувся до типу через typeof», а насправді
typeof null === "object"іtypeof [] === "object". Наnullперевіряйте через=== null, на масив — черезArray.isArray.
Підсумок
- Примітиви (
string,number,bigint,boolean,undefined,symbol,null) незмінні й передаються за значенням; обʼєкти й масиви — за посиланням, тож мутація видима всім, хто тримає це посилання. let/constмають блокову область і TDZ;constфіксує звʼязок, а не вміст обʼєкта;varу новому коді не використовуємо.- У тестах завжди
===:==приховано приводить типи й дає фальшиво-зелені результати; єдиний прагматичний виняток —x == nullдля одночасної перевірки наnullіundefined. - Хибних значень вісім (
false,0,-0,0n,"",null,undefined,NaN);"0",[]і{}— істинні, тому перевірку наявності черезif (value)треба замінити на явну. NaNне дорівнює собі; перевіряти його лише черезNumber.isNaN.typeofбреше наnull("object") і не відрізняє масив від обʼєкта.
Що питають на співбесіді
- «У чому різниця між
==і===? Наведіть приклад, де це небезпечно.» Інтервʼюер перевіряє, чи розумієте ви приведення типів, а не просто завчили «використовуй три знаки рівності». Сильна відповідь називає механізм (нестрога рівність приводить типи) і показує тестовий сценарій, де==ховає баг. - «Що поверне
typeof nullі чому?» Класика на знання підводних каменів мови. Очікують"object"і згадку, що це історична вада, а надійна перевірка —value === null. - «Перелічіть хибні значення.» / «Чи істинний порожній масив у
if?» Дивляться, чи не сплутаєте ви[](істинний) з інтуїтивним «порожній = хибний», і чи знаєте пастку з0як валідним значенням поля. - «Різниця між
nullіundefined?» Хочуть почути семантику (не задано системою проти явно порожнього) і практичний наслідок — поведінкуJSON.stringifyі різні перевірки контракту API. - «Що станеться, якщо передати обʼєкт у функцію і змінити його поле?» Питання на передачу за посиланням. Слабкий кандидат каже «нічого, це копія»; сильний пояснює call by sharing і одразу згадує ризик мутації спільних тестових даних.
- «Чим
isNaNвідрізняється відNumber.isNaN?» Перевірка на глибину: приведення аргументу глобальноюisNaNпроти чесної перевіркиNumber.isNaN.
На такі питання відповідайте прикладом із тестового коду, а не абстракцією: «== небезпечний, бо status == 200 пройде для рядка "200" і сховає баг серіалізації» звучить переконливіше за переказ специфікації.
Джерела
- MDN — JavaScript data types and data structures — канонічний опис примітивів, обʼєктів і моделі памʼяті.
- MDN — Equality comparisons and sameness — таблиці
==проти===і правила приведення. - MDN — Falsy та Truthy — повний перелік хибних значень.
- ECMAScript Language Specification (ECMA-262) — першоджерело:
typeof, абстрактні операції рівності,Number.isNaN. - TypeScript Handbook — Everyday Types — як ці типи виглядають під статичною перевіркою TypeScript.
Чим відрізняються var, let і const?
Відмінність поведінкова, а не стильова. var має функціональну область видимості (function scope) і «спливає» на початок функції через hoisting, тож звернення до неї до рядка оголошення повертає undefined замість помилки. let і const живуть лише в межах свого блоку { } і кидають ReferenceError, якщо їх торкнутися до оголошення. Ще одна різниця: const не дозволяє переприсвоїти змінну повторно, let дозволяє, var теж дозволяє. Практичне правило для тестового коду просте: типово const, let — тільки коли значення справді доведеться замінити, var — не використовуємо взагалі.
Що таке hoisting і temporal dead zone?
Hoisting — це те, що оголошення змінних і функцій обробляються рушієм ще до виконання коду, ніби «підняті» вгору області. Для var це означає, що змінна існує з самого початку функції зі значенням undefined, тому звернення до неї до присвоєння не падає, а мовчки дає undefined. Для let і const оголошення теж «підняте», але доступ до змінної заблокований від початку блоку до рядка оголошення — цей проміжок і називають мертвою зоною (temporal dead zone, TDZ). Спроба прочитати змінну в TDZ кидає ReferenceError. Це навмисна страховка: код падає одразу й голосно, а не працює далі з випадковим undefined.
const робить обʼєкт незмінним?
Ні, і це найпоширеніше непорозуміння. const захищає лише звʼязок імені зі значенням (binding) — не дозволяє присвоїти змінній інше значення. Але якщо значення — обʼєкт, його внутрішній вміст мутувати можна: const config = {} не заважає написати config.timeout = 5000. Заборонене лише повне переприсвоєння на кшталт config = {}, яке кине TypeError. Якщо треба заморозити саме вміст, це вже інші інструменти — Object.freeze або дисципліна команди, а не ключове слово const.
Які примітивні типи є в JavaScript і чим примітив відрізняється від обʼєкта?
Примітивів сім: string, number, bigint, boolean, undefined, symbol і null. Ключова властивість примітива — незмінність (immutability): будь-яка операція над рядком чи числом повертає нову величину, а не редагує стару на місці. Примітиви зберігаються прямо в змінній і копіюються за значенням. Усе інше — обʼєкти: літерали {}, масиви, функції, Date, Map, Set. Обʼєкт лежить у купі (heap), а змінна тримає лише посилання (reference) на нього, тому обʼєкти копіюються за посиланням. Ця межа визначає геть усе подальше: як значення копіюються при присвоєнні й передачі в функцію та як порівнюються між собою.
Чому два обʼєкти з однаковим вмістом не рівні, а два однакові числа — рівні?
Бо порівняння обʼєктів іде за посиланням, а не за вмістом. Числа й інші примітиви зберігаються в змінній безпосередньо, тож 42 і його копія — це два незалежні значення, і === бачить їх рівними. Обʼєкт же представлений посиланням на місце в купі; { a: 1 } === { a: 1 } дає false, бо це два різні місця в памʼяті, хоч поля й збігаються. Для QA це щоденний факт: асерт на рівність обʼєктів через === перевіряє тотожність, а не однаковість, тому для порівняння структур беруть глибоке порівняння (toEqual у більшості фреймворків), а не toBe.
Що поверне typeof null і чому саме так?
typeof null повертає рядок "object" — це давня вада самого JavaScript, закладена в першій версії й залишена назавжди заради зворотної сумісності. null не є обʼєктом, просто оператор typeof на ньому «бреше». Через це перевіряти значення на null через typeof не можна: правильний спосіб — пряме порівняння value === null. Це класичне питання співбесіди на знання підводних каменів мови, тому варто мати напоготові і сам факт, і причину, і надійну альтернативу.
Як надійно визначити, що значення — масив?
Через typeof — ніяк, бо масив технічно є обʼєктом, і typeof [1, 2, 3] дає те саме "object", що й звичайний літерал. Для цього існує окремий метод Array.isArray(value), який повертає true тільки для масивів. На практиці для QA це перша лінія оборони, коли з API приходить поле невідомої форми: перш ніж викликати на ньому методи масиву чи звертатися до властивостей обʼєкта, варто перевірити фактичну форму через Array.isArray і typeof, інакше легко впертися в TypeError на несподіваному типі.
Чим null відрізняється від undefined?
Обидва означають «значення немає», але семантика протилежна за походженням. undefined — це «система не задала значення»: змінну оголошено без присвоєння, властивості в обʼєкті немає, функція нічого не повернула, аргумент не передали. null — це «розробник свідомо поставив порожнє»: хтось навмисно написав null, щоб позначити відсутність. При нестрогому порівнянні null == undefined дає true (вони рівні лише одне одному), а при строгому null === undefined — false, бо це різні типи. Для QA різниця не теоретична: «поля немає» і «поле дорівнює null» — два різні контракти API, які й перевіряти треба по-різному.
Як поводяться null і undefined при JSON.stringify?
По-різному, і це щоденно спливає в API-тестуванні. Поле зі значенням undefined при серіалізації зникає з JSON повністю, а поле з null лишається у вигляді null: JSON.stringify({ a: undefined, b: null }) дає рядок, де є тільки b. Практичний наслідок: щоб перевірити відсутність поля, питають "a" in body === false, а щоб перевірити явний null — питають body.a === null. Плутати ці дві перевірки означає писати тест, який пропускає реальну розбіжність контракту.
У чому різниця між == і ===?
=== (строга рівність, strict equality) порівнює без жодного приведення: якщо типи операндів різні — результат одразу false. == (нестрога рівність, loose equality) спершу приводить операнди до спільного типу за набором історичних правил, і лише потім порівнює. Саме це приховане приведення (coercion) породжує результати, які виглядають абсурдно: "200" == 200 дає true, 0 == "" дає true, false == 0 дає true. Висновок для тестового коду однозначний: завжди ===. Єдиний прагматичний виняток — навмисна перевірка value == null, що одним порівнянням ловить і null, і undefined.
Наведіть приклад, де == небезпечний саме в автотесті.
Класика — прихований баг серіалізації. Уявіть, що сервер через помилку повертає статус як рядок "404" замість числа 404. Тест написаний як expect(status == 404) — оператор == приведе рядок до числа, порівняння дасть true, і тест буде зелений, хоча контракт зламано. Зі === типи різні, порівняння дає false, і тест чесно червоніє, вказуючи на реальну проблему. Тому == у продакшн-тесті — це запрошення до фальшиво-зелених результатів; недарма лінтер із правилом eqeqeq забороняє його за замовчуванням.
Перелічіть хибні (falsy) значення в JavaScript.
Їх вісім: false, 0, -0, 0n (нуль типу BigInt), "" (порожній рядок), null, undefined і NaN. Усе, чого немає в цьому списку, у булевому контексті приводиться до true. Найпідступніше для тестів те, що істинними є "0" (рядок з нулем), "false" (теж рядок), [] (порожній масив) і {} (порожній обʼєкт) — інтуїція часто підказує протилежне. Тому питання «чи істинний порожній масив» на співбесіді перевіряє, чи не сплутає кандидат [] з «порожній = хибний».
Чому перевірка if (response.body.count) небезпечна?
Бо вона перевіряє істинність значення, а не його наявність, і легітимний нуль тихо провалюється. Якщо поле count законно може дорівнювати 0 (нуль замовлень — валідна відповідь), то if (response.body.count) вважатиме його відсутнім, бо 0 — хибне значення. Те саме станеться з порожнім рядком "" як валідним значенням поля. Правильно питати про наявність явно: if (response.body.count !== undefined) або "count" in response.body. А для «взяти значення, якщо воно задане» беруть оператор ?? (nullish coalescing), який реагує тільки на null і undefined, а не на всі хибні значення.
Чому NaN === NaN дає false і як тоді перевіряти на NaN?
NaN (Not-a-Number) — єдине значення мови, яке не дорівнює саме собі: і NaN === NaN, і NaN == NaN дають false. Це закладено в стандарті чисел з плаваючою комою, тож звичайним порівнянням NaN не зловити. Перевіряти треба через Number.isNaN(value), який повертає true, лише коли значення справді є NaN. Типовий для QA сценарій: тест дістає число з тексту сторінки через Number(await locator.textContent()), і якщо в елементі опинився не той текст, результат — NaN. Перевірка Number.isNaN(parsed) перед асертом дає зрозумілу помилку одразу, а не загадковий провал десь далі по коду.
Чим isNaN відрізняється від Number.isNaN?
Різниця в тому, чи функція приводить аргумент до числа. Глобальна isNaN спершу приводить аргумент, а вже потім перевіряє, тому вона «бреше»: isNaN("abc") повертає true, бо рядок "abc" при приведенні перетворюється на NaN, хоча сам по собі він — не NaN. Number.isNaN (з ES2015) нічого не приводить і повертає true, тільки якщо переданий аргумент уже є NaN — Number.isNaN("abc") дає false. Правило для тестів: завжди Number.isNaN, бо глобальна версія дасть хибно-позитивний результат на будь-якому нечисловому вводі.
Що станеться, якщо передати обʼєкт у функцію і змінити його поле?
Зміна буде видима зовні, бо обʼєкти передаються за посиланням. Точніше — копіюється саме посилання, тож і викликач, і функція дивляться на той самий обʼєкт у купі; мутація поля всередині функції змінює оригінал. Примітив натомість передається за значенням: функція отримує копію, і присвоєння всередині оригіналу не торкнеться. Цю модель називають call by sharing. Слабкий кандидат каже «нічого, це ж копія»; сильний одразу згадує ризик випадково зіпсувати спільні тестові дані через таку мутацію.
Чому спільний фікстурний обʼєкт спричиняє «плаваючі» падіння тестів?
Бо один тест може мутувати спільний обʼєкт, а наслідки прилетять іншому. Уявіть базовий обʼєкт користувача { role: "admin" }, який тест 1 «підправляє під себе» на role: "guest"; тест 2 очікує admin і падає, хоча його власний код правильний. Симптом упізнаваний: тест червоніє залежно від порядку запуску або «через раз» при паралельному прогоні — це територія флакі-тестів. Причина в тому, що присвоєння const b = a для обʼєкта копіює лише посилання, тож a і b стають двома іменами одного обʼєкта. Лікування — не мутувати спільні дані, а працювати з копією.
Як безпечно скопіювати обʼєкт із тестовими даними?
Залежно від того, чи він вкладений. Для плоского обʼєкта вистачає поверхневої копії через spread — { ...baseUser } створює новий обʼєкт із тими самими значеннями полів. Але spread копіює лише верхній рівень: якщо всередині є вкладені обʼєкти чи масиви, вони й далі спільні за посиланням, і мутація вкладеного поля зачепить оригінал. Для таких випадків потрібна глибока копія — structuredClone(baseUser) створює повністю незалежну структуру. Правило для фікстур: якщо дані містять вкладеність, поверхнева копія оманлива, бери глибоку.
Три кейси, де тип значення вирішує, чесний тест чи фальшиво-зелений: як == ховає баг серіалізації статусу, як хибний нуль провалює перевірку наявності поля, і як мутація спільної фікстури робить сюїт залежним від порядку запуску. Скрізь — що саме подивитися і чому.
Кейс 1. == маскує баг серіалізації статусу
Бекенд через регресію почав повертати HTTP-статус як рядок "200" замість числа 200. Функціонально відповідь та сама, але контракт зламано — і саме такі речі має ловити тест. Подивимось, як вибір оператора рівності вирішує, помітимо ми регресію чи ні.
// поле, яке насправді прийшло від зламаного бекенду
const status = "200"; // рядок, а мав бути number
status == 200; // true ← приведення "200" → 200, баг сховано
status === 200; // false ← типи різні, баг видно
У тесті це виглядає так:
import { test, expect } from '@playwright/test';
test('статус створення ресурсу', async ({ request }) => {
const res = await request.post('https://api.example.com/orders', {
data: { item: 'sku-1', qty: 1 },
});
const status = res.status(); // Playwright віддає number — це добре
// але якщо код дістає статус із тіла відповіді, тип уже не гарантований
const reported = (await res.json()).status; // тут може бути "201"
// Погано: приховане приведення зробить зелено навіть на рядку
// expect(reported == 201).toBeTruthy();
// Добре: строге порівняння впаде на розбіжності типів
expect(reported).toBe(201);
});
Що дивитися і чому:
==приводить операнди до спільного типу перед порівнянням."200" == 200спершу перетворює рядок на число, тож розбіжність типів зникає ще до порівняння — і баг серіалізації лишається непоміченим.===не приводить нічого. Різні типи — одразуfalse, тест чесно червоніє й показує реальну проблему контракту.toBeу Playwright/Jest — це===під капотом. Тому асертexpect(reported).toBe(201)автоматично строгий; писати ручні порівняння через==немає причини.- Увімкніть
eqeqeqв ESLint. Правило забороняє==/!=на рівні лінтера, тож фальшиво-зелений асерт не доживе навіть до код-рев'ю.
Кейс 2. Хибний нуль провалює перевірку наявності поля
API повертає лічильник замовлень користувача. Нуль замовлень — цілком валідна відповідь, але наївна перевірка «чи є поле» через істинність тихо його відкидає, бо 0 — хибне значення.
// відповідь для нового користувача — нуль замовлень, це легітимно
const body = { userId: 7, count: 0 };
// Погано: 0 — falsy, тож перевірка вважає поле відсутнім
if (body.count) {
// сюди не зайдемо навіть при валідному нулі
}
// Добре: питаємо саме про наявність, а не про істинність
if (body.count !== undefined) {
// спрацює і для 0, і для будь-якого числа
}
У тесті пастка виглядає так:
import { test, expect } from '@playwright/test';
test('поле count присутнє навіть при нулі замовлень', async ({ request }) => {
const res = await request.get('https://api.example.com/users/7/stats');
const body = await res.json();
// Погано: || 'немає' підмінить легітимний 0 на рядок
// const shown = body.count || 'немає';
// Добре: ?? реагує лише на null/undefined, нуль лишається нулем
const shown = body.count ?? 'немає';
expect('count' in body).toBe(true); // поле в контракті є
expect(typeof body.count).toBe('number'); // і саме число
expect(shown).toBe(0); // нуль не підмінився
});
Що дивитися і чому:
0,""іNaN— хибні, але валідні дані. Перевірка черезif (value)чиvalue || fallbackвідкидає їх разом зі справжньою відсутністю, тож нуль замовлень або порожнє імʼя мовчки губляться.- Наявність поля перевіряють явно.
"count" in bodyабоbody.count !== undefinedвідповідають на питання «поле є?», не плутаючи його з питанням «поле істинне?». ??замість||для дефолтів. Оператор??(nullish coalescing) підставляє запасне значення лише наnull/undefined, а||— на будь-якому хибному, тому й підмінює легітимний0.
Кейс 3. Мутація спільної фікстури = флак, залежний від порядку
Класична причина «зелено поодинці, червоно в наборі»: тести ділять один обʼєкт із даними, а присвоєння його не копіює — воно копіює посилання. Один тест мутує обʼєкт «під себе», сусідній падає.
// спільна фікстура на весь файл
const baseUser = { email: 'qa@example.com', role: 'admin', profile: { theme: 'dark' } };
// тест 1: думає, що взяв копію, а взяв друге імʼя того самого обʼєкта
const u1 = baseUser;
u1.role = 'guest'; // зіпсував baseUser для всіх наступних тестів
// тест 2: очікує admin і падає, хоча власний код правильний
baseUser.role === 'admin'; // false — прилетіло від тесту 1
Симптом — падіння залежить від порядку запуску або зникає/зʼявляється при паралельному прогоні. Лікування — працювати з копією, і тут важливо не сплутати поверхневу з глибокою:
import { test, expect } from '@playwright/test';
const baseUser = { email: 'qa@example.com', role: 'admin', profile: { theme: 'dark' } };
test('поверхнева копія рятує плоскі поля', async () => {
const user = { ...baseUser }; // spread — новий обʼєкт верхнього рівня
user.role = 'guest'; // baseUser.role лишається 'admin'
expect(baseUser.role).toBe('admin');
});
test('але вкладене поле в spread усе ще спільне', async () => {
const user = { ...baseUser };
user.profile.theme = 'light'; // profile — те саме посилання!
// baseUser.profile.theme теж став 'light' — оце і є пастка
expect(baseUser.profile.theme).toBe('light'); // на жаль, true
// Правильно для вкладених структур — глибока копія:
const safe = structuredClone(baseUser);
safe.profile.theme = 'dark';
expect(baseUser.profile.theme).not.toBe('dark'); // тепер незалежні
});
Що дивитися і чому:
- Присвоєння обʼєкта не копіює обʼєкт.
const u1 = baseUserдає друге імʼя тієї самої структури; будь-яка мутація черезu1видима черезbaseUser. - Spread копіює лише верхній рівень.
{ ...baseUser }захищає плоскі поля, але вкладенийprofileлишається спільним за посиланням — типове джерело «непояснюваного» флаку. structuredCloneдає повну ізоляцію. Для фікстур із вкладеністю це надійний вибір; поверхневої копії досить лише для гарантовано плоских даних.- Найкраще — фабрика фікстур. Функція, що повертає свіжий обʼєкт на кожен виклик (
makeUser()), узагалі знімає питання спільного стану, бо тести не ділять один екземпляр.
Оголошення змінних
- Можу пояснити різницю області видимості:
var— функціональна,let/const— блокова, до{ }включно. - Розумію hoisting:
varдо оголошення даєundefined, аlet/constкидаютьReferenceErrorу мертвій зоні (TDZ). - Знаю, що
constфіксує лише звʼязок імені, а не вміст обʼєкта —const obj = {}дозволяєobj.x = 1, але неobj = {}.
Примітиви й обʼєкти
- Можу перелічити сім примітивів:
string,number,bigint,boolean,undefined,symbol,null. - Розумію, що примітиви незмінні й копіюються за значенням, а обʼєкти лежать у купі й копіюються за посиланням.
typeof і перевірка типів
- Памʼятаю вісім можливих результатів
typeofі три «сюрпризи»:null, масив і функція. - Можу пояснити, чому
typeof null === "object"— історична вада мови, і що надійна перевірка наnullцеvalue === null. - Знаю, що масив від обʼєкта
typeofне відрізняє, тож перевіряю черезArray.isArray(value).
null проти undefined
- Розрізняю семантику:
undefined— не задано системою,null— явно порожньо за рішенням розробника. - Можу пояснити поведінку
JSON.stringify:undefined-поле зникає з JSON,nullлишається.
Рівність і приведення типів
- Можу пояснити різницю
==vs===: нестрога рівність приводить типи, строга — ні; прикладstatus == 200проходить і для рядка"200". - Тримаю висновок: у тестах завжди
===; єдиний виправданий виняток —value == null.
truthy і falsy
- Можу перелічити вісім хибних значень:
false,0,-0,0n,"",null,undefined,NaN. - Знаю пастку:
"0","false",[]і{}— істинні, хоч інтуїція підказує протилежне. - Розумію, чому
if (body.count)небезпечна, коли0— валідне значення, і замінюю її на!== undefinedабоin.
NaN
- Можу пояснити, чому
NaN !== NaN, і що перевіряти наNaNтреба лише черезNumber.isNaN. - Знаю різницю
isNaNvsNumber.isNaN: глобальна приводить аргумент і дає хибнийtrueна"abc", аNumber.isNaN— ні.
Передача за значенням і за посиланням
- Можу пояснити call by sharing: примітив у функцію йде копією, обʼєкт — посиланням, тож його мутація видима зовні.
- Розумію, чому
const b = aдля обʼєкта дає друге імʼя того самого обʼєкта, а не незалежну копію. - Знаю різницю копій: spread
{ ...obj }— поверхнева (плоскі дані),structuredClone— глибока (вкладені структури).
Що поверне `typeof null`?
Питання
var, let і const — головна різниця в області видимості?