13 березня 2022 р.

Базовий синтаксис класу

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

Вікіпедія

На практиці ми часто повинні створювати багато об’єктів одного й того ж виду, наприклад користувачів, або товари чи що завгодно.

Як ми вже знаємо з розділу Конструктори, оператор "new", new function може допомогти з цим.

Але в сучасному JavaScript існує більш просунута конструкція “клас”, яка вводить нові чудові функції, які корисні для об’єктно-орієнтованого програмування.

Синтаксис “class”

Базовий синтаксис:

class MyClass {
  // методи класу
  constructor() { ... }
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}

Потім використовуйте new MyClass(), щоб створити новий об’єкт з усіма перерахованими методами.

Метод constructor() викликається автоматично за допомогою new, в ньому ми можемо ініціалізувати об’єкт.

Наприклад:

class User {

  constructor(name) {
    this.name = name;
  }

  sayHi() {
    alert(this.name);
  }

}

// Використання:
let user = new User("Іван");
user.sayHi();

Коли new User("Іван") викликається:

  1. Створюється новий об’єкт.
  2. constructor запускається з даним аргументом і присвоює його в this.name.

…Потім ми можемо викликати методи об’єкту, такі як user.sayHi().

Кома між методами класу не потрібна

Часта помилка розробників-початківців полягає в тому, що ставляться коми між методами класу, що призводить до синтаксичної помилки.

Синтаксис класів не слід плутати з літералами об’єктів. У межах класу не треба ставити кому.

Що таке клас?

Отже, чим насправді є class? Він не є цілком новою сутністю мови програмування, як можна подумати.

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

У JavaScript клас є своєрідною функцією.

Погляньте на це:

class User {
  constructor(name) { this.name = name; }
  sayHi() { alert(this.name); }
}

// доказ: User -- це функція
alert(typeof User); // function

Конструкція class User {...} в дійсності робить наступне:

  1. Створює функцію, що називається User та стає результатом оголошення класу. Код цієї функції береться з методу constructor (вважається порожнім, якщо ми не написали такий метод).
  2. Записує методи класу, такі як sayHi, до User.prototype.

Після того, як new User створився, коли ми викликаємо його метод, він береться з прототипу, як описано в розділі F.prototype. Таким чином, об’єкт має доступ до методів класу.

Ми можемо проілюструвати результат оголошення class User наступним чином:

Ось код, щоб проаналізувати це:

class User {
  constructor(name) { this.name = name; }
  sayHi() { alert(this.name); }
}

// клас -- це функція
alert(typeof User); // function

// ...або, точніше, метод конструктора
alert(User === User.prototype.constructor); // true

// Методи знаходяться в User.prototype, наприклад:
alert(User.prototype.sayHi); // код sayHi методу

// у прототипі існує рівно два методи
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi

Не просто синтаксичний цукор

Іноді люди кажуть, що class – це “синтаксичний цукор” (синтаксис, який призначений для того, щоб полегшити читання, але не додає нічого нового), тому що фактично ми могли б оголосити те саме без використання ключового слова class:

// перепишемо клас User в чистих функціях

// 1. Створимо функцію-конструктор
function User(name) {
  this.name = name;
}
// прототип функції має властивість "constructor" за замовчуванням,
// тому нам не потрібно її створювати

// 2. Додамо метод до прототипу
User.prototype.sayHi = function() {
  alert(this.name);
};

// Використання:
let user = new User("Іван");
user.sayHi();

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

Проте, існують важливі відмінності.

  1. По-перше, функція, що створена за допомогою class, позначена спеціальною внутрішньою властивістю [[IsClassConstructor]]: true. Так що це не зовсім те саме, що створити її вручну.

    Рушій перевіряє цю властивість у різних місцях. Наприклад, на відміну від звичайної функції, її треба викликати з new:

    class User {
      constructor() {}
    }
    
    alert(typeof User); // function
    User(); // Помилка: Конструктор класу User неможливо викликати без 'new'

    Також, рядкове представлення конструктора класу у більшості рушіїв JavaScript починається з “class…”

    class User {
      constructor() {}
    }
    
    alert(User); // class User { ... }

    Є й інші відмінності, ми побачимо їх найближчим часом.

  2. Методи класу неперелічувані. Оголошення класу встановлює прапор enumerable у false для всіх методів в "prototype".

    Це добре тому, що коли ми перебираємо об’єкт за допомогою for..in, ми зазвичай не хочемо обробляти методи класу.

  3. Клас завжди use strict. Весь код всередині конструкції класу автоматично знаходиться в суворому режимі.

Крім того, синтаксис class приносить багато інших особливостей, які ми дослідимо пізніше.

Class Expression

Так само, як функції, класи можуть бути визначені всередині іншого виразу, передані, повернуті, присвоєні тощо.

Ось приклад class expression:

let User = class {
  sayHi() {
    alert("Привіт");
  }
};

Подібно до Named Function Expression, Class Expression може мати назву.

Якщо Class Expression має назву, то її видно лише всередині класу:

// "Named Class Expression"
// (немає такого терміну у специфікації, але це схоже на Named Function Expression)
let User = class MyClass {
  sayHi() {
    alert(MyClass); // назву MyClass видно тільки всередині класу
  }
};

new User().sayHi(); // працює, показує визначення MyClass

alert(MyClass); // помилка, назву MyClass не видно за межами класу

