Уважаемые коллеги-разработчики 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 выполняет несколько разных функций, и это немного сложно.

  1. Он создает новый объект на основе того, что находится внутри функции конструктора, этот объект будет возвращен из функции конструктора, и именно так вы получаете объект из всей сделки (в приведенном выше примере это объект, который установлен в Дэвид переменная). Каждый раз, когда ключевое слово this используется в функции конструктора, оно обращается к контексту этого вновь созданного объекта.
  2. Он устанавливает внутреннее недоступное свойство прототипа этого нового объекта в свойство доступного прототипа конструктора функции (функции являются объектами в JavaScript, и все функции имеют свойство прототипа, которое является объектом, существующим в функции). Таким образом, свойство прототипа функции, ConstructorFunction.prototype, является прототипом / родителем вновь созданного объекта, который возвращается.
  3. И, как упоминалось ранее, объект возвращается из функции-конструктора. Прототипом этого объекта является объект-прототип в функции 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

Спасибо за прочтение!