Middle: питання на співбесіді з теми «Функції й замикання»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
У звичайній функції this визначається в момент виклику, а не там, де функцію написано:
obj.method()-thisцеobj;- просто
fn()-undefinedу строгому режимі (у модулях і класах він завжди строгий), глобальний об'єкт - у нестрогому; new Fn()- новий об'єкт;fn.call(ctx),fn.apply(ctx),fn.bind(ctx)- явно заданийctx;- в обробнику
addEventListener- елемент, на якому висить обробник.
Типова пастка - метод, переданий як колбек:
class Timer {
seconds = 0;
tick() { this.seconds++; }
}
const timer = new Timer();
setInterval(timer.tick, 1000); // this === undefined -> TypeError
Метод «відірвався» від об'єкта, і викликається вже як просто функція.
Стрілкова функція не має власного this - вона бере this з місця, де її створено (лексично), і змінити його не можна навіть через call чи bind:
setInterval(() => timer.tick(), 1000); // працює
// або поле-стрілка в класі:
class Timer2 { seconds = 0; tick = () => { this.seconds++; }; }
Ще відмінності стрілкових функцій: немає власного arguments, їх не можна викликати через new, вони не мають prototype.
Коли НЕ стрілка: методи об'єкта, яким потрібен this цього об'єкта ({ name: 'x', greet: () => this.name } не працює), і обробники, де потрібен this як елемент.
Усі три дозволяють явно задати this для звичайної функції:
fn.call(ctx, a, b)- викликає функцію одразу, аргументи передаються через кому.fn.apply(ctx, [a, b])- те саме, але аргументи - масивом.fn.bind(ctx, a)- не викликає, а повертає нову функцію з назавжди прив'язанимthis(і, за бажанням, частиною аргументів).
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`;
}
const user = { name: 'Оля' };
greet.call(user, 'Привіт', '!'); // 'Привіт, Оля!'
greet.apply(user, ['Вітаю', '.']); // 'Вітаю, Оля.'
const hello = greet.bind(user, 'Привіт');
hello('?'); // 'Привіт, Оля?'
Де це трапляється:
bind- щоб передати метод як колбек, не загубившиthis:button.addEventListener('click', this.onClick.bind(this)). Сьогодні частіше пишуть стрілкову функцію.- Часткове застосування:
const double = multiply.bind(null, 2). applyісторично використовували, щоб передати масив як аргументи:Math.max.apply(null, numbers). Тепер є spread:Math.max(...numbers).- Позичання методів:
Array.prototype.slice.call(arguments)- тепер замість цьогоArray.from().
Пастки: функцію, прив'язану через bind, повторний bind уже не перевизначить. А на стрілкові функції call, apply і bind щодо this не впливають узагалі.
IIFE (immediately invoked function expression) - функція, яку оголошують і одразу викликають:
(function () {
const secret = 'видно лише тут';
// ...
})();
(() => {
// те саме стрілковою функцією
})();
Дужки навколо функції потрібні, щоб рушій сприйняв її як вираз, а не як оголошення функції (яке не можна одразу викликати).
Навіщо їх використовували:
- ізоляція області видимості. До ES-модулів усі скрипти на сторінці ділили одну глобальну область. Змінні з
varна верхньому рівні ставали властивостямиwindowі конфліктували між бібліотеками. IIFE створювала приватну область; - шаблон «модуль» - приватний стан плюс публічний інтерфейс:
const counter = (function () {
let count = 0;
return {
increment: () => ++count,
current: () => count,
};
})();
- копія значення в циклі з
var- до появиletкожна ітерація ділила одну змінну.
Що змінилося:
- ES-модулі мають власну область видимості: змінні верхнього рівня модуля не глобальні. Vite, вбудовані
<script type="module">- IIFE для ізоляції не потрібні; letіconstмають блокову область видимості - досить фігурних дужок;- замикання для приватного стану тепер часто замінюють приватні поля класів (
#count).
Де IIFE досі трапляються:
- асинхронний код на верхньому рівні звичайного скрипта (не модуля), де немає top-level
await:
(async () => {
const config = await loadConfig();
start(config);
})();
- результат збирання. Збирачі пакують код для старих оточень і бібліотеки у формат IIFE (наприклад,
format: 'iife'у Rollup/Vite) - так вони підключаються одним<script>без конфліктів; - ініціалізація константи складною логікою в одному виразі (хоча зазвичай читабельніша окрема функція).
Пастка: рядок перед IIFE без крапки з комою. const a = b і на наступному рядку (function () {})() рушій прочитає як виклик b(...). Звідси звичка ставити ; перед IIFE у коді без крапок з комою.
Змінні let, const (і класи) теж «піднімаються» на початок своєї області видимості, але не ініціалізуються. Від початку блоку до рядка оголошення змінна існує, але звертання до неї кидає ReferenceError. Цей проміжок називають тимчасовою мертвою зоною (temporal dead zone).
{
console.log(price); // ReferenceError: Cannot access 'price' before initialization
let price = 100;
}
З var поведінка інша - змінна ініціалізується як undefined, і помилка тихо проходить:
console.log(total); // undefined
var total = 100;
Чому це «тимчасова», а не «просторова» зона. Важливий момент виконання, а не позиція в тексті:
function showPrice() {
console.log(price); // функцію оголошено вище за let...
}
let price = 100;
showPrice(); // ...але викликано після ініціалізації - працює
Неочевидні випадки:
typeofне рятує. Для неоголошеної змінноїtypeof xповертає'undefined', але для змінної в TDZ - кидаєReferenceError;- затінення:
const limit = 10;
function check() {
console.log(limit); // ReferenceError: внутрішня limit у TDZ
const limit = 20;
}
Внутрішнє оголошення «закриває» зовнішнє для всього блоку, а не лише від свого рядка;
- параметри за замовчуванням:
function f(a = b, b = 1) {}- при викликуf()параметрbще в TDZ; - класи:
new User()до рядкаclass User {}теж кидає помилку, на відміну від функцій.
Навіщо це зроблено так:
- помилка доступу до неініціалізованої змінної видна одразу, а не перетворюється на загадкове
undefined; constповинна мати рівно одне значення - побачити її «до» присвоєння було б порушенням цього правила.
Практичний висновок: оголошувати змінні до використання, на початку блоку чи безпосередньо перед ним. Помилки TDZ найчастіше трапляються при циклічних імпортах модулів: модуль звертається до експортованого const іншого модуля, який ще не встиг виконатися.