Ми можемо навіть зробити класи динамічними “на вимогу”, наприклад:

function makeClass(phrase) {
  // оголошуємо клас і повертаємо його
  return class {
    sayHi() {
      alert(phrase);
    }
  };
}

// Створюємо новий клас
let User = makeClass("Привіт");

new User().sayHi(); // Привіт

Геттери/сеттери

Подібно до літералів об’єктів, класи можуть включати геттери/сеттери, обчислені атрибути тощо.

Ось приклад для user.name, що реалізований за допомогою get/set:

class User {

  constructor(name) {
    // викликає сеттер
    this.name = name;
  }

  get name() {
    return this._name;
  }

  set name(value) {
    if (value.length < 4) {
      alert("Ім’я занадто коротке.");
      return;
    }
    this._name = value;
  }

}

let user = new User("Іван");
alert(user.name); // Іван

user = new User(""); // Ім’я занадто коротке.

Технічно, таке оголошення класу працює шляхом створення геттерів та сеттерів в User.prototype.

Обчислені назви […]

Ось приклад з обчисленою назвою методу за допомогою використання дужок [...]:

class User {

  ['say' + 'Hi']() {
    alert("Привіт");
  }

}

new User().sayHi();

Такі особливості легко запам’ятати, оскільки вони нагадують літерали об’єктів.

Поля класу

Старим браузерам може знадобитися поліфіл

Поля класу – це недавнє доповнення до мови.

Раніше наші класи мали лише методи.

“Поля класу” – це синтаксис, який дозволяє додавати будь-які властивості.

Наприклад, додаймо властивість name до class User:

class User {
  name = "Іван";

  sayHi() {
    alert(`Привіт, ${this.name}!`);
  }
}

new User().sayHi(); // Привіт, Іван!

Отже, ми просто пишемо <property name> = <value> в оголошенні, і все.

Важливою відмінністю полів класу є те, що вони задаються в окремих об’єктах, а не в User.prototype:

class User {
  name = "Іван";
}

let user = new User();
alert(user.name); // Іван
alert(User.prototype.name); // undefined

Ми також можемо присвоїти значення, використовуючи складніші вирази та виклики функцій:

class User {
  name = prompt("Ім’я, будь ласка?", "Іван");
}

let user = new User();
alert(user.name); // Іван

Створення методів, що пов’язані з полями класу

Як показано в розділі Прив’язка контексту до функції, функції в JavaScript мають динамічний this. Він залежить від контексту виклику.

Отже, якщо метод об’єкта передається і викликається в іншому контексті, this більше не буде посиланням на цей об’єкт.

Наприклад, цей код покаже undefined:

class Button {
  constructor(value) {
    this.value = value;
  }

  click() {
    alert(this.value);
  }
}

let button = new Button("привіт");

setTimeout(button.click, 1000); // undefined

Ця проблема називається "втратою this".

Існує два підходи до розв’язання цієї проблеми, як обговорювалося в розділі Прив’язка контексту до функції:

  1. Передати функцію-обгортку, наприклад setTimeout(() => button.click(), 1000).
  2. Зв’язати метод з об’єктом за допомогою функції bind, наприклад у конструкторі.

Поля класу надають інший, досить елегантний синтаксис:

class Button {
  constructor(value) {
    this.value = value;
  }
  click = () => {
    alert(this.value);
  }
}

let button = new Button("привіт");

setTimeout(button.click, 1000); // привіт

Поле класу click = () => {...} створюється на основі конкретного об’єкта, існує окрема функція для кожного об’єкта Button, з this всередині неї, що посилається на цей об’єкт. Ми можемо передати кнопку куди завгодно, а значення this завжди буде коректним.

Це особливо корисно в середовищі браузера, для слухачів подій.

Підсумки

Основний синтаксис класу виглядає так:

class MyClass {
  prop = value; // властивість

  constructor(...) { // конструктор
    // ...
  }

  method(...) {} // метод

  get something(...) {} // геттер метод
  set something(...) {} // сеттер метод

  [Symbol.iterator]() {} // метод з обчисленим ім’ям (символом в цьому випадку)
  // ...
}

MyClass технічно є функцією (тою, яку ми задаємо як constructor), тоді як методи, геттери та сеттери записуються до MyClass.prototype.

У наступних розділах ми дізнаємося більше про класи, включаючи наслідування та інші особливості.

Завдання

важливість: 5

Клас Clock написано в функціональному стилі. Перепишіть його в синтаксис класу.

P.S. Годинник тікає у консолі, відкрийте її, щоб побачити.

Відкрити пісочницю для завдання.

class Clock {
  constructor({ template }) {
    this.template = template;
  }

  render() {
    let date = new Date();

    let hours = date.getHours();
    if (hours < 10) hours = '0' + hours;

    let mins = date.getMinutes();
    if (mins < 10) mins = '0' + mins;

    let secs = date.getSeconds();
    if (secs < 10) secs = '0' + secs;

    let output = this.template
      .replace('h', hours)
      .replace('m', mins)
      .replace('s', secs);

    console.log(output);
  }

  stop() {
    clearInterval(this.timer);
  }

  start() {
    this.render();
    this.timer = setInterval(() => this.render(), 1000);
  }
}


let clock = new Clock({template: 'h:m:s'});
clock.start();

Відкрити рішення в пісочниці.

Навчальна карта

Коментарі

прочитайте це, перш ніж коментувати…
  • Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
  • Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
  • Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)