Уважаемые коллеги-разработчики JavaScript! Я здесь, чтобы поговорить о прототипной системе объектно-ориентированного программирования JavaScript. Может возникнуть некоторая путаница в том, как ООП работает в JavaScript, когда люди переходят из стандартных систем классов, которые используются почти во всех других языках ООП. Это будет объяснение прототипов, с упором на то, как работает наследование, и увидим, чем оно отличается от наследования на основе классов.
Я имею в виду, что прототипы - это просто странный синтаксис для классов, верно ??
Это то, о чем я думал некоторое время, когда изучал JavaScript. Я думал, что «Прототипы» просто предоставили уникальную возможность взглянуть на классы, в которых, как ни странно, вы делали классы из функций, потому что знаете, JavaScript такой странный. Но нет, хотя прототипы и классы достигают целей парадигмы ООП, на самом деле они совершенно разные, и если вам нужно делать много вещей типа ООП, таких как наследование, в ваших программах JavaScript, тогда вы должны знать, чем он отличается от Классы, к которым вы привыкли.
В JavaScript прототип просто ссылается на родительский объект объекта. Родитель объекта - это его прототип. У каждого объекта есть внутреннее и недоступное свойство [[prototype]], которое указывает на родительский объект этого объекта. Его не следует путать с доступным объектом prototype в функциях конструктора.
// The external, accessible prototype object on Array Array.prototype /* The internal, inaccessible prototype property on all objects can't be accessed directly, but you can view it through the Object.getPrototypeOf() method. */ Object.getPrototypeOf(someObj);
Когда вы пытаетесь получить доступ к методу или значению объекта, JavaScript сначала проверяет, принадлежит ли это свойство непосредственно объекту. Если он не находит его там, он проверяет прототип объекта и продолжает цепочку прототипов до тех пор, пока либо не найдет ключ, который соответствует имени искомого свойства, либо не достигнет вершины цепочки прототипов и не вернет не определено.
Объекты на первом месте! Что не так классно
JavaScript - это все об объектах, так и должно быть, потому что классов нет! В языках, основанных на классах, наследование происходит через классы. Классы наследуются от других классов. А классы действуют как чертежи для объектов. Когда объект создается из класса, говорят, что объект был создан из класса, что означает, что фактический экземпляр (объект) был создан из чертежа (класса). Вы можете думать о классе как о в основном содержащем метаданные, которые описывают, как из него должны быть созданы объекты. Сами классы инертны, они фактически ничего не могут делать в программе, для этого и предназначены объекты - объекты - это физическое проявление класса в программе.
Но в JavaScript нет классов, поэтому прототипное наследование основано на объектах. Объекты наследуются от отдельных объектов, что сильно отличается от наследования от схемы создания объекта. Это означает, что наследование проходит напрямую через объекты, поэтому родительский элемент одного объекта - это какой-то другой конкретный индивидуальный объект, а не тип объекта, как в наследовании на основе классов, где классы наследуются от классов, а объекты являются просто проявлением (экземпляром) объекта класс.
Цепочка прототипов
Очевидно, что в JavaScript должен быть базовый объект, поскольку системы на основе классов имеют базовый класс, от которого наследуются все остальные объекты. Это объект Object. Прототип объекта Object имеет значение null. Также существуют различные другие встроенные объекты, и все они наследуются от объекта Object.
Если вы создадите объект, который будет действовать как прототип некоторого дочернего объекта, тогда цепочка прототипов для этого дочернего объекта будет следующей:
child --> parent --> Object --> null
Поэтому, если вы попытаетесь получить доступ к свойству дочернего объекта, JavaScript сначала проверит это свойство на дочернем объекте, если он не найдет его там, он проверит его на родительском объекте, если он не найдет его там, он проверит наличие его в Object, и если он не найдет его там, он вернет undefined для значения, потому что это свойство не существует в цепочке прототипов дочернего элемента.
Инструменты для исследования прототипов
Есть несколько методов, которые можно использовать для исследования наследования в JavaScript.
Используйте это, чтобы увидеть прототип любого объекта:
Object.getPrototypeOf(someObj);
Используйте это, чтобы проверить, является ли объект прототипом / родителем другого объекта:
someObj.isPrototypeOf(anotherObj); // returns a boolean
И, конечно же, используйте консоль Developer Tools любого браузера, который вы используете, чтобы узнать, какие свойства есть у ваших объектов, и найти цепочку прототипов.
Кроме того, некоторые браузеры создали неофициальное свойство __proto__, которое существует для объектов и указывает на прототип объекта. Обратите внимание, что это не официальная часть спецификации JavaScript и работает только в тех браузерах, которые решили ее использовать, поэтому не используйте ее в коде приложения. Однако вы можете воспользоваться этим, если просто хотите исследовать свою цепочку прототипов.
// DON'T USE IN APPLICATION CODE someObj.__proto__
Способы изготовления предметов
Есть три основных способа создавать объекты в JavaScript. Самый простой и наиболее распространенный способ - создать объектный литерал, когда вы просто назначаете группу пар ключ-значение, составляющих объект, непосредственно переменной. Литерал объекта всегда имеет объект Object в качестве прототипа.
// object literal let earth = { body: 'planet', habitable: true, name: 'Earth' }; Object.getPrototypeOf(earth); // Object object
Второй способ - создать объект с помощью Object.create (). Вы передаете объект в эту функцию как прототип или родительский объект, который вы хотите создать, и она возвращает ваш новый объект. В приведенном ниже примере earth не имеет собственных свойств после создания с помощью Object.create (), но имеет доступ к трем свойствам на планете объект
// object.create let planet = { body: 'planet', habitable: false, name: 'Unknown' }; let earth = Object.create(planet); console.log(earth); // {} Object.getPrototypeOf(earth); // planet is earth's prototype console.log(earth.body); // 'planet'
Третий способ - использовать функцию-конструктор. Функции-конструкторы похожи на классы, но это не классы, это просто обычные функции. Чтобы сделать функцию конструктором, вы просто добавляете перед ней ключевое слово new при вызове функции. Функция-конструктор возвращает объект на основе переменных и / или методов, которые вы определяете внутри определения функции. Он также устанавливает свойство функции .prototype в качестве прототипа любого объекта, который она создает (подробнее позже). Вы можете создавать свои собственные функции, которые будут действовать как функции-конструкторы, или использовать встроенные функции-конструкторы. Способ создания базового объекта с помощью встроенной функции-конструктора заключается в использовании конструктора Object, который является функцией-конструктором для базового объекта JavaScript: объекта Object. При желании можно передать литерал объекта конструктору Object, чтобы создать этот объект, прототипом которого является объект Object. При создании функции, которая должна использоваться как функция-конструктор по соглашению, вы должны использовать имя функции с заглавной буквы, так же, как имена классов пишутся с заглавной буквы в системах на основе классов, чтобы отличать ее от функций, которые не используются в качестве конструкторов. . Чтобы увидеть список всех встроенных функций конструктора, посетите эту страницу: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects
// defining a function to be used as a constructor function Person(name) { this.name = name; } let joe = new Person('joe'); // using custom constructor function console.log(joe); // { name: 'joe' } let myArr = new Array(1,2,3); // using built-in Array constructor console.log(myArr); // [1,2,3] let obj = new Object({color: 'blue'}); // using Object constructor console.log(obj); // { color: 'blue' }
Еще одно различие между ООП на основе прототипов и ООП на основе классов состоит в том, что когда вы создаете объект из функции-конструктора, объект просто копируется из функции. Функции - это объекты, а конструктор просто предоставляет копию на основе того, что вы вставляете в функцию. В то время как на других языках класс создает экземпляр объекта на основе чертежа в классе, что отличается от процесса копирования.
Функции-конструкторы выглядят как классы, но пахнут странно.
В JavaScript есть функции-конструкторы, которые могут служить схемой для создания объектов. На первый взгляд они очень похожи на классы. Функция называется функцией-конструктором, когда она вызывается с ключевым словом new перед ней.
// Constructor functions serve as outlines for creating objects function Person(name, age) { this.name = name; this.age = age; } let david = new Person('David', 27); console.log(david); // { name: 'David', age: 27 }
Приведенный выше код очень похож на использование класса на других языках, за исключением того, что заголовок определения класса и конструктор класса здесь объединены в виде функции. Но на самом деле у нас есть обычная функция, и тогда волшебство здесь творит особое ключевое слово new, new.
Ключевое слово new выполняет несколько разных функций, и это немного сложно.
- Он создает новый объект на основе того, что находится внутри функции конструктора, этот объект будет возвращен из функции конструктора, и именно так вы получаете объект из всей сделки (в приведенном выше примере это объект, который установлен в Дэвид переменная). Каждый раз, когда ключевое слово this используется в функции конструктора, оно обращается к контексту этого вновь созданного объекта.
- Он устанавливает внутреннее недоступное свойство прототипа этого нового объекта в свойство доступного прототипа конструктора функции (функции являются объектами в JavaScript, и все функции имеют свойство прототипа, которое является объектом, существующим в функции). Таким образом, свойство прототипа функции, ConstructorFunction.prototype, является прототипом / родителем вновь созданного объекта, который возвращается.
- И, как упоминалось ранее, объект возвращается из функции-конструктора. Прототипом этого объекта является объект-прототип в функции ConstructorFunction.
Используя приведенный выше пример, ключевое слово new устанавливает следующую цепочку наследования:
david --> Person.prototype --> Object --> null
Объект david наследуется от объекта Person.prototype, который наследуется от встроенного в JavaScript объекта Object, который является базовым объектом для всех объектов в language, а прототип Object просто null, потому что он ни от чего не наследуется.
Как мы видим, все элементы в цепочке наследования являются отдельными объектами, даже Person.prototype, который исходит из функции конструктора, не является схемой какого-то типа класса, а является реальным существующим объектом. на объекте функции Person.
Если мы создали другой объект, назовем его susan из конструктора Person, тогда Person.prototype будет прототипом обоих этих дочерних объектов. . Однако, если мы возьмем Дэвида и установим его в качестве прототипа для некоторого нового объекта, arthur, тогда arthur унаследует только от david, а не Сьюзен, потому что, хотя Дэвид и Сьюзан имеют один и тот же прототип / родитель, они являются двумя разными объектами, и наследование JavaScript передается через отдельные объекты, а не классы объектов. Какой-нибудь код прояснит это.
function Person(name) { this.name = name; } let david = new Person('david'); // david --> Person.prototype let susan = new Person('susan'); // susan --> Person.prototype let arthur = Object.create(david); // arthur --> david // inheritance chain for arthur: // arthur --> david --> Person.prototype --> Object --> null
Объект создает
Версия JavaScript для EcmaScript 5 предлагает метод Object.create () для создания объектов путем передачи в качестве аргумента объекта, который вы хотите установить в качестве прототипа / родителя нового объекта, который возвращается. Помните, что Object - это базовый объект в системе наследования прототипов JavaScript, а create () - это метод, определенный в функции конструктора Object. Вот несколько вариантов того, как использовать его для создания наследования:
let parent = { whoAmI: 'a parent' }; function Person() { this.name = 'guy'; } let child = Object.create(parent); // child --> parent let child = new Person(); // child --> Person.prototype let child = Object.create(new Person()); // inheritance chain for the final line of code // child --> {name: 'guy'} --> Person.Prototype
Первый Object.create () должен иметь смысл - создание нового объекта с родительским элементом в качестве его прототипа. Следующая строка просто выполняет другой конструктор функции, как мы уже знаем, просто для сравнения. Но последняя строка кода делает кое-что интересное и действительно показывает, как работают конструктор функций и Object.create ().
Здесь снова отображается полная цепочка прототипов.
// child --> {name: 'guy'} --> Person.prototype --> Object --> null let child = Object.create(new Person());
Как подробно описано выше, функция-конструктор возвращает новый объект, прототипом которого является объект-прототип функции. А Object.create () принимает в качестве аргумента то, что станет родителем возвращаемого объекта. Итак, родительский элемент child - это литерал объекта, который возвращается из new Person (), потому что это то, что делает Object.create (). Это объект, который мы не сохраняли в переменную. Затем родительский элемент - Person.prototype, потому что это то, что делают функции-конструкторы. И, конечно же, выше, что в цепочке наследования прототипа стоит Object, а затем null.
Новый метод ES6: Object.setPrototypeOf
ES6, новая версия JavaScript, представила метод Object.setPrototypeOf (obj, prototype). Это позволяет, данным объектам, установить один объект в качестве прототипа другого.
let square = { length: 5, sides: 4 }; let polygon = { color: 'blue', sides: 'Unknown' }; Object.setPrototypeOf(square, polygon); console.log(square.length); // 5 console.log(square.sides); // 4 console.log(square.color); // 'blue' // Prototype chain for square: square --> polygon --> Object --> null
Все разные способы создания наследования в JavaScript
Мы видели использование функций-конструкторов и Object.create () для реализации наследования. Вы также можете установить объект, равный свойству .prototype функции-конструктора, что сделает этот объект прототипом любых объектов, созданных с помощью этого конструктора. Это связано с тем, что прототипом объекта, созданного конструктором, является свойство этого конструктора .prototype, но вы переназначаете свойство prototype значению некоторого другого объекта, поэтому этот другой объект является новым прототипом для объекты, сделанные из конструктора. Используйте это только в том случае, если вы по какой-то причине хотите переключить прототип объекта, созданный с помощью функции-конструктора.
function Person(name) { this.name = name; } let deep = new Person('Deep'); // Prototype chain: deep --> Person.prototype --> Object --> null let creature = {alive: true}; Person.prototype = creature; let joe = new Person('Joe'); // Prototype chain: joe --> creature --> Object --> null joe.alive; // true deep.alive; // undefined, deep's parent is still Person.prototype
Итак, три основных способа создания наследования в JavaScript таковы:
Constructor.prototype = someObject; objLiteral = Object.create(someObject); Object.setPrototypeOf(objToBeChild, objToBeParent); // Also, unofficially (don't use these): objLiteral[‘__proto__’] = objectLit; objLiteral[‘__proto__’] = new Constructor();
Резюме
Как мы видели, прототипы JavaScript выполняют те же функции, что и системы ООП на основе классов, но делают это совершенно иначе, наследование передается через отдельные объекты, а не через типы объектов (классы). У каждого объекта есть внутреннее недоступное свойство прототипа, указывающее на его прототип. Прототип объекта просто ссылается на своего родителя в цепочке прототипов. JavaScript - это одинарное наследование, потому что свойство прототипа каждого объекта может указывать только на один объект. Встроенный объект Object является базовым объектом для системы наследования прототипов, поэтому он всегда является самым высоким объектом в цепочке прототипов. Детали прототипов сильно отличаются от систем на основе классов, но с некоторой практикой это начинает обретать смысл.
Если вы хотите узнать больше о том, как создавать объекты и управлять ими, например, об использовании функций-конструкторов, публичных и частных переменных в объектах и т. Д., Пожалуйста, ознакомьтесь с моей первой статьей об объектах JavaScript: Манипулирование объектами в JavaScript
Спасибо за прочтение!