7 питань з JS-тесту Академії, що виявилися найскладнішими для студентів

Кожної весни ми активно працюємо над тим, щоб вкотре провести Binary Studio Academy. І як би пафосно це не звучало, ми справді багато працюємо над тим, щоб удосконалити майже всі аспекти: запустити абсолютно новий портал Академії та мобільний додаток до нього, покращити процес відбору, оновити технічний матеріал і лекції. І звичайно, одним з важливих етапів Академії є тестування, про яке і піде далі мова.

Загалом тестовий відбір до Академії призначений для того, щоб від усієї кількості зареєстрованих відібрати тих, хто має достатній технічний рівень: володіє основами JavaScript (або залежно від напряму, PHP чи .Net), має базові знання по ООП та базам даних, а ще максимально вмотивований далі навчатися в академії (і не “заб’є” на тест). Адже в подальшому ми орієнтуємось на те, що студент знає основи і зможе комфортно почувати себе на лекціях і встигати за іншими.

Кожного року ми валідуємо тести: ретельно переглядаємо кожне питання, видаляємо ті, які вже застаріли або не актуальні, формулюємо нові. Наприклад, якщо питання стосується застарілого патерну програмування, який де-факто вже вважається анти-паттерном, то ми видалимо це питання. І навпаки, якщо в JS зараз майже всі користуються новими можливостями останніх версій ECMAScript, то чого б це не додати в тести?

Ми щороку покращуємо етап відбору і тому проводимо аналіз тестів за минулі роки, щоб побачити кореляцію тестових результатів та успішності студентів. І це дало нам змогу оцінити тестові питання із зовсім іншого ракурсу. Результати показали нам, що є питання, на які більшість успішних студентів не змогла відповісти, і навпаки, ті питання, на які відповів майже кожен. І це мотивувало нас переглянути питання більш детально, переробити велику частину, а мене особисто - написати невеличкий розбір деяких з них. В цій статті ми розглянемо лише тестові питання JS напрямку, але мені здається, що розбір цих питань може бути цікавий всім. Для напрямків PHP та .Net буде розбір в наступних статтях.

Але спершу рекомендую пройти невеличкий тест з питань, які будуть розглянуті:

Пройти тест

Пройшли? А тепер докладно розберемо питання.

1. Який результат буде виведено в консоль?

let cars1 = ["Mersedes", "Volkswagen", "Ford", "Skoda"];
let cars2 = ["Mersedes", "Volkswagen", "Ford", "Skoda"];
let result = (cars1 === cars2) +" "+ (cars1 == cars2);
console.log(result);

a. “true true”
b. “true false”
c. “false true”
d. “false false”


87% відповіли невірно.

Перш ніж розбирати це питання, треба нагадати деякі базові поняття:

  • Оператор “==” перед операцією порівняння приводить значення до єдиного типу, тому 0 == false буде приведено до булевого значення, і результатом порівняння буде істина (true).
  • Оператор строгого порівняння “===” не приводить типи, а порівнює “як вже там є”. Тобто 0 та false не дорівнюють один одному, оскільки мають різні типи.
  • При порівнянні масивів (або інших типів з посиланням таких, як об’єкт або функція) порівнюються не значення, а їх посилання. Тобто let cat = {name: ‘Bulka’} не дорівнює let evil = {name: ‘Bulka’}, але cat дорівнює member, якщо let member = cat;

Тепер давайте розберемо присвоєння let result = (cars1 === cars2) +" "+ (cars1 == cars2), розклавши на доданки:

  • Порівняння зліва (cars1 === cars2) - це строге порівняння, яке порівнює значення одного і того ж типу. Хоча в нашому випадку обидві змінні типу масив, але посилання в них різні, тому false.
  • Справа - не строге порівняння з приведенням типу, але в нас типи однакові, а посилання - ні, тому також false.

Отже, правильна відповідь: d) “false false”

Це класне, на мою думку, питання, яке показує наскільки ви добре знайомі з поняттями порівняння, строгого порівняння та порівняння об’єктів.

2. Які маніпуляції обов'язково треба здійснити з кодом, щоб змінна “result” після виконання дорівнювала числу 37?

let a = 12
let b = 25;
let result = (a, b) => a + b;

a. “Написати return одразу після стрілки”
b. “Виконати функцію наступним рядком після її присвоєння до змінної”
c. “Взяти тіло функції у фігурні дужки”
d. “Перетворити функцію на таку, що викликає сама себе (IIFE)”
e. “Не потрібно виконувати жодних маніпуляцій”


85% відповіли невірно.

