Пошук уроків, статей та іншого контенту
Створюватимете ієрархії конструкторів через Object.create, правильно відновлюючи constructor і викликаючи батьківський конструктор.
Наслідування між конструкторними функціями дає змогу створювати ієрархії об’єктів у JavaScript. Дочірній конструктор може використовувати методи батьківського прототипу, а також успадковувати й ініціалізувати властивості батьківського об’єкта.
Для цього зазвичай потрібно виконати три кроки:
Викликати батьківський конструктор усередині дочірнього.
Пов’язати прототип дочірнього конструктора з прототипом батьківського через Object.create.
Відновити властивість constructor.
Конструкторна функція використовується разом з оператором new:
function User(name) {
this.name = name;
}
User.prototype.getName = function () {
return this.name;
};
const user = new User("Олена");
console.log(user.name); // Олена
console.log(user.getName()); // Олена
console.log(user.constructor === User); // trueПід час виклику new User("Олена") JavaScript:
Створює новий об’єкт.
Встановлює його прототипом User.prototype.
Викликає User із this, що посилається на новий об’єкт.
Повертає створений об’єкт.
Метод getName зберігається у прототипі, тому всі екземпляри User можуть використовувати один і той самий метод.
Нехай є конструктор User, від якого потрібно успадкувати конструктор Admin:
function User(name) {
this.name = name;
}
function Admin(name, permissions) {
User.call(this, name);
this.permissions = permissions;
}
const admin = new Admin("Марко", ["read", "write"]);
console.log(admin.name); // Марко
console.log(admin.permissions); // [ "read", "write" ]Вираз:
User.call(this, name);викликає User із поточним об’єктом Admin як значенням this.
Без цього виклику властивість name не буде створена:
function User(name) {
this.name = name;
}
function Admin(name, permissions) {
// Батьківський конструктор не викликано
this.permissions = permissions;
}
const admin = new Admin("Марко", ["read"]);
console.log(admin.name); // undefinedcallЯкщо просто написати User(name), функція не обов’язково працюватиме з об’єктом, який створюється через new Admin(...). Нам потрібно явно передати правильний контекст:
User.call(this, name);Метод call викликає функцію одразу, а його перший аргумент стає значенням this.
Object.createВиклик батьківського конструктора копіює або створює властивості екземпляра, але не передає методи прототипу. Для цього потрібно встановити прототип дочірнього конструктора:
Admin.prototype = Object.create(User.prototype);Після цього об’єкти Admin зможуть знаходити методи в User.prototype:
function User(name) {
this.name = name;
}
User.prototype.getName = function () {
return this.name;
};
function Admin(name, permissions) {
User.call(this, name);
this.permissions = permissions;
}
Admin.prototype = Object.create(User.prototype);
const admin = new Admin("Марко", ["read", "write"]);
console.log(admin.getName()); // МаркоПошук методу відбувається за таким ланцюжком:
admin
→ Admin.prototype
→ User.prototype
→ Object.prototype
→ nullЯкщо методу немає в самому об’єкті або в Admin.prototype, JavaScript шукає його далі — у User.prototype.
constructorПісля виконання:
Admin.prototype = Object.create(User.prototype);властивість Admin.prototype.constructor більше не посилається на Admin. Вона буде успадкована з User.prototype:
function User(name) {
this.name = name;
}
function Admin(name, permissions) {
User.call(this, name);
this.permissions = permissions;
}
Admin.prototype = Object.create(User.prototype);
console.log(Admin.prototype.constructor === User); // trueЗазвичай це небажано, оскільки constructor має описувати функцію, яка створює об’єкти цього типу.
Тому після налаштування прототипу потрібно явно відновити constructor:
Admin.prototype.constructor = Admin;Тепер:
console.log(Admin.prototype.constructor === Admin); // trueПісля налаштування прототипу можна додати методи, специфічні для дочірнього типу:
function User(name) {
this.name = name;
}
User.prototype.getName = function () {
return this.name;
};
function Admin(name, permissions) {
User.call(this, name);
this.permissions = permissions;
}
Admin.prototype = Object.create(User.prototype);
Admin.prototype.constructor = Admin;
Admin.prototype.hasPermission = function (permission) {
return this.permissions.includes(permission);
};
const admin = new Admin("Марко", ["read", "write"]);
console.log(admin.getName()); // Марко
console.log(admin.hasPermission("write")); // true
console.log(admin.hasPermission("delete")); // falseAdmin використовує:
властивість name, створену батьківським конструктором;
метод getName із User.prototype;
властивість permissions, створену дочірнім конструктором;
метод hasPermission із Admin.prototype.
Розглянемо ієрархію транспортних засобів:
function Vehicle(brand) {
this.brand = brand;
}
Vehicle.prototype.describe = function () {
return `Транспортний засіб: ${this.brand}`;
};
function Car(brand, doors) {
Vehicle.call(this, brand);
this.doors = doors;
}
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;
Car.prototype.drive = function () {
return `${this.brand} рухається дорогою`;
};
function ElectricCar(brand, doors, batteryCapacity) {
Car.call(this, brand, doors);
this.batteryCapacity = batteryCapacity;
}
ElectricCar.prototype = Object.create(Car.prototype);
ElectricCar.prototype.constructor = ElectricCar;
ElectricCar.prototype.charge = function () {
return `${this.brand} заряджається`;
};
const car = new Car("Toyota", 4);
const electricCar = new ElectricCar("Tesla", 4, 75);
console.log(car.describe()); // Транспортний засіб: Toyota
console.log(car.drive()); // Toyota рухається дорогою
console.log(electricCar.describe()); // Транспортний засіб: Tesla
console.log(electricCar.drive()); // Tesla рухається дорогою
console.log(electricCar.charge()); // Tesla заряджається
console.log(electricCar instanceof ElectricCar); // true
console.log(electricCar instanceof Car); // true
console.log(electricCar instanceof Vehicle); // true
console.log(electricCar.constructor === ElectricCar); // trueУ цьому прикладі:
ElectricCar
→ Car
→ Vehicle
→ ObjectКонструктор ElectricCar викликає Car, а Car викликає Vehicle. Так дочірній об’єкт отримує всі потрібні властивості.
Дочірній прототип може перевизначити метод батьківського прототипу:
function Vehicle(brand) {
this.brand = brand;
}
Vehicle.prototype.describe = function () {
return `Транспортний засіб: ${this.brand}`;
};
function Car(brand, doors) {
Vehicle.call(this, brand);
this.doors = doors;
}
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;
Car.prototype.describe = function () {
return `Автомобіль: ${this.brand}, дверей: ${this.doors}`;
};
const car = new Car("Honda", 4);
console.log(car.describe()); // Автомобіль: Honda, дверей: 4Пошук зупиняється на першому знайденому методі. Оскільки describe є в Car.prototype, версія з Vehicle.prototype не використовується автоматично.
За потреби можна викликати батьківську версію методу через call:
function Vehicle(brand) {
this.brand = brand;
}
Vehicle.prototype.describe = function () {
return `Транспортний засіб: ${this.brand}`;
};
function Car(brand, doors) {
Vehicle.call(this, brand);
this.doors = doors;
}
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;
Car.prototype.describe = function () {
const parentDescription = Vehicle.prototype.describe.call(this);
return `${parentDescription}, дверей: ${this.doors}`;
};
const car = new Car("Honda", 4);
console.log(car.describe());
// Транспортний засіб: Honda, дверей: 4Тут:
Vehicle.prototype.describe.call(this);викликає метод батьківського прототипу з поточним дочірнім об’єктом як this.
Властивості, які залежать від конкретного екземпляра, потрібно створювати в конструкторі:
function User(name) {
this.name = name;
}Методи, спільні для всіх екземплярів, зазвичай розміщують у прототипі:
User.prototype.getName = function () {
return this.name;
};Це дозволяє уникнути створення окремої копії функції для кожного об’єкта.
Невдалий варіант:
function User(name) {
this.name = name;
// Функція створюється заново для кожного екземпляра
this.getName = function () {
return this.name;
};
}Кращий варіант:
function User(name) {
this.name = name;
}
User.prototype.getName = function () {
return this.name;
};Для дочірнього конструктора зручно використовувати такий порядок:
function Parent(value) {
this.value = value;
}
Parent.prototype.getValue = function () {
return this.value;
};
function Child(value, extraValue) {
Parent.call(this, value);
this.extraValue = extraValue;
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.getExtraValue = function () {
return this.extraValue;
};Спочатку створюється зв’язок із батьківським прототипом, потім відновлюється constructor, і лише після цього додаються власні методи дочірнього типу.
function User(name) {
this.name = name;
}
function Admin(name) {
// User.call(this, name) відсутній
}
const admin = new Admin("Олена");
console.log(admin.name); // undefinedЯкщо батьківський конструктор відповідає за ініціалізацію властивостей, його потрібно викликати явно.
Child.prototype = Parent.prototypeНе слід робити так:
Child.prototype = Parent.prototype;У цьому випадку обидва конструктори використовують один і той самий об’єкт прототипу. Додавання методу до прототипу Child також додасть його до прототипу Parent:
function Parent() {}
function Child() {}
Child.prototype = Parent.prototype;
Child.prototype.onlyForChild = function () {};
console.log(typeof Parent.prototype.onlyForChild); // functionПравильний варіант створює окремий об’єкт:
Child.prototype = Object.create(Parent.prototype);constructorПісля заміни прототипу:
Child.prototype = Object.create(Parent.prototype);потрібно виконати:
Child.prototype.constructor = Child;Інакше перевірка конструктора може дати неправильний результат.
Невдалий порядок:
Child.prototype.someMethod = function () {};
Child.prototype = Object.create(Parent.prototype);Після заміни прототипу метод someMethod буде втрачено.
Правильний порядок:
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.someMethod = function () {};newКонструкторні функції зазвичай очікують, що this буде новим об’єктом:
function User(name) {
this.name = name;
}
const user = User("Олена");
console.log(user); // undefinedДля створення екземпляра використовуйте new:
const user = new User("Олена");instanceofОператор instanceof перевіряє, чи є прототип конструктора частиною прототипного ланцюжка об’єкта:
function User(name) {
this.name = name;
}
function Admin(name) {
User.call(this, name);
}
Admin.prototype = Object.create(User.prototype);
Admin.prototype.constructor = Admin;
const admin = new Admin("Олена");
console.log(admin instanceof Admin); // true
console.log(admin instanceof User); // true
console.log(admin instanceof Object); // trueПеревірка admin instanceof User повертає true, оскільки User.prototype є частиною ланцюжка прототипів admin.
Батьківський конструктор викликається через Parent.call(this, ...).
Наслідування прототипу налаштовується через Child.prototype = Object.create(Parent.prototype).
Після заміни прототипу потрібно відновити Child.prototype.constructor = Child.
Методи батьківського прототипу стають доступними дочірнім екземплярам через прототипний ланцюжок.
Власні методи дочірнього конструктора потрібно додавати після налаштування прототипу.
Не слід використовувати Child.prototype = Parent.prototype, оскільки це створює спільний об’єкт прототипу.
instanceof допомагає перевірити, чи належить об’єкт до певної ієрархії конструкторів.