В Части 1 мы обсуждали шаблон OLOO: объекты, связанные с другими объектами в JavaScript. Мы узнали, что объекты создаются не из классов, а из других объектов, называемых прототипами. Затем прототипы могут получать запросы от объектов в нижней части цепочки прототипов, чтобы хранить общие данные и поведение. Как только прототипы будут поняты, OLOO может стать очень простым и динамичным шаблоном для создания объектов.
Предшественником OLOO является псевдоклассический шаблон. Известно, что это более сложная система, в которой функции конструктора и прототипы работают вместе для создания объектов.
Теперь вам может быть интересно, почему более сложный шаблон исключает шаблон OLOO. Дуглас Крокфорд — постоянно влияющий на развитие JavaScript — объясняет, что было много смешанных чувств по поводу прототипической природы JavaScript. Это было связано с тем, что во всех популярных языках применялся классический подход — классы, создающие объекты (как показано в нашем примере с Ruby в Части 1).
Есть несколько ключевых элементов, которые часто представляют языки на основе классов:
- Какая-то функция-конструктор для создания объектов
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)
происходит следующее:
Chair
становится конструктором и создает новый пустой объект:
В этом пустом объекте нет ничего особенного — думайте о нем как о простом старом пустом объекте{}
.- Свойство hidden
[[Prototype]]
пустого объекта настроено на ссылку на объектChair.Prototype
:
Как упоминалось ранее, теперь наш объект кресла может получать наследование на основе прототипа. - Контекст выполнения (
this
) устанавливается из глобального объекта в пустой объект:
Поскольку у вызоваChair
не было явного вызывающего объекта, по умолчаниюthis
ссылается на глобальный объект, а не на наш пустой объект. Прежде чем мы сможем присвоить свойства нашему пустому объекту,this
должен сначала указать на него. - Выполните функцию, чтобы добавить свойства к объекту стула:
Наша функция-конструктор, наконец, выполняется, и выполняется весь код в определении. В этом случае оба переданных аргументаwidth
иminHeight
назначаются свойствам с тем же именем и устанавливаются для нашего нового объекта стула. - Новый объект стула возвращается из функции конструктора
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 поддерживает наследование на основе прототипов: наследование может выполняться только с использованием объектов и прототипов. Все остальное, что претендует на то, чтобы выглядеть как наследование, — это просто объекты, работающие под поверхностью.
- Объекты делегируют общие общие свойства своим прототипам с помощью цепочки прототипов: таким образом, объекты содержат только необходимые свойства, что делает их уникальными, сохраняя их компактность и эффективность.
Я надеюсь, что вы нашли эту пару статей проницательными, и большое спасибо за то, что нашли время, чтобы прочитать их. Удачной учебы!