Пошук уроків, статей та іншого контенту
Розберете, як this встановлюється під час виклику функції-конструктора через new.
new працює з функцією-конструкторомУ JavaScript функція-конструктор — це звичайна функція, яку викликають із оператором new.
Її використовують, щоб створювати багато однотипних об’єктів:
function User(name, age) {
this.name = name;
this.age = age;
}
const firstUser = new User("Олена", 25);
const secondUser = new User("Андрій", 31);
console.log(firstUser);
console.log(secondUser);Результат:
User { name: 'Олена', age: 25 }
User { name: 'Андрій', age: 31 }Під час виклику new User(...) значення this усередині User посилається на новий об’єкт.
Тому ці присвоєння:
this.name = name;
this.age = age;додають властивості саме створюваному об’єкту.
newВиклик:
const user = new User("Олена", 25);приблизно відповідає такій послідовності:
Створюється новий порожній об’єкт.
Цей об’єкт зв’язується з User.prototype.
Новий об’єкт встановлюється як this під час виконання User.
Виконується тіло функції-конструктора.
Якщо функція явно не повернула інший об’єкт, результатом стає новий об’єкт.
У спрощеному вигляді це можна уявити так:
const user = Object.create(User.prototype);
User.call(user, "Олена", 25);
return user;Це не точна реалізація оператора new, але хороша модель для розуміння його роботи.
newДля ілюстрації можна написати функцію, яка повторює основні кроки:
function createInstance(Constructor, ...args) {
// Створюємо об'єкт із правильним прототипом
const instance = Object.create(Constructor.prototype);
// Викликаємо конструктор із новим об'єктом як this
const result = Constructor.apply(instance, args);
// Об'єкт, явно повернений конструктором, має пріоритет
if (result !== null && (typeof result === "object" || typeof result === "function")) {
return result;
}
return instance;
}
function User(name, age) {
this.name = name;
this.age = age;
}
User.prototype.describe = function () {
return `${this.name}, ${this.age} років`;
};
const user = createInstance(User, "Олена", 25);
console.log(user.describe());
console.log(user instanceof User);Результат:
Олена, 25 років
truethis у функції-конструкторіУ функції-конструкторі this — це об’єкт, який буде повернутий після використання new.
function Product(name, price) {
this.name = name;
this.price = price;
this.getInfo = function () {
return `${this.name}: ${this.price} грн`;
};
}
const book = new Product("JavaScript для початківців", 500);
const keyboard = new Product("Механічна клавіатура", 2500);
console.log(book.getInfo());
console.log(keyboard.getInfo());Під час першого виклику:
new Product("JavaScript для початківців", 500);this посилається на об’єкт book.
Під час другого:
new Product("Механічна клавіатура", 2500);this посилається на об’єкт keyboard.
Тобто одна функція-конструктор може створити багато об’єктів, а this щоразу буде іншим.
Властивості екземпляра зазвичай створюють усередині конструктора:
function Rectangle(width, height) {
this.width = width;
this.height = height;
}
const rectangle = new Rectangle(10, 5);
console.log(rectangle.width); // 10
console.log(rectangle.height); // 5Методи краще додавати до прототипу, а не створювати заново для кожного об’єкта:
function Rectangle(width, height) {
this.width = width;
this.height = height;
}
Rectangle.prototype.getArea = function () {
return this.width * this.height;
};
Rectangle.prototype.getPerimeter = function () {
return 2 * (this.width + this.height);
};
const firstRectangle = new Rectangle(10, 5);
const secondRectangle = new Rectangle(4, 3);
console.log(firstRectangle.getArea()); // 50
console.log(secondRectangle.getPerimeter()); // 14
console.log(
firstRectangle.getArea === secondRectangle.getArea
); // trueМетод getArea один раз зберігається в Rectangle.prototype, а екземпляри знаходять його через ланцюжок прототипів.
Якщо ж оголосити метод усередині конструктора:
function Rectangle(width, height) {
this.width = width;
this.height = height;
this.getArea = function () {
return this.width * this.height;
};
}то для кожного екземпляра буде створена окрема функція:
const firstRectangle = new Rectangle(10, 5);
const secondRectangle = new Rectangle(10, 5);
console.log(firstRectangle.getArea === secondRectangle.getArea);
// falseЦе не завжди помилка, але для великої кількості об’єктів може бути менш ефективним.
instanceofОператор instanceof перевіряє, чи є прототип конструктора в ланцюжку прототипів об’єкта:
function User(name) {
this.name = name;
}
const user = new User("Олена");
console.log(user instanceof User); // true
console.log(user instanceof Object); // true
console.log({} instanceof User); // falseПісля використання new зв’язок із User.prototype створюється автоматично. Саме завдяки цьому працює instanceof.
newФункція-конструктор залишається звичайною функцією. Її можна викликати без new, але тоді спеціальне встановлення this не відбудеться.
"use strict";
function User(name) {
this.name = name;
}
const user = User("Олена");У строгому режимі this усередині User буде undefined, тому спроба записати:
this.name = name;спричинить помилку:
TypeError: Cannot set properties of undefinedУ нестрогому режимі this у звичайній функції, викликаній без контексту, може посилатися на глобальний об’єкт. Це може призвести до випадкового створення глобальних змінних.
Щоб захиститися від виклику без new, можна перевіряти this:
function User(name) {
if (!(this instanceof User)) {
return new User(name);
}
this.name = name;
}
const user = User("Олена");
console.log(user.name); // ОленаОднак у сучасному коді зазвичай використовують class, який забороняє виклик без new автоматично.
classСинтаксис class є зручнішим способом описувати подібні об’єкти:
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
describe() {
return `${this.name}, ${this.age} років`;
}
}
const user = new User("Олена", 25);
console.log(user.describe());
console.log(user instanceof User);Виклик конструктора класу без new спричинить помилку:
const user = User("Олена", 25);
// TypeError: Class constructor User cannot be invoked without 'new'У класі методи також зберігаються в прототипі, а не копіюються в кожен екземпляр.
Функції-конструктори все ще важливо розуміти, тому що:
багато старого JavaScript-коду використовує цей підхід;
класи побудовані на прототипній моделі JavaScript;
бібліотеки та API можуть повертати об’єкти, створені конструкторами;
розуміння new допомагає краще працювати з this і прототипами.
Функція-конструктор може містити return, але його поведінка залежить від типу поверненого значення.
Якщо конструктор повертає примітивне значення, воно ігнорується:
function User(name) {
this.name = name;
return "інше значення";
}
const user = new User("Олена");
console.log(user.name); // Олена
console.log(typeof user); // objectДо примітивних значень належать:
рядки;
числа;
булеві значення;
null;
undefined;
symbol;
bigint.
Якщо конструктор явно повертає об’єкт, new поверне саме цей об’єкт:
function User(name) {
this.name = name;
return {
name: "Інше ім'я",
role: "admin"
};
}
const user = new User("Олена");
console.log(user);
// { name: "Інше ім'я", role: "admin" }
console.log(user instanceof User); // falseСтворений this у цьому випадку не стає результатом виклику, бо конструктор повернув інший об’єкт.
Така поведінка рідко потрібна у звичайних конструкторах, тому не варто повертати об’єкти випадково.
Стрілкові функції не мають власного this і не підтримують виклик із new:
const User = (name) => {
this.name = name;
};
const user = new User("Олена");
// TypeError: User is not a constructorДля конструктора потрібно використовувати звичайну функцію:
function User(name) {
this.name = name;
}
const user = new User("Олена");Важливо не плутати:
функцію-конструктор, яку викликають через new;
стрілкові методи або функції, що використовуються всередині об’єкта.
new і звичайним викликомПорівняймо два варіанти:
function Counter(start) {
this.value = start;
}
const counter = new Counter(10);
console.log(counter.value); // 10Тут new створює новий об’єкт і встановлює його як this.
Без new:
"use strict";
function Counter(start) {
this.value = start;
}
const counter = Counter(10);У строгому режимі this буде undefined, тому код завершиться помилкою.
new не просто викликає функцію. Він змінює спосіб її виклику та створює новий контекст для this.
Конструктор може перевіряти аргументи до збереження їх у властивостях:
function BankAccount(owner, balance = 0) {
if (typeof owner !== "string" || owner.trim() === "") {
throw new TypeError("Власник рахунку має бути непорожнім рядком");
}
if (typeof balance !== "number" || balance < 0) {
throw new TypeError("Баланс має бути невід'ємним числом");
}
this.owner = owner;
this.balance = balance;
}
BankAccount.prototype.deposit = function (amount) {
if (typeof amount !== "number" || amount <= 0) {
throw new TypeError("Сума поповнення має бути додатним числом");
}
this.balance += amount;
};
const account = new BankAccount("Олена", 1000);
account.deposit(250);
console.log(account.owner); // Олена
console.log(account.balance); // 1250У такому коді this використовується для збереження стану конкретного рахунку.
newfunction User(name) {
this.name = name;
}
const user = User("Олена");Потрібно:
const user = new User("Олена");Або використати class.
const User = (name) => {
this.name = name;
};Стрілкові функції не можна викликати через new.
this під час передачі методуНавіть якщо об’єкт створено через new, метод може втратити контекст під час передачі як callback:
function User(name) {
this.name = name;
}
User.prototype.sayHello = function () {
return `Привіт, я ${this.name}`;
};
const user = new User("Олена");
const greet = user.sayHello;
// У строгому режимі this усередині методу буде undefined
// console.log(greet());Метод можна прив’язати до об’єкта:
const greet = user.sayHello.bind(user);
console.log(greet()); // Привіт, я ОленаАбо викликати його безпосередньо через об’єкт:
console.log(user.sayHello());Такий код працює, але може бути менш зрозумілим:
function User(name) {
this.name = name;
}Тут параметр name і властивість this.name мають однакове ім’я, але це різні сутності:
name — локальний параметр функції;
this.name — властивість створюваного об’єкта.
Зазвичай таке іменування є нормальним, якщо різниця зрозуміла.
function User(name) {
this.name = name;
return {};
}У цьому випадку результатом буде порожній об’єкт, а не екземпляр із властивістю name.
function User(name) {
this.name = name;
this.sayHello = function () {
return `Привіт, я ${this.name}`;
};
}Для невеликої кількості об’єктів це нормально. Але якщо екземплярів багато, краще використати прототип:
function User(name) {
this.name = name;
}
User.prototype.sayHello = function () {
return `Привіт, я ${this.name}`;
};Створіть конструктор Task, який:
приймає назву завдання;
зберігає її у властивості title;
встановлює completed у false;
має метод complete, який змінює completed на true;
має метод getStatus, який повертає статус завдання.
Один із можливих варіантів:
function Task(title) {
if (typeof title !== "string" || title.trim() === "") {
throw new TypeError("Назва завдання має бути непорожнім рядком");
}
this.title = title;
this.completed = false;
}
Task.prototype.complete = function () {
this.completed = true;
};
Task.prototype.getStatus = function () {
return this.completed ? "Виконано" : "Не виконано";
};
const task = new Task("Вивчити оператор new");
console.log(task.getStatus()); // Не виконано
task.complete();
console.log(task.getStatus()); // Виконано
console.log(task instanceof Task); // trueФункція-конструктор — це звичайна функція, яку викликають через new.
Під час виклику new створюється новий об’єкт.
Цей об’єкт встановлюється як this усередині конструктора.
Новий об’єкт зв’язується з Constructor.prototype.
Якщо конструктор не повертає об’єкт явно, результатом є створений екземпляр.
Повернення примітиву ігнорується, а повернення об’єкта замінює стандартний результат.
Стрілкові функції не можуть бути конструкторами.
Методи зазвичай варто зберігати в прототипі.
instanceof перевіряє зв’язок об’єкта з прототипом конструктора.
Виклик конструктора без new може спричинити помилки або зміну неправильного об’єкта.