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 - не використовувати.
У 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) на це покладаються.