Полный цикл в digital

Prototype в JavaScript

При создании объектов, каждый из них будет содержать специальное внутреннее свойство Prototype, указывающее на его прототип. В JavaScript прототипы используются для организации наследования и расширения.

Установка прототипа методу

Допустим у нас имеется конструктор Box:

function Box(width, height) {
  this.width = width;
  this.height = height;
}

При объявлении конструктор или класса, у него автоматически появится свойство prototype. Оно содержит прототип, прототип – это объект. В данном случае им будет являться Box.prototype. Это очень важный момент.

Прототип будет автоматически назначаться всем объектам, которые будут создаваться с помощью этого конструктора:

// создание объекта с помощью конструктора Box
const box1 = new Box(25, 30);

При создании объекта, в данном случае box1, он автоматически будет иметь ссылку на прототип, то есть на свойство Box.prototype.

Это очень легко проверить, получить доступ к прототипу можно двумя способами:

  1. С помощью статического метода Object.setPrototypeOf(), правильный вариант
  2. С помощью свойства__proto__, лучше не использовать

Проверяем:

// true
Object.getPrototypeOf(box1) === Box.prototype
// true
box1.__proto__ === Box.prototype

Свойство prototype имеется у каждой функции за исключением стрелочных. Это свойство как мы уже отмечали выше в качестве значения имеет объект. По умолчанию в нём находится только одно свойство constructor, которое содержит ссылку на саму эту функцию:

// true
Box.prototype.constructor == Box 

Если создать ещё один объект класса Box, то он тоже будет иметь точно такой же прототип. Как мы уже отмечали выше прототипы в JavaScript используются для организации наследования. То есть, если мы сейчас в Box.prototype добавим какой-нибудь метод, он будет доступен для всех экземпляров класса Box:

// создадим конструктор Box
function Box(width, height) {
  this.width = width;
  this.height = height;
}
// добавим метод print в прототип Box
Box.prototype.print = function () {
  return `Box Size: ${this.width} x ${this.height}`
}
// создадим объекты
const box1 = new Box(25, 30);
const box2 = new Box(50, 70);
// выведем размеры ящиков в консоль
// Box Size: 25 x 30
console.log(box1.print());
// Box Size: 50 x 70
console.log(box2.print());

Обратите внимание, что метода print нет у объектов box1 и box2. Но если раскрыть значение свойства [[Prototype]] в консоли веб-браузере, то увидите его. То есть этот метод находится на уровне класса Box и наследуется всеми его экземплярами.

Соответственно получается, что мы можем вызвать print как метод объектов box1 и box2. Таким образом нам доступны не только собственные свойства и методы, но также наследуемые. А наследование, как вы уже понимаете, осуществляется в JavaScript на основе прототипов.

Так что же такое прототип? Прототип в JavaScript – это просто ссылка на объект, который используется для наследования.

Наследование

Если после переменной, содержащей некоторый объект поставить точку, то вы увидите все доступные для него свойства и методы:

Здесь width и height – это его собственные свойства. Далее на уровне родительского класса находятся методы constructor и print. Т.е. вы можете вызвать метод print, потому что он наследуется всеми экземплярами класса Box. Кроме этого, здесь имеются методы класса Object, такие как hasOwnProperty, isPrototypeOf, toString и так далее. Эти методы тоже доступны, потому что Box.prototype наследует все свойства и методы Object.prototype.

Таким образом, кроме собственных свойств и методов объекту также доступны свойства и методы из прототипов. Такие свойства и методы называются наследованными.

В этом примере, объект box1 имеет свои собственные свойства width и height, а также наследует все свойства и методы Box.prototype и Object.prototype.

Цепочка прототипов

В JavaScript наследование осуществляется только на уровне объектов через прототипы. То есть один объект имеет ссылку на другой через специальное внутреннее свойство [[Prototype]]. Тот в свою очередь тоже имеет ссылку и т.д. В результате получается цепочка прототипов.

Таким образом наследование, которые мы рассмотрели выше на примере объекта box1 происходит благодаря существованию следующей цепочки прототипов:

box1 -> Box.prototype -> Object.prototype

Заканчивается цепочка на прототипе глобального класса Object, потому что он не имеет прототипа, то есть его значение __proto__ равно null.

При этом, когда мы пытаемся получить доступ к некоторому свойству или методу этого объекта, поиск всегда начинается с самого объекта. Если данного свойства или метода у него нет, то поиск перемещается в прототип, потом в прототип прототипа и так далее.

Если указанное свойство или метод не найден, то возвращается undefined.

Например, если метод print мы добавим в сам объект box1, то будет использоваться уже он, а не тот, который находится в прототипе Box.prototype:

box1.print = function() {
  return `Размеры коробки: ${this.width} x ${this.height}`;
}

Поиск сразу прекращается, как только указанный метод будет найден. А в данном случае он будет найден сразу в объекте, поэтому переход в прототип не осуществится.

Значение this внутри методов

Значение this внутри методов определяется только тем для какого объекта мы его вызываем.

Рассмотрим следующий пример:

function Counter() {
  this.value = 0;
}
Counter.prototype.up = function() {
  this.value++;
  return this.value;
}
const counter1 = new Counter();
const counter2 = new Counter();
// 1
console.log(counter1.up());
// 2
console.log(counter1.up());
// 1
console.log(counter2.up());

Здесь мы вызываем up как метод объектов counter1 и counter2. Данный метод не является собственным для этих объектов, он наследуется и находится на уровне класса Counter. Но на самом деле это не имеет значения. Единственное, что важно для this – это только то, для какого объекта мы вызываем этот метод, то есть что стоит перед точкой. Это и будет this.

При вызове counter1.up(), this внутри этого метода будет указывать на counter1:

counter1.up();

На строчке перед точкой стоит counter2, значит this внутри up будет указывать на него:

counter2.up();

Таким образом, не важно, где находится метод, this внутри него всегда будет указывать на объект, для которого он вызывается, и, следовательно, с помощью this мы всегда сначала будем получать доступ к собственным свойствам и методам этого объекта.

Установка прототипа объекту

Установить прототип объекту можно двумя способами:

  1. С помощью статического метода Object.setPrototypeOf(), правильный вариант
  2. С помощью свойства__proto__, лучше не использовать

Object.setPrototypeOf()

Чтобы было более понятно как работает метод Object.setPrototypeOf, рассмотрим его синтаксис:

Object.setPrototypeOf(obj, prototype)
  • obj – объект, для которого необходимо установить прототип
  • prototype – объект, который будет использоваться в качестве прототипа для obj, или null, если у obj не должно быть прототипа

Пример установки прототипа с помощью Object.setPrototypeOf():

const message = {
  text: 'Сообщение 1...',
  color: 'black',
  getText() {
    return `
${this.text}
`; } } const errorMessage = { text: 'Сообщение 2...', color: 'red' } Object.setPrototypeOf(errorMessage, message);

В этом примере мы в качестве прототипа для errorMessage установили message.

Очень важный момент заключается в том, что мы не можем указать в качестве прототипа объект, который уже имеется в цепочке, то есть замкнуть её.

Следовательно, мы получим ошибку, если попытаемся для message установить в качестве прототипа errorMessage:

Object.setPrototypeOf(message, errorMessage);

Кроме этого, в JavaScript нет множественного наследования, то есть нельзя одному объекту назначить несколько прототипов.

__proto__

Например, создадим два объекта и установим для второго объекта в качестве прототипа первый объект с помощью __proto__:

const person1 = {
  name: 'Tom',
  printName() {
    return `Name: ${this.name}`
  }
}
const person2 = {
  name: 'Bob',
  __proto__: person1
}

Проверить что прототипом для person2 является person1 очень просто:

// true
person2.__proto__ === person1
// true
Object.getPrototypeOf(person2) === person1

При этом метод printName становится наследуемым, то есть доступным для объекта person2:

// "Name: Bob"
person2.printName();

Наследование

Допустим у нас имеется конструктор Person:

function Person(name, age) {
  this.name = name;
  this.age = age;
}
person.prototype.getName = function() {
  return this.name;
}

Создадим конструктор Student, который будет расширять класс Person:

function Student(name, age, schoolName) {
  // вызываем функцию, передавая ей в качестве this текущий объект
  Person.call(this, name, age);
  this.schoolName = schoolName;
}
Student.prototype.getSchoolName = function() {
  return this.schoolName;
}

Но на данном этапе, они сейчас полностью независимы. Но для того чтобы класс Student расширял Person нужно указать, что прототипом для Student.prototype является Person.prototype. Например, выполним это с помощью свойства __proto__:

Student.prototype.__proto__ = Person.prototype;

Создадим новый экземпляр класса Student:

const student = new Student('Bob', 15, 'ABC School');

Для этого объекта будет доступен как метод getSchoolName, так и getName.

Получение данных из prototype

Прототип объекта хранится в свойстве __proto__, которое реализованно как псевдоним внутреннего свойства [[Prototype]]. Кроме того, получить прототип объекта можно с помощью метода getPrototypeOf():

const message = {
    text: 'Сообщение 1...',
    color: 'black',
    getText() {
        return `${this.text}`;
    }
}
const errorMessage = {
    text: 'Сообщение 2...',
    color: 'red'
}
Object.setPrototypeOf(errorMessage, message);
// black
console.log(errorMessage.__proto__.color);
// black
console.log(Object.getPrototypeOf(errorMessage).color);

Свойство constructor

У каждой функции, кроме стрелочных, по умолчанию имеется свойство prototype. Этот прототип автоматически будут иметь все объекты, если мы эту функцию будем использовать как конструктор, то есть для создания объектов.

По умолчанию свойство prototype функции содержит следующий объект:

function Article(title) {
  this.title = title;
}
Article.prototype = {
  constructor: Article
}

Здесь мы свойству prototype присвоили объект вручную, но точно такой же генерируется автоматически. Этот объект изначально содержит только свойство constructor, которое указывает на сам конструктор, то есть на функцию.

Свойство constructor можно использовать для создания объектов:

const article1 = new Article('Про витамины');
const article2 = new article1.constructor('Про мёд');

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

Не рекомендуется полностью перезаписывать значение свойства prototype, потому что в этом случае вы потеряете constructor и его придётся добавлять вручную:

// так делать не нужно
Article.prototype = {
  getTitle() {
    return this.title;
  }
}

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

Article.prototype.getTitle = function() {
  return this.title;
}

Встроенные прототипы

В JavaScript практически всё является объектами. То есть функции, массивы, даты и так далее. Исключением являются только примитивные типы данных: строка, число и так далее.

Например, при создании объекта { name: 'Tom' } внутренне используется конструктор Object:

const person = { name: 'Tom' }

Прототипом такого объекта соответственно становится Object.prototype и в этом легко убедиться:

// true
person.__proto__ === Object.prototype 

Поэтому нам на уровне этого объекта доступны различные методы, они берутся из Object.prototype. Например, метод hasOwnProperty:

// true
person.hasOwnProperty('name');

Этот метод возвращает true, когда указанное свойство является для этого объекта родным, в противном случае false.

При этом Object.prototype является корнем иерархии других встроенных прототипов. Но при этом он сам не имеет прототипа.

На рисунке видно, что конструктор Object имеет по умолчанию свойство prototype. Это значение будет автоматически записываться в свойство [[Prototype]] объектов, которые будет создаваться с помощью этого конструктора. В Object.prototype имеется свойство constructor, которые указывает на сам конструктор. Эти связи между Object и Object.prototype показаны на схеме. Кроме этого Object.prototype не имеет прототипа. То есть его значение [[Prototype]] содержит null.

Теперь давайте рассмотрим, как выполняется создание даты в JavaScript. Осуществляется это очень просто посредством конструктора Date:

const now = new Date();

Следовательно, прототипом даты является Date.prototype:

// true
now.__proto__ === Date.prototype 

Этот прототип содержит большое количество методов для работы с датой, например, такие как getDate, getHours и так далее. Их нет в now, но они доступны нам посредством наследования.

Объект Date.prototype имеет в качестве прототипа Object.prototype:

// true
Date.prototype.__proto__ === Object.prototype
// true
now.__proto__.__proto__ === Object.prototype

Следовательно, методы Object.prototype, которых нет в Date.prototype также доступны для now. Например, hasOwnProperty:

// false
now.hasOwnProperty('getYear')

Таким образом можно нарисовать следующую схему:

Другие встроенные объекты устроены подобным образом.

Метод Object.create

Object.create предназначен для создания нового объекта, который будет иметь в качестве прототипа объект, переданный в этот метод в качестве аргумента:

const rect1 = {
  a: 8,
  b: 5,
  calcArea() {
    return this.a * this.b
  }
}
// создали новый объект, который будет иметь в качестве прототипа rect1
const rect2 = Object.create(rect1);
rect2.a = 10;
rect2.b = 5;
// 50
rect2.calcArea();

Создание объекта с прототипом Object.prototype:

const obj = Object.create(Object.prototype);

Данный пример аналогичен этому:

const obj = {};

Создание объекта без прототипа:

const obj = Object.create(null);

Во 2 аргументе мы можем объекту сразу передать необходимые свойства. Описываются они в полном формате с использованием специальных атрибутов как в Object.defineProperties:

const person = Object.create(null, {
  name: {
    value: 'John'
  },
  age: {
    value: '18',
    writable: true
  }
});

Здесь мы описали два свойства: name и age. С помощью value мы устанавливаем значение свойству, а посредством аргумента writable задаем доступно ли свойство для изменения.

Пример, в котором мы с помощью Object.create установим для Book.prototype в качестве прототипа объект Product.prototype:

// конструктор Product
function Product(params) {
  this.name = params.name;
  this.price = params.price;
  this.discount = params.discount;
}
// конструктор Book
function Book(params) {
  Product.call(this, params);
  this.isbn = params.isbn;
  this.author = params.author;
  this.totalPages = params.totalPages;
}
// устанавливаем для Book.prototype в качестве прототипа объект Product.prototype
Book.prototype = Object.create(Product.prototype, {
  constructor: {
    value: Book
  },
  // геттер
  fullName: {
    get() {
      return `${this.author}: ${this.name}`;
    }
  }
});
Product.prototype.calcDiscountedPrice = function() {
  return (this.price * (1 - this.discount / 100)).toFixed(2);
}
// создадим новый объект
const book = new Book({
  name: 'Краткая история времени',
  author: 'Стивен Хокинг',
  isbn: '978-5-17102-284-6',
  totalPages: 232,
  price: 611.00,
  discount: 5
});
// цена книги со скидкой, 580.45
console.log(book.calcDiscountedPrice());

Здесь мы Object.create используем для создания нового объекта, который будет использоваться в качестве прототипа для Book.prototype. При этом свою очередь этот новый объект будет иметь в качестве прототипа Product.prototype. Для этого мы передаем его в качестве аргумента методу Object.create. Кроме этого к этому объекту мы сразу же добавили два свойства: constructor и fullName. С помощью constructor мы восстановили конструктор, который был в прототипе пока мы ему не присвоили новое значение. А динамическое свойство fullName, представляющее собой геттер, будем использовать для получения имени книги с автором.

Заполните форму уже сегодня!
Для начала сотрудничества необходимо заполнить заявку или заказать обратный звонок. В ответ получите коммерческое предложение, которое будет содержать индивидуальную стратегию с учетом требований и поставленных задач
Работаем по будням с 9:00 до 18:00. Заявки, отправленные в выходные, обрабатываем в первый рабочий день до 12:00.
Спасибо, ваш запрос принят и будет обработан!
Эйч Маркетинг