Все, что вам нужно знать о создании объектов в JavaScript

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

Фабрики объектов

Фабрики объектов - это функции, возвращающие объекты, которые можно использовать для автоматизации процесса создания объектов. Все объекты будут иметь один и тот же «тип» в том смысле, что они будут иметь одинаковые свойства для состояния и одни и те же методы, но использование Object.getPrototypeOf вернет Object.prototype/{}, поэтому вы не сможете понять, какую функцию вы использовали для создания объектов экземпляра. .

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

> let cello = createInstrument("cello","squeak",500) 
> Object.getPrototypeOf(cello) // {}

Чтобы написать функцию фабрики объектов, вы объявляете функцию, которая принимает аргументы, которые будут составлять состояние объекта. В теле функции return литерал объекта. Свойства должны быть установлены с использованием аргументов, как показано ниже для instrument, noise и value, и могут быть добавлены методы. В целом, вы создаете скелетную версию объектов, которые создадите с помощью этой функции, и все свойства будут добавляться к каждому новому объекту, который вы создаете с помощью этой функции, отсюда и неэффективность памяти.

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

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

Фабрики объектов с добавками

Использование фабрик объектов с добавками удобно для моделирования объектов, не имеющих четкой связи « is-a» . В приведенном ниже примере мы можем создать три различных и не связанных между собой типа объектов - утконосов (также известных как platypi или platypodes), пингвинов и людей - которые имеют какое-то общее поведение, но никоим образом не поддаются подклассу. Вместо того, чтобы дублировать код в методах для каждой фабрики объектов, поведение смешано с использованием Object.assign, который копирует все перечисляемые свойства из исходного объекта в целевой объект.

В коде details, swim и layEggs - это объекты со свойствами, которые имеют значения функций. Есть три функции для создания трех разных типов объектов: createPlatypus, createPenguin и createHuman. Эти три функции можно использовать для создания новых экземпляров этих типов объектов.

Взглянув на createPlatypus, мы видим, что функция принимает параметр имени. В теле функции Object.assign используется для присвоения перечислимых свойств из объектов details, swim и layEggs пустому объекту. После того, как свойства этих трех объектов скопированы в новый объект, он использует метод addDetails, который был скопирован в createPlatypus из объекта details, когда мы использовали метод Object.assign, который копируется в перечислимые свойства. Таким образом, он создает новый объект утконоса со свойством name и методами из объектов details, swim и layEggs.

Если вы проверите свойства объектов, созданных из этих функций, что, по вашему мнению, вы увидите?

> let platypus = createPlatypus("platypus");
> let penguin = createPenguin("penguin");
> let human = createHuman("human");
> human
{ displayDetails: [Function: displayDetails], swim: [Function: swim], name: 'human' }
> penguin
{ displayDetails: [Function: displayDetails], swim: [Function: swim], layEggs: [Function: layEggs], name: 'penguin' }
> platypus
{ displayDetails: [Function: displayDetails], swim: [Function: swim], layEggs: [Function: layEggs], name: 'platypus'}

Если использование метода addDetails там сбивало с толку, вот еще одна версия, в которой мы устанавливаем детали в самой функции. Обратите внимание, что в этих функциях контекст выполнения будет глобальным объектом, но мы можем использовать пустой объект, назначенный переменной, чтобы добавить некоторые свойства локально, как показано в строках 22–25.

Глядя на строки 30–32 ниже, мы видим, что мы используем метод Object.assign для копирования свойств из объектов details, swim и layEggs в цель, которая является пустым объектом {}. Затем мы связываем вызов другого метода и используем addDetails, который теперь имеет объект, и добавляем аргумент name, который мы передали при вызове функции.

Конструктор Pattern

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

  • Переменная clarinet назначается новому объекту, созданному путем вызова функции конструктора Instrument с оператором new.

  • Свойство [[Prototype]] / __proto__ объекта clarinet установлено на прототип функции конструктора.
  • Контекст выполнения для выполнения функции (this) указывает на новый объект.
  • Функция конструктора выполняется.
  • Возвращается новый объект.

Конструкторы с прототипами (псевдоклассический паттерн)

В JavaScript функции имеют свойство prototype, которое имеет свойство constructor, указывающее на функцию. Для экземпляров объекта фактический прототип (свойство __proto__) для объекта, созданного new functionName(), указывает на свойство prototype функции, используемой для создания объекта, functionName.prototype.

Чтобы избежать определения функций для каждого вновь созданного объекта, функции могут быть добавлены в Instrument.prototype, поэтому методы, вызываемые экземплярами, могут быть делегированы свойству Instrument prototype.

