Все, что вам нужно знать о создании объектов в 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_.
Использование псевдоклассического шаблона вместо функций фабрики объектов дает несколько ключевых преимуществ.
- Теперь вы можете моделировать отношения между объектами.
- Общее поведение живет в прототипе и делегируется, а не копируется в каждый новый объект.
- Если вам нужно изменить поведение / методы в прототипе, они отражаются изначально, и вам не нужно редактировать свои объекты.
Наследование по псевдоклассическому паттерну
Псевдоклассика относится к тому, как наследование конструктора имитирует классы из других языков ООП.
При псевдоклассическом наследовании прототип конструктора наследуется от прототипа другого конструктора, то есть подтип наследуется от супертипа.
В коде все объекты, созданные конструктором 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, наследуют методы от объекта-прототипа, тогда как фабричные функции копируют методы в каждый новый объект, созданный с помощью фабричной функции.
Дайте мне знать, если у вас возникнут дополнительные вопросы, и я смогу ответить или добавить больше к статье!