JavaScript — это объектно-ориентированный язык программирования, основанный на прототипах. После обновлений ES6 в JavaScript разрешено «прототипное наследование», что означает, что объекты и методы можно использовать совместно, расширять и копировать.

Совместное использование объектов позволяет легко наследовать структуру (поля данных), поведение (функции/методы) и состояние (значения данных).

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

Сегодня мы хотим познакомить вас с наследованием прототипов в JavaScript, чтобы вы были в курсе возможностей ES6.

Мы рассмотрим следующее:

  • Что такое прототипическое наследование?
  • Минусы прототипного наследования
  • Важные термины
  • Настройка прототипов отношений
  • Наследование методов
  • Трехуровневое наследование и масштабируемость
  • Подведение итогов и ресурсы

Что такое прототипическое наследование?

Проще говоря, прототипическое наследование относится к возможности доступа к свойствам объекта из другого объекта. Мы используем прототип JavaScript для добавления новых свойств и методов в существующий конструктор объектов. Затем мы можем указать нашему JS-коду наследовать свойства от прототипа. Прототипное наследование позволяет нам повторно использовать свойства или методы из одного объекта JavaScript в другой через функцию указателя ссылки.

Все объекты JavaScript наследуют свойства и методы от прототипа:

  • Date объекты наследуются от Date.prototype.
  • Array объекты наследуются от Array.prototype.
  • Player объекты наследуются от Player.prototype.

Object.prototype находится на вершине цепочки наследования прототипов. Объекты Date, объекты Array и объекты Player наследуются от Object.prototype.

Возвращаясь к старому примеру

Давайте рассмотрим пример прототипического наследования, с которым вы, вероятно, знакомы из начальной школы: все квадраты являются прямоугольниками, но не все прямоугольники являются квадратами. Если представить это как JS-программу, можно сказать, что прямоугольник является прототипом квадрата: квадрат наследует все свойства прямоугольника (т. е. четырехсторонний и замкнутый), а также добавляет новую функцию (т. е. все стороны имеют одинаковую длину).

Однако мы не могли построить ту же самую концепцию, используя квадрат в качестве прототипа, потому что есть свойства квадрата, которые не применимы к прямоугольникам (т. е. все стороны имеют одинаковую длину).

Мы можем увидеть, как работает прототипное наследование на основе указания категорий внутри группы от наименее специфичных к наиболее — от прямоугольника к квадрату. В коде эта концепция иногда может быть потеряна в синтаксисе. Если вы обнаружите, что это происходит, проговорите отношения между объектами и послушайте, где вы проводите различия. Если вы слышите «все ___ являются __, но… не все __ являются ___», именно здесь следует добавить новое прототипическое отношение.

Минусы прототипного наследования

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

  • Наследование не может проходить по кругу, так как это создаст ошибку. Например, если user связать premiumFamily в качестве прототипа в приведенной выше программе, возникнет ошибка, поскольку это создаст цикл.
  • Объекты не могут наследовать от нескольких прототипов. Как мы видели выше, они могут наследовать несколько свойств объекта через цепочку, однако другой объект, явно связанный в качестве прототипа, вызовет ошибку. Это так, даже если дополнительный прототип находится в той же цепочке. Например, familyPremium не может иметь явных ссылок одновременно на premiumUser и user.
  • Прототипные отношения могут быть созданы только для объектов. Это связано с тем, что функция __proto__ работает как пересылка, указывая программе, где найти искомое значение. Поскольку программа либо знает, где искать, либо нет, функция может быть либо null, либо объектом. Все остальные типы будут отброшены.

Важные термины

__proto__ имущество

В Javascript каждый объект имеет собственное скрытое внутреннее свойство [[Prototype]]. Мы можем получить доступ к этому [[Prototype]], используя свойство __proto__. Это вызывает программу, чтобы пометить объект шаблона как скрытый тип. Объекты JavaScript должны быть связаны с этим объектом-прототипом. Теперь к свойствам объекта может обращаться объект-наследник.

Давайте посмотрим на синтаксис для доступа и установки свойства [[Prototype]] объекта.

//using __proto__ to access and set the [[Prototype]] of "anObject"
anObject.__proto__ = someotherObject

Объект.создать

JavaScript ECMAScript 5 поставляется с функцией Object.create( ). Этот метод можно использовать для заменыnew. Мы можем использовать его для создания пустого объекта на основе определенного прототипа, а затем назначить его другому прототипу. Взгляните на синтаксис:

Object.create(proto, [propertiesObject])

Методы Object.create могут принимать два аргумента: propertiesObject и prototypeObject.

Объект.прототип.конструктор

Все объекты имеют свойство конструктора. Если объект создается без использования функции конструктора, он будет иметь свойство конструктора. Свойство конструктора вернет ссылку на функцию конструктора Object объекта. Он вернет 1, true1 и ”test”. Взгляните на пример ниже.

let o = {} o.constructor === Object // true 

let o = new Object o.constructor === Object // true 

let a = [] a.constructor === Array // true 

let a = new Array a.constructor === Array // true 

let n = new Number(3) n.constructor === Number // true

hasOwnProperty

Используя hasOwnProperty, мы можем проверить, содержит ли объект определенное свойство прототипа; метод вернет true или false в зависимости от. Это поможет вам выяснить, имеет ли объект свое собственное свойство или вместо этого он наследует. Взгляните на синтаксис ниже:

obj.hasOwnProperty(prop)

Цепочка прототипов

