Фундамент Ф1 · урок 8 з 12
this і класи
Компонент Angular — це клас. Сервіс — клас. У коді на кожному кроці this.tasks(),
this.selected.emit(…), this.form.value. Цей урок пояснює, що таке клас, хто такий
this і чому він іноді «губиться» — найпідступніша помилка, з якою стикаються всі, хто приходить в Angular.
Клас — креслення для обʼєктів
Коли обʼєктів одного виду багато — задачі, користувачі, — їх зручно створювати за одним кресленням. Клас і є таким кресленням: які поля в обʼєкта і що він уміє.
class Task {
constructor(title) { // виконується при створенні: new Task(…)
this.title = title;
this.done = false;
}
toggle() { // метод: функція, що належить обʼєктам класу
this.done = !this.done;
}
}
const report = new Task('Звіт'); // екземпляр — обʼєкт, створений за кресленням
const release = new Task('Реліз');
report.toggle();
report.done; // true
release.done; // false — кожен екземпляр має свої дані
new створює новий порожній обʼєкт і викликає constructor, де цей обʼєкт доступний як
this. Методи пишуть один раз у класі, а працюють вони з даними того обʼєкта, на якому їх викликали.
Хто такий this
this — це «обʼєкт, на якому викликали метод»: те, що стоїть ліворуч від крапки в момент виклику.
report.toggle() — всередині this це report. release.toggle() —
release. Як слово «я» в реченні: воно означає того, хто говорить зараз.
Саме в цьому пастка: this визначається в момент виклику, а не там, де метод написано. Якщо
відірвати метод від обʼєкта й викликати окремо, ліворуч від крапки нічого немає — і this стає
undefined:
const toggle = report.toggle; // взяли саму функцію, без обʼєкта
toggle(); // ✗ TypeError: Cannot read properties of undefined (reading 'done')
Так ніхто не пише навмисно. Але те саме відбувається непомітно, коли метод передають як колбек —
у setTimeout, addEventListener, subscribe. Функцію передали, а викличе її вже
хтось інший, і this буде тим, що підставить він: undefined, глобальний обʼєкт
window чи щось зовсім стороннє. Твого обʼєкта там не буде точно.
Запусти й порівняй два рядки виводу. Другий — від setTimeout: помилки немає, і це
гірше за помилку. setTimeout підставив як this глобальний обʼєкт, тож title —
undefined, а done мовчки записався не в задачу. Потім заміни останній рядок на
setTimeout(() => report.toggle(), 0);
Як не загубити this
Рішення спирається на одну властивість стрілкових функцій: у стрілки немає свого this. Вона
бере його з місця, де її створили, — як будь-яку іншу змінну з рюкзака (урок 7). Тому:
// 1. обгорнути виклик у стрілку — найчастіший спосіб
setTimeout(() => report.toggle(), 0);
// 2. метод-стрілка як поле класу: this назавжди привʼязаний до екземпляра
class Task {
done = false;
toggle = () => { this.done = !this.done; };
}
// 3. bind — старіший спосіб, трапляється в чужому коді
setTimeout(report.toggle.bind(report), 0);
В Angular у шаблоні (click)="save()" про це думати не треба: Angular сам викликає метод на
компоненті. А от коли передаєш метод колбеком — stream.subscribe(this.handle) — ось тут
this загубиться. Пиши subscribe(v => this.handle(v)).
Timer виводить тік NaN: метод tick віддали в
setTimeout без обʼєкта, і this.ticks рахується не в таймері. Виправ start() так,
щоб вивелось тік 1.
start напиши setTimeout(() => this.tick(), 10); — стрілка візьме
this з методу start, а там це сам таймер.Поля, геттери, приватне
Сучасний клас зазвичай пишуть без constructor — поля оголошують прямо в тілі:
class Cart {
items = []; // поле з початковим значенням
#discount = 0; // приватне поле: ззовні недоступне
add(price) {
this.items.push(price);
}
get total() { // геттер: читається як поле, рахується як функція
return this.items.reduce((sum, p) => sum + p, 0) - this.#discount;
}
}
const cart = new Cart();
cart.add(100);
cart.total; // 100 — без дужок, бо це геттер
Саме так виглядають компоненти Angular: поля (tasks = signal([])), методи
(add(), remove()), геттери. А замість # у TypeScript частіше пишуть
private — про це у Ф3.
Допиши клас Cart: поле items (порожній масив), метод
add(price) і геттер total — сума цін. Має вивестись 150.
Наслідування — коротко
class Bug extends Task { // Bug — це Task з чимось ще
constructor(title, severity) {
super(title); // спершу конструктор батька
this.severity = severity;
}
}
extends бере все з батьківського класу, super(…) викликає його конструктор. В Angular
наслідування трапляється рідко: замість «бути різновидом» компонент отримує потрібне через DI — урок про сервіси в
модулі 2.
Як це виглядає в Angular
export class TaskList { // компонент — звичайний клас
tasks = signal<Task[]>([]); // поле; <Task[]> — тип з TypeScript, Ф3
add(title: string) { // метод; : string — теж тип
this.tasks.update(list => [...list, { title, done: false }]);
}
}
Тепер кожен шматок має бути знайомим: поле класу, метод, this, стрілка-колбек, spread і коротка
властивість { title }. Типи TypeScript — останнє, чого бракує, і цьому присвячено Ф3.
- Метод передали колбеком —
setTimeout(this.save),subscribe(this.handle)— іthisзагубився. Іноді це помилка, а іноді тиха робота не з тим обʼєктом. Обгортай у стрілку. - Забутий
new:Task('Звіт')замістьnew Task('Звіт')— помилка. - Геттер викликають з дужками:
cart.total()— а це не функція. - У методі звертаються до поля без
this:items.push(…)замістьthis.items.push(…).
Підсумок
- Клас — креслення,
newстворює екземпляр,constructorйого налаштовує. this— обʼєкт ліворуч від крапки в момент виклику. Відірвав метод від обʼєкта —thisзагубився.- У стрілки немає свого
this, вона бере його з місця створення — тому колбеки обгортають у стрілки. - Компонент Angular — клас з полями, методами й геттерами.