vyvchy
    Теми розділу

    08 · JavaScript/TypeScript для AQA

    Змінні, типи даних і приведення типів

    Зміст

    Автотест — це код, а код працює з даними: тіло відповіді API, текст із , число з бази, із конфігу. І майже кожен фальшивий результат, який доводилося ловити руками, зводиться до типів. Тест порівнює рядок "200" зі статусом 200 через == — і зелений там, де мав би бути червоний. Перевірка if (response.body.count) мовчки пропускає легітимне значення 0, бо нуль — «хибний». Один тест мутує спільний обʼєкт із , а падає інший — бо обʼєкти передаються за посиланням. Це не екзотика, це щоденний фон роботи AQA.

    Тому ця глава — не про синтаксис заради синтаксису. Це про модель, за якою JavaScript зберігає, копіює й порівнює значення. Хто її тримає в голові, той відрізняє баг застосунку від бага у власному тесті за секунди, а не за годину.

    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, . Обʼєкт зберігається в купі (), а змінна тримає лише посилання (reference) на нього. Тому обʼєкти передаються й копіюються за посиланням — про наслідки цього нижче окремий розділ.

    Обʼєкти (купа)

    Змінні (стек)

    count = 42

    name = 'test'

    user →

    role: admin
    id: 7

    Обʼєкти (купа)

    Змінні (стек)

    count = 42

    name = 'test'

    user →

    role: admin
    id: 7

    Примітивне значення лежить у змінній безпосередньо; змінна-обʼєкт зберігає лише стрілку на дані в купі. Звідси все інше: два різні обʼєкти з однаковим вмістом не рівні, бо стрілки вказують у різні місця, а 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 === undefinedfalse, бо це різні типи.

    == проти === і неявне приведення

    Це серце теми. У JavaScript два оператори рівності.

    === (, strict equality) порівнює без : якщо типи різні — одразу false, жодної магії.

    == (нестрога рівність, loose equality) спершу намагається привести операнди до спільного типу за набором історичних правил, і аж потім порівнює. Саме це приведення (coercion) і породжує результати, які виглядають абсурдно:

    ВиразРезультатЧому
    "200" == 200trueрядок приводиться до числа
    0 == ""trueобидва приводяться до числа 0
    0 == "0"true"0" → число 0
    "" == "0"falseобидва рядки, приведення немає, різні рядки
    null == undefinedtrueспецправило: рівні лише одне одному
    null == 0falsenull не приводиться до числа тут
    false == 0truefalse0
    [] == 0true[]""0
    NaN == NaNfalseNaN не рівний нічому, навіть собі

    Зверніть увагу на «ланцюжкову» непослідовність: "" == "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" і сховає баг серіалізації» звучить переконливіше за переказ специфікації.

    Джерела

    var, let, const і блокова область

    • MDN — Hoisting (glossary) — чотири різні поведінки під словом «підняття» і тимчасова мертва зона let/const.
    • MDN — Closures — поява блокової області разом із let/const; до ES6 областей було лише дві.

    Примітиви проти обʼєктів

    typeof і його сюрпризи

    null проти undefined

    == проти === і неявне приведення

    truthy і falsy

    NaN і Number.isNaN

    • MDN — NumberNumber.isNaN перевіряє значення без приведення — на відміну від глобальної isNaN.
    • MDN — JavaScript data types and data structures — примітиви проти обʼєктів, null проти undefined і набір правил приведення типів.
    • MDN — Equality comparisons and sameness — чотири алгоритми рівності, правила приведення в == і те, чому NaN не дорівнює сам собі.

    Передача за значенням проти передачі за посиланням

    • MDN — Working with objects (JavaScript Guide) — обʼєкт як посилальний тип: два різні обʼєкти ніколи не рівні, true дає лише порівняння посилання із самим собою.
    • MDN — JavaScript data types and data structures — примітиви проти обʼєктів, null проти undefined і набір правил приведення типів.
    • MDN — Array — усі вбудовані операції копіювання поверхневі — і spread, і Object.assign.
    • MDN — Window: structuredClone() methodstructuredClone() як штатна глибока копія, що переживає циклічні посилання.

    Типові помилки

    • MDN — Falsy (glossary) — повний перелік хибних значень, включно з єдиним хибним обʼєктом document.all.
    • MDN — Equality comparisons and sameness — чотири алгоритми рівності, правила приведення в == і те, чому NaN не дорівнює сам собі.
    • MDN — Working with objects (JavaScript Guide) — обʼєкт як посилальний тип: два різні обʼєкти ніколи не рівні, true дає лише порівняння посилання із самим собою.
    • MDN — NumberNumber.isNaN перевіряє значення без приведення — на відміну від глобальної isNaN.
    • MDN — JavaScript data types and data structures — примітиви проти обʼєктів, null проти undefined і набір правил приведення типів.

    Підсумок

    • MDN — JavaScript data types and data structures — примітиви проти обʼєктів, null проти undefined і набір правил приведення типів.
    • MDN — Hoisting (glossary) — чотири різні поведінки під словом «підняття» і тимчасова мертва зона let/const.
    • MDN — Equality comparisons and sameness — чотири алгоритми рівності, правила приведення в == і те, чому NaN не дорівнює сам собі.
    • MDN — Falsy (glossary) — повний перелік хибних значень, включно з єдиним хибним обʼєктом document.all.
    • MDN — NumberNumber.isNaN перевіряє значення без приведення — на відміну від глобальної isNaN.
    • MDN — Working with objects (JavaScript Guide) — обʼєкт як посилальний тип: два різні обʼєкти ніколи не рівні, true дає лише порівняння посилання із самим собою.

    Можливі питання

    • MDN — Equality comparisons and sameness — чотири алгоритми рівності, правила приведення в == і те, чому NaN не дорівнює сам собі.
    • MDN — JavaScript data types and data structures — примітиви проти обʼєктів, null проти undefined і набір правил приведення типів.
    • MDN — Falsy (glossary) — повний перелік хибних значень, включно з єдиним хибним обʼєктом document.all.
    • MDN — Truthy (glossary) — істинне — усе, чого немає в переліку хибних: [], {}, "0", "false".
    • MDN — Working with objects (JavaScript Guide) — обʼєкт як посилальний тип: два різні обʼєкти ніколи не рівні, true дає лише порівняння посилання із самим собою.
    • MDN — NumberNumber.isNaN перевіряє значення без приведення — на відміну від глобальної isNaN.

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.