Наследование прототипов использует концепцию цепочки прототипов. Давайте рассмотрим эту концепцию. Каждый созданный объект содержит [[Prototype]], который указывает либо на другой объект, либо на null. Представьте себе объект C со свойством [[Prototype]], которое указывает на объект B. Свойство [[Prototype]] объекта B указывает на объект-прототип A. Это продолжается дальше, образуя своего рода цепочку, называемую цепочкой прототипов.

Эта концепция используется при поиске нашего кода. Когда нам нужно найти свойство в объекте, оно сначала ищется в объекте, а если не найдено, оно ищется в прототипе этого объекта и так далее. Таким образом, проходится вся цепочка прототипов, пока не будет найдено свойство или не будет достигнуто null.

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

Пример кода 1: настройка прототипных отношений

В этом первом примере мы напишем простую прототипическую связь между двумя объектами, user и premiumUser, используя функцию ._proto_. Каждый из этих объектов имеет свои собственные свойства, которые будут общими для всех учетных записей на этом уровне: все users имеют доступ к потоковым шоу, showAccess = true, и все premiumUsers имеют отключенную рекламу, ads = false.

let user = {    //create the user object
    showAccess: true  //create and set showAccess property of user
};
let premiumUser = {  //repeat of the above for this object
    ads: false
};
 
premiumUser.__proto__ = user; //user is the prototype of premiumUser
 
console.log(premiumUser.showAccess); // "true"

Прототип отношений здесь гарантирует, что premiumUser наследует набор свойств showAccess от user без необходимости устанавливать его вручную на уровне Premium.

Чтобы убедиться, что это унаследовано правильно, мы добавляем строку, чтобы консоль печатала текущее значение showAccess для premiumUser. Поскольку он возвращает true, мы видим, что premiumUser унаследовал это свойство от user.

Пример кода 2: Наследование методов

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

В приведенном ниже примере мы строим наш предыдущий код и теперь добавляем свойства email и IDnumber для пользователя, отслеживая информацию об учетной записи для этого пользователя, а также метод установки, accountInfo, который при вызове будет анализировать переданную строку, устанавливая email и IDnumber для новые переданные значения.

let user = {
    email: "[email protected]", //create and set email property
    IDnumber: "#12345",  //create and set the Idnumber property
    showAccess: true,
 
    set accountInfo(value) { //Setter method to change values of email and ID
        [this.email, this.IDnumber] = value.split(" ");
    },
 
// defined method in the prototype
    get accountInfo() {
        return `${this.email} ${this.IDnumber}`;
    }
};
 
let premiumUser = {
    __proto__: user,
    ads: false
};
 
// calls the inherited getter method
console.log(premiumUser.accountInfo); // "[email protected] #12345"
 
premiumUser.accountInfo = "[email protected] #54321"; // calls the inherited setter method
 
console.log(premiumUser.accountInfo); // "[email protected] #54321"
//ID and Email values are now different for each object
console.log(user.accountInfo);      // "[email protected] #12345"

Ключевым разделом этого примера является вызов трех методов внизу. Каждый из этих методов определен в объекте user и поэтому обычно недоступен для premiumUser. Однако, поскольку user является прототипом premiumUser, все методы и свойства user используются совместно с любыми объектами-наследниками.

Из двух последних методов мы также видим, что значение общих свойств не стагнирует, а может быть установлено новое значение независимо от значений свойств в прототипе.

Пример кода 3: трехуровневое наследование и масштабируемость

Как вы могли заметить, приведенные выше примеры позволяют использовать только одну учетную запись в user и одну учетную запись в premiumUser. Чтобы обеспечить столь необходимую масштабируемость, мы отказываемся от использования этих объектов в качестве переменных и вместо этого используем их как эквиваленты классов.

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

В приведенном ниже примере объект me будет моей учетной записью. Затем этот объект вызывает унаследованный метод установки, чтобы установить значения для свойств email и IDnumber, эксклюзивные для этой учетной записи, и установить его уровень, сделав недавно добавленный объект familyPremium своим прототипом.

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

let user = {
    email: "missing email",  //fillers to reveal errors in inheritance at print
    IDnumber: "missing ID number",
    showAccess: true,
 
    set accountInfo(value) {
        [this.email, this.IDnumber] = value.split(" ");
    },
 
    get accountInfo() {
        return `${this.email} ${this.IDnumber}`;
    }
};
 
let premiumUser = {
    __proto__: user,
    Ads: false
};
 
let familyPremium = {  //our new third tier of membership
    __proto__: premiumUser, // in an inheritance chain with prior two objects
    multipleDevices: true
};
 
let me = {    //an object for an individual user, me in this case
    __proto__: familyPremium,  //inheritance to decide class
    email: "[email protected]", //setting property values exclusive to this object
    IDnumber: "#67899"
};
 
console.log(me.multipleDevices); // true
console.log(me.accountInfo);  // [email protected] #67899
 
//Least specific to most: not all user accounts are premium accounts, not all premium accounts are family premium accounts.

Даже с тремя уровнями наследования мы видим, что me имеет доступ к данным по всей цепочке, от непосредственно унаследованного свойства multipleDevices до унаследованного метода accountInfo, определенного на вершине своей цепочки в user.

Независимо от того, сколько уровней имеет цепочка наследования, вся информация с предыдущих уровней сохраняется и доступна.

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

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

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

Подведение итогов

Как и сам JavaScript, наследование прототипов — бесценный инструмент в мире разработки веб-сайтов и управления серверами.

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

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

Продолжить чтение о Javascript на Educative

Начать обсуждение

Какой учебник по веб-разработке вы хотели бы прочитать следующим? Была ли эта статья полезна? Дайте нам знать в комментариях ниже!