24 жовтня 2023 р.

Конструктори, оператор "new"

Звичайний синтаксис {...} дозволяє створити тільки один об’єкт. Проте часто нам потрібно створити багато однотипних об’єктів, таких як, наприклад, користувачі чи елементи меню тощо.

Це можна зробити за допомогою функції-конструктора та оператора "new".

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

Технічно, функції-конструктори – це звичайні функції. Однак є дві загальні домовленості:

  1. Ім’я функції-конструктора повинно починатися з великої літери.
  2. Функції-конструктори повинні виконуватися лише з оператором "new".

Наприклад:

function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Джек");

alert(user.name); // Джек
alert(user.isAdmin); // false

Коли функція виконується з new, відбуваються наступні кроки:

  1. Створюється новий порожній об’єкт, якому присвоюється this.
  2. Виконується тіло функції. Зазвичай воно модифікує this, додає до нього нові властивості.
  3. Повертається значення this.

Інакше кажучи, виклик new User(...) робить щось на зразок:

function User(name) {
  // this = {};  (неявно)

  // додає властивості до this
  this.name = name;
  this.isAdmin = false;

  // return this;  (неявно)
}

Отже, let user = new User("Джек") дає той самий результат, що:

let user = {
  name: "Джек",
  isAdmin: false
};

Тепер, якщо ми хочемо створити інших користувачів, ми можемо викликати new User("Ганна"), new User("Аліса") тощо. Така конструкція значно коротша, ніж використання літералів кожного разу, а також легша для читання.

Це і є основною метою конструкторів – зручне перевикористання коду зі створення об’єктів.

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

new function() { … }

Якщо у нас є багато рядків коду, які створюють єдиний складний об’єкт, ми можемо обернути їх у функцію-конструктор, яка одразу буде викликана, таким чином:

// створити функцію і негайно викликати її за допомогою new
let user = new function() {
  this.name = "Джон";
  this.isAdmin = false;

  // ...інший код для створення користувача
  // можливо складна логіка та інструкції
  // локальні змінні тощо
};

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

Перевірка виклику у режимі конструктора: new.target

Просунуті можливості

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

Використовуючи спеціальну властивість new.target всередині функції, ми можемо перевірити чи була ця функція викликана за допомогою оператора new чи без нього.

Якщо функція була викликана за допомогою new, то в new.target буде сама функція, в іншому разі отримаємо undefined:

function User() {
  alert(new.target);
}

// виклик без "new":
User(); // undefined

// виклик з "new":
new User(); // function User { ... }

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

Ми також можемо зробити, щоб обидва виклики, з new та звичайний, робили одне й те саме, таким чином:

function User(name) {
  if (!new.target) { // якщо ви викликали без оператора new
    return new User(name); // ...додамо оператор new за вас
  }

  this.name = name;
}

let john = User("Джон"); // перенаправляє виклик до new User
alert(john.name); // Джон

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

Проте використання такого підходу скрізь не є хорошою практикою, тому що відсутність new робить код менш очевидним. З оператором new ми усі знаємо, що буде створено новий об’єкт.

Повернення значення з конструктора return

Зазвичай конструктори не мають інструкції return. Їх завдання – записати усе необхідне у this, яке автоматично стане результатом.

Але якщо є інструкція return, то застосовується просте правило:

  • Якщо return викликається з об’єктом, тоді замість this буде повернено цей об’єкт.
  • Якщо return викликається з примітивом, примітив ігнорується.

Інакше кажучи, return з об’єктом повертає цей об’єкт, у всіх інших випадках повертається this.

У наступному прикладі return перезаписує this, повертаючи об’єкт:

function BigUser() {

  this.name = "Джон";

  return { name: "Ґодзілла" };  // <-- повертає цей об’єкт
}

alert( new BigUser().name );  // Ґодзілла, отримали цей об’єкт

А ось приклад з порожнім return (або ми можемо розмістити примітив після нього, не має значення):

