Прототипы

JavaScript простым языком

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

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 и в который в любом случае, JavaScript приводит наш метод определения:

const cat = new Object({
    name: 'Кот',
    weight: 3,
    meow: function() {
      console.log('meow');
    }
});

Т.е. создаётся новый объект типа Object используя ключевое слово new. И внутрь этого Object мы передаём наш объект. В результате ничего абсолютно не меняется. Попробуем вывести объект cat в консоль:

console.log(cat);

Результат:

Как видишь, мы поменяли метод инициализации нашего объекта, но абсолютно ничего не поменялось в итоговом результате.

Так как мы создаём все наши объекты используя эту конструкцию new Object(...), то от этого самого Object к нашему объекту добавляются дополнительные свойства, к которым и относится то самое, непонятно откуда взявшееся, до текущего момента, свойство __proto__.

Получается, все объекты, которые мы создаем основываясь на базовом классе JavaScript - 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) никак не изменились.

В целом это всё что нужно знать о прототипах.

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

Кругом обман

На самом деле, все предыдущие материалы о типах данных в JavaScript были немного прикрыты страшной тайной. На самом деле в JavaScript – нет строкового типа, числового, логического и т.д. В JavaScript – всё объекты.

Это достаточно просто понять. К примеру, создадим обычную строку:

const str = 'строка';

Несмотря на то, что это обычная строка – у неё есть методы. К примеру:

console.log(str.toUpperCase()); // выведет: "СТРОКА"

Мы не определяли этот метод, но он существует. Он переводит всю строку к верхнему регистру.

Это происходит потому, что строковый тип, это на самом деле тоже объект, а именно String. Который в свою очередь основан на объекте Object.

Получается, что фактически написав такую вот конструкцию:

const str = 'строка';

Для JavaScript это то же самое что и:

const str = new String('строка');

Строка str на самом деле является объектом класса String, а прототипом объекта класса String является объект класса Object.

Все в JavaScript создается на основе класса Object. Словом, он здесь главный.

Last updated