← Academy Blog

Марафон підготовки 2021

Стартував марафон підготовки до тестів - JS трек - Binary Studio Academy 2021! Щотижневі питання з відповідями та пояснення чекають на нашому блозі.

  • [JS] Оберіть варіант, у якому при зверненні до методу helper не виведеться помилка. (itemHandler.addItem(2)):

    a)

    const itemHandler = {
      addItem: function (arg) {
        let a = this.helper(arg);
      },
      helper: (prop) => {
        return prop;
      },
    };

    b)

    const itemHandler = {
      addItem: (arg) => {
        let a = this.helper(arg);
      },
      helper: (prop) => {
        return prop;
      },
    };

    c) Обидва правильні

    Розгорнути правильну відповідь з поясненням

    Чергове питання з рубрики "захоплюючий" javascript. У цьому питанні перевіряється, чи знаєш ти, що таке контекст функції і як працюють arrow functions. Це питання часто виникає при роботі з React, коли ти намагаєшся передати метод через властивості компонента, і не можеш зрозуміти, чому в методі компонента стан іншого компонента 🤷🏿 Але не хвилюйтеся, зараз ми з цим розберемося 😉

    Коли ми оголошуємо метод/функцію через function, то при її виклику буде використовуватися контекст (this), в якому вона викликається. Але варто нам записати функцію в інший об'єкт, this відразу зміниться. Щоб це зрозуміти, давайте проведемо експеримент:

    // оголошуємо функцію
    function f() {
      return this;
    }
    
    // записуємо ії в об’єкт
    const a = { f: f };
    
    // перезаписуємо ії з об’єкта
    const b = {};
    b.f = a.f;
    
    // викликаємо їі з методу об’єкта
    const c = {
      f: function () {
        return f();
      },
    };
    
    // перевіряємо
    console.log(f() === globalThis); // true
    console.log(a.f() === a); // true
    console.log(b.f() === b); // true
    console.log(c.f() === globalThis); // true

    У першому варіанті повернеться globalThis - це глобальний контекст (у браузері це window). Іншими словами, якщо функція викликається не як метод об'єкта, то this буде глобальний. Але варто нам записати її в якийсь об'єкт, то this зміниться. Крім того в JS у функції є 3 методи, які дозволяють змінити контекст насильно:

    1. f.apply(context, [...args]) - викличе функцію з контекстом context, значення аргументів функції будуть відповідати елементам масиву args
    2. f.call(context, ...args) - викличе функцію з контекстом context, аргументи (args) передаються послідовно
    3. f.bind(context, ...args) - змінить контекст і поверне нову функцію, без виклику

    Метод bind також дозволяє нам зафіксувати контекст, тобто якщо ми запишемо функцію в інший об'єкт, контекст не зміниться:

    const a = {};
    const d = {};
    d.f = f.bind(a);
    console.log(d.f() === a); // true

    Arrow function у свою чергу успадковує контекст з місця свого визначення. Причому, якщо ми визначимо arrow function у інший arrow function, то js буде підніматися вгору, поки не зустріне перший-ліпший контекст 🤯 Давайте подивимося, що буде якщо f визначити через arrow function:

    const f = () => this;
    
    const a = { f: f };
    
    const b = {};
    b.f = a.f;
    
    const c = {
      f: function () {
        return f();
      },
    };
    
    console.log(f() === globalThis); // true
    console.log(a.f() === globalThis); // true
    console.log(b.f() === globalThis); // true
    console.log(c.f() === globalThis); // true

    Тобто на відміну від function, контекст у arrow function не змінився, незалежно від того, де ми її викликали. Давайте розглянемо ще один приклад:

    const contextA = {
      f1: function () {
        const foo = () => this;
        return foo(); // викликаємо
      },
      f2: function () {
        const bar = () => this;
        return bar; // не викликаємо
      },
    };
    const contextB = {};
    contextB.f1 = contextA.f1; // (1)
    contextB.f2 = contextA.f2(); // (2)
    
    console.log(contextA.f1() === contextA); // true
    console.log(contextA.f2()() === contextA); // true
    console.log(contextB.f1() === contextB); // true
    console.log(contextB.f2() === contextA); // true

    Зверніть увагу, що коли ми поміняли контекст методу f1 з contextA на contextB (1), то він автоматично змінився для внутрішньої foo, тому що контекст визначення foo змінився.

    А в методі f2 ми визначили функцію bar в контексті contextA та привласнили її contextB (2), при цьому контекст не змінився, тому що місце визначення залишилося в contextA.

    Отже, щоб бути впевненим у тому, що контекст функції буде той, який ми очікуємо, незалежно від того, де ми будемо її викликати, ми можемо використовувати або bind(), або arrow function.

    Фуух, це було складно, але я думаю, тепер ви готові відповісти на питання 🙂 У варіанті b) ми визначаємо функцію в глобальному контексті, а не в контексті об'єкта, а оскільки в globalThis ми не визначили метод helper(), то буде помилка this.helper is not a function. Значить, варіант b) неправильний, відповідно і відповідь c) - теж. Залишається тільки a), ну а ми й без цього вже знаємо, що якщо метод визначений через function, то контекст буде той, де ми цей метод викличемо, в даному випадку на об'єкті itemHandler.

    Щоб краще підготуватися до вступного тесту, рекомендую ознайомитися з функціями за наступними посиланнями:

    https://javascript.info/function-expressions
    https://javascript.info/arrow-functions-basics
    https://javascript.info/javascript-specials
    https://javascript.info/advanced-functions

  • [JS] Чи можна замінити вираз 1 виразом 2?
    const someBooleanValue = isNaN(+someVariable);
    const someBooleanValue = +someVariable == NaN;

    a) Так, у будь-якому випадку
    b) Ні
    c) Так, якщо someVariable в обох виразах буде числом

    Розгорнути правильну відповідь з поясненням

    У цьому питанні приховані одразу два підводні камені JS: приведення типів та властивості NaN.

    Знак + перед змінною приведе значення до числового типу, тобто +someVariable еквівалентно Number(someVariable). Наприклад, якщо привести до числа наступний рядок 34, то ми отримаємо число:

    +'34' + 6; //  40

    Але якщо додати рядок до змінної, то незважаючи на значення, результатом буде рядок, тобто typeof ('' + someVariable) === 'string'. Наприклад:

    '' + '34' + 6; // “346”

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

    console.log(+true); // 1
    console.log(+'2'); // 2
    console.log(+2); // 2
    console.log(+null); // 0
    console.log(+{}); // NaN
    console.log(+[]); // 0
    console.log(+undefined); // NaN
    console.log(+'a'); // NaN

    Як бачите, у випадках, коли JS не може перетворити значення у число, ми отримуємо NaN (Not a Number). До речі, чи знаєте ви, чому пустий масив дорівнює 0? Пишіть свою відповідь у коментарях 😉

    Знаючи це, ми можемо відсіяти відповідь с) , бо someVariable не обов’язково зберігати число. Але щоб відповісти правильно, ми маємо знати головну властивість NaN: NaN не дорівнює жодному зі значень, включаючи NaN, тобто:

    console.log(NaN === NaN); // false

    Тобто, вираз 2 не буде коректним, якщо +someVariable поверне NaN.

    Для того, щоб перевірити, чи дорівнює змінна NaN, треба використовувати функцію isNaN(someVariable) або ж Number.isNaN(someVariable). Тому вираз 1 є єдиним правильним способом перевірити, чи значення є числом, і це означає, що вони не еквівалентні між собою. Тому правильна відповідь b) Ні.

    До речі, якщо в масиві одне з значень буде NaN, то indexOf(NaN) поверне -1, а от includes(NaN) поверне true:

    let arr = [2, 4, NaN, 12];
    arr.indexOf(NaN); // -1
    arr.includes(NaN); // true

    Більш детально з NaN можете ознайомитись за посиланням.