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

    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

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

    Джерела