Что такое прототипное наследование в JavaScript? Поясняется примерами кода

Наследование прототипов может показаться сложной концепцией, окутанной техническим жаргоном. Но не бойтесь! В этом руководстве мы разберем это на понятных примерах, которые выходят за рамки типичных объяснений в учебниках.

К концу этого поста вы станете профессионалом в области наследования прототипов и поймете, что это было не так уж и сложно!

Введение в концепцию

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

https://www.freecodecamp.org/news/content/images/size/w1600/2024/05/Screenshot-2024-05-09-at-23.12.29.png

Вы можете видеть, что дедушка с бабушкой находятся на вершине этого генеалогического древа, в то время как родитель является прямым потомком дедушки с бабушкой, а ребенок - потомком родителя.Если вы попытаетесь вернуться назад, то увидите, что внук - это дочерний элемент родителя, а его собственный родитель - дочерний элемент дедушки и бабушки.

Что такое объекты JavaScript?

Возможно, в какой-то момент вы сталкивались с таким утверждением: "В JavaScript почти все является объектом".

Обратите внимание, как я пишу "Объект"? Когда я использую слова "Объект" и object в этой статье, они будут означать разные вещи.

Object - это конструктор, используемый для создания объектов. То есть: один из них является родительским/предком, а другой - дочерним.

Используя иллюстрацию выше, давайте попытаемся продемонстрировать, как работает генеалогическое древо в JavaScript.

Объект - это прародитель.

Такие конструкторы, как Array, String, Number, Function и Boolean, являются потомками Object.

Все они порождают потомки разных типов: массивы, строки, числа, функции и логические значения. Однако, если проследить их происхождение, все они являются объектами.

https://www.freecodecamp.org/news/content/images/size/w1600/2024/05/Screenshot-2024-05-10-at-00.08.57.png

Итак, если вас спросят, почему в JavaScript все (кроме null и undefined) рассматриваются как объекты, то это потому, что все они являются потомками конструктора Object.Конструкторы, перечисленные на рисунке выше, отвечают за различные типы данных, которые вы используете в JavaScript. То есть они используются негласно для создания знакомых типов данных (и вы также можете использовать их для явного создания значений разных типов).

Давайте попробуем использовать некоторые фрагменты кода.

Как создать объект

// Using the regular object syntax
const newObj = {}

// Using the object constructor
const newObjWithConstructor = new Object()

Как создать массив

// Using the regular array syntax
const newArr = []

// Using the array constructor
const newArrWithConstructor = new Array()

Как создать номер

// Using the regular syntax
const num = 3

// Using the number constructor
const numWithConstructor = new Number(3)

Как создать функцию

// Using regular function syntax
function logArg (arg) {
	console.log(arg)
}

// Using the Function constructor
const logArgWithConstructor = new Function('arg1', 'console.log(arg1)')

Как создать логическое значение

// Using the regular boolean syntax
const isValid = true

// Using the Boolean constructor
const isValidWithConstructor = new Boolean(true)

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

Примечание: Важно отметить, что каждый метод создания значений имеет свой собственный вариант использования и побочные эффекты, но мы не будем вдаваться в подробности в этой статье.
У конструкторов этих различных значений есть нечто, называемое прототипом.

Что такое прототип объекта?

В JavaScript есть нечто, называемое prototype. Наиболее близкая к этому концепция - ДНК человека.

Точно так же, как ДНК действует как схема, определяющая характеристики, которые передаются из поколения в поколение в генеалогическом древе человека, прототипы в JavaScript используются для определения свойств и методов, которые наследуются объектами, расположенными ниже по дереву объектов JavaScript.

Давайте объединим рис. 1 и рис. 2, приведенные выше, и обновим их, чтобы они соответствовали концепции ДНК и прототипа.

https://www.freecodecamp.org/news/content/images/size/w1600/2024/05/Screenshot-2024-05-10-at-01.01.47.png

В JavaScript все конструкторы имеют прототип. Прототип конструктора - это словарь всего, что должны наследовать значения, созданные с помощью конструктора.

