ООП в прототипном стиле
Прототип объекта
В JavaScript объекты можно организовать в цепочки, связанные через специальную ссылку __proto__. При чтении свойства объекта, если оно отсутствует у него, то будет искаться в его объекте __proto__, то есть в его прототипе
Прототип используется только при чтении свойства, запись и удаление совершаются над самим объектом
Свойство __proto__ не поддерживается в браузерах IE10-
let user = { isActive: false };
let admin = { name: "John Doe" };
admin.__proto__ = user; // admin прототипно наследует user
console.log(admin); // { name: "John" }
console.log(admin.isActive); //false
admin.isActive = true;
console.log(admin.isActive); // true
console.log(user.isActive); // falseObject.create(null)
При обычном, литеральном способе создания объект “нагружается” встроенными функциями из глобального объекта Object, доступными по ссылке __proto__
Иногда необходимо использовать объекты как коллекции произвольных пар ключ-значение, без методов и свойств его прототипа
Метод Object.create(proto[, descriptors]) позволяет создать объект с конкретным прототипом. Объектом __proto__ при этом является первый аргумент метода
Способ Object.create(null) позволяет создать объект без прототипа, что означает, он не будет наследоваться от базового объекта Object и не будет содержать его методов
let user = {};
console.log(user.toString); // function toString()
let user = Object.create(null);
console.log(user.toString); // undefinedЧтобы дать прототип объекту, его нужно передать в метод create
let user = { name: "Anonymous" };
let admin = Object.create(user);
console.log(admin.name); //"Anonymous"Метод hasOwnProperty
При переборе свойств объекта в цикле for..in нет разницы между собственными свойствами объекта и свойствами его прототипа
Вызов obj.hasOwnProperty(key) возвращает true, если свойство key принадлежит объекту obj, в противном случае – false
let user = { isActive: false };
let admin = Object.create(user);
admin.name = "John Doe";
/*
let admin = {
name: "John Doe",
__proto__: user
}
*/
for (let key in admin) {
console.log("property", key);
if (admin.hasOwnProperty(key)) {
console.log("own property", key);
}
}Встроенные классы
Встроенные классы (конструкторы) Array, String, Date и др. используют прототипы для обеспечения наследования базового функционала. При этом все они наследуют от Object.prototype
[ ] → Array.prototype → Object.prototype
Примитивы не являются объектами, но наследуют от соответствующих классов методы из прототипа: String.prototype , Number.prototype , Boolean.prototype … Свойство constructor указывает на класс-родитель
let str = "xa";
alert(str.constructor === String); // true
str = str.repeat(3);
alert(str); // "хахаха"Примитивные типы null и undefined не имеют соответствующего им конструктора, следовательно, никакие методы к ним не применимы
let x = null;
alert(x.constructor); // Uncaught TypeError: Cannot read property "constructor" of nullРасширение глобальных объектов
Свойство prototype глобальных объектов (классов) позволяет добавлять новые методы к ним
String.prototype.sprintf = function () {
let string = this.valueOf(), key;
for (key in arguments) {
string = string.replace("%s", arguments[key]);
}
return string;
}
alert("%s, greetings!".sprintf("John Doe")); // "John Doe, greetings!"
alert("You have earned $%s, %s!".sprintf(100, "John Doe")); // You have earned $100, John Doe!Характеристики
- Метод прототипа автоматически доступен всем экземплярам класса
- Изменение встроенных прототипов глобально влияет на все скрипты
- Возможен конфликт методов/свойств, определенных в других библиотеках
Проверка принадлежности классу
Оператор instanceof позволяет проверить, принадлежит ли объект определенному классу, с учетом прототипного наследования
[] instanceof Array // true
[] instanceof Object // true
[] instanceof String // falseПрототипное ООП
В объектно-ориентированных программах объекты создаются посредством вызова функции- конструктора через оператор new
Чтобы указать прототип объектам класса, нужно задать его в свойстве prototype конструктора
Прототипом может быть только объект
let figure = {
color: "black"
};
function Circle(x, y, r) {
this.x = x;
this.y = y;
this.r = r;
}
Circle.prototype = figure;
let sun = new Circle(0, 0, 10); // Circle { x: 0, y: 0, r: 10 }
// => __proto__ { color: "black" }
console.log(sun.color); // "black"Инкапсуляция в прототипном ООП
Понятия приватных и защищенных свойств в прототипном подходе отсутствуют. Все методы и свойства фактически являются публичными, поскольку записываются в this или prototype
Если необходимо обозначить свойства как private, их названия маркируют двойным подчеркиванием; protected свойства маркируются одним подчеркиванием
function Figure(x, y) {
this.__x = x || 0;
this.__y = y || 0;
}Наследование в прототипном ООП
Наследование класса реализуется через свойство prototype. Экземпляр класса использует свойства и методы из своего прототипа, а если их нет, тогда свойства и методы родителя
sun → Circle.prototype → Figure.prototype
Прототипный подход, в отличие от функционального, позволяет не дублировать методы в каждом объекте и опционально использовать конструктор родителя
В prototype по умолчанию находится свойство constructor , указывающее на функцию-конструктор
Circle.prototype.constructor === Circle
Если переопределяем свой prototype, но хотим сохранить свой конструктор, ссылку на него необходимо явно указать
function Figure(x, y) {
this.__x = x || 0;
this.__y = y || 0;
}
Figure.prototype.draw = function() {
console.log("Draw the figure");
}
function Circle(x, y, r) {
Figure.call(this, x, y); // Выполняем конструктор "у себя"
}
Circle.prototype = Object.create(Figure.prototype); // Наследуем методы из прототипа родителя
Circle.prototype.constructor = Circle; // Возвращаем себе свой конструктор
let sun = new Circle(0, 0, 10);
console.log(sun);Полиморфизм в прототипном ООП
В классе-наследнике можно переопределять или расширять методы родительского класса, реализуя идею полиморфизма
...
Circle.prototype.draw = function() {
Figure.prototype.draw.call(this); // Выполняем метод из прототипа родителя в своем контексте
console.log("circle"); // Расширяем функционал родительского метода
}
let sun = new Circle(0, 0, 10);
sun.draw();Классы
Синтаксис для классов выглядит так:
class Название [extends Родитель] {
constructor
методы
}Функция конструктор запускается при создании new “Название Класса”, остальные методы записываются в “Название Класса”.prototype.
class User {
constructor(name) {
this.name = name;
}
sayHello() {
alert("Hi " + this.name);
}
}
let user = new User("John");
user.sayHello();function User(name) {
this.name = name;
}
User.prototype.sayHello = function() {
alert(this.name);
}В обоих случаях new User будет создавать объекты. Метод sayHello также в обоих случаях находится в прототипе.
Но при объявлении через class есть ряд отличий:
- User нельзя вызывать без new, будет ошибка
- Объявление класса с точки зрения области видимости ведёт себя как let. В частности, оно видно только в текущем блоке и только в коде, который находится ниже объявления (Function Declaration видно и до объявления).
Методы, объявленные внутри class, также имеют ряд особенностей:
- Метод sayHello является именно методом, значит имеет доступ к super.
- Все методы класса работают в строгом режиме use strict, даже если он не указан
- Все методы класса не перечислимы. То есть в цикле for…in по объекту их не будет
Наследование и super
Синтаксис:
class Child extends Parent {
....
}class Person {}
class User extends Person {}
alert( User.prototype.__proto__ == Person.prototype ); // trueКак видим, в new User доступны как свои методы, так и (через super) методы родителя.
class Person {
constructor(name) {
this.name = name;
}
say() {
alert("Hi " + this.name);
}
}
class User extends Person {
say() {
super.say();
alert("...and Jane!");
}
}
new User("John").say();
// Hi John
// ... and Jane!Как видно из примера выше, методы родителя (say) можно переопределить в наследнике. При этом для обращения к родительскому методу используют super.say().
Конструктор constructor родителя наследуется автоматически. То есть, если в потомке не указан свой constructor, то используется родительский. В примере User, таким образом, использует constructor от Person.
Если у потомка свой constructor, то, чтобы в нём вызвать конструктор родителя – используется синтаксис super() с аргументами для родителя.
class Person {
constructor(name) {
this.name = name;
}
say() {
alert("Hi " + this.name);
}
}
class User extends Person {
constructor() {
// вызвать конструктор Person с аргументом "John"
super("John"); // то же, что и Person.call(this, "John")
}
}
new User ().say(); // Hi JohnДля такого вызова есть небольшие ограничения:
- Вызвать конструктор родителя можно только изнутри конструктора потомка. В частности, super() нельзя вызвать из произвольного метода.
- В конструкторе потомка мы обязаны вызвать super() до обращения к this. До вызова super не существует this, так как по спецификации в этом случае именно super инициализирует this
super
Оно предназначено только для использования в методах объекта.
Вызов super.parentProperty() позволяет из метода объекта получить свойство его прототипа.
Свойство super работает через прототип, на уровне методов объекта.
При обращении через super используется [[HomeObject]] текущего метода, и от него берётся __proto__. Поэтому super работает только внутри методов.
Материалы для прочтения
- ООП в прототипном стиле https://learn.javascript.ru/prototypes
- Классы https://learn.javascript.ru/class