function SmallUser() {

  this.name = "Джон";

  return; // <-- повертає this
}

alert( new SmallUser().name );  // Джон

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

Відсутність дужок

До речі, ми можемо опустити дужки після new, якщо виклик конструктора відбувається без аргументів:

let user = new User; // <-- немає дужок
// те саме, що
let user = new User();

Пропуск дужок не є гарною практикою, та специфікація дозволяє такий синтаксис.

Створення методів у конструкторі

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

Звичайно, ми можемо додати до this не лише властивості, але й методи.

У наведеному нижче прикладі, new User(name) створює об’єкт із заданим name та методом sayHi:

function User(name) {
  this.name = name;

  this.sayHi = function() {
    alert( "Моє ім’я: " + this.name );
  };
}

let john = new User("Джон");

john.sayHi(); // Моє ім’я: Джон

/*
john = {
   name: "Джон",
   sayHi: function() { ... }
}
*/

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

Підсумки

  • Функції-конструктори або, коротко, конструктори, є звичайними функціями, але існує загальна домовленість ім’я такої функції починати з великої літери.
  • Конструктори повинні викликатися тільки з використанням оператора new. Такий виклик передбачає створення порожнього this на початку та повернення заповненого у кінці.

Ми можемо використовувати конструктори для створення численних подібних об’єктів.

JavaScript надає функції-конструктори для багатьох вбудованих об’єктів мови: наприклад, Date, Set та інших, які ми плануємо вивчати далі.

Об’єкти, ми ще до них повернемось!

У цьому розділі ми розглядаємо лише основи об’єктів та конструкторів. Вони є важливими для подальшого вивчення типів даних та функцій у наступних розділах.

Після того, як ми це вивчемо, ми повернемося до об’єктів для більш детального їх вивчення у розділах Прототипи, наслідування та Класи.

Завдання

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

Чи можливо створити функції A та B, щоб new A() == new B()?

function A() { ... }
function B() { ... }

let a = new A();
let b = new B();

alert( a == b ); // true

Якщо так – наведіть приклад коду таких функцій.

Так, це можливо.

Якщо функція повертає об’єкт, тоді new повертає його замість this.

Так функції A та B можуть, наприклад, повертати один і той самий об’єкт obj, визначений незалежно від цих функцій:

let obj = {};

function A() { return obj; }
function B() { return obj; }

alert( new A() == new B() ); // true
важливість: 5

Створіть функцію-конструктор Calculator, який створює об’єкти з трьома методами:

  • read() запитує два значення за допомогою prompt і записує їх у властивості об’єкта з іменами a та b.
  • sum() повертає суму цих властивостей.
  • mul() повертає результат множення даних властивостей.

Наприклад:

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

Запустити демонстрацію

Відкрити пісочницю з тестами.

function Calculator() {

  this.read = function() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  };

  this.sum = function() {
    return this.a + this.b;
  };

  this.mul = function() {
    return this.a * this.b;
  };
}

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

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

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

Створіть функцію-конструктор Accumulator(startingValue).

Об’єкт, який він створює повинен:

  • Зберігати “поточне значення” у властивості value. Початкове значення має значення аргументу конструктора startingValue.
  • Метод read() повинен використовувати prompt для зчитування нового числа та додавати його до value.

Іншими словами, властивість value – це сума всіх введених користувачем значень разом із початковим значенням startingValue.

Нижче ви можете подивитись демонстрацію роботи коду:

let accumulator = new Accumulator(1); // початкове значення 1

accumulator.read(); // додає введене користувачем значення
accumulator.read(); // додає введене користувачем значення

alert(accumulator.value); // показує суму цих значень

Запустити демонстрацію

Відкрити пісочницю з тестами.

function Accumulator(startingValue) {
  this.value = startingValue;

  this.read = function() {
    this.value += +prompt('Скільки додати?', 0);
  };

}

let accumulator = new Accumulator(1);
accumulator.read();
accumulator.read();
alert(accumulator.value);

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

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