Треки
/
JavaScript
JavaScript
/
Вправи
/
Віконна система
Віконна система

Віконна система

Навчальна вправа

Вступ

JavaScript має можливості для обʼєктно-орієнтованого програмування (ООП). В ООП ми хочемо створювати обʼєкти (екземпляри) з «шаблонів» (класів), щоб вони містили певні дані та функціональність. Властивості з даними в контексті ООП називають полями, а властивості-функції - методами.

До того, як 2015 року класи додали до специфікації мови, у JavaScript їх не було взагалі, але він дозволяв обʼєктно-орієнтоване програмування через успадкування на основі прототипів. І хоча нині вже доступне ключове слово class, JavaScript усе ще залишається прототипною мовою.

Щоб зрозуміти, що означає бути прототипною мовою і як JavaScript насправді працює, повернімося в часи, коли класів ще не було.

Синтаксис прототипів

Функція-конструктор

У JavaScript роль шаблону (класу) виконує звичайна функція. Коли функцію мають використовувати як такий шаблон, її називають функцією-конструктором, і за домовленістю її назва має починатися з великої літери. Екземпляри (обʼєкти) створюються з шаблону за допомогою ключового слова new під час виклику функції-конструктора.

function Car() {
  // ...
}

const myCar = new Car();
const yourCar = new Car();

Важливо зауважити, що в JavaScript екземпляри й функція-конструктор зберігають звʼязок один з одним навіть після створення екземплярів. Кожен обʼєкт-екземпляр містить приховану внутрішню властивість, яку в специфікації мови позначено як [[prototype]]. Вона зберігає посилання на значення ключа prototype функції-конструктора. Так, усе правильно: функція JavaScript може мати пари ключ-значення, адже за лаштунками вона також є обʼєктом.

Note

Підсумуймо:

  • Конструктори в JavaScript є звичайними функціями.
  • Створення нового екземпляра дає обʼєкт зі звʼязком зі своїм конструктором, який називають його прототипом.
  • Функції є обʼєктами (обʼєктами, які можна викликати), а отже, можуть мати властивості.
  • Властивість prototype функції-конструктора стане прототипом екземпляра.

Поля екземпляра

Часто ми хочемо, щоб усі похідні обʼєкти (екземпляри) містили певні поля, і під час створення обʼєкта передаємо для них початкові значення. Цього можна досягти за допомогою ключового слова this. Усередині функції-конструктора this позначає новий обʼєкт, який буде створено через new. this автоматично повертається з функції-конструктора, коли її викликають з new.

Це означає, що ми можемо додавати поля до нового екземпляра, додаючи їх до this у функції-конструкторі.

function Car(color, weight) {
  this.color = color;
  this.weight = weight;
  this.engineRunning = false;
}

const myCar = new Car('red', '2mt');
myCar.color;
// => 'red'
myCar.engineRunning;
// => false

Методи екземпляра

Методи додаються через властивість prototype функції-конструктора. Усередині методу ми можемо отримати доступ до полів екземпляра через this. Це працює завдяки такому загальному правилу.

Коли функцію викликають як метод обʼєкта, її this вказує на обʼєкт, для якого викликано метод. 1

function Car() {
  this.engineRunning = false;
  // ...
}

Car.prototype.startEngine = function () {
  this.engineRunning = true;
};

Car.prototype.addGas = function (litre) {
  // ...
};

const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true

Ланцюжок прототипів

myCar із прикладу вище є звичайним обʼєктом JavaScript, і якби ми його оглянули (наприклад, у консолі браузера), то не знайшли б властивості startEngine зі значенням-функцією безпосередньо в обʼєкті myCar. То як тоді взагалі працює наведений код?

Секрет тут називається ланцюжком прототипів. Коли ми намагаємося отримати доступ до будь-якої властивості (поля чи методу) обʼєкта, JavaScript спершу перевіряє, чи відповідний ключ можна знайти безпосередньо в самому обʼєкті. Якщо ні, він продовжує шукати ключ в обʼєкті, на який посилається властивість [[prototype]] початкового обʼєкта. Як зазначалося раніше, у нашому прикладі [[prototype]] вказує на властивість prototype функції-конструктора. Саме там JavaScript знайде функцію startEngine, бо ми додали її туди.

function Car() {
  // ...
}

Car.prototype.startEngine = function () {
  // ...
};

І ланцюжок на цьому не закінчується. Властивість [[prototype]] обʼєкта Car.prototype (myCar.[[prototype]].[[prototype]]) посилається на Object.prototype (властивість prototype функції-конструктора Object). Вона містить загальні методи, доступні для всіх обʼєктів JavaScript, наприклад toString(). Отже, ми можемо викликати myCar.toString(), і цей метод існуватиме, бо JavaScript шукає його по всьому ланцюжку прототипів.