Эта диаграмма + видеообзор помогают продемонстрировать, что все это означает:

Главное, что нужно различать, - это разница между свойством / объектом prototype в нашей функции Instrument и прототипом объекта-экземпляра, который связан через внутреннее свойство _55 _ / _ 56_.

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

  1. Теперь вы можете моделировать отношения между объектами.
  2. Общее поведение живет в прототипе и делегируется, а не копируется в каждый новый объект.
  3. Если вам нужно изменить поведение / методы в прототипе, они отражаются изначально, и вам не нужно редактировать свои объекты.

Наследование по псевдоклассическому паттерну

Псевдоклассика относится к тому, как наследование конструктора имитирует классы из других языков ООП.

При псевдоклассическом наследовании прототип конструктора наследуется от прототипа другого конструктора, то есть подтип наследуется от супертипа.

В коде все объекты, созданные конструктором StringInstrument, наследуются от StringInstrument.prototype, который наследуется от Instrument.prototype. Благодаря этому все объекты струнных инструментов могут получить доступ к методам из Instrument.prototype.

Если метод из Instrument.prototype необходимо изменить для работы с StringInstrument.prototype, мы можем определить это для объекта StringInstrument.prototype с тем же именем свойства метода, чтобы он имел общий интерфейс, т. Е. Метод может быть вызван для любого объекта и вернуть ожидаемый результат. .

В приведенном ниже коде мы создаем две функции: Instrument и StringInstrument. Чтобы установить свойство prototype, мы используем Object.create() в строке 16. Почему мы создаем новый объект? Если мы присвоим существующий прототип функции Instrument свойству prototype функции StringInstrument, у нас больше не будет структуры наследования, потому что объекты прототипа будут такими же в памяти.

Чего не делать:

. // code omitted for brevity
.
.
StringInstrument.prototype = Instrument.prototype; // DON'T DO THIS
StringInstrument.prototype.tuneStrings = function() {             
    console.log("I have strings.")
}
let oboe = new Instrument("oboe","squeaky squeak");// Oboe will think it is both a string instrument + an instrument
oboe.tuneStrings(); // I have strings. => should be a TypeError
console.log(oboe instanceof StringInstrument); // true => should be false

Другой способ установить цепочку наследования

Если вы посмотрите на свойства, которые вы устанавливаете в функциях Instrument и StringInstrument, вы увидите, что они похожи. Это говорит о том, что вы можете использовать конструктор Instrument в StringInstrument. Для этого вызовите Instrument с контекстом выполнения, явно установленным в контексте выполнения StringInstrument, как показано с помощью метода функции call ниже. Первый аргумент - this, затем вы передаете любое количество аргументов.

Вот то же самое, но с синтаксисом класса ES6:

В MDN docs есть отличный обзор, поэтому здесь он адаптирован к приведенному выше коду:

  • Метод constructor() определяет функцию-конструктор, представляющую наш класс Instrument.
  • play() - это метод класса. Любые методы, которые вы хотите связать с классом, определяются внутри него после конструктора.
  • Для подклассов инициализация this для вновь выделенного объекта всегда зависит от конструктора родительского класса, то есть функции конструктора класса, из которого вы расширяетесь.
  • Здесь мы расширяем класс Instrument - подкласс StringInstrument является расширением класса Instrument. Итак, для StringInstrument инициализация this выполняется конструктором Instrument.
  • Чтобы вызвать родительский конструктор, мы должны использовать оператор super().

Объекты, связанные с другими объектами (OLOO)

OLOO заботится только об объектах, связанных с другими объектами. OLOO - это скорее шаблон делегирования, чем шаблон наследования. Смотрим, какие объекты хотят делегировать другим объектам. Объекты OLOO не используют конструкторы или .prototype.

Если вы создаете экземпляр нового объекта с Object.create() с присвоенным таким образом именем переменной cello: let cello = Object.create(instrumentPrototype), этот объект в настоящее время не имеет свойств.

> cello // {}

Чтобы завершить инициализацию вашего объекта с использованием шаблона OLOO, используйте метод инициализации из объекта-прототипа. Обычно в качестве имени метода инициализации используется init, но это не обязательно. Когда вы используете object.create для создания нового объекта, объект, который вы передаете в качестве аргумента, является предполагаемым прототипом нового объекта, поэтому теперь все методы из объекта instrumentPrototype доступны для объекта cello через делегирование свойств.

Вы можете использовать Object.getOwnPropertyNames(obj), чтобы убедиться, что методы не были скопированы в новый объект.

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

Дайте мне знать, если у вас возникнут дополнительные вопросы, и я смогу ответить или добавить больше к статье!