В Части 1 мы обсуждали шаблон OLOO: объекты, связанные с другими объектами в JavaScript. Мы узнали, что объекты создаются не из классов, а из других объектов, называемых прототипами. Затем прототипы могут получать запросы от объектов в нижней части цепочки прототипов, чтобы хранить общие данные и поведение. Как только прототипы будут поняты, OLOO может стать очень простым и динамичным шаблоном для создания объектов.

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

Теперь вам может быть интересно, почему более сложный шаблон исключает шаблон OLOO. Дуглас Крокфорд — постоянно влияющий на развитие JavaScript — объясняет, что было много смешанных чувств по поводу прототипической природы JavaScript. Это было связано с тем, что во всех популярных языках применялся классический подход — классы, создающие объекты (как показано в нашем примере с Ruby в Части 1).

Есть несколько ключевых элементов, которые часто представляют языки на основе классов:

  1. Какая-то функция-конструктор для создания объектов
  2. new ключевое слово, которое инициализирует создание нового объекта.

Чтобы сохранить знакомый синтаксис, в первой версии JavaScript (ES1) для создания объектов использовались функции-конструкторы и ключевое слово new. Однако под капотом скрывается истинная природа JavaScript: система объектов и прототипов, тайно связанных друг с другом.

Функции конструктора, прототипы и объекты

Чтобы объяснить, как все это работает, давайте вернемся к нашему объекту myChair, который мы создали в Части 1:

Во-первых, ниже приведен литерал объекта, который создает простой стул, как показано выше:

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

А вот и псевдоклассический подход к созданию того самого стула:

function Chair(width, minHeight) {
  this.width = width;
  this.minHeight = minHeight;
}
let myChair = new Chair(50, 45);
myChair;                           // { width: 50, minHeight: 45 }

Основываясь на этом коде, мы можем сделать некоторые выводы:

  • Функция Chair вызывается с помощью оператора new, который превращает обычную функцию в «функцию-конструктор» (о конструкторах мы поговорим позже). Конструкторы обычно написываются с большой буквы, чтобы отличать их от обычных функций и других переменных.
  • В конструкторе Chair this устанавливает свойства width и minHeight для себя и присваивает переданные значения этим новым свойствам (опять же, это будет объяснено позже).
  • Объект возвращается из метода, как указано в последней строке, когда мы ссылаемся на объект, назначенный myChair — он возвращает то же значение, что и наш литерал объекта.

Хорошо… это отчасти объясняет, что происходит с конструктором, «создающим экземпляр» объекта, но как насчет псевдоклассического наследования? Наш объект myChair наследуется от чего-либо?

На самом деле да! Но чтобы объяснить наследование, мы должны сначала понять, что…

Все функции являются объектами — у них есть свойства и методы, как и у обычных объектов!

Функции на самом деле являются объектами! Теперь, когда мы это знаем, мы можем, наконец, понять, как работает псевдоклассическое наследование:

Наша функция-конструктор Chair — да и вообще все функции в этом отношении — имеют специальное свойство, называемое свойствоprototype, которое ссылается... да, как вы уже догадались, объект-прототип. Он называется Chair.prototype.

typeof Chair.prototype;    // "object"

Этот объект Chair.prototype поддерживает наследование на основе прототипов в JavaScript. Объект myChair, созданный нашей функцией-конструктором Chair, будет ссылаться на этот объект Chair.prototype в своем собственном скрытом свойстве [[Prototype]]:

Теперь вы можете задаться вопросом: «Почему JavaScript должен проходить через все эти проблемы только для того, чтобы связать два объекта вместе с помощью функции-конструктора?!» Это было сделано для того, чтобы JavaScript чувствовал себя более доступным для программистов, более знакомых с программированием на основе классов. Поэтому этот шаблон называется Псевдо-классическим. JavaScript есть и всегда был языком, основанным на прототипах. Он не имеет собственных классов и может использовать объекты и прототипы только для выполнять наследование на основе прототипов.

Обратите внимание, что свойство prototype функции-конструктора и скрытое свойство [[Prototype]] объекта — это две разные вещи. Однако в этом примере они оба ссылаются на один и тот же объект — Chair.prototype. И как мы узнали из Части 1, все объекты по умолчанию имеют Object.prototype — самый простой объект — в качестве своего прототипа.

