Послушав несколько студентов во время учебы в области разработки программного обеспечения, я заметил много путаницы, связанной с Шаблонами создания объектов JavaScript. Для новичков это может быть особенно сложно — и без тщательного изучения может привести к колебаниям в использовании объектов. Имея это в виду, я впервые приготовился к глубокому погружению в изучение шаблонов создания объектов JavaScript.

К моему облегчению, мой прогресс был гладким, поскольку я изучил эти шаблоны в Школе запуска — практически без путаницы. Как это было сделано? Ну, я очень рад поделиться своими выводами здесь в двух частях:

  • Часть 1 охватывает наследование и делегирование прототипов в JavaScript с помощью шаблона OLOO.
  • Часть 2 посвящена псевдоклассическому шаблону — сочетанию функций-конструкторов и прототипов, которые создают объекты в JavaScript.

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

Традиционные объекты на основе классов

Мы будем расширять метафору блестящего стула инженера-программиста Мехди Моджуда:

В языке, основанном на классах, таком как Ruby: классы подобны чертежам. Если бы мы хотели, например, изготовить стул, который имеет width и height, мы бы создали класс, который служит «чертежом», и создали (или создали экземпляр) стул на основе этого чертежа:

class Chair
  def initialize(width, height)
    @width = width
    @height = height
  end
end
my_chair = Chair.new(50, 45)

Что, если бы мы захотели создать более конкретный объект, скажем, стул для рабочего стола? Мы должны внести изменения в схему. Настольные стулья, как правило, более сложны, чем обычные стулья — их можно регулировать по высоте и у них есть колеса. Чтобы разместить оба объекта стула, мы должны сохранить наш исходный класс Chair (суперкласс) и создать более конкретный класс DeskChair (подкласс), который будет устанавливать исходные свойства нашего класса Chair с помощью ключевого слова super:

class Chair
  def initialize(width, min_height)
    @width = width
    @min_height = min_height
  end
end
class DeskChair < Chair
  def initialize(width, min_height, max_height, wheels)
    super(width, min_height)
    @max_height = max_height
    @wheels = wheels
  end
end
my_chair = Chair.new(50, 45)
my_desk_chair = DeskChair.new(50, 45, 52, 5)

JavaScript-прототипы

Продолжая метафору стула Мехди Моджуда — когда дело доходит до прототипов, мы не создаем чертежи или классы, мы просто создаем объект стула. Представьте, что вы нарубили немного дерева и построили из него стул. Это полностью функциональный объект стула, который может служить прототипом для будущих объектов стула:

let myChair = {
  width: 50,
  height: 45,
};

Далее Моджуд объясняет: «В мире прототипов вы строите стул и просто делаете его «дубликаты». Если мы хотим построить наш письменный стул, мы «дублируем» наш оригинальный стул, используя Object.create, даем нашему новому стулу регулируемую высоту, добавляем к нему колеса и та-да! У нас есть рабочее кресло! Нам не нужно было создавать план для этого, не так ли?

let myChair = {
  width: 50,
  minHeight: 45,
};
let myDeskChair = Object.create(myChair);
myDeskChair.maxHeight = 52;
myDeskChair.wheels = 5;

И если нам нужен объект chair из любыхwidth и height, мы можем просто добавить метод init для установки начальных значений для стула любого размера:

let myChair = {
  init(width, height) {
    this.width = width;
    this.height = height;
    return this;
  }
};
let anotherChair = Object.create(myChair).init(40,60);

Между объектами на основе классов и объектами-прототипами существует большое, но тонкое различие. Если бы мы сравнили объект рабочего стула Ruby с объектом стула JavaScript, помимо поверхностной информации об идентификаторе объекта Ruby: #<DeskChair: 0x000055e0cfa1fddc8, мы обнаружили бы важное различие. Можете ли вы найти это в коде ниже?

Рабочий стул Руби:

p my_desk_chair
#<DeskChair:0x000055e0cfa1fdc8 
@width=50, @min_height=45, @max_height=52, @wheels=5>

Рабочий стол JavaScript:

console.log(myDeskChair);
{ maxHeight: 52, wheels: 5 }

Чтобы заметить разницу, нам сначала нужно понять цепочку прототипов JavaScript.

Цепочка прототипов: наследование и делегирование

Если вы посмотрите достаточно внимательно, вы, возможно, заметите, что стул Ruby содержит все свои свойства: width, min_height, max_height и wheels, тогда как наш новый объект стула JavaScript содержит только maxHeight и wheels. Странно… разве мы не «продублировали» наш оригинальный стул? Разве он не должен обладать всеми свойствами нашего оригинального прототипа стула?

На самом деле… наш новый объект рабочего стола действительно имеет доступ ко всем своим свойствам:

console.log(myDeskChair);    // { maxHeight: 52, wheels: 5 }
myDeskChair.width;           // 50
myDeskChair.min_height;      // 45

Подождите… что происходит?! Эти свойства не существуют на myDeskChair, но мы по-прежнему сохраняем к ним доступ — как это возможно?

Как оказалось, myDeskChair начинается как пустой объект, к которому мы добавили свойства maxHeight и wheels. Каждый объект в JavaScript содержит скрытое свойство с именем [[Prototype]]. Это свойство содержит доступ к прототипу объекта. Когда мы создали наше рабочее кресло, Object.create устанавливает скрытое свойство myDeskChair [[Prototype]] для ссылки на объект myChair в качестве его значения.

Чтобы найти «отсутствующие» свойства в myDeskChair, мы можем получить доступ к любым свойствам в прототипе нашего объекта, используя цепочку прототипов JavaScript.

Когда мы пытаемся получить доступ к свойству width из myDeskChair, JavaScript сначала проверит объект myDeskChair. Свойство width не найдено, поэтому JavaScript будет искать свою цепочку прототипов через скрытое свойство [[Prototype]] myDeskChair. Его прототип — myChair — который JavaScript будет искать дальше и обнаружит, что свойство width существует. Он получит доступ и вернет первое свойство width, которое увидит, что позволит переопределить свойства прототипа, расположенные дальше по цепочке поиска, которые имеют то же имя.

На вершине цепочки прототипов находится самый простой объект-прототип под названием Object.prototype. Все объекты будут иметь этот объект в качестве прототипа по умолчанию. Object.prototype не имеет собственного прототипа и вернет null, если мы попытаемся получить доступ к его прототипу:

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

Заключение

Этот шаблон создания объектов известен как OLOO или «Связывание объектов с другими объектами» и лежит в основе того, как объекты на самом деле работают в JavaScript. Исторически сложилось так, что OLOO был введен после псевдоклассического шаблона в попытке вернуться к истинной природе объектов, прототипов и делегирования JavaScript.

Однако шаблон создания псевдоклассических объектов был оригинальным способом создания объектов в JavaScript и сам по себе является допустимым шаблоном. Во второй части мы обсудим, как это работает, и как объекты тайно работают за кулисами!

Перейти к части 2 нашего обсуждения шаблонов JS OOP…