Прочтите строчку выше еще раз и продолжайте, когда все станет ясно.

Представьте конструктор как родительский элемент, а прототип - как ДНК. Когда конструктор (родительский элемент) создает (порождает) потомство (значение), потомство наследует от ДНК (прототипа) своего родительского элемента конструктор.

Давайте рассмотрим другую диаграмму:

https://www.freecodecamp.org/news/content/images/size/w1600/2024/05/Screenshot-2024-05-10-at-01.28.05.png

На рис. 4 вы можете видеть, что дочерний элемент наследуется непосредственно от своего родителя, а его родитель наследует черты от прародителя. В этой цепочке наследования дочерний элемент фактически наследует как от прародителя, так и от родителя.

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

Вот как работает прототипное наследование в JavaScript.

Свойства в прототипе конструктора наследуются дочерними элементами, созданными этим конструктором. Это продолжается по цепочке. Вы можете рассуждать об этом следующим образом:

Каждый потомок в цепочке наследования наследует все, что имеется в прототипе его предков.

https://www.freecodecamp.org/news/content/images/size/w1600/2024/05/Screenshot-2024-05-10-at-02.07.22.png

Из приведенной выше диаграммы вы можете видеть, что все остальные прототипы наследуются от прототипа Object. Таким образом, любое значение, созданное с помощью конструктора Array (например), будет наследоваться от прототипа Array, а также от прототипа Object.

Это происходит потому, что прототип массива наследуется от прототипа объекта.

Термин Array prototype в JavaScript записывается как Array.prototype, в то время как Object prototype - это Object.prototype.Прототип.

Примечание: Важно отметить, что концепция ДНК сложна, поэтому, если ее расширить, мы быстро обнаружим, что есть некоторые нюансы и различия между тем, как работают ДНК и прототипы, но на высоком уровне они очень похожи.

Таким образом, понимание наследования в генеалогическом древе человека дало бы нам четкое представление о прототипном наследовании в JavaScript.

Если вы лучше разбираетесь в видео, посмотрите это, прежде чем продолжить.

Как работать с прототипом конструктора

https://www.freecodecamp.org/news/content/images/size/w1600/2024/05/Screenshot-2024-05-10-at-02.16.07.png

Чтобы просмотреть содержимое прототипа конструктора, мы просто пишем имя конструктора.prototype. Например, Array.prototype, Object.prototype, String.prototype и так далее.

Вы когда-нибудь представляли, как можно написать [2, 8, 10].map(...)? Это потому, что в прототипе конструктора Array есть ключ, называемый map. Таким образом, даже если вы не создавали карту свойств самостоятельно, она была унаследована значением массива, поскольку это значение было создано конструктором массива внутри системы.

Прочтите приведенное выше утверждение таким образом: Вы когда-нибудь задумывались, почему у вас именно такая группа крови? Это потому, что вы получаете свою группу крови от генов, которые унаследовали от своих родителей!

Поэтому в следующий раз, когда вы будете использовать такие свойства и методы, как .length, .map, .reduce, .valueOf, .find, .hasOwnProperty для значения, просто помните, что все они унаследованы от прототипа конструктора или какого-либо прототипа в цепочке прототипов (родословной).

Вы можете рассматривать прототип конструктора как прототип объекта, используемого для создания значения.

Вы должны знать, что .prototype каждого конструктора - это объект. Сам конструктор - это функция, но его прототип - это объект.

console.log(typeof Array) // function
console.log(typeof Array.prototype) // object

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

Если вы помните, мы можем добавлять новые свойства к уже существующим свойствам объектов и извлекать значения из них с помощью точки . обозначение. Например: objectName.propertyName - Имя объекта.

const user = {
	name: "asoluka_tee",
    stack: ["Python", "JavaScript", "Node.js", "React", "MongoDB"],
    twitter_url: "https://twitter.com/asoluka_tee"
}

// Using the syntax objectName.propertyName, to access the name key we'll write; user.name 
const userName = user.name;
console.log(userName) // asoluka_tee

// To add a new property to the object we'd write;
user.eyeColor = "black"

