← Academy Blog

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

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

  • [JS] Який з наведених варіантів є вірним рішенням наступної задачі?

    Є масив array, заповнений довільними числами. Необхідно наповнити масив newArray числами з array, які не рівні 0.

    let array = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55];
    
    let newArray = [];

    a)

    for (let i = 0; i < array.length; i++) {
      if (array[i]) {
        newArray.push(array[i]);
      }
    }

    b)

    array.forEach((index, element) => {
      if (element) {
        newArray.push(element);
      }
    });

    c)

    newArray = array.map((element) => {
      if (element) {
        return element;
      }
    });
    1. Жоден

    2. Всі


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

    Якщо вам це питання здається легким і ви вибрали будь який інший варіант окрім варіанту a) - рекомендую розібрати це питання разом з нами. Якщо ви вибрали правильний варіант - вітаю, ви молодець, але розбір все одно буде корисно глянути:)

    Отож, одразу почнемо з варіантів відповідей:

    а) В даному варіанті для перебору масиву використовується звичайний цикл for. Тобто для кожного з елементів масиву array виконується перевірка if (array[i]), яка являє собою неявне приведення числового типу до логічного. Згадаємо, що усі значення числового типу, окрім 0, при приведенні до логічного типу повернуть true. Тому всі ненульові елементи успішно пройдуть перевірку, і далі для кожного з них виконається newArray.push(array[i]). Метод масиву push додає один або більше елементів в кінець масиву - якраз те, що нам потрібно!

    Отже варіант а) є вірним. Але давайте розберемо, чому не підходять інші варіанти.

    b) Тут реалізація рішення схожа на варіант а), за винятком того, що для перебору елементів array замість циклу for використовується метод масиву forEach. Метод forEach приймає як параметр функцію, яка буде викликана для кожного елемента масиву один раз. Ця функція буде викликана з трьома параметрами: function callback(currentValue, index, array),

    де currentValue - значення поточного елемента масиву,
    index - індекс поточного елемента масиву,
    array - масив, для якого було викликано forEach.

    Ось тут і прихована помилка, яку уважні вже помітили! У варіанті b) за значення елементу масиву береться другий параметр функції, коли насправді ж другий параметр відповідає індексу елемента. В результаті ми отримаємо щось на кшталт:
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    Саме тому цей варіант не спрацює коректно і є невірним.

    с) В третьому варіанті використовується метод map, який як і forEach, приймає як параметр функцію-callback та викликає її один раз для кожного елемента масиву. При цьому він створює новий масив зі значень, які поверне ця функція-callback. Отже, якщо значення елементу array не дорівнює 0, то він пройде перевірку if (element) і функція поверне значення даного елемента, яке запишеться у масив newArray. Але якщо елемент дорівнює 0, то наш callback поверне undefined і масив newArray буде містити undefined окрім числових значень, а це не задовольняє умову:
    [undefined, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
    Тому варіант с) невірний.

    Зрозуміло, що останні два варіанти також невірні. Отже, правильна відповідь a).

  • [JS] Необхідно отримати масив зі значеннями об’єкта. Які методи неправильні?

    a)

    for (let key of someData) {
      someList.push(someData[key]);
    }

    b)

    someList = Object.keys(someData).map((key) => {
      return [someData[key]];
    });

    c)

    someList = Object.values(someData);

    d)

    for (let key in someData) {
      someList.push(someData[key]);
    }

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

    Давай по порядку розберемо кожну із функцій. Для зразку використовуватимемо об'єкт

    const someData = { a: 1, b: 2, c: 3 };

    Object.keys - повертає масив із назвами полів (ключами або властивостями) об'єкта. Тобто результатом Object.keys(someData) буде масив ['a', 'b', 'c']

    Object.values натомість повертає значення всіх полів. Результатом Object.values(someData) буде масив [1, 2, 3]

    for ... in - використовується для обходу в циклі усіх властивостей об'єкта. Для

    прикладу наведений нижче код виведе у консолі усі ключі об'єкта та їх значення

    for (const key of someData) {
      console.log(`${key} - ${someData[key]}`);
    }
    
    // у консолі
    // a - 1
    // b - 2
    // c - 3

    for ... of - використовується для обходу в циклі усіх елементів ітерабельних об'єктів (масивів, рядків, NodeList, Set, Map та ін.). З об'єктами дана конструкція дасть помилку. Приклад:

    const someArray = [1, 2, 3];
    
    for (const item of someArray) {
      console.log(item);
    }
    
    // у консолі
    // 1
    // 2
    // 3

    Тому відразу відкидаємо варіант, який використовує for ... of. Всі решта варіантів дійсно присвоюють someList масив. Проте поглянемо уважно на варіант b): return [someData[key]] - виявляється, тут зайва пара квадратних дужок. В результаті для нашого someData отримаємо двовимірний масив [[1], [2], [3]]. Отож неправильними є відповіді a) та b).

  • [JS] Який код потрібно використати, щоб в консоль були виведені результати асинхронної функції asyncFoo()?
    const asyncFoo = () => fetch(`https://google.com`);
    
    const foo = async () => {
      let data;
    
      /* код тут */
    
      console.log(data);
    };

    a) data = async asyncFoo();
    b) data = () => asyncFoo();
    c) data = asyncFoo();
    d) data = await asyncFoo();
    e) data = asyncFoo;


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

    Такі питання зазвичай складні для більшості студентів через використання асинхронних функцій. Асинхронні функції використовуються для операцій, які можуть виконуватися довгий час, і мережевий запит - чудовий приклад такої операції. Варто згадати, що JavaScript однопоточна мова програмування, і, щоб не зупиняти основний потік виконання, асинхронні функції виконуються окремо від решти через Event loop. Такі функції оголошуються з модифікатором async і неявно повертають Promise.

    Повернемося до нашого завдання. В нас є асинхронна функція foo, всередині якої нам потрібно отримати дані з функції asyncFoo. Як бачимо, asyncFoo повертає результат виконання методу fetch, який в свою чергу виконує мережевий запит за URL адресою, вказаною першим аргументом, і повертає Promise. Отже, після виконання, функція asyncFoo також поверне Promise.

    Тепер, якщо ми просто виконаємо функцію data = asyncFoo(), то в консоль буде виведено Promise в стані очікування - не те, що ми хотіли побачити. Нам потрібно дочекатися виконання Promise-у, щоб отримати дані із запиту. Для цього використовується оператор await, він призупиняє виконання асинхронної функції допоки Promise не буде виконаний або відхилений. Отже правильна відповідь d)

    Якщо ви переконані в тому що правильний інший варіант, розглянемо їх також:

    a) В цьому варіанті виникне помилка через неправильне використання ідентифікатора async.
    b) Такий варіант теж не вірний тому, що в змінній data буде міститися функція, результат котрої поверне Promise.
    c) Це не вірний варіант, бо в data буде Promise в стані очікування.
    d) Вірний варіант.
    e) Цей варіант невірний, адже в data присвоюється сама функція asyncFoo, а не результат її виконання

  • [JS] Які значення будуть в змінних після виконання коду?
    let a = [1, 3, 2, 4];
    let b = a.sort();

    a) a = [1, 3, 2, 4]; b = [1, 2, 3, 4];
    b) a = [1, 2, 3, 4]; b = [1, 2, 3, 4];
    c) a = [1, 2, 3, 4]; b = 4;
    d) a = [1, 2, 3, 4]; b = [1, 3, 2, 4];


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

    Для початку давайте розглянемо самий метод sort. Цей метод сортує елементи масиву та повертає відсортований масив. При цьому, метод може приймати один параметр - функцію для порівняння елементів масиву за якою і буде відбуватись сортування. Важливо зауважити, що метод саме змінює (мутує) вихідний масив, а не створює новий.

    Метод працює наступним чином: за замовчуванням елементи приводяться до рядка та порівнюються значення їх UTF-16 послідовності. Наприклад, при виконанні наступного коду [9, 80].sort() результатом буде масив [80, 9] . В числовій послідовності 9 менше за 80, але числа конвертуються в строки та порівнюється їх значення в Unicode послідовності в котрій 80 менше за 9. В нашому прикладі масив a містить всі значення як одноцифрові числа тому результат співпадає з відсортованим чисельним результатом.

    Розберемо наданий код

    1. let a = [1, 3, 2, 4]; ініціалізуємо змінну та присвоюємо їй масив.
    2. let b = a.sort(); ініціалізуєму змінну b та присвоюємо їй значення результату виконання сортування масиву a. Тобто масив a вже також містить відсортовані елементи.

    А тепер розглянемо варіанти відповідей

    1. а) - невірна відповідь тому, що метод sort змінює вихідний масив, а не створює новий, тобто змінна a повинна мати значення відсортованого масиву.
    2. b) - вірна, після виконання коду змінні a та b мають значення відсортованого масиву.
    3. c) - невірна тому, що змінна b повинна мати значення відсортованого масиву
    4. d) - теж невірна тому, що метод sort повертає відсортований масив, а не вихідний.

    Отже, правильна відповідь b)

  • [JS] Що буде виведено в консоль під час виконання функції?
    function () {
      const numbers = [1, 2, 3];
      numbers.reduce((sum, number) => {
        console.log(sum, number);
        sum += number;
        return sum;
      });
    }

    a)

    0 1
    1 2
    3 3

    b)

    1 2
    3 3

    c)

    undefined 1
    1 2
    3 3

    d)

    1 2
    3 3
    6 undefined

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

    Для того, щоб відповісти на це питання, потрібно знати, як працює метод массиву reduce. Цей метод приймає наступні параметри:

    arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

    Де accumulator - значення, яке ми повертаємо при ітерації, currentValue - поточне значення при ітерації, індекс масиву та початкове значення. В нашому випадку акумулятором буде сума, а поточним значенням при ітерації будуть значення масиву (по черзі 1, 2, 3).

    Отже, оскільки початкове значення для функції не задано (початкове значення суми), то при першому проході в консолі ми повинні побачити undefined 1. Далі відповідно 1 2 та 3 3.

    Якщо хтось подумав так само, то це не правильна відповідь. Якщо не передати initialValue в метод reduce, то він в якості нього візьме перший елемент масиву array. А отже, ітерації будуть всього дві, починаючи з другого елементу (з першого, якщо рахувати з нуля) і результат консолі виведений всього двічі. Правильна відповідь b).

  • [JS] Що поверне функція?
    function someFunction() {
      const numbers = [1, 2, 3];
      if (typeof numbers === 'array') {
        return true;
      }
    }

    a) null
    b) undefined
    c) true
    d) false


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

    Все просто - достатньо відкрити консоль Google Chrome і глянути результат. Або трохи подумати і розібрати функцію по рядкам:

    1. const numbers = [1, 2, 3]; - ініціалізація масиву. Тут все просто і без помилок. Йдемо далі.
    2. if (typeof numbers === 'array') - умова, в якій ми порівнюємо тип масиву. Поверне false, оскільки typeof numbers дорівнює “Object”, що в свою чергу не дорівнює “Array”. Якщо ти не знаєш, чому typeof поверне не “Array”, то радимо глянути на статтю в нашому блозі - в ній один з наших коучів розбирає подібну задачу.
    3. { return true; } - Оскільки умова ніколи не виконається, то цей рядок ми пропускаємо.
    4. І оскільки наша функція явно нічого не повертає, то вона поверне неявно undefined.

    Тобто:

      function () {
        const numbers = [1, 2, 3];
        if (typeof numbers === 'array') {
          return true;
        }
          return undefined;
        }

    Очевидно, що так функцію ніхто не стане писати - якщо функція перевіряє, чи numbers являє собою масив, то ми б отримали цей массив аргументом функції та явно повертали значення true або false.

    function сheckArray(numbers) {
      return Array.isArray(numbers);
    }
    або
    function сheckArray(numbers) {
      return numbers instanceof Array;
    }

    Але в якості тестового питання це хороший приклад, щоб перевірити ваші знання.

  • [JS] Які функції у наведеному фрагменті коду можна назвати чистими (pure function)?
    let a1 = 10;
    let a2 = 5;
    const obj1 = {
      prop1: 2,
      prop2: 4,
    };
    
    function func1(arg1, arg2) {
      return arg1 + arg2;
    }
    
    function func2(arg1, arg2) {
      return arg1 + a2 * arg2;
    }
    
    function func3(arg1, arg2) {
      a1 = arg2;
      return arg1 + a1;
    }

    a) func1
    b) func2 и func3
    c) func1 и func2
    d) жодна

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

    Спершу розберемося, що таке “чиста функція”. Чистими функціями називають функції, які відповідають двом умовам:

    • При одних і тих самих аргументах функція завжди повертає одне і те саме значення
    • Не містить побічних ефектів (side effects)

    У першій умові йдеться про те, що функція завжди повертатиме однаковий результат, якщо вхідні аргументи також однакові. Наприклад, функція added завжди повертатиме суму вхідних аргументів.

    function adder(x, y) {
      return x + y;
    }
    
    adder(7, 5); //12
    adder(7, 5); //12
    adder(7, 5); //12

    Але якщо використати аргументи ззовні, то результат вже не буде одним і тим самим, навіть якщо аргументи функції одні й ті ж самі:

    let y = 5;
    
    function adder(x) {
      return x + y;
    }
    
    adder(7); //12
    y = 10;
    adder(7); //17

    Результат функції непередбачуваний, оскільки залежить від змінної y, і такі функції називають не чистими. Чисті функції не повинні покладатись на змінні за межами їх області видимості.

    Друга умова стверджує, що чиста функція не повинна ніяк впливати на змінні зовні чи змінювати стан додатку, тобто містити так звані side effects. До побічних ефектів можна віднести роботу з DOM, роботу з файловою системою або HTTP запити.

    Наприклад, функція adder хоча і повертає завжди один і той самий результат, та все одно окрім цього змінює стан ззовні (записує дані у файл):

    function adder(x) {
      let result = x + y;
      fs.writeFileSync('result.txt', result);
      return result;
    }
    
    adder(7, 5); //12
    adder(7, 5); //12

    Така поведінка не є очевидною для функції, яка повинна просто повернути суму двох чисел. Однак, якщо виникає потреба записати результат у файл, можна зробити так:

    function adder(x) {
      return x + y;
    }
    
    let result = adder(7, 5);
    fs.writeFileSync('result.txt', result);

    Наступний приклад також не буде чистою функцією, оскільки змінює об'єкт ззовні та окрім того нічого не повертає:

    var obj = {
      active: false,
    };
    
    function setActive(obj) {
      obj.active = true;
    }
    
    setActive(obj);

    Отже, повертаючись до питання:

    • функція func1 чиста.
    • функція func2 нечиста, оскільки використовує аргумент a2, запозичений ззовні.
    • функція func3 також не чиста, оскільки мутує аргумент a1.

    Правильна відповідь: а — func1

    Чисті функції роблять наш код більш зрозумілим і дають можливість бути впевненими, що певна функція працює правильно і завжди поверне один і той самий результат, при цьому жодним чином не впливатиме на інші частини програми. Окрім цього, такий код зручно тестувати та дебажити, адже кожна функція незалежна і виконує конкретну дію. Використовуйте чисті функції при розробці — це хороша звичка для професійного розробника.