Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Junior: питання на співбесіді з теми «Функції й замикання»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

4 питання

Замикання - це функція разом зі змінними того місця, де її створено. Внутрішня функція «пам'ятає» змінні зовнішньої навіть після того, як зовнішня вже завершилася.

function makeCounter() {
  let count = 0;
  return () => ++count;
}

const next = makeCounter();
next(); // 1
next(); // 2
// count недоступний ззовні, але живе, поки живе функція next

Де замикання трапляються щодня:

  • Обробники подій і колбеки - вони бачать змінні з коду, що їх створив.
  • Приватний стан - як count вище: змінити його можна лише через функцію.
  • Фабрики функцій - const add5 = makeAdder(5).
  • Хуки React - кожен рендер створює нові функції, що замикають значення стану саме цього рендеру.

Класична пастка з var у циклі:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i)); // 3, 3, 3
}

var має одну змінну на всю функцію, і до моменту спрацювання таймерів вона вже дорівнює 3. З let у кожної ітерації своя змінна, і виведеться 0, 1, 2.

Нюанс для пам'яті: поки замикання живе (наприклад, обробник не знято), живуть і всі змінні, які воно використовує, разом з великими об'єктами в них.

Докладніше в документації: Замикання

var let const
Область видимості функція блок {} блок {}
Повторне присвоєння так так ні
Повторне оголошення так ні ні
Доступ до оголошення undefined помилка (TDZ) помилка (TDZ)
if (true) {
  var a = 1;
  let b = 2;
}
a; // 1 - var «протікає» з блоку
b; // ReferenceError

Підняття (hoisting): оголошення всіх трьох обробляються до виконання коду, але по-різному. var одразу отримує undefined, тож звернення до нього до рядка оголошення не дає помилки - лише тихий undefined. let і const до оголошення перебувають у «тимчасовій мертвій зоні» (TDZ): звернення кидає ReferenceError. Друге поведінка краща: помилка видна одразу.

const не робить значення незмінним - лише забороняє переприсвоїти змінну:

const user = { name: 'Оля' };
user.name = 'Іра';   // можна: змінюємо вміст об'єкта
user = {};           // TypeError

Для незмінності вмісту - Object.freeze() (поверхнево).

Сучасне правило: const за замовчуванням, let - коли значення справді змінюється, var - не використовувати.

Докладніше в документації: let

У JavaScript функцію можна створити кількома способами, і вони відрізняються не лише синтаксисом.

// оголошення функції (function declaration)
function add(a, b) {
  return a + b;
}

// функціональний вираз (function expression)
const multiply = function (a, b) {
  return a * b;
};

// стрілкова функція (arrow function)
const square = (n) => n * n;

1. Підняття (hoisting). Оголошення функції доступне до рядка, де воно написане, - рушій «піднімає» його на початок області видимості:

greet();                  // працює
function greet() { console.log('Привіт'); }

sayHi();                  // TypeError: sayHi is not a function (з var)
var sayHi = function () {};

wave();                   // ReferenceError (з const/let)
const wave = () => {};

Вирази й стрілкові функції - це значення, присвоєні змінним, тож діють правила змінних: з const/let до ініціалізації звертатися не можна.

2. this. Звичайні функції отримують this залежно від способу виклику. Стрілкові функції власного this не мають - беруть його з місця, де їх створено. Тому стрілки зручні в колбеках усередині методів класу, але не підходять як методи об'єкта, яким потрібен this цього об'єкта.

3. Інші відмінності стрілкових функцій:

  • немає об'єкта arguments (використовують rest-параметри ...args);
  • не можна викликати з new - вони не конструктори;
  • короткий запис з неявним поверненням: (n) => n * 2. Щоб повернути об'єкт, його беруть у дужки: () => ({ id: 1 }).

Що обирати:

  • оголошення функції - для «іменованих» функцій верхнього рівня модуля: їх зручно читати й шукати, і порядок у файлі не важливий;
  • стрілкові функції - для колбеків (map, filter, обробники подій) і коротких допоміжних функцій;
  • методи класів і об'єктів - звичайний синтаксис методу save() { ... }.

Іменований функціональний вираз (const f = function factorial(n) { ... }) має ім'я, видиме лише всередині, - для рекурсії й читабельних стеків помилок.

Докладніше в документації: Функції

Параметри за замовчуванням задають значення, якщо аргумент не передано:

function formatPrice(amount, currency = 'грн', digits = 2) {
  return `${amount.toFixed(digits)} ${currency}`;
}

formatPrice(100);          // '100.00 грн'
formatPrice(100, '$');     // '100.00 $'

Значення за замовчуванням спрацьовує лише для undefined, але не для null, 0 чи порожнього рядка:

formatPrice(100, undefined);  // '100.00 грн'
formatPrice(100, null);       // '100.00 null'

Значення обчислюється при кожному виклику і може посилатися на попередні параметри:

function createUser(name, slug = name.toLowerCase(), createdAt = new Date()) { /* ... */ }

Кожен виклик отримає нову дату чи новий масив (items = []) - на відміну від Python, де значення за замовчуванням створюється один раз.

Rest-параметри збирають «решту» аргументів у справжній масив:

function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3);   // 6

function log(level, ...messages) {
  console[level](...messages);
}

Rest-параметр може бути лише один і лише останнім.

Рядом - старий об'єкт arguments, псевдомасив усіх переданих аргументів. Він не має методів масиву, недоступний у стрілкових функціях і погано читається. У сучасному коді його замінюють rest-параметри.

Іменовані параметри через деструктуризацію - зручний прийом для функцій з багатьма необов'язковими налаштуваннями:

function fetchUsers({ page = 1, perPage = 20, sort = 'name' } = {}) { /* ... */ }

fetchUsers({ sort: 'created_at' });
fetchUsers();   // працює завдяки "= {}"

Порядок аргументів більше не важливий, а виклик сам себе документує. Без = {} наприкінці виклик без аргументів кине помилку: деструктуризувати undefined неможливо.

function.length рахує лише параметри до першого зі значенням за замовчуванням і без rest - деякі бібліотеки (каррування, DI) на це покладаються.

Докладніше в документації: Параметри за замовчуванням