JavaScript – это не классический объектно-ориентированный язык, основанный на классах, однако в нем есть способ для их реализации.
Согласно Википедии класс-ориентированное программирование – это стиль объектно-ориентированного программирования (ООП), в котором наследование происходит через определение классов объектов, а не через сами объекты.
Классовая модель – это самая популярная реализация объектно-ориентированного программирования. Однако ООП в JavaScript основано не на классах, а на прототипах. В этой модели для создания новых объектов используется шаблонный объект – прототип.
Посмотрим на пример кода:
let names = {
fname: "Dillion",
lname: "Megida"
}
console.log(names.fname); // Dillion
console.log(names.hasOwnProperty("mname")); // false
В переменную names мы записали объект, у которого есть два свойства – fname и lname – и ни одного метода. Откуда же взялся метод hasOwnProperty?
Дело в том, что объект names не существует сам по себе, у него есть прототип – Object.prototype.
Выведем эту переменную в консоль:
console.log(names);
Вот что мы увидим:
Развернем загадочное свойство __proto__:
Мы можем увидеть конструктор объекта names – функцию Object() – и множество методов под ним. Среди них – и hasOwnProperty. Все эти методы хранятся в прототипе Object, но доступны и самому объекту names.
Другими словами, все объекты в JavaScript создаются с использованием прототипа Object.prototype:
names.__proto__ === Object.prototype; // true
И каждый объект имеет доступ ко всем методам этого прототипа, не обладая ими напрямую.
Свойство __proto__
Каждый объект в JavaScript имеет свойство __proto__, в котором хранится ссылка на другой объект, являющийся его прототипом.
Через это свойство, этот объект и получает доступ к свойствам и методам прототипа.
По умолчанию у всех объектов в свойство __proto__ записана ссылка на Object.prototype. Однако мы можем изменить это – иначе говоря «унаследовать» объект от другого прототипа.
Изменение прототипа
Не следует менять свойство __proto__ напрямую, для этого существуют специальные методы.
Object.create()
function DogObject(name, age) {
let dog = Object.create(constructorObject);
dog.name = name;
dog.age = age;
return dog;
}
let constructorObject = {
speak: function(){
return "I am a dog"
}
}
let bingo = DogObject("Bingo", 54);
console.log(bingo);
Посмотрим на объект bingo в консоли:
Теперь в свойстве __proto__ находится другая функция-конструктор и метод speak.
Object.create создает новый объект, автоматически устанавливая ему указанный прототип.
Ключевое слово new
function DogObject(name, age) {
this.name = name;
this.age = age;
}
DogObject.prototype.speak = function() {
return "I am a dog";
}
let john = new DogObject("John", 45);
john.__proto__ === DogObject.prototype; // true
Теперь свойство john.__proto__ ссылается на DogObject.prototype. Обратите внимание – не на функцию DogObject (она является только конструктором объектов), а на объект, записанный в ее свойстве prototype.
Данные и методы хранятся только в объектах. Функции-конструкторы вродеDogObjectилиObjectтолько предлагают удобный способ конфигурации (через this) и создания новых объектов.Функция-конструктор имеет свойствоprototype, в котором хранится объект-прототип. По его образу эта функция будет создавать новые объекты.
Прототип объекта, хранящийся в его свойстве__proto__, и объект, хранящийся в свойствеprototypeконструктора – одно и то же.
В свою очередь прототип прототипа john ссылается на уже знакомый нам Object.prototype.
DogObject.prototype.__proto__ === Object.prototype;
То, что мы видим, называется цепочкой прототипов. Это основа прототипно-ориентированного программирования. Объект может получить доступ к любому свойству или методу, которое есть у любого звена его прототипной цепочки.
Сколько бы прототипов ни было в этой цепочке, последний почти всегда будет наследовать от исходногоObject.prototype. При необходимости можно избавиться от дефолтного прототипа, вызвав методObject.createи передав емуnull. Это бывает полезно для создания словарей, в которых не должно быть ничего лишнего.
Ключевое слово new, которое превращает обычную функцию в конструктор объектов, делает по сути ту же самую вещь, что и Object.create(), только проще.
Функции – тоже объекты
Если вас смутило наличие свойств у функции DogObject, доступных через точку (prototype), то вспомните, что в JS функции – это тоже объекты.
В качестве прототипа они имеют объект Function.prototype, который в свою очередь наследует от Object.prototype.
В Function.prototype содержится много дополнительных свойств и методов, которые наследует от него любая функция (call, apply, isPrototypeOf).
Переходим к классам
Прототипная реализация ООП интересна и имеет свои преимущества, но она не так популярна, как классовая. Поэтому когда в ECMAScript 2015 появилось ключевое слово class, разработчики были очень этому рады.
JS стал походить на классический привычный объектно-ориентированный язык. Однако не будем забывать, что классы в нем – всего лишь синтаксический сахар над теми самыми прототипами. На вид – классы, под капотом – по-прежнему прототипы.
Вот так выглядит обычное создание класса в JavaScript:
class Animals {
constructor(name, specie) {
this.name = name;
this.specie = specie;
}
sing() {
return `${this.name} can sing`;
}
dance() {
return `${this.name} can dance`;
}
}
let bingo = new Animals("Bingo", "Hairy");
console.log(bingo);
Посмотрим в консоль:
Ничего не напоминает?
Все то же свойство __proto__, которое ссылается на Animals.prototype (-> Object.prototype).
Мы видим, что собственные значения свойств (name и specie) определяются внутри метода constructor. Кроме него создаются дополнительные функции sing и dance – методы прототипа.
Подкапотную реализацию этой конструкции мы разбирали только что – это функция-конструктор + ключевое слово new.
function Animals(name, specie) {
this.name = name;
this.specie = specie;
}
Animals.prototype.sing = function(){
return `${this.name} can sing`;
}
Animals.prototype.dance = function() {
return `${this.name} can dance`;
}
let Bingo = new Animals("Bingo", "Hairy");
Субклассирование
Субклассирование – это наследование и расширение родительского класса дочерним.
Предположим, вам нужно создать класс Cats. Вы могли бы написать его с нуля, но зачем, если уже есть класс Animals, который реализует часть нужного вам функционала. Вы можете просто унаследовать Cats от Animals и добавить то, чего не хватает (например, цвет усов).
Наследование в класс-ориентированном синтаксисе выглядит так:
class Animals {
constructor(name, age) {
this.name = name;
this.age = age;
}
sing() {
return `${this.name} can sing`;
}
dance() {
return `${this.name} can dance`;
}
}
class Cats extends Animals {
constructor(name, age, whiskerColor) {
super(name, age);
this.whiskerColor = whiskerColor;
}
whiskers() {
return `I have ${this.whiskerColor} whiskers`;
}
}
let clara = new Cats("Clara", 33, "indigo");
Объект класса Cats clara может использовать свойства и методы как класса Cats, так и класса Animals.
console.log(clara.sing()); // "Clara can sing"
console.log(clara.whiskers()); // "I have indigo whiskers"
Вот так clara выглядит в консоли:
В свойстве clara.__proto__.constructor лежит класс Cats, через него осуществляется доступ к методу whiskers(). Дальше в цепочке прототипов – класс Animals, с методами sing() и dance(). name и age – это свойства самого объекта.
Перепишем этот код в прототипном стиле с использованием метода Object.create():
function Animals(name, age) {
let newAnimal = Object.create(animalConstructor);
newAnimal.name = name;
newAnimal.age = age;
return newAnimal;
}
let animalConstructor = {
sing: function() {
return `${this.name} can sing`;
},
dance: function() {
return `${this.name} can dance`;
}
}
function Cats(name, age, whiskerColor) {
let newCat = Animals(name, age);
Object.setPrototypeOf(newCat, catConstructor);
newCat.whiskerColor = whiskerColor;
return newCat;
}
let catConstructor = {
whiskers() {
return `I have ${this.whiskerColor} whiskers`;
}
}
Object.setPrototypeOf(catConstructor, animalConstructor);
const clara = Cats("Clara", 33, "purple");
clara.sing(); // "Clara can sing"
clara.whiskers(); // "I have purple whiskers"
Метод Object.setPrototypeOf принимает два аргумента (объект, которому нужно установить новый прототип, и собственно сам желаемый прототип).
Функция Animals возвращает объект, прототипом которого является animalConstructor. Функция Cats создает объект с помощью конструктора Animals, но принудительно меняет его прототип на catConstructor, добавляя таким образом новые свойства. catConstructor в свою очередь тоже получает прототипом animalConstructor, чтобы образовалась цепочка прототипного наследования.
Таким образом, обычные животные будут иметь доступ только к методам animalConstructor, а кошки – еще и к catConstructor.
JavaScript оказался достаточно гибок, чтобы превратить свое прототипное ООП в классовое для удобства разработчиков.
Комментарии