Прототипы
Сегодня речь пойдет о прототипах. Чтобы лучше все понять – создаем сразу же простой объект.
const cat = { name: 'Кот', weight: 3, meow: function() { console.log('meow'); } }; console.log(cat);
Результат:
Попробуем вывести в консоль результат действия функции meow
:
console.log(cat.meow()); //выведет строку: meow
Все логично и просто. Попробуем вызвать несуществующую функцию:
cat.woof();
Результат ожидаем:
JavaScript правильно подсказывает – woof
не является функцией. Оно так и есть, ведь мы не определяли эту функцию внутри нашего объекта.
Но, для примера, давай попробуем вызвать еще один метод, который мы не определяли:
cat.valueOf();
Результат:
Ошибки не произошло. И даже вывелись все данные о нашем объекте.
Попробуем еще:
cat.toString();
Результат:
Снова что-то вывелось и снова никакой ошибки. Но как так?
Мы не определяли никакого метода valueOf
внутри нашего объекта, ровно как и не определяли метод toString
. Магия вне Хогвартса? Нет. Все куда проще.
Еще раз посмотрим на первый скриншот в этой статье:
Это наш объект. Показаны все свойства и методы, которые мы определяли: meow
, name
, weight
.
Но что за свойство __proto__
? Давай посмотрим, что лежим внутри него:
Очень много всего. Непонятно зачем, а главное – непонятно откуда.
В этом всем списке, мы видим и те методы, которые мы вызывали: valueOf
и toString
.
Когда мы вызывали данные методы, JavaScript искал их сначала в пределах нашего объекта, а так как там он их не нашел, то пошел искать их в свойство __proto__
.
Как это работает и зачем нужно?
Давай сначала разберемся с созданием объекта. Тот синтаксис, который мы использовали для определения нашего объекта cat
является упрощенным:
const cat = { name: 'Кот', weight: 3, meow: function() { console.log('meow'); } };
Мы можем определить это другим методом, тем, который более понятен для самого JavaScript и в который, в любом случае, JS приводит наш метод определения:
const cat = new Object({ name: 'Кот', weight: 3, meow: function() { console.log('meow'); } });
Т.е. создается новый объект типа Object
используя ключевое слово new
. И внутрь этого Object
мы передаем наш объект. В результате, ничего абсолютно не меняется. Попробуем вывести объект cat в консоль:
console.log(cat);
Результат:
Как видишь, мы поменяли метод инициализации нашего объекта, но абсолютно ничего не поменялось в итоговом результате.
Так как мы создаем все наши объекты используя эту конструкцию new Object(...)
, то от этого самого Object
к нашему объекту добавляются дополнительные свойства. К которым и относится то самое, непонятно откуда взявшееся, до текущего момента, свойство __proto__
.
Получается, все объекты, которые мы создаем основываются на базовом классе JS - Object
.
У класса Object
имеется свойство prototype
. Посмотрим, что там внутри:
console.log(Object.prototype);
Результат:
И мы видим, что внутри этого свойства лежат те же методы, которые добавились к нашему объекту в свойство __proto__
.
Теперь, примерно объясню, зачем это нужно и как это можно использовать.
Давай в свойство prototype
класса Object
добавим какую-нибудь свою функцию. К примеру, функцию woof
, которую мы пытались вызывать и у нашего объекта, но у нас происходила ошибка:
Object.prototype.woof = function() { console.log('woof'); }
Теперь, ничего не меняя в нашем объекте, т.е. он останется такого же вида:
const cat = new Object({ name: 'Кот', weight: 3, meow: function() { console.log('meow'); } });
Попробуем вызвать метод woof:
console.log(cat.woof());
Результат:
Как видишь, никакой ошибки и все прекрасно отработало. Еще раз посмотрим на наш объект в консоли:
console.log(cat);
Результат:
Метод woof
, который мы добавляли в свойство prototype
объекта Object
успешно передалось в свойство __proto__
нашего объекта, поэтому и не произошло никакой ошибки.
Естественно, даже если мы будем использовать первоначальный синтаксис создания нашего объекта без использования new Object(...)
, то все равно все будет работать ровно так же.
Надеюсь, ты уже понял, что с помощью прототипов мы получаем возможность расширять возможности наших объектов. К примеру, если оставить данный метод:
Object.prototype.woof = function() { console.log('woof'); }
И создать несколько своих объектов, то из каждого у них в свойстве __proto__
добавится метод woof
. Т.е. написав этот метод один раз в базовом классе Object
– мы можем использовать его в неограниченном количестве объектов созданных нами.
Object.create
У базового класса Object
существует метод create
. Он напрямую связан с прототипами, поэтому я решил рассказать и о нем.
Для того чтобы объяснить как работает этот метод и что он делает, создадим 2 объекта.
Первый объект будет представлять из себя основу для второго объекта. Называться он будет employee
, что в переводе означает "работник".
const employee = { name: 'Работник', position: 'Повар' }; console.log(employee);
Вывод в консоль нам даст:
Для создания второго объекта нам понадобится метод create
из класса Object
:
const manager = Object.create(employee);
Что мы только что сделали? Мы создали новый объект, который должен описывать работника, который является менеджером.
Object.create(employee)
говорит о том, что нужно создать новый объект, прототипом которого будет являться объект employee
.
Давай выведем наш объект в консоль:
console.log(manager);
Результат:
Объект пустой. И это неудивительно, мы же не задали ему ни одного свойства и метода. Но, что же у него теперь находится в свойстве __proto__
? Смотрим:
А внутри него лежит объект employee
, как мы и заказывали. И как видишь, у этого объекта, тоже есть свое свойство __proto__
:
И как ты заметил это класс Object
.
У нас получилась цепочка:
- Создав объект
employee
обычным способом, мы получили объект, прототипом которого является базовый классObject
. - Мы создали объект
manager
с помощьюObject.create(employee)
, прототипом которого указали объектemployee
.
Теперь, имея объект manager
, давай установим для него имя и должность:
manager.name = 'Сергей'; manager.position = 'Менеджер';
Теперь еще раз посмотрим на наш объект:
У нашего объекта появились свои свойства name
и position
, при этом одноименные свойства в прототипе (employee
) никак не изменились.
В целом это все что нужно знать о прототипах.
Если подитожить, то получается, что прототип – это базовый объект другого объекта. Этот базовый объект присутствует у других объектов и каким-то образом "расширяет" их возможности.
Кругом обман
На самом деле, все предыдущие материалы о типах данных в JS были немного прикрыты страшной тайной. На самом деле в JS – нет строкового типа, числового, логического и т.д. В JS – всё объекты.
Это достаточно просто понять. К примеру, создадим обычную строку:
const str = 'строка';
Несмотря на то, что это обычная строка – у нее есть методы. К примеру:
console.log(str.toUpperCase()); //выведет: "СТРОКА"
Мы не определяли этот метод, но он существует. Он переводит всю строку к верхнему регистру.
Это происходит потому, что строковый тип, это на самом деле тоже объект, а именно, объект String
. Который в свою очередь основан на объекте Object
.
Получается, что фактически написав такую вот конструкцию:
const str = 'строка';
Для JS это тоже самое что и:
const str = new String('строка');
Строка str
на самом деле является объектом класса String
, а прототипом объекта класса String
является объект класса Object
.
Все в JS создается на основе класса Object
. Он главный, он батька.