08 · JavaScript/TypeScript для AQA
this, класи та ООП у тестовому коді
Зміст
Відкрий будь-який тестовий фреймворк на JS/TS — і побачиш класи: LoginPage, ApiClient, BaseTest, компоненти сторінки. , найпоширеніший патерн автоматизації, — це просто клас, а всередині класу все тримається на слові this. Тому питання «як визначається this?» і «чим bind відрізняється від call?» — не абстрактна теорія, а те, що ламає реальні тести.
Класична сцена: метод page object раптом падає з TypeError: Cannot read properties of undefined. Виглядає як баг застосунку або кривий , а насправді метод відірвали від об'єкта, і this став undefined. Хто розуміє механізм this, класів і прототипів (prototype) під капотом — читає такі помилки за секунди, а не гадає годину. Ця глава — про мовний фундамент; сам патерн Page Object детально розбирає розділ «Автоматизація: стратегія».
Як визначається this
Головна ідея, яку більшість вивчає найдовше: this не визначається там, де функцію оголосили. Він визначається в момент виклику — за тим, як функцію викликали. Одна й та сама функція в різних викликах отримує різний this. Тому дивитися треба не на тіло функції, а на місце виклику (call-site).
Правил прив'язки (binding) рівно чотири (коли на один виклик претендує кілька, пріоритет такий: new → явна прив'язка → метод → простий виклик):
- Виклик як метод —
obj.method().thisдорівнює об'єкту зліва від крапки, тобтоobj. Це найчастіший випадок у page object:loginPage.open()→ всерединіopenthis— цеloginPage. - Простий виклик —
fn(), без об'єкта зліва. У строгому режимі (strict mode)thisдорівнюєundefined. Тіло класу й ES-модулі завжди строгі, тож у тестовому коді це майже завждиundefined, а не глобальний об'єкт. - Виклик з
new—new Fn(). Створюється новий порожній об'єкт, іthisвказує на нього. Саме тому в конструкторіthis.page = pageзаписує поле в новий екземпляр. - Явна прив'язка —
fn.call(obj),fn.apply(obj),fn.bind(obj).this— це те, що ти передав першим аргументом.
Окремий, п'ятий випадок — (arrow functions). У них немає власного this: вони беруть його лексично, з місця оголошення, і назавжди. Стрілку не можна «перепризначити» через call чи bind — її this зафіксований тим оточенням, де вона написана. Це не виняток заради винятку, а головний робочий інструмент проти втрати контексту (детальніше про лексичну область видимості — у главі «Функції, замикання та область видимості»).
Втрата контексту і bind/call/apply
Тепер найпоширеніша пастка. this прив'язується під час виклику, а не оголошення — тож щойно ти відриваєш метод від об'єкта й передаєш його як значення, зв'язок губиться.
class Toolbar {
label = 'Menu';
logLabel() {
console.log(this.label);
}
}
const bar = new Toolbar();
bar.logLabel(); // 'Menu' — виклик через крапку, this = bar
const detached = bar.logLabel;
detached(); // TypeError: Cannot read properties of undefined (reading 'label')
detached — це та сама функція, але виклик уже простий, без об'єкта зліва. this став undefined, і звернення this.label падає. У реальному коді це стається, коли метод передають як : обробник події, аргумент у setTimeout, array.forEach(this.check), підписка на емітер. Функцію викличуть, але вже не «через крапку».
Рятують три методи, які є в кожної функції:
bind(thisArg)— не викликає функцію, а повертає нову функцію з назавжди прив'язанимthis. Її потім можна передавати куди завгодно.call(thisArg, arg1, arg2)— викликає функцію негайно з заданимthis, аргументи перелічуються через кому.apply(thisArg, [arg1, arg2])— те саме, щоcall, але аргументи одним масивом.
const bound = bar.logLabel.bind(bar);
bound(); // 'Menu' — this зафіксовано на bar
bar.logLabel.call(bar); // 'Menu' — виклик негайно
Мнемоніка різниці: call → comma (кома), apply → array (масив), bind → пізніше.
Але в сучасному коді bind руками пишуть рідко. Той самий ефект дає стрілковий метод як поле класу: його this лексично прив'язаний до екземпляра, тож метод можна відривати й передавати безкарно.
class Toolbar {
label = 'Menu';
logLabel = () => {
console.log(this.label); // this завжди — екземпляр
};
}
const bar = new Toolbar();
const detached = bar.logLabel;
detached(); // 'Menu' — контекст не губиться
Нюанс, який тут легко проґавити: такий стрілковий метод — це вже не спільний метод на прототипі, а окрема копія-поле в кожному екземплярі. Для десятка page object це неважливо; для тисяч об'єктів у гарячому циклі — вже аргумент. Знати різницю варто (див. розділ «Прототипи під капотом» нижче).
Класи: constructor, методи, static
Клас (class) — це шаблон об'єктів: він описує, які в них поля й методи. constructor — спеціальний метод, який виконується під час new і зазвичай приймає залежності. У тестовому коді конструктор page object майже завжди отримує page (об'єкт сторінки Playwright) і зберігає його в поле.
import { Page } from '@playwright/test';
class LoginPage {
constructor(private readonly page: Page) {}
async open(): Promise<void> {
await this.page.goto('/login');
}
async loginAs(user: string, password: string): Promise<void> {
await this.page.getByLabel('Email').fill(user);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Sign in' }).click();
}
}
Запис private readonly page: Page у параметрах конструктора — це «параметр-властивість» TypeScript: короткий спосіб оголосити поле й одразу присвоїти йому аргумент. Звичайні методи (open, loginAs) — це дії сторінки, і всередині них this — конкретний екземпляр LoginPage.
Ключове слово static робить член належним самому класу, а не екземпляру. Статику не видно через this об'єкта — до неї звертаються через ім'я класу. Типове застосування — фабричні методи, які інкапсулюють складне створення об'єкта, і константи.
class ApiClient {
private constructor(private readonly token: string) {}
// Фабрика: залогінитись і повернути готового клієнта
static async authorized(request: APIRequestContext): Promise<ApiClient> {
const token = await requestToken(request);
return new ApiClient(token);
}
}
const client = await ApiClient.authorized(request);
Приватний конструктор плюс статична фабрика — акуратний спосіб гарантувати, що клієнта не створять без токена.
extends і super: успадкування з мірою
extends створює підклас, який успадковує поля й методи батька. super() у конструкторі викликає конструктор батька, а super.method() — батьківський метод. Правило, яке ловить новачків: у конструкторі підкласу до super() не можна звертатися до this — це кине ReferenceError. Спершу побудуй батьківську частину, потім користуйся this.
class BasePage {
constructor(protected readonly page: Page) {}
async open(path: string): Promise<void> {
await this.page.goto(path);
}
}
class LoginPage extends BasePage {
async open(): Promise<void> {
await super.open('/login'); // виклик батьківського open
}
}
Синтаксис — простий. Складне питання — коли ним користуватися. Тут діє канон «композиція проти успадкування (composition over inheritance)» з розділу «Автоматизація: стратегія». Коротко: неглибокий BasePage зі справді спільною механікою (базовий URL, спільне очікування) — норма. А от глибокі ієрархії (BasePage → AuthPage → AdminPage → UsersPage) швидко стають болем: зміна в корені ламає гілки, а логіка розмазана рівнями, і незрозуміло, звідки метод.
Тому для частіше беруть композицію: has-a замість is-a. Сторінка не успадковує хедер і таблицю — вона містить їх як окремі об'єкти-компоненти в полях.
class DashboardPage {
readonly header: HeaderComponent;
readonly grid: DataGrid;
constructor(private readonly page: Page) {
this.header = new HeaderComponent(page);
this.grid = new DataGrid(page);
}
}
// у тесті:
await dashboard.header.logout();
await dashboard.grid.sortBy('Name');
HeaderComponent і DataGrid — самостійні класи, які можна перевикористати на будь-якій сторінці з хедером чи таблицею, не будуючи ієрархію. Успадкування прив'язує намертво; композицію легко перескладати.
Геттери, сеттери і приватні поля
Геттер (getter) і сеттер (setter) — методи, оголошені через get/set, до яких звертаються як до звичайних властивостей, без дужок. Ззовні cart.size виглядає як поле, а насправді щоразу виконується функція. Зручно для обчислюваних або похідних значень.
class Cart {
#items: string[] = [];
add(item: string): void {
this.#items.push(item);
}
get size(): number {
return this.#items.length; // читаємо як cart.size, без дужок
}
}
const cart = new Cart();
cart.add('book');
console.log(cart.size); // 1
Поле з решіткою — #items — це (private field), справжня інкапсуляція на рівні мови. Звернутися до #items можна лише зсередини класу; будь-яка спроба ззовні — синтаксична помилка ще до запуску. Це критична відмінність від модифікатора private у TypeScript: private існує тільки під час компіляції, у зібраному JS поле лишається звичайним і доступним через obj['field']. Тобто private — це підказка компілятора й дисципліна, а # — жорсткий бар'єр у рантаймі. Для тестового коду частіше достатньо private з TypeScript (докладніше про його типи — у главі «TypeScript: типи, інтерфейси та строгий режим»); # беруть там, де приватність треба гарантувати самою мовою.
Прототипи під капотом
Класи в JavaScript великою мірою — синтаксичний цукор над прототипами (хоча додають і власну семантику: приватні поля #, строгий режим за замовчуванням, заборону виклику без new). Розуміти механізм варто, бо він пояснює і instanceof, і те, чому методи «спільні», і звідки взагалі береться метод, якого немає в самому об'єкті.
У кожного об'єкта є прихований зв'язок — [[Prototype]] — на інший об'єкт, його прототип. Коли ти звертаєшся до obj.method(), шукає method спочатку в самому obj; не знайшовши — йде по цьому зв'язку вгору, до прототипу, потім до прототипу прототипу, аж поки не знайде або не впреться в null. Це і є ланцюжок прототипів (prototype chain).
Коли ти пишеш class LoginPage, звичайні методи (open, loginAs) кладуться не в кожен екземпляр, а один раз — у LoginPage.prototype. Усі екземпляри LoginPage посилаються на цей один об'єкт як на свій прототип. Тому метод у пам'яті один, а не копіюється в кожен об'єкт — на відміну від стрілкових полів-методів, які таки лежать у кожному екземплярі.
extends просто зшиває два такі ланцюжки: прототип LoginPage отримує своїм прототипом BasePage.prototype. Тому виклик успадкованого методу — це знахідка на другому кроці підйому ланцюжком. І loginPage instanceof BasePage повертає true саме тому, що BasePage.prototype трапляється десь у ланцюжку.
Мовний фундамент Page Object
Складемо все докупи. Page Object — це не магія фреймворку, а звичайний клас, який тримається рівно на тому, що ми розібрали:
- клас і
constructorприймаютьpageяк залежність і зберігають у поле; thisусередині методів указує на екземпляр сторінки — поки метод викликають через крапку;- методи на прототипі — це дії сторінки, спільні для всіх екземплярів;
- композиція компонентів (хедер, таблиця, модалка окремими об'єктами в полях) масштабує структуру краще за глибоке успадкування;
- інкапсуляція (
private,#) ховає локатори всередині, лишаючи назовні лише осмислені дії.
Тобто, щойно ти зрозумів this, класи й прототипи, Page Object читається як проста конструкція, а не як заклинання. Сам патерн — його межі, «god object», перевірки в page object — це тема розділу «Автоматизація: стратегія»; тут — його мова.
Типові помилки
this is undefinedпри виклику методу. Виглядає як баг класу чи зламаний локатор, а насправді метод відірвали від об'єкта (передали як колбек, уsetTimeout,forEach) — іthisставundefined. Лікування: стрілковий метод-поле абоbind.- «Стрілковий метод безкоштовно чинить
this». Виглядає як безкоштовний фікс, а насправді такий метод — окреме поле в кожному екземплярі, а не спільний метод на прототипі. Для page object байдуже, але на питання «де живе метод?» це правильна відповідь. - «TS
privateприховав поле». Виглядає як справжня інкапсуляція, а насправдіprivateзникає при компіляції — у рантаймі поле доступне. Гарантовану приватність дає лише#. - Звернення до
thisдоsuper(). Виглядає як дивнийReferenceErrorна порожньому місці, а насправді в конструкторі підкласу до викликуsuper()thisще не існує. - Глибока ієрархія
extendsзаради перевикористання. Виглядає як економія коду, а насправді жорстка зв'язка: зміна в корені ламає всі гілки. Часто правильна відповідь — композиція.
Підсумок
thisвизначається в момент виклику за тим, як викликали функцію, а не де її оголосили; стрілочні функції — виняток, вони берутьthisлексично.- Метод, відірваний від об'єкта, губить
this; повертають контекстbind(нова функція),call/apply(виклик негайно) або стрілковий метод-поле. staticналежить класу, а не екземпляру;#field— справжня приватність у рантаймі, тоді як TypeScriptprivateіснує лише під час компіляції.- Класи — цукор над прототипами: звичайні методи лежать на прототипі (спільні для всіх екземплярів),
extendsзшиває ланцюжки прототипів,instanceofперевіряє цей ланцюжок. - Page Object — це клас, і для перевикористання композиція компонентів зазвичай краща за глибоке успадкування.
Можливі питання
- «Як визначається
thisу JavaScript?» Інтерв'юер хоче почути головне:thisзалежить від місця виклику, а не оголошення. Сильна відповідь перелічує чотири правила (метод, простий виклик,new,call/bind) і окремо — лексичнийthisстрілок. - «Чим відрізняються
call,applyіbind?» Дивляться, чи розумієш, щоbindповертає нову функцію, аcall/applyвикликають негайно, і що різниця міжcallіapply— у форматі аргументів. - «Чому метод падає з
undefined, коли його передали в колбек?» Класична перевірка на розуміння втрати контексту. Плюс — назвати два способи це виправити. privateу TypeScript проти#. Перевіряють, чи знаєш, що TS-приватність — це лише перевірка компілятора, а#тримається в рантаймі.- «Класи — це справжнє ООП чи цукор?» Хочуть почути про прототипи: методи на
prototype, ланцюжок пошуку, як працюєextendsіinstanceof. - «Композиція чи успадкування для page object?» Оцінюють інженерну зрілість: чи не тягнеш глибоку ієрархію туди, де достатньо композиції компонентів.
Джерела
- MDN — this —
thisвизначає спосіб виклику, а не місце оголошення; у строгому режимі відірваний метод дістаєundefined,call/applyзадають контекст на виклик,bind— назавжди. - MDN — Arrow function expressions — стрілка не створює власної привʼязки
this,call/bindна неї не діють, а виняток — стрілка як поле класу. - MDN — Classes (JavaScript reference) — класи як «special functions»: тіло завжди в строгому режимі,
super()передthis, методи екземпляра на прототипі,static— на класі.
Втрата контексту і bind/call/apply
- MDN — this —
thisвизначає спосіб виклику, а не місце оголошення; у строгому режимі відірваний метод дістаєundefined,call/applyзадають контекст на виклик,bind— назавжди. - MDN — Arrow function expressions — стрілка не створює власної привʼязки
this,call/bindна неї не діють, а виняток — стрілка як поле класу. - MDN — Closures — кожен екземпляр функції тримає власну область, тому методи
should normallyлежати на прототипі, а не створюватися заново.
Класи: constructor, методи, static
- MDN — Classes (JavaScript reference) — класи як «special functions»: тіло завжди в строгому режимі,
super()передthis, методи екземпляра на прототипі,static— на класі.
extends і super: успадкування з мірою
- MDN — Classes (JavaScript reference) — класи як «special functions»: тіло завжди в строгому режимі,
super()передthis, методи екземпляра на прототипі,static— на класі. - MDN — Inheritance and the prototype chain — ланцюг прототипів до
null, пошук властивості вгору по ньому й те, що класи нової моделі успадкування не вводять. - Wikipedia — Fragile base class — : безпечна на вигляд зміна в базі ламає нащадків, і оглядом самої бази це не встановлюється.
- Martin Fowler — PageObject — page object обгортає сторінку або фрагмент застосунковим API; обʼєкти будують на значущі елементи, а не на сторінку цілком.
- Selenium — Page object models — компонентні обʼєкти: застосунок будують із компонентів сторінки, і рівно одне місце знає структуру її HTML.
Геттери, сеттери і приватні поля
- MDN — Classes (JavaScript reference) — класи як «special functions»: тіло завжди в строгому режимі,
super()передthis, методи екземпляра на прототипі,static— на класі. - MDN — Private elements (#) — приватність
#забезпечує сама мова: звернення ззовні — помилка, а неundefined. - TypeScript Handbook — Classes —
private/protectedдіють лише під час перевірки типів (soft private);#компілюється вhard private.
- MDN — Inheritance and the prototype chain — ланцюг прототипів до
null, пошук властивості вгору по ньому й те, що класи нової моделі успадкування не вводять. - MDN — Classes (JavaScript reference) — класи як «special functions»: тіло завжди в строгому режимі,
super()передthis, методи екземпляра на прототипі,static— на класі. - MDN — Working with objects (JavaScript Guide) — кожен обʼєкт успадковує щонайменше від одного іншого.
- TypeScript Handbook — Narrowing —
instanceofперевіряє, чи містить ланцюг прототипів значенняFoo.prototype.
- MDN — Classes (JavaScript reference) — класи як «special functions»: тіло завжди в строгому режимі,
super()передthis, методи екземпляра на прототипі,static— на класі. - MDN — Inheritance and the prototype chain — ланцюг прототипів до
null, пошук властивості вгору по ньому й те, що класи нової моделі успадкування не вводять. - TypeScript Handbook — Classes —
private/protectedдіють лише під час перевірки типів (soft private);#компілюється вhard private. - Martin Fowler — PageObject — page object обгортає сторінку або фрагмент застосунковим API; обʼєкти будують на значущі елементи, а не на сторінку цілком.
- Selenium — Page object models — компонентні обʼєкти: застосунок будують із компонентів сторінки, і рівно одне місце знає структуру її HTML.
- Playwright — Page Object Models — page object як класи, що інкапсулюють операції на сторінці й тримають селектори в одному місці.
- MDN — this —
thisвизначає спосіб виклику, а не місце оголошення; у строгому режимі відірваний метод дістаєundefined,call/applyзадають контекст на виклик,bind— назавжди. - MDN — Classes (JavaScript reference) — класи як «special functions»: тіло завжди в строгому режимі,
super()передthis, методи екземпляра на прототипі,static— на класі. - MDN — Arrow function expressions — стрілка не створює власної привʼязки
this,call/bindна неї не діють, а виняток — стрілка як поле класу. - TypeScript Handbook — Classes —
private/protectedдіють лише під час перевірки типів (soft private);#компілюється вhard private. - Wikipedia — Fragile base class — крихкий базовий клас: безпечна на вигляд зміна в базі ламає нащадків, і оглядом самої бази це не встановлюється.
- MDN — this —
thisвизначає спосіб виклику, а не місце оголошення; у строгому режимі відірваний метод дістаєundefined,call/applyзадають контекст на виклик,bind— назавжди. - MDN — Arrow function expressions — стрілка не створює власної привʼязки
this,call/bindна неї не діють, а виняток — стрілка як поле класу. - MDN — Classes (JavaScript reference) — класи як «special functions»: тіло завжди в строгому режимі,
super()передthis, методи екземпляра на прототипі,static— на класі. - MDN — Private elements (#) — приватність
#забезпечує сама мова: звернення ззовні — помилка, а неundefined. - TypeScript Handbook — Classes —
private/protectedдіють лише під час перевірки типів (soft private);#компілюється вhard private. - MDN — Inheritance and the prototype chain — ланцюг прототипів до
null, пошук властивості вгору по ньому й те, що класи нової моделі успадкування не вводять. - Martin Fowler — PageObject — page object обгортає сторінку або фрагмент застосунковим API; обʼєкти будують на значущі елементи, а не на сторінку цілком.
- MDN — this —
thisвизначає спосіб виклику, а не місце оголошення; у строгому режимі відірваний метод дістаєundefined,call/applyзадають контекст на виклик,bind— назавжди. - MDN — Arrow function expressions — стрілка не створює власної привʼязки
this,call/bindна неї не діють, а виняток — стрілка як поле класу. - TypeScript Handbook — Classes —
private/protectedдіють лише під час перевірки типів (soft private);#компілюється вhard private. - MDN — Private elements (#) — приватність
#забезпечує сама мова: звернення ззовні — помилка, а неundefined. - MDN — Inheritance and the prototype chain — ланцюг прототипів до
null, пошук властивості вгору по ньому й те, що класи нової моделі успадкування не вводять. - Martin Fowler — PageObject — page object обгортає сторінку або фрагмент застосунковим API; обʼєкти будують на значущі елементи, а не на сторінку цілком.
Від чого залежить значення this у JavaScript?
this — це не фіксоване посилання, прив'язане до тіла функції, а значення, яке підставляється щоразу під час виклику. Ключова ідея: важливо не де функцію написали, а як її покликали — тобто місце виклику (call-site). Одну й ту саму функцію можна викликати п'ятьма різними способами й отримати п'ять різних this. Тому, читаючи код, дивись не на оголошення, а на рядок, де стоять дужки виклику. Для AQA це не абстракція: саме тут ховається половина падінь методів .
Перелічи правила прив'язки this та їхній пріоритет.
Основних правил чотири. Виклик як метод (obj.method()) дає this, що дорівнює об'єкту зліва від крапки. Простий виклик (fn()) у строгому режимі дає undefined. Виклик через new створює новий об'єкт і робить його значенням this. Явна прив'язка (call, apply, bind) підставляє те, що передали першим аргументом. Коли на один виклик претендує кілька правил, старшинство таке: new перемагає явну прив'язку, та перемагає метод, а метод перемагає простий виклик. стоять осторонь цієї ієрархії — вони взагалі не мають власного this.
Чому стрілочні функції поводяться з this інакше?
Стрілка не заводить власного this — вона підхоплює його лексично, з того оточення, де її написали, і більше ніколи не змінює. Це означає, що жоден call, apply чи bind не здатен перепризначити this стрілки: спроба просто ігнорується. Наслідок практичний: стрілочна функція запам'ятовує контекст назавжди в момент оголошення, тому її безпечно передавати як . Саме тому стрілковий метод-поле в класі — головний робочий спосіб не загубити прив'язку до екземпляра. Це не примха мови, а інструмент проти найпоширенішої пастки з this.
Метод page object падає з TypeError: Cannot read properties of undefined. Що це зазвичай означає?
У переважній більшості випадків метод відірвали від свого об'єкта, і всередині нього this став undefined. Виглядає як баг застосунку чи зламаний , а насправді функцію передали кудись як значення — у колбек обробника події, аргументом у setTimeout, у forEach — і викликали вже без крапки й об'єкта зліва. Прив'язка this вирішується під час виклику, тож щойно зникає obj. перед викликом, зникає й контекст. Звернення на кшталт this.page після цього падає, бо undefined не має полів. Перед заведенням дефекту тут варто перевірити не продукт, а те, як саме викликали метод.
Чим відрізняються call, apply і bind?
Усі три задають this явно, але по-різному щодо моменту й формату. call викликає функцію негайно, а додаткові аргументи перелічують через кому. apply теж викликає негайно, але аргументи передають одним масивом. bind нічого не викликає — він повертає нову функцію з навіки зафіксованим this, яку можна зберегти й покликати пізніше. Мнемоніка проста: call — comma, apply — array, bind — на потім. У тестовому коді bind руками пишуть нечасто, бо той самий ефект дає стрілковий метод-поле.
Як повернути this методу, який передають у колбек?
Є два робочі способи. Перший — обгорнути метод у bind: obj.method.bind(obj) повертає нову функцію із зафіксованим контекстом, і її вже можна безпечно передавати. Другий, сучасніший — оголосити метод як стрілкове поле класу (method = () => { ... }), тоді його this лексично прив'язаний до екземпляра й не губиться при відриві. Обидва рішення прибирають TypeError. У сучасному тестовому коді частіше беруть стрілкове поле, бо воно читабельніше й не потребує пам'ятати про прив'язку на кожному колбеку.
Стрілковий метод-поле й звичайний метод класу — де вони живуть у пам'яті?
Різниця тут не термінологічна, а в пам'яті. Звичайний метод кладеться один раз у прототип класу, і всі екземпляри посилаються на цю єдину копію. Стрілкове поле-метод натомість створюється заново в кожному екземплярі — це вже не спільний метод на прототипі, а окреме поле об'єкта. Для десятка page object різниця несуттєва, і зручність стрілки переважує. Але якщо об'єктів тисячі й вони живуть у гарячому циклі, кожна зайва копія методу — це витрата пам'яті, і тут аргумент уже на боці звичайного методу з bind.
Що таке constructor і що зазвичай отримує конструктор page object?
constructor — спеціальний метод, який автоматично спрацьовує під час new і готує свіжий екземпляр до роботи. Його типова робота — прийняти залежності й розкласти їх по полях. У page object конструктор майже завжди приймає об'єкт сторінки Playwright (page) і зберігає його, щоб методи потім могли ним користуватися. У TypeScript це часто записують коротко через параметр-властивість (private readonly page: Page прямо в дужках конструктора) — це оголошує поле й одразу присвоює йому аргумент. Усередині конструктора this — це новостворений об'єкт, який new щойно виготовив.
Навіщо потрібне ключове слово static і статичні фабричні методи?
static робить член належним самому класу, а не його екземплярам — до нього звертаються через ім'я класу, а не через this об'єкта. Типове застосування — константи й фабричні методи, які інкапсулюють складне створення об'єкта. Наприклад, статичний метод може спершу залогінитися, дістати токен, а вже потім повернути готовий екземпляр клієнта. У парі з приватним конструктором це дає гарантію, що об'єкт не створять в недобудованому стані — єдиний легальний шлях створення проходить через фабрику. Для API-клієнтів у тестах це акуратний спосіб не дозволити зробити клієнта без авторизації.
Що роблять extends і super і чому не можна чіпати this до super()?
extends створює підклас, який успадковує поля й методи батьківського класу. super() у конструкторі викликає конструктор батька, а super.method() дає доступ до батьківської версії перевизначеного методу. Правило, на якому ловлять новачків: у конструкторі підкласу до виклику super() звертатися до this заборонено — це кине ReferenceError. Причина в тому, що екземпляр ще фізично не побудований, поки батьківський конструктор не відпрацював. Тобто спершу будуємо батьківську частину об'єкта через super(), і лише потім this існує й ним можна користуватися.
Композиція чи успадкування для page object — що обрати?
За замовчуванням варто схилятися до композиції, а глибокі ієрархії extends тримати під підозрою. Неглибокий базовий клас зі справді спільною механікою (базовий URL, спільне очікування) — нормальна річ. Проблеми починаються з глибоких ланцюгів на кшталт базова → авторизована → адмінська → сторінка користувачів: зміна в корені ламає всі гілки, а логіка розмазана рівнями, і незрозуміло, звідки взявся метод. Композиція розв'язує це через has-a замість is-a: сторінка не успадковує хедер і таблицю, а містить їх окремими об'єктами-компонентами в полях. Такі компоненти легко перевикористати будь-де, тоді як успадкування прив'язує намертво.
Що таке геттери й сеттери і навіщо вони в класі?
Геттер (getter, оголошується через get) і сеттер (setter, через set) — це методи, до яких звертаються як до звичайних властивостей, без дужок виклику. Зовні cart.size виглядає як просте поле, а насправді щоразу під капотом виконується функція. Це зручно для обчислюваних або похідних значень, які не хочеться зберігати окремо й тримати в синхронізації. Геттер може, наприклад, повертати довжину прихованого приватного масиву, а сеттер — валідувати значення перед присвоєнням. Синтаксично це виглядає як звичайне поле, тому користувач класу навіть не помічає, що працює логіка.
Чим #-приватне поле відрізняється від private у TypeScript?
Різниця в тому, коли саме діє приватність. #field — це справжня інкапсуляція на рівні мови: до нього не можна звернутися ззовні класу навіть у зібраному JavaScript, будь-яка спроба — синтаксична помилка ще до запуску. Модифікатор private з TypeScript існує лише під час компіляції: він допомагає редактору й компілятору, але в скомпільованому JS поле лишається звичайним і доступним, наприклад через obj['field']. Тобто private — це домовленість і підказка інструменту, а # — жорсткий бар'єр у рантаймі. Для більшості тестового коду достатньо private; # беруть там, де приватність треба гарантувати самою мовою, а не дисципліною.
Що таке прототип і ланцюжок прототипів?
Кожен об'єкт у JavaScript має прихований зв'язок ([[Prototype]]) на інший об'єкт — свій прототип. Коли ти звертаєшся до властивості чи методу, шукає його спершу в самому об'єкті; не знайшовши — йде по цьому зв'язку вгору, до прототипу, потім до прототипу прототипу, і так аж до null. Ця послідовність об'єктів і є ланцюжок прототипів (prototype chain). Саме він пояснює, звідки береться метод, якого в самому об'єкті немає — його знайшли на одному з верхніх рівнів. Розуміння цього механізму знімає магію і з instanceof, і з успадкування.
Де насправді живуть методи класу — у кожному екземплярі чи спільно?
Звичайні методи класу лежать не в кожному екземплярі, а один раз — у КласName.prototype. Усі створені об'єкти посилаються на цей єдиний об'єкт як на свій прототип і беруть методи звідти через ланцюжок пошуку. Тому в пам'яті метод один, скільки б екземплярів ти не наплодив — його не копіюють у кожен об'єкт. Виняток — стрілкові поля-методи: вони таки лежать окремою копією в кожному екземплярі, бо це поля, а не методи прототипу. Ця відмінність — прямий наслідок того, як класи влаштовані поверх прототипів.
Класи в JavaScript — це справжнє ООП чи синтаксичний цукор?
Значною мірою це цукор над прототипами: за фасадом class стоїть той самий прототипний механізм, що й раніше. Звичайні методи лягають на прототип, extends просто зшиває два прототипні ланцюжки, а instanceof перевіряє, чи трапляється потрібний прототип у ланцюжку об'єкта. Водночас класи додають і власну семантику, якої немає в голих прототипах: #, строгий режим у тілі за замовчуванням, заборону викликати клас без new. Тому чесна відповідь — це цукор, але не порожній: синтаксис зручніший і має кілька справжніх нових можливостей. Сильний кандидат згадає і механізм прототипів, і ці додаткові речі.
Як працює instanceof?
instanceof не порівнює типи як рядки — він перевіряє ланцюжок прототипів. Вираз obj instanceof Клас повертає true, якщо Клас.prototype трапляється десь у ланцюжку прототипів об'єкта. Тому для підкласу перевірка проти батьківського класу теж дає true: extends вставив прототип батька в ланцюжок, і instanceof його там знаходить. Це прямий наслідок того, як влаштоване успадкування через прототипи. Знання цього механізму пояснює, чому одна перевірка instanceof може бути правдивою одразу для кількох класів у ієрархії.
Чому кажуть, що Page Object — це «просто клас»?
Бо коли розібратися в this, класах і прототипах, у Page Object не лишається магії. Клас із конструктором приймає page залежністю й кладе в поле; this усередині методів указує на екземпляр сторінки, поки метод кличуть через крапку; методи-дії лежать на прототипі спільно для всіх екземплярів; композиція компонентів масштабує структуру краще за глибоке успадкування; а private чи # ховають локатори всередині, лишаючи назовні лише осмислені дії. Тобто патерн тримається рівно на мовних механізмах, які ми розібрали, і читається як звичайна конструкція. Межі самого патерну (, перевірки всередині page object) — це вже окрема інженерна тема, але його мова — це this і класи.
Три ситуації, у яких this, класи й прототипи вирішують, зелений тест чи години гадань: як метод губить контекст у й два способи це полагодити, як прочитати this за місцем виклику через , і як глибоку ієрархію extends перескласти на композицію компонентів. Скрізь — що дивитися й чому.
Кейс 1. Метод page object губить this у колбеку
Класична сцена: метод сторінки раптом падає з TypeError, і виглядає це як баг застосунку. Насправді метод відірвали від об'єкта, і this став undefined. Ось спрощений відбиток проблеми — метод передали як значення в forEach:
class SearchPage {
private readonly results: string[] = [];
constructor(private readonly page: Page) {}
collect(title: string): void {
this.results.push(title); // this потрібен на екземпляр
}
async collectAll(titles: string[]): Promise<void> {
// метод відірвали від об'єкта — this всередині стане undefined
titles.forEach(this.collect);
// TypeError: Cannot read properties of undefined (reading 'results')
}
}
forEach викликає передану функцію просто як fn(item), без об'єкта зліва від крапки. Прив'язка this вирішується під час виклику, тож this.collect — це вже гола функція без контексту, і всередині this дорівнює undefined.
Спосіб перший — зберегти прив'язку явно, через стрілку на місці виклику або bind:
titles.forEach((title) => this.collect(title)); // стрілка бере this лексично
titles.forEach(this.collect.bind(this)); // bind фіксує контекст
Спосіб другий, який прибирає проблему в корені, — оголосити сам метод як стрілкове поле класу. Тоді його this навіки прив'язаний до екземпляра, і метод можна відривати й передавати куди завгодно:
class SearchPage {
private readonly results: string[] = [];
collect = (title: string): void => {
this.results.push(title); // this завжди — екземпляр, хай як викликають
};
}
Що дивитися й чому:
- Симптом оманливий.
TypeError: Cannot read properties of undefinedвиглядає як зламаний чи падіння продукту, а насправді питання лише в тому, як покликали метод. Перш ніж заводити дефект — перевір call-site. - Стрілка на місці виклику vs стрілкове поле. Обгортка
(title) => this.collect(title)лагодить один конкретний виклик; стрілкове поле лагодить метод раз і назавжди, тож для колбек-важких класів воно зручніше. - У стрілкового поля є ціна. Це вже не спільний метод на прототипі, а окрема копія в кожному екземплярі. Для десятка page object байдуже, але саме про це можуть спитати на співбесіді: «де живе цей метод?».
Кейс 2. Таблиця рішень: як прочитати this за місцем виклику
Коли метод поводиться дивно, не вгадуй — прочитай this за одним рядком, де стоять дужки виклику. Порядок перевірки згори вниз, перше збіжне правило виграє:
| Що бачиш на місці виклику | Значення this | Приклад |
|---|---|---|
| Стрілочна функція | З оточення, де оголошено (лексично) | arr.map(x => this.check(x)) |
fn.call(o) / fn.apply(o) / fn.bind(o) | Об'єкт, який передали | open.call(loginPage) |
new Fn() | Новий створений об'єкт | new LoginPage(page) |
| Є об'єкт зліва від крапки | Той об'єкт | loginPage.open() → loginPage |
| Простий виклик, нічого зліва | undefined (строгий режим) | const f = obj.m; f() |
Кілька коротких перевірок, щоб таблиця осіла:
class Toolbar {
label = 'Menu';
show() { return this.label; }
}
const bar = new Toolbar();
bar.show(); // 'Menu' — об'єкт зліва від крапки
const detached = bar.show;
detached(); // впаде — простий виклик, this = undefined
bar.show.call({ label: 'X' }); // 'X' — явна прив'язка перемагає метод
Що дивитися й чому:
- Дивись на виклик, не на оголошення. Одна й та сама
showдає то'Menu', то падіння, то'X'— усе вирішує рядок виклику, а не тіло функції. - Пріоритет має значення. У
bar.show.call(...)є і крапка, іcall; виграє явна прив'язка, бо вона старша за виклик-метод.newстарший за все, крім лексичногоthisстрілки. - Строгий режим — стан за замовчуванням. Тіло класу й ES-модулі завжди строгі, тож простий виклик дає
undefined, а не глобальний об'єкт — і саме тому падає, а не тихо працює з чужим контекстом.
Кейс 3. Від глибокого extends до композиції компонентів
Спокуса перевикористати код через успадкування швидко породжує ланцюг на кшталт базова сторінка → авторизована → адмінська → сторінка користувачів. Спершу зручно, а далі — зміна в корені ламає всі гілки, і незрозуміло, з якого рівня взявся метод. Ось як така сторінка виглядає до рефакторингу:
// крихка ієрархія: логіка розмазана рівнями
class BasePage { /* goto, waitForReady */ }
class AuthPage extends BasePage { /* logout у хедері */ }
class AdminPage extends AuthPage { /* сортування таблиці */ }
class UsersPage extends AdminPage { /* дії саме над користувачами */ }
Проблема не в синтаксисі, а в жорсткій зв'язці is-a: UsersPage тягне за собою всі рівні, і хедер із таблицею неможливо перевикористати на іншій сторінці, не потягнувши всю гілку. Композиція розв'язує це через has-a — сторінка не успадковує хедер і таблицю, а містить їх окремими об'єктами в полях:
class HeaderComponent {
constructor(private readonly page: Page) {}
async logout(): Promise<void> { /* дії хедера */ }
}
class DataGrid {
constructor(private readonly page: Page) {}
async sortBy(column: string): Promise<void> { /* дії таблиці */ }
}
class UsersPage {
readonly header: HeaderComponent;
readonly grid: DataGrid;
constructor(private readonly page: Page) {
this.header = new HeaderComponent(page);
this.grid = new DataGrid(page);
}
}
// у тесті читається прозоро, видно звідки дія:
await usersPage.header.logout();
await usersPage.grid.sortBy('Name');
Що дивитися й чому:
has-aзамістьis-a.HeaderComponentіDataGrid— самостійні класи, які кладуть на будь-яку сторінку з хедером чи таблицею, не будуючи ієрархію. Успадкування прив'язує намертво, композицію легко перескладати.- Неглибокий базовий клас — це ще норма. Композиція проти успадкування не означає «ніколи не успадковуй»: один тонкий шар зі справді спільною механікою (базовий URL, спільне очікування) прийнятний. Болять саме глибокі ланцюги.
- Видно, звідки метод. У
usersPage.grid.sortBy(...)одразу читається, що сортування — робота таблиці, а не загадкового предка за три рівні вгору. Це прямий виграш у читабельності тесту.
Як визначається this
- Розумію, що
thisпідставляється в момент виклику за тим, як функцію покликали, а не там, де її оголосили. - Можу назвати чотири правила прив'язки (метод через крапку, простий виклик,
new, явна) та їхній пріоритет:new→ явна → метод → простий виклик. - Пам'ятаю, що при простому виклику в строгому режимі
thisдорівнюєundefined, а тіло класу й модулі завжди строгі. - Можу пояснити, чому бере
thisлексично й чомуcall/bindїї не перепризначують.
Втрата контексту і bind/call/apply
- Розумію, чому відірваний від об'єкта метод губить
thisі падає зTypeError: Cannot read properties of undefined. - Знаю типові місця втрати контексту: обробника,
setTimeout,forEach, підписка на емітер. - Можу пояснити різницю
call(аргументи через кому, виклик негайно),apply(аргументи масивом),bind(нова функція на потім). - Знаю два способи повернути контекст:
bindабо стрілкове поле-метод, і чому в сучасному коді частіше беруть друге. - Розумію, що стрілкове поле-метод — окрема копія в кожному екземплярі, а не спільний метод на прототипі.
Класи: constructor, static, extends
- Можу пояснити, що робить
constructorпід часnew, чому приймає нимpage, і що таке параметр-властивість (private readonly page: Pageу дужках конструктора). - Розумію, що
staticналежить класу, а не екземпляру, і навіщо статичні фабрики з приватним конструктором. - Знаю правило: у конструкторі підкласу до
super()не можна звертатися доthis— будеReferenceError. - Можу пояснити, чому для page object композиція компонентів зазвичай краща за глибоке успадкування
extends.
Інкапсуляція: геттери, сеттери, приватні поля
- Розумію, що геттер/сеттер (
get/set) викликають без дужок, як звичайне поле, а насправді виконується функція. - Можу пояснити різницю
#fieldvsprivate: перший — бар'єр у рантаймі, другий зникає при компіляції (у зібраному JS поле доступне черезobj['field']). - Розумію, коли достатньо
private, а коли приватність треба гарантувати самою мовою через#.
Прототипи під капотом
- Можу пояснити, що таке
[[Prototype]]і ланцюжок прототипів (пошук властивості вгору доnull). - Знаю, що звичайні методи лежать один раз на
prototype, спільні для всіх екземплярів. - Розумію, що класи — цукор над прототипами, який додає
#, строгий режим і заборону виклику безnew. - Можу пояснити, як
extendsзшиває ланцюжки прототипів і чомуinstanceofперевіряє саме цей ланцюжок.
Квіз
Перед стартом
- Питань: 15
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Від чого залежить this у звичайній функції?