// If we log the user object to the console now, we should see eyeColor as part of the object properties with the value of 'black'

Вы когда-нибудь слышали о мутации ДНК? Идея заключается в изменении ДНК. В JavaScript это возможно с помощью прототипов.

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

Как изменить прототип конструктора

В JavaScript можно изменять объект-прототип конструктора точно так же, как и в случае с обычным объектом JavaScript (как показано выше).На этот раз нам просто нужно следовать этому синтаксису constructorName.prototype.newPropertyName = value. Например, если вы хотите добавить новое свойство с именем currentDate к объекту-прототипу конструктора Array, вы должны написать:

//constructorName.prototype.newPropertyName
Array.prototype.currentDate = new Date().toDateString();

С этого момента в вашем коде, поскольку currentDate теперь существует в прототипе конструктора Array (Array.prototype), каждый массив, созданный в нашей программе, может получить к нему доступ следующим образом: [1, 2, 3].currentDate и результатом будет сегодняшняя дата. Если вы хотите, чтобы каждый объект в вашей JavaScript-программе имел доступ к currentDate, вам нужно добавить его в объект prototype конструктора объектов (Object.prototype) вместо этого:

//constructorName.prototype.newPropertyName
Object.prototype.currentDate = new Date().toDateString();

const newArr = [1, 2, 3]
const newObj = {}
const newBool = true

// NB: The date shown is the date of writing this article
console.log(newArr.currentDate) // 'Fri May 10 2024'
console.log(newObj.currentDate) // 'Fri May 10 2024'
console.log(newBool.currentDate) // 'Fri May 10 2024'

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

  1. Array.prototype.reduce: We'll call ours .reduceV2
// Add our new function to the prototype object of the Array constructor
Array.prototype.reduceV2 = function (reducer, initialValue) {
  let accum = initialValue;
  for (let i = 0; i < this.length; i++) {
    accum = reducer(accum, this[i]);
  }
  return accum;
};

// Create an array of scores
let scores = [10, 20, 30, 40, 50];

// Use our own version of Array.prototype.reduce to sum the values of the array
const result = scores.reduceV2(function reducer(accum, curr) {
  return accum + curr;
}, 0);

// Log the result to the console
console.log(result);

Цель здесь не в том, чтобы объяснить весь синтаксис, а в том, чтобы показать вам, что, используя цепочку прототипов, вы можете создавать свои собственные методы и использовать их точно так же, как те, которые предоставляет JavaScript.

Обратите внимание, что вы можете просто заменить наш .reduceV2 на исходный .reduce, и он все равно будет работать (крайние случаи здесь не рассматриваются).2. Array.prototype.map: Мы назовем наш .mapV2

// Add mapV2 method to the prototype object of the Array constructor
Array.prototype.mapV2 = function (func) {
  let newArray = [];
  this.forEach((item, index) => newArray.push(func(item, index)));
  return newArray;
};

// Create an array of scores 
const scores = [1, 2, 3, 4, 5];

// Use our mapV2 method to increment every item of the scores array by 2
const scoresTimesTwo = scores.mapV2(function (curr, index) {
	return curr * 2;
})

// Log the value of scoresTimesTwo to the console.
console.log(scoresTimesTwo)

Примечание: Важно отметить, что это ни в коем случае не идеальная реализация оригинальных версий метода map в JavaScript. Это просто попытка показать вам, что возможно с помощью объекта-прототипа конструктора.

Прежде чем мы завершим этот урок, есть еще одна вещь, о которой я должен упомянуть; это свойство __proto__ каждого объекта.

Свойство __proto__

__proto__ - это средство установки и получения [[prototype]] свойства объекта. Это означает, что оно используется для установки или получения прототипа объекта (например, объекта, от которого наследуется другой объект).

Рассмотрим этот фрагмент кода;

const user = {}
const scores = []

user.prototype // undefined
scores.prototype // undefined

В приведенном выше фрагменте мы попытались получить доступ к объекту-прототипу непосредственно из значений. В JavaScript это невозможно.

Это имеет смысл, потому что только к конструкторам привязано свойство prototype.

