May 14, 2020

Прототипы

Сегодня речь пойдет о прототипах. Чтобы лучше все понять – создаем сразу же простой объект.

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.

У нас получилась цепочка:

  1. Создав объект employee обычным способом, мы получили объект, прототипом которого является базовый класс Object.
  2. Мы создали объект 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. Он главный, он батька.