Caution

Зауважмо, що ланцюжком прототипів проходять лише під час читання значення. Безпосереднє встановлення чи видалення властивості обʼєкта-екземпляра стосується лише цього конкретного екземпляра. Це може бути не тим, на що ми очікуємо, якщо звикли до мови з успадкуванням на основі класів.

Синтаксис класів

Сьогодні JavaScript підтримує визначення класів за допомогою ключового слова class. Це додали до специфікації мови 2015 року. З одного боку, це дає синтаксичний цукор, який робить класи зрозумілішими для читання й написання. Новий синтаксис більше схожий на те, як класи пишуть у таких мовах, як C++ чи Java. Розробникам, які переходять із цих мов, легше адаптуватися. З іншого боку, синтаксис класів відкриває шлях для нових можливостей мови, недоступних у синтаксисі прототипів.

Оголошення класів

У новому синтаксисі класи визначають ключовим словом class, після якого йде назва класу та тіло класу у фігурних дужках. Тіло містить визначення функції-конструктора, тобто спеціального методу з назвою constructor. Ця функція працює так само, як функція-конструктор у синтаксисі прототипів. Тіло класу також містить усі визначення методів. Синтаксис методів подібний до скороченого запису, який ми бачили для додавання функцій як значень усередину обʼєкта, див. Концепція «Обʼєкти».

class Car {
  constructor(color, weight) {
    this.color = color;
    this.weight = weight;
    this.engineRunning = false;
  }

  startEngine() {
    this.engineRunning = true;
  }

  addGas(litre) {
    // ...
  }
}

const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true

Памʼятаймо, що за лаштунками JavaScript усе ще залишається прототипною мовою. Усі механізми, про які ми дізналися в розділі «Синтаксис прототипів» вище, усе ще діють.

Приватні поля, гетери й сетери

Типово всі поля екземпляра в JavaScript є публічними. До них можна безпосередньо звертатися й присвоювати їм значення.

Однак існує усталена домовленість, що поля й методи, які починаються з підкреслення, слід вважати приватними. До них ніколи не слід звертатися безпосередньо ззовні класу.

Приватні поля іноді супроводжують гетери і сетери. За допомогою ключових слів get і set ми можемо визначати функції, які виконуються, коли звертаються до властивості з тією ж назвою, що й у функції, або присвоюють їй значення.

class Car {
  constructor() {
    this._mileage = 0;
  }

  get mileage() {
    return this._mileage;
  }

  set mileage(value) {
    throw new Error(`Mileage cannot be manipulated, ${value} is ignored.`);
    // Just an example, usually you would not provide a setter in this case.
  }
}

const myCar = new Car();
myCar.mileage;
// => 0
myCar.mileage = 100;
// => Error: Mileage cannot be manipulated, 100 is ignored.

  1. Приклади this - як метод обʼєкта, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method (дата звернення: 3 грудня 2021 р.) ↩

Вказівки

У цій вправі ми змоделюємо компʼютерну систему, побудовану на вікнах. Ми створимо кілька вікон, які можна переміщати та змінювати в розмірі. Зображення нижче ілюструє значення, з якими ми будемо працювати далі.

                  <--------------------- screenSize.width --------------------->

       ^          ┌────────────────────────────────────────────────────────────┐
       |          │                                                            │
       |          │         position.x, _                                      │
       |          │         position.y   \                                     │
       |          │                       \<----- size.width ----->            │
       |          │                 ^      *──────────────────────┐            │
       |          │                 |      │        title         │            │
       |          │                 |      ├──────────────────────┤            │
screenSize.height │                 |      │                      │            │
       |          │            size.height │                      │            │
       |          │                 |      │       contents       │            │
       |          │                 |      │                      │            │
       |          │                 |      │                      │            │
       |          │                 v      └──────────────────────┘            │
       |          │                                                            │
       |          │                                                            │
       v          └────────────────────────────────────────────────────────────┘

📣 Щоб потренувати свої різноманітні навички JavaScript, спробуйте розвʼязати завдання 1 і 2 із прототипним синтаксисом, а решту завдань - із класовим синтаксисом.

1. Визначте Size для зберігання розмірів вікна

Визначте клас (функцію-конструктор) з назвою Size. Він повинен мати два поля, width і height, які зберігають поточні розміри вікна. Функція-конструктор повинна приймати початкові значення для цих полів. Ширина передається як перший параметр, а висота - як другий. Типова ширина та висота повинні бути 80 і 60 відповідно.