Точно так же, как мутация ДНК сопряжена с риском, возня с прототипом объекта может привести к хаосу, если вы не совсем понимаете, что делаете.При нормальных обстоятельствах ребенку не следует пытаться изменить ДНК своего предка или даже определять, от кого унаследовать черты 😉

Однако язык JavaScript предоставляет нам возможность получить доступ к объекту prototype из значений, которые не являются конструкторами, используя свойство __proto__.

Это устаревший метод, и его не следует использовать для новых проектов. Я упоминаю __proto__, потому что вас могут нанять для работы с кодовой базой, которая все еще использует его.

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

Например, у нас есть объект с именем human, и мы хотим, чтобы другой объект с именем parent наследовался от human, это можно сделать с помощью свойства __proto__ parent следующим образом;

// Create a human object
const human = {
    walk: function () { console.log('sleeping') },
    talk: function () { console.log('talking') },
	sleep: function () { console.log('sleeping') }
}

// Create a parent object and configure it to inherit from human.
const parent = {
    __proto__: human
}

// Use a method from the ancestor of parent
parent.sleep() // sleeping

Обратите внимание, что мы можем вызвать метод sleep для parent, потому что parent теперь наследуется от human.

Существуют более современные рекомендуемые методы для использования при взаимодействии с объектом-прототипом, такие как Object.getPrototypeOf и Object.setPrototypeOf

const user = {}
const scores = []

// Get the prototype of the user object
console.log(Object.getPrototypeOf(user))

// Change the prototype of the scores array. This is like switching ancestry and should be done with great care.
console.log(Object.setPrototypeOf(scores, {}))

// Check the prototype of scores now
console.log(Object.getPrototypeOf(scores)) // {}

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

Если вы дочитали до этого места, то теперь знаете основы Array.prototype, и с этого момента вам будет легче понять любую другую концепцию, построенную на ее основе в JavaScript.Давайте подведем итог тому, что вы узнали на данный момент.

Резюме

В JavaScript есть разные конструкторы: Array, Boolean, Function, Number, String и Object. Object является родительским для всех остальных конструкторов.

У каждого конструктора есть объект .prototype, и этот объект содержит свойства и методы, к которым могут быть доступны значения, созданные с помощью конструктора. Например, значение, созданное с помощью конструктора Array, будет иметь доступ ко всем свойствам и методам, доступным в объекте Array.prototype, и это наследование продолжается по всей длине.Другими словами, значение, созданное с помощью конструктора Array (явно или неявно), будет иметь доступ не только к свойствам и методам объекта Array.prototype, но и к свойствам и методам объекта Object.prototype.Это связано с концепцией прототипного наследования. Object является родительским элементом Array, и каждый дочерний элемент, созданный Array, будет иметь доступ к признакам как из массива, так и из объекта.

Вот что происходит, когда вы пытаетесь получить свойство из значения, которое явно не указано в этом значении. Смотрите фрагмент кода ниже:

const user = {}

// trying to retireve .valueOf property from the user object
console.log(user.valueOf)

Очевидно, что объект user не имеет свойства .valueOf, поэтому он ищет в своей цепочке прототипов любой прототип, обладающий этим свойством, и если он найден, возвращается значение. В противном случае мы получаем undefined.

Мы также узнали, что мы можем изменить прототип любого конструктора, чтобы добавить функциональности, и делать это следует с осторожностью.

Наконец, мы узнали о том, как __proto__, getPrototypeOf и setPrototypeOf можно использовать для извлечения и установки прототипа значения.

Чем это полезно?

Представьте, что вы хотите создать метод, который создает новый объект на основе массива и возвращает его при вызове из массива.Этот метод предназначен для того, чтобы вы могли попробовать его сами.

// Array.prototype.toObject
const names = ['Austin', 'Tola', 'Joe', 'Victor'];

// Write your implementation of toObject here.

console.log(names.toObject()) // {0: 'Austin', 1: 'Tola', 2: 'Joe', 3: 'Victor'}

Спасибо, что прочитали! Удачного написания кода!