Спершу здається, що питання заплутане, і майже кожен варіант підходить. Тому в даному випадку краще буде довести це від супротивного, тобто пройтись по кожному твердженню і відкинути неправильні варіанти. Але перед цим варто розібратись, що таке “функція, що викликає сама себе ” або Self-invoking functions (Immediately Invoked Function Expression, IIFE). Це вираз, що одночасно оголошує функцію та здійснює її виклик, для зручності далі я буду використовувати “IIFE”. Такий підхід дає змогу використовувати власний scope, не забруднюючи при цьому глобальний scope. Якщо ви з цим поняттям не знайомі, то це бажано повторити. Тепер приклади:

a. Якщо написати return після стрілки, то функція буде не валідною, оскільки з return у стрілковій функції повинні використовуватись фігурні дужки: let result = (a, b) => { return a + b }; Але навіть якщо зробити функцію коректною, то це ніяк не вплине на результат, це просто інший синтаксис функції.
b. Якщо просто виконати функцію result, то значення змінної result не зміниться.
c. Знову ж таки, якщо взяти тіло функції у фігурні дужки, то функція без return буде не валідна.
d. Якщо перетворити функцію в IIFE, тобто result = ((a, b) => a + b)(a, b), то функція буде виконана при присвоєнні result = ((a, b) => a + b)(12, 25), і в змінну result запишеться сума 37.
e. Якщо жодних операцій не виконувати, то змінна зберігатиме лише саму функцію

Отже, правильна відповідь: d) “Перетворити функцію на таку, що викликає сама себе (IIFE)”

3. Що буде зберігати змінна count в результаті виконання наступного коду?

var count = 3;
 
function doSomething() {
  count = 5;
  
  if(true) {
    return count + 4;
  }
  function count() {}
}

doSomething();

a. 3
b. 5
c. 9
d. function count(){/*…*/}


91% відповіли невірно.

Давайте розберемо приклад по крокам:

  1. Оголошуємо глобальну змінну count та ініціалізуємо її одночасно зі значенням 3. Далі оголошуємо функцію doSomething, і тут пропоную зупинитися детально. В першому рядку ми переписуємо значення глобальної змінної count на 5.
  2. Наступним кроком ми написали умову, яка обов'язково виконається, і функція поверне суму. В кінці функції ми оголошуємо функцію count, але до цього моменту функція вже поверне значення суми, і код не виконається.
  3. Останній крок - виклик функції doSomething.

Саме так інтерпретували цей приклад більшість із вас, але це не правильно. Оголошення function count() {} всередині функції doSomething - це оголошення функції (в змінній count) в області видимості функції doSomething. А оскільки функція count була оголошена як Function Declaration, то змінна для неї була створена до виконання функції, тому не важливо, де саме була “задекларована” функція - до умови if(true){} чи після.

Це пояснюється механізмом підняття (hoisting), при якому змінні та оголошення функцій переміщуються вверх по своїй області видимості перед тим, як буде виконаний код. Рекомендую почитати статтю на цю тему, щоб детально з цим ознайомитись. Взагалі,тема області видимості часто збиває з пантелику як новачків, так і досвідчених JS розробників. Тому дуже важливо перед тестом в ній добре розібратись. Повертаючись до нашого прикладу - саме через hoisting  змінних число 5 було присвоєно не глобальній змінній, а локальній. Таким чином, значення глобальної змінної не буде змінено, а отже правильна відповідь: a) “3”.

Це одне з тих питань, які ми видалили з тесту. Такого роду питання мають на меті не стільки перевірити знання по JS, скільки заплутати і подивитися, наскільки людина уважна.

4. Вкажіть кількість областей видимості?

(function() {
  function bar(number) {
	const foo = y => {
  	  return x * y;
	}
 
	return foo(4);
  }
 
  bar(2);
})();

a. 2
b. 1
c. 3
d. 4


84% відповіли невірно.

У нас знову питання на тему області видимості (як я вже наголошував, це дуже важливо знати) але на відміну від попереднього, воно набагато зрозуміліше. Для наглядності розкладемо код на окремі функції:

(function() {
  ...function body...
})(); 
function bar(number) {
  ...function body...
}  
const foo = y => {
  ...function body...
}

Отже, у нас три функції, кожна з яких має свою власну область видимості, плюс глобальна область видимості, в якій виконується код (наприклад, window). Отже, правильна відповідь: d) 4

5. Які з наведених нижче змінних при перетворенні в булеве значення отримають значення “false” (виберіть два варіанти)?

a. "var a1 = 1;"
b. "let a2 = 0;"
c. "const a3 = () => null;"
d. "const a3 = (() => null)();"
e. "var a5 === '' + 0;"

83% відповіли невірно.

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

a. Будь-яке позитивне чи негативне числове значення (тип number), включаючи безкінечність (Infinity/-Infinity), буде істиною (true).
b. Однак 0 та NaN хоч також є числовими значеннями, але при приведенні до булевого типу будуть false.
c. В цьому випадку змінна буде функцію "() => null;", а функція як об'єкт при приведенні до булевого значення буде істиною (true).
d. Даний приклад хоч і схожий на попередній, та насправді є функцією, що викликає сама себе (IIFE), тобто змінна зберігатиме не функцію, а її результат, у нашому випадку - це null. А null в приведенні до булевого буде false. (Можливо, вам здалося, що null - це об’єкт, а будь-який об’єкт, навіть пустий масив, у приведенні до булевого значення (Boolean([])) буде true. Але null - це не об’єкт, а окремий тип)
e. Тут взагалі замість оператора присвоєння використовується оператор порівняння, тому буде синтаксична помилка.

Отже, правильна відповідь:
b) "let a2 = 0;", c) "const a3 = (() => null)();"

6. Як передати символ “/” як параметр в рядку запита у браузері?

a. “Використовувати екранування зворотнім слешем \/”
b. “Використовувати екранування прямим слешем //”
c. “Закодувати символ за допомогою функції encodeURI()
d. “Використовувати екранування подвійним зворотнім слешем \\/”
e. “Огорнути символ в знаки відсотку %/%”

79% відповіли невірно.

Щоб відповісти на це питання, треба бодай трохи бути знайомим зі стандартами URI. Символ слеша (/) є зарезервований тому як параметр його використовувати не можна, навіть екранованим прямим чи оберненим слешем. Для того, щоб передати “заборонений” символ, його треба закодувати згідно зі специфікацією (де слеш закодованим виглядатиме %2F). Для цього можна використати метод encodeURI().

Отже, правильна відповідь: c) Закодувати символ за допомогою функції encodeURI()

7. Чому наведений нижче код не працюватиме?

const links = document.getElementsByClassName('link');

links.forEach((link, i) => {
  link.innerHTML += i;
})

a. “Не можна динамічно редагувати вміст HTML”
b. “Тому що змінна links є константою”
c. “Неправильна кількість аргументів передана в callback-функцію forEach”
d. “Сучасні браузери не підтримують стрілкові функції”
e. “Масив links не має методу forEach”


76% відповіли невірно.

Це питання дозволяє чудово побачити, наскільки людина розбирається в структурах даних і в DOM зокрема. Справа в тому, що метод “getElementsByClassName” повертає не звичайний масив, а колекцію елементів, HTMLCollection (в старіших версіях повертався NodeList ). Ці структури даних не підтримують “класичні” методи масиву типу “push”, “slice”, “concat” і т.д. Хоча деякі методи можуть працювати, оскільки є методами класу HTMLCollection або NodeList. Це, наприклад, стосується методу “forEach” класу NodeList, який можна використовувати для ітерації ( підтримується не всіма браузерами) або методів ітерації for/of (працює для обох класів та підтримується всіма браузерами). Для HTMLCollection метод “forEach” не працюватиме. Для того, щоб реалізувати це, необхідно перетворити цей список у масив або викликати на ньому метод з прототипу Array.prototype:

Array.prototype.forEach.call(links, (link, i) => link.innerHTML += i)

або

[].forEach.call(links, (link, i) => link.innerHTML += i)

З ECMAScript 2015 можна використати “Spread” оператор

[...links].forEach((link, i) => link.innerHTML += i) 

або конвертувати в масив

Array.from(links).forEach((link, i) => link.innerHTML += i)

Інші варіанти досить очевидні, щоб їх розглядати, хіба що за виключенням варіанту b. Варто запам’ятати, що const створює змінну, яку не можна змінювати, але можна змінювати властивості всередині неї, в нашому випадку це елементи масиву. Більш детально тут.

Отже правильна відповідь: e) “Масив links не має методу forEach”

Підсумок

Тести - це не частина навчання, а частина відбору до Академії, тому треба вже бути технічно підготовленим, щоб пройти цей етап. Але якщо добре володіти бодай базовими знаннями (а краще більше, ніж базовими) і достатньо готуватись, це не складе жодних труднощів. Головне тут - бути уважним та не поспішати. А наша команда, в свою чергу, докладає багато зусиль, щоб тестові питання були максимально чіткі та якісні. Ну і в цілому, якщо ти плануєш складати тест чи тобі просто цікаво спробувати свої сили (а я рекомендую спробувати), переходь на портал Академії та пройди Demo Test. Так само на порталі можна знайти інформацію про корисні ресурси, які ми рекомендуємо для підготовки. Хай щастить!

P.S. Жодне з розібраних вище питань не буде в фінальному тесті:)