5 шагов конструкторов JavaScript

Давайте теперь подробнее рассмотрим, как работают конструкторы, используя наш пример Chair. Если мы вспомним нашу псевдоклассическую реализацию раньше:

function Chair(width, minHeight) {
  this.width = width;
  this.minHeight = minHeight;
}
let myChair = new Chair(50, 45);

При выполнении new Chair(50,45) происходит следующее:

  1. Chair становится конструктором и создает новый пустой объект:
    В этом пустом объекте нет ничего особенного — думайте о нем как о простом старом пустом объекте {}.
  2. Свойство hidden[[Prototype]] пустого объекта настроено на ссылку на объект Chair.Prototype:
    Как упоминалось ранее, теперь наш объект кресла может получать наследование на основе прототипа.
  3. Контекст выполнения (this) устанавливается из глобального объекта в пустой объект:
    Поскольку у вызова Chair не было явного вызывающего объекта, по умолчанию this ссылается на глобальный объект, а не на наш пустой объект. Прежде чем мы сможем присвоить свойства нашему пустому объекту, this должен сначала указать на него.
  4. Выполните функцию, чтобы добавить свойства к объекту стула:
    Наша функция-конструктор, наконец, выполняется, и выполняется весь код в определении. В этом случае оба переданных аргумента width и minHeight назначаются свойствам с тем же именем и устанавливаются для нашего нового объекта стула.
  5. Новый объект стула возвращается из функции конструктора Chair:
    По умолчанию конструкторы неявно возвращают объект, созданный на шаге 1, если только не используется явный return для возврата какого-либо другого объекта.

Анимация ниже показывает, как работают все 5 шагов конструктора:

Установка поведения для псевдоклассических объектов

Теперь, когда вы понимаете, как на самом деле работают конструкторы — поздравляю! Это не маленькая задача!

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

Скажем, мы хотели создать 2 объекта стула: myChair и anotherChair, каждый с разными свойствами width и minHeight. Эти свойства различаются от одного объекта стула к другому, поэтому имеет смысл сохранить эти различия для каждого объекта:

function Chair(width, minHeight) {
  this.width = width;
  this.minHeight = minHeight;
}
let myChair = new Chair(50, 45);
let anotherChair = new Chair(40, 60);
myChair;                        // { width: 50, minHeight: 45 }
anotherChair;                   // { width: 40, minHeight: 60 }

Но как насчет свойств, которые остаются одинаковыми для всех объектов стула?

Поведение обычно одинаково для объектов одного типа. Скажем, например, все объекты Chair можно было сложить и убрать. Мы могли бы создать метод fold следующим образом:

function fold() {
  console.log('chair folded!');
}

Большой! Теперь, когда у нас есть функция fold, как нам прикрепить ее ко всем нашим объектам стула?

Мы могли бы передать этот функциональный объект конструктору и назначить его, как и другие свойства, например. this.fold = fold;. Однако это приведет к ненужному дублированию, поскольку каждый объект стула будет содержать один и тот же метод. Как мы узнали в Части 1: объекты должны хранить только то, что остается для них уникальным, и делегировать общие данные своему прототипу.

Так мы и будем делать! Мы можем добавить нашу функцию fold в качестве метода к Chair.prototype — прототипу всех объектов стула:

Chair.prototype.fold = function() {
  console.log('chair folded!');
}

Теперь, если какой-либо объект кресла — или любой будущий объект кресла в этом отношении — должен вызвать свой метод fold, он находится в их цепочке прототипов в объекте Chair.prototype. Он доступен через скрытое свойство [[Prototype]] каждого стула:

Заключение

Фу! Когда дело доходит до OLOO и псевдоклассических шаблонов создания объектов, требуется многое, поэтому я рад, что вы дочитали до конца!

Вот несколько важных моментов, которые следует помнить о создании объектов в JavaScript:

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

Я надеюсь, что вы нашли эту пару статей проницательными, и большое спасибо за то, что нашли время, чтобы прочитать их. Удачной учебы!