Крім того, визначте метод resize(newWidth, newHeight), який приймає нову ширину та висоту як параметри й змінює поля відповідно до нового розміру.

const size = new Size(1080, 764);
size.width;
// => 1080
size.height;
// => 764

size.resize(1920, 1080);
size.width;
// => 1920
size.height;
// => 1080

2. Визначте Position для зберігання позиції вікна

Визначте клас (функцію-конструктор) з назвою Position із двома полями x і y, які зберігають відповідно поточну горизонтальну та вертикальну позицію верхнього лівого кута вікна. Функція-конструктор повинна приймати початкові значення для цих полів. Значення x передається як перший параметр, а значення y - як другий. Типовим значенням для обох полів повинен бути 0.

Позиція (0, 0) - це верхній лівий кут екрана, у якому значення x зростають у міру руху праворуч, а значення y зростають у міру руху вниз.

Також визначте метод move(newX, newY), який приймає нові параметри x та y і змінює властивості відповідно до нової позиції.

const point = new Position();
point.x;
// => 0
point.y;
// => 0

point.move(100, 200);
point.x;
// => 100
point.y;
// => 200

3. Визначте клас ProgramWindow

Визначте клас ProgramWindow з такими полями:

  • screenSize: містить фіксоване значення типу Size зі значеннями width 800 і height 600
  • size: містить значення типу Size, початковим значенням є типове значення екземпляра Size
  • position: містить значення типу Position, початковим значенням є типове значення екземпляра Position

Коли вікно відкривається (створюється), воно завжди має типові розмір і позицію.

const programWindow = new ProgramWindow();
programWindow.screenSize.width;
// => 800

// Similar for the other fields.

Примітка: назву ProgramWindow використано замість Window, щоб відрізнити цей клас від вбудованого класу Window, який існує в середовищі браузера.

4. Додайте метод для зміни розміру вікна

Клас ProgramWindow повинен мати метод resize. Він повинен приймати параметр типу Size як вхідні дані та намагатися змінити розмір вікна до вказаного.

Однак новий розмір не може перевищувати певні межі.

  • Мінімальна дозволена висота або ширина становить 1. Запитана висота або ширина, менша за 1, буде зведена до 1.
  • Максимальні висота та ширина залежать від поточної позиції вікна; краї вікна не можуть виходити за краї екрана. Значення, більші за ці межі, будуть зведені до найбільшого розміру, який вони можуть мати. Наприклад, якщо позиція вікна становить x = 400, y = 300 і запитується зміна розміру до height = 400, width = 300, то вікно буде змінено до height = 300, width = 300, оскільки екран недостатньо великий у напрямку y, щоб повністю задовольнити цей запит.
const programWindow = new ProgramWindow();

const newSize = new Size(600, 400);
programWindow.resize(newSize);
programWindow.size.width;
// => 600
programWindow.size.height;
// => 400

5. Додайте метод для переміщення вікна

Крім можливості змінювати розмір, клас ProgramWindow також повинен мати метод move. Він повинен приймати параметр типу Position як вхідні дані. Метод move схожий на resize, однак цей метод змінює позицію вікна відповідно до запитаного значення, а не розмір.

Як і у випадку з resize, нова позиція не може перевищувати певні межі.

  • Найменша позиція становить 0 для обох значень x і y.
  • Максимальна позиція в кожному з напрямків залежить від поточного розміру вікна. Краї не можуть виходити за краї екрана. Значення, більші за ці межі, будуть зведені до найбільшого розміру, який вони можуть мати. Наприклад, якщо розмір вікна становить x = 250, y = 100 і запитується переміщення до x = 600, y = 200, то вікно буде переміщено до x = 550, y = 200, оскільки екран недостатньо великий у напрямку x, щоб повністю задовольнити цей запит.
const programWindow = new ProgramWindow();

const newPosition = new Position(50, 100);
programWindow.move(newPosition);
programWindow.position.x;
// => 50
programWindow.position.y;
// => 100

6. Змініть програмне вікно

Реалізуйте функцію changeWindow, яка приймає екземпляр ProgramWindow як вхідні дані та змінює вікно до вказаних розміру й позиції. Функція повинна повернути той самий екземпляр ProgramWindow, який було передано, після застосування змін.

Вікно повинно отримати ширину 400, висоту 300 і розташуватися за x = 100, y = 150.

const programWindow = new ProgramWindow();
changeWindow(programWindow);
programWindow.size.width;
// => 400

// Similar for the other fields.
Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
JavaScript Exercism

Час розпочати Віконна система?

Зареєструйтеся на Exercism, щоб вивчати й опановувати JavaScript, а також 37 концепцій159 вправ та справжнє наставництво від людей, і все це безкоштовно.