Перейти к содержимому

Что такое прототип в js

  • автор:

Прототип объекта

Материал на этой странице устарел, поэтому скрыт из оглавления сайта.

Более новая информация по этой теме находится на странице https://learn.javascript.ru/prototype-inheritance.

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

Связующим звеном выступает специальное свойство __proto__ .

Прототип proto

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

Свойство __proto__ доступно во всех браузерах, кроме IE10-, а в более старых IE оно, конечно же, тоже есть, но напрямую к нему не обратиться, требуются чуть более сложные способы, которые мы рассмотрим позднее.

Пример кода (кроме IE10-):

var animal = < eats: true >; var rabbit = < jumps: true >; rabbit.__proto__ = animal; // в rabbit можно найти оба свойства alert( rabbit.jumps ); // true alert( rabbit.eats ); // true
  1. Первый alert здесь работает очевидным образом – он выводит свойство jumps объекта rabbit .
  2. Второй alert хочет вывести rabbit.eats , ищет его в самом объекте rabbit , не находит – и продолжает поиск в объекте rabbit.__proto__ , то есть, в данном случае, в animal .

Иллюстрация происходящего при чтении rabbit.eats (поиск идёт снизу вверх):

Объект, на который указывает ссылка __proto__ , называется «прототипом». В данном случае получилось, что animal является прототипом для rabbit .

Также говорят, что объект rabbit «прототипно наследует» от animal .

Обратим внимание – прототип используется исключительно при чтении. Запись значения, например, rabbit.eats = value или удаление delete rabbit.eats – работает напрямую с объектом.

В примере ниже мы записываем свойство в сам rabbit , после чего alert перестаёт брать его у прототипа, а берёт уже из самого объекта:

var animal = < eats: true >; var rabbit = < jumps: true, eats: false >; rabbit.__proto__ = animal; alert( rabbit.eats ); // false, свойство взято из rabbit

Другими словами, прототип – это «резервное хранилище свойств и методов» объекта, автоматически используемое при поиске.

У объекта, который является __proto__ , может быть свой __proto__ , у того – свой, и так далее. При этом свойства будут искаться по цепочке.

Ссылка proto в спецификации

Если вы будете читать спецификацию ECMAScript – свойство __proto__ обозначено в ней как [[Prototype]] .

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

Метод hasOwnProperty

Обычный цикл for..in не делает различия между свойствами объекта и его прототипа.

Он перебирает всё, например:

var animal = < eats: true >; var rabbit = < jumps: true, __proto__: animal >; for (var key in rabbit) < alert( key + " = " + rabbit[key] ); // выводит и "eats" и "jumps" >

Иногда хочется посмотреть, что находится именно в самом объекте, а не в прототипе.

Вызов obj.hasOwnProperty(prop) возвращает true , если свойство prop принадлежит самому объекту obj , иначе false .

var animal = < eats: true >; var rabbit = < jumps: true, __proto__: animal >; alert( rabbit.hasOwnProperty('jumps') ); // true: jumps принадлежит rabbit alert( rabbit.hasOwnProperty('eats') ); // false: eats не принадлежит

Для того, чтобы перебрать свойства самого объекта, достаточно профильтровать key через hasOwnProperty :

var animal = < eats: true >; var rabbit = < jumps: true, __proto__: animal >; for (var key in rabbit) < if (!rabbit.hasOwnProperty(key)) continue; // пропустить "не свои" свойства alert( key + " = " + rabbit[key] ); // выводит только "jumps" >

Object.create(null)

Зачастую объекты используют для хранения произвольных значений по ключу, как коллекцию:

var data = <>; data.text = "Привет"; data.age = 35; // . 

При дальнейшем поиске в этой коллекции мы найдём не только text и age , но и встроенные функции:

var data = <>; alert(data.toString); // функция, хотя мы её туда не записывали

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

Чтобы этого избежать, мы можем исключать свойства, не принадлежащие самому объекту:

var data = <>; // выведет toString только если оно записано в сам объект alert(data.hasOwnProperty('toString') ? data.toString : undefined);

Однако, есть путь и проще:

var data = Object.create(null); data.text = "Привет"; alert(data.text); // Привет alert(data.toString); // undefined

Объект, создаваемый при помощи Object.create(null) не имеет прототипа, а значит в нём нет лишних свойств. Для коллекции – как раз то, что надо.

Методы для работы с proto

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

Чтение: Object.getPrototypeOf(obj) Возвращает obj.__proto__ (кроме IE8-) Запись: Object.setPrototypeOf(obj, proto) Устанавливает obj.__proto__ = proto (кроме IE10-).

Кроме того, есть ещё один вспомогательный метод:

Создание объекта с прототипом: Object.create(proto, descriptors) Создаёт пустой объект с __proto__ , равным первому аргументу (кроме IE8-), второй необязательный аргумент может содержать дескрипторы свойств.

Итого

  • В JavaScript есть встроенное «наследование» между объектами при помощи специального свойства __proto__ .
  • При установке свойства rabbit.__proto__ = animal говорят, что объект animal будет «прототипом» rabbit .
  • При чтении свойства из объекта, если его в нём нет, оно ищется в __proto__ . Прототип задействуется только при чтении свойства. Операции присвоения obj.prop = или удаления delete obj.prop совершаются всегда над самим объектом obj .

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

В современных браузерах есть методы для работы с прототипом:

  • Object.getPrototypeOf(obj) (кроме IE8-)
  • Object.setPrototypeOf(obj, proto) (кроме IE10-)
  • Object.create(proto, descriptors) (кроме IE8-)

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

Также мы рассмотрим, как свойство __proto__ используется внутри самого языка JavaScript и как организовать классы с его помощью.

Прототипы в JavaScript

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

Сразу же начнем с примера и создадим два объекта student и programs , где в первом будет лежать информация о студенте, а во втором программы для изучения JavaScript согласно уровня ученика.

 let student = < name: 'Дмитрий', age: 28, level: 'junior', >let programs = < junior: 'Основы JavaScript', middle: 'Продвинутый уровень', senior: 'Продвинутая архитектура проекта', >Object.setPrototypeOf(student, programs) console.log(student.junior) // Основы JavaScript 

С помощью метода Object.setPrototypeOf() устанавили прототип объекта programs для student . Другими словами сделали доступным объекту student все свойства и методы, которые есть в programs . Проверить это можно обратившись через объект student к свойству junior — результатом будет значение Основы JavaScript.

Задать прототип, можно также с помощью свойства __proto__ , которое до сих пор поддерживается браузерами, но считается устаревшим.

 student.__proto__ = programs 

[[Prototype]]

Прототип — это объект, который хранится в [[Prototype]] и содержит в себе различные свойства и методы.

 let testObject = <> console.log(testObject) 

Если создать пустой объект и посмотреть в console, то можно увидеть свойство [[Prototype]] и то, что оно ссылается на объект, который содержит в себе другие свойства. Также если здесь прописать наш объект и поставить после него точку, браузер предложит доступные методы, как раз из прототипа.

прототип

Прототипы и функция конструктор

Для понимания работы функции конструктора посмотрим на работу встроенного объекта Date .

 let date = new Date() console.dir(date) 

date object

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

В JavaScript можно самостоятельно писать похожие объекты и использовать их для создания отдельных экземпляров.

Создадим функцию конструктор Student и экземпляр объекта, который запишем в переменную student1 .

 function Student(name, age, level) < this.name = name this.age = age this.level = level this.message = `Студент $, в возрасте $ лет пришел на курсы JavaScript c уровнем знания $` > let student1 = new Student('Дмитрий', 28, 'junior') console.log(student1.message) // Студент Дмитрий, в возрасте 28 лет пришел на курсы JavaScript c уровнем знания junior' 

Теперь для того, чтобы создавать однотипные объекты в которых будет хранится информация о студентах и будут доступны общие методы, нам не нужно прописывать все с нуля, а достаточно создать экземпляр объекта Student , в прототипе которого и будет записана вся нужная нам информация.

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

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

Возьмем для примера все тот же объект Date .

 let date = new Date() console.dir(date) 

date object

И в console видим, что для date доступны, как методы связанные с датой, так и стандартные, к каким относится toString() . Развернув [[Prototype]] мы сможем увидеть вложенность прототипов.

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

 let programAcademy = < course: 'JavaScript' >let programAcademy2 = < course: 'React' >Object.setPrototypeOf(programAcademy, programAcademy2) console.log(programAcademy.course) // JavaScript 

Свойство course из прототипа programAcademy2 игнорируется.

Итого

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

2. Все объекты в JavaScript имеют свойство [[Prototype]] , которое ссылается на объект, называемым прототипом.

3. Свойства __proto__ считается устаревшим, а для получения и установки прототипов рекомендуется использовать методы getPrototypeOf() и setPrototypeOf() .

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

5. Прототипы это тема, которая необходима для понимания основ объектно ориентированного программирования.

Skypro — научим с нуля

Встроенные прототипы

Свойство «prototype» широко используется внутри самого языка JavaScript. Все встроенные функции-конструкторы используют его.

Сначала мы рассмотрим детали, а затем используем «prototype» для добавления встроенным объектам новой функциональности.

Object.prototype

Давайте выведем пустой объект:

let obj = <>; alert( obj ); // "[object Object]" ?

Где код, который генерирует строку «[object Object]» ? Это встроенный метод toString , но где он? obj ведь пуст!

…Но краткая нотация obj = <> – это то же самое, что и obj = new Object() , где Object – встроенная функция-конструктор для объектов с собственным свойством prototype , которое ссылается на огромный объект с методом toString и другими.

Вот что происходит:

Другие встроенные объекты устроены аналогично. Даже функции – они объекты встроенного конструктора Function , и все их методы ( call / apply и другие) берутся из Function.prototype . Также у функций есть свой метод toString .

function f() <> alert(f.__proto__ == Function.prototype); // true alert(f.__proto__.__proto__ == Object.prototype); // true, наследует от Object

Примитивы

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

Как мы помним, они не объекты. Но если мы попытаемся получить доступ к их свойствам, то тогда будет создан временный объект-обёртка с использованием встроенных конструкторов String , Number и Boolean , который предоставит методы и после этого исчезнет.

Эти объекты создаются невидимо для нас, и большая часть движков оптимизирует этот процесс, но спецификация описывает это именно таким образом. Методы этих объектов также находятся в прототипах, доступных как String.prototype , Number.prototype и Boolean.prototype .

Значения null и undefined не имеют объектов-обёрток

Специальные значения null и undefined стоят особняком. У них нет объектов-обёрток, так что методы и свойства им недоступны. Также у них нет соответствующих прототипов.

Изменение встроенных прототипов

Встроенные прототипы можно изменять. Например, если добавить метод к String.prototype , метод становится доступен для всех строк:

String.prototype.show = function() < alert(this); >; "BOOM!".show(); // BOOM!

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

Прототипы глобальны, поэтому очень легко могут возникнуть конфликты. Если две библиотеки добавляют метод String.prototype.show , то одна из них перепишет метод другой.

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

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

Полифил – это термин, который означает эмуляцию метода, который существует в спецификации JavaScript, но ещё не поддерживается текущим движком JavaScript.

Тогда мы можем реализовать его сами и добавить во встроенный прототип.

if (!String.prototype.repeat) < // Если такого метода нет // добавляем его в прототип String.prototype.repeat = function(n) < // повторить строку n раз // на самом деле код должен быть немного более сложным // (полный алгоритм можно найти в спецификации) // но даже неполный полифил зачастую достаточно хорош для использования return new Array(n + 1).join(this); >; > alert( "La".repeat(3) ); // LaLaLa

Заимствование у прототипов

В главе Декораторы и переадресация вызова, call/apply мы говорили о заимствовании методов.

Это когда мы берём метод из одного объекта и копируем его в другой.

Некоторые методы встроенных прототипов часто одалживают.

Например, если мы создаём объект, похожий на массив (псевдомассив), мы можем скопировать некоторые методы из Array в этот объект.

let obj = < 0: "Hello", 1: "world!", length: 2, >; obj.join = Array.prototype.join; alert( obj.join(',') ); // Hello,world!

Это работает, потому что для внутреннего алгоритма встроенного метода join важны только корректность индексов и свойство length , он не проверяет, является ли объект на самом деле массивом. И многие встроенные методы работают так же.

Альтернативная возможность – мы можем унаследовать от массива, установив obj.__proto__ как Array.prototype , таким образом все методы Array станут автоматически доступны в obj .

Но это будет невозможно, если obj уже наследует от другого объекта. Помните, мы можем наследовать только от одного объекта одновременно.

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

Итого

  • Все встроенные объекты следуют одному шаблону:
    • Методы хранятся в прототипах ( Array.prototype , Object.prototype , Date.prototype и т.д.).
    • Сами объекты хранят только данные (элементы массивов, свойства объектов, даты).

    Задачи

    Добавить функциям метод «f.defer(ms)»

    важность: 5

    Добавьте всем функциям в прототип метод defer(ms) , который вызывает функции через ms миллисекунд.

    После этого должен работать такой код:

    function f() < alert("Hello!"); >f.defer(1000); // выведет "Hello!" через 1 секунду

    Прототипы и наследование в JavaScript

    В этой статье мы изучим всё что касается прототипов в JavaScript. Разберём зачем они нужны, что такое наследование и цепочка прототипов, как работает this внутри методов, рассмотрим пример расширения классов и многое другое.

    Что такое прототипы?

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

    Допустим у нас имеется конструктор Box :

    JavaScript

    // конструктор Box function Box(width, height) { this.width = width; this.height = height; }

    При объявлении конструктора или класса у него автоматически появится свойство prototype . Оно содержит прототип . Прототип – это объект. В данном случае им будет являться Box.prototype . Это очень важный момент.

    Этот прототип будет автоматически назначаться всем объектам , которые будут создаваться с помощью этого конструктора:

    JavaScript

    // создание объекта с помощью конструктора Box const box1 = new Box(25, 30);

    Таким образом при создании объекта, в данном случае, box1 , он автоматически будет иметь ссылку на прототип, то есть на свойство Box.prototype .

    Это очень легко проверить:

    JavaScript

    Object.getPrototypeOf(box1) === Box.prototype // true box1.__proto__ === Box.prototype // true

    Прототип объекта в JavaScript

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

    Свойство prototype имеется у каждой функции за исключением стрелочных. Это свойство как мы уже отмечали выше в качестве значения имеет объект. По умолчанию в нём находится только одно свойство constructor , которое содержит ссылку на саму эту функцию:

    JavaScript

    Box.prototype.constructor = Box // true

    То есть Box.prototype.constructor – это сам конструктор Box .

    Если создать ещё один объект класса Box , то он тоже будет иметь точно такой же прототип. Как мы уже отмечали выше прототипы в JavaScript используются для организации наследования. То есть, если мы сейчас в Box.prototype добавим какой-нибудь, например, метод, то он будет доступен для всех экземпляров класса Box :

    JavaScript

    // создадим конструктор Box function Box(width, height) { this.width = width; this.height = height; } // добавим метод print в прототип Box Box.prototype.print = function () { return `Box Size: ${this.width} x ${this.height}` } // создадим объекты const box1 = new Box(25, 30); const box2 = new Box(50, 70); // выведем размеры ящиков в консоль console.log(box1.print()); // Box Size: 25 x 30 console.log(box2.print()); // Box Size: 50 x 70

    Прототип объекта на уровне родительского класса в JavaScript

    Обратите внимание, что метода print нет у объектов box1 и box2 . Но если раскрыть значение свойства [[Prototype]] в консоли в веб-браузере, то вы увидите его. То есть этот метод находится на уровне класса Box и наследуется всеми его экземплярами.

    Соответственно получается, что мы можем вызвать print как метод объектов box1 и box2 . Таким образом нам доступны не только собственные свойства и методы, но также наследуемые. А наследование, как вы уже понимаете, осуществляется в JavaScript на основе прототипов.

    Так что же такое прототип? Прототип в JavaScript – это просто ссылка на объект, который используется для наследования .

    Наследование

    Что такое наследование? Если после переменной, содержащей некоторый объект поставить точку, то вы увидите все доступные для него свойства и методы:

    Список доступных свойств и методов на уровне объекта box1

    Здесь width и height – это его собственные свойства. Далее на уровне родительского класса находятся методы constructor и print . Т.е. вы можете вызвать метод print , потому что он наследуется всеми экземплярами класса Box . Кроме этого, здесь имеются методы класса Object , такие как hasOwnProperty , isPrototypeOf , toString и так далее. Эти методы тоже доступны, потому что Box.prototype наследует все свойства и методы Object.prototype .

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

    Следовательно, в этом примере объект box1 имеет свои собственные свойства width и height , а также наследует все свойства и методы Box.prototype и Object.prototype .

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

    В JavaScript наследование осуществляется только на уровне объектов через прототипы. То есть один объект имеет ссылку на другой через специальное внутреннее свойство [[Prototype]] . Тот в свою очередь тоже имеет ссылку и т.д. В результате получается цепочка прототипов.

    Таким образом наследование, которые мы рассмотрели выше на примере объекта box1 происходит благодаря существованию следующей цепочки прототипов:

    box1 -> Box.prototype -> Object.prototype

    Заканчивается цепочка на прототипе глобального класса Object , потому что он не имеет прототипа, то есть его значение __proto__ равно null .

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

    Если указанное свойство или метод не найден, то возвращается undefined .

    Например, если метод print мы добавим в сам объект box1 , то будет использоваться уже он, а не тот, который находится в прототипе Box.prototype :

    JavaScript

    box1.print = function() { return `Размеры коробки: ${this.width} x ${this height}`; }

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

    Значение this внутри методов

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

    Рассмотрим следующий пример:

    JavaScript

    function Counter() { this.value = 0; } Counter.prototype.up = function() { this.value++; return this.value; } const counter1 = new Counter(); const counter2 = new Counter(); counter1.up(); // 1 counter1.up(); // 2 counter2.up(); // 1

    Здесь мы вызываем up как метод объектов counter1 и counter2 . Данный метод не является собственным для этих объектов, он наследуется и находится на уровне класса Counter . Но на самом деле это не имеет значения. Единственное, что важно для this – это только то, для какого объекта мы вызываем этот метод, то есть что стоит перед точкой. Это и будет this .

    При вызове counter1.up() , this внутри этого метода будет указывать на counter1 :

    JavaScript

    counter1.up();

    На строчке перед точкой стоит counter2 , значит this внутри up будет указывать на него :

    JavaScript

    counter2.up();

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

    Установка прототипа объекту

    Установить прототип объекту можно с помощью статического метода Object.setPrototypeOf() или свойства __proto__ .

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

    JavaScript

    const person1 = { name: 'Tom', printName() { return `Name: ${this.name}` } } const person2 = { name: 'Bob', __proto__: person1 }

    Проверить что прототипом для person2 является person1 очень просто:

    JavaScript

    person2.__proto__ === person1 // true Object.getPrototypeOf(person2) === person1 // true

    Установка объекта прототипа с помощью специального свойства __proto__

    При этом метод printName становится наследуемым, то есть доступным для объекта person2 :

    JavaScript

    person2.printName(); // "Name: Bob"

    Пример установки прототипа с помощью Object.setPrototypeOf() :

    JavaScript

    const message = { text: 'Сообщение 1. ', color: 'black', getText() { return `
    ${this.text}
    `; } } const errorMessage = { text: 'Сообщение 2. ', color: 'red' } Object.setPrototypeOf(errorMessage, message);

    Установка объекта прототипа с помощью Object.setPrototypeOf()

    В этом примере мы в качестве прототипа для errorMessage установили message .

    Чтобы было более понятно как работает метод Object.setPrototypeOf , рассмотрим его синтаксис:

    JavaScript

    Object.setPrototypeOf(obj, prototype)
    • obj – объект, для которого необходимо установить прототип;
    • prototype – объект, который будет использоваться в качестве прототипа для obj , или null , если у obj не должно быть прототипа.

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

    Следовательно, мы получим ошибку, если попытаемся для message установить в качестве прототипа errorMessage :

    JavaScript

    Object.setPrototypeOf(message, errorMessage);

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

    Наследование классов

    Допустим у нас имеется конструктор Person :

    JavaScript

    function Person(name, age) { this.name = name; this.age = age; } person.prototype.getName = function() { return this.name; }

    Создадим конструктор Student , который будет расширять класс Person :

    JavaScript

    function Student(name, age, schoolName) { // вызываем функцию, передавая ей в качестве this текущий объект Person.call(this, name, age); this.schoolName = schoolName; } Student.prototype.getSchoolName = function() { return this.schoolName; }

    Но на данном этапе, они сейчас полностью независимы. Но для того чтобы класс Student расширял Person нужно указать, что прототипом для Student.prototype является Person.prototype . Например, выполним это с помощью свойства __proto__ :

    JavaScript

    Student.prototype.__proto__ = Person.prototype;

    Создадим новый экземпляр класса Student :

    JavaScript

    const student = new Student('Bob', 15, 'ABC School');

    Для этого объекта будет доступен как метод getSchoolName , так и getName .

    Рассмотрим ещё один очень интересный пример с наследованием классов:

    JavaScript

    // конструктор для создания объектов класса Rectangle function Rectangle(x1, y1, x2, y2, bgColor) { Rectangle.counter++; this.id = `${this.constructor.name.toLowerCase()}-${Rectangle.counter}`; [this.left, this.top] = [Math.min(x1, x2), Math.min(y1, y2)]; [this.width, this.height] = [Math.max(x1, x2) - this.left, Math.max(y1, y2) - this.top]; this.bgColor = bgColor; } // конструктор для создания объектов класса Square function Square(x, y, side, bgColor) { // вызываем конструктор Rectangle в контексте текущего создаваемого объекта Rectangle.call(this, x, y, x + side, y + side, bgColor); } // конструктор для создания объектов класса Circle function Circle(x, y, r, bgColor) { Square.call(this, x, y, r, bgColor); this.radius = r; } // счетчик количества созданных объектов Rectangle.counter = 0; // установим для Square.prototype прототип Rectangle.prototype Square.prototype.__proto__ = Rectangle.prototype; // установим для Circle.prototype прототип Square.prototype Circle.prototype.__proto__ = Square.prototype; // объявляем метод draw в прототипе Rectangle Rectangle.prototype.draw = function() { document.body.insertAdjacentHTML('beforeend', ` `); } // переопределяем метод draw в прототипе Circle Circle.prototype.draw = function() { const el = Rectangle.prototype.draw.call(this); document.querySelector(`#${this.id}`).style.borderRadius = `${this.radius}px`; } // создадим новые объекты const rectangle = new Rectangle(70, 80, 200, 300, '#673ab7'); const square = new Square(250, 50, 150, '#ff9800'); const circle = new Circle(450, 140, 90, '#4caf50'); // вызовем для каждого из них метод draw() rectangle.draw(); square.draw(); circle.draw();

    Здесь у нас имеются 3 класса: Rectangle , Square и Circle . Для того чтобы объекты класса Square наследовали свойства и методы Rectangle.prototype мы прописали следующую связь:

    JavaScript

    Square.prototype.__proto__ = Rectangle.prototype;

    Похожим образом мы это сделали также для объектов класса Circle :

    JavaScript

    Circle.prototype.__proto__ = Square.prototype;

    Таким образом, объекты, являющиеся экземплярами класса Circle наследуют свойства и методы Circle.prototype , Square.prototype , Rectangle.prototype и Object.prototype . Это происходит благодаря следующей цепочки прототипов:

    circle -> Circle.prototype -> Square.prototype -> Rectangle.prototype -> Object.prototype

    Для вызова в Square родительского конструктора мы используем метод call . С помощью call мы задаём контекст, в котором нам нужно вызвать функцию. В данном случае мы вызываем Rectangle в контексте текущего создаваемого объекта.

    Свойство constructor

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

    По умолчанию свойство prototype функции содержит следующий объект:

    JavaScript

    function Article(title) { this.title = title; } Article.prototype = { constructor: Article }

    Здесь мы свойству prototype присвоили объект вручную, но точно такой же генерируется автоматически. Этот объект изначально содержит только свойство constructor , которое указывает на сам конструктор, то есть на функцию.

    Свойство constructor можно использовать для создания объектов:

    JavaScript

    const article1 = new Article('Про витамины'); const article2 = new article1.constructor('Про мёд');

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

    Не рекомендуется полностью перезаписывать значение свойства prototype , потому что в этом случае вы потеряете constructor и его придётся добавлять вручную:

    JavaScript

    // так делать не нужно Article.prototype = { getTitle() { return this.title; } }

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

    JavaScript

    Article.prototype.getTitle = function() { return this.title; }

    Встроенные прототипы

    В JavaScript практически всё является объектами. То есть функции, массивы, даты и так далее. Исключением являются только примитивные типы данных: строка, число и так далее.

    Например, при создании объекта { name: ‘Tom’ } внутренне используется конструктор Object :

    JavaScript

    const person = { name: 'Tom' }

    Прототипом такого объекта соответственно становится Object.prototype и в этом легко убедиться:

    JavaScript

    person.__proto__ === Object.prototype // true

    Поэтому нам на уровне этого объекта доступны различные методы, они берутся из Object.prototype . Например, метод hasOwnProperty :

    JavaScript

    person.hasOwnProperty('name'); // true

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

    При этом Object.prototype является корнем иерархии других встроенных прототипов. Но при этом он сам не имеет прототипа.

    JavaScript - Object.prototype

    На рисунке видно, что конструктор Object имеет по умолчанию свойство prototype . Это значение будет автоматически записываться в свойство [[Prototype]] объектов, которые будет создаваться с помощью этого конструктора. В Object.prototype имеется свойство constructor , которые указывает на сам конструктор. Эти связи между Object и Object.prototype показаны на схеме. Кроме этого Object.prototype не имеет прототипа. То есть его значение [[Prototype]] содержит null .

    Теперь давайте рассмотрим, как выполняется создание даты в JavaScript . Осуществляется это очень просто посредством конструктора Date :

    JavaScript

    const now = new Date();

    Методы прототипа конструктора Date

    Следовательно, прототипом даты является Date.prototype :

    JavaScript

    now.__proto__ === Date.prototype // true

    Этот прототип содержит большое количество методов для работы с датой, например, такие как getDate , getHours и так далее. Их нет в now , но они доступны нам посредством наследования.

    Объект Date.prototype имеет в качестве прототипа Object.prototype :

    JavaScript

    Date.prototype.__proto__ === Object.prototype // true // или так now.__proto__.__proto__ === Object.prototype // true

    Следовательно, методы Object.prototype , которых нет в Date.prototype также доступны для now . Например, hasOwnProperty :

    JavaScript

    now.hasOwnProperty('getYear') // false

    Таким образом можно нарисовать следующую схему:

    JavaScript - Date.prototype

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

    Метод Object.create

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

    JavaScript

    const rect1 = { a: 8, b: 5, calcArea() { return this.a * this.b } } // создали новый объект, который будет иметь в качестве прототипа rect1 const rect2 = Object.create(rect1); rect2.a = 10; rect2.b = 5; rect2.calcArea(); // 50

    Создание объекта с прототипом Object.prototype :

    JavaScript

    const obj = Object.create(Object.prototype);

    Данный пример аналогичен этому:

    JavaScript

    const obj = {};

    Создание объекта без прототипа:

    JavaScript

    const obj = Object.create(null);

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

    JavaScript

    const person = Object.create(null, { name: { value: 'John' }, age: { value: '18', writable: true } });

    Здесь мы описали два свойства: name и age . С помощью value мы устанавливаем значение свойству, а посредством аргумента writable задаем доступно ли свойство для изменения.

    Пример, в котором мы с помощью Object.create установим для Book.prototype в качестве прототипа объект Product.prototype :

    JavaScript

    // конструктор Product function Product(params) { this.name = params.name; this.price = params.price; this.discount = params.discount; } // конструктор Book function Book(params) { Product.call(this, params); this.isbn = params.isbn; this.author = params.author; this.totalPages = params.totalPages; } // устанавливаем для Book.prototype в качестве прототипа объект Product.prototype Book.prototype = Object.create(Product.prototype, { constructor: { value: Book }, // геттер fullName: { get() { return `${this.author}: ${this.name}`; } } }); Product.prototype.calcDiscountedPrice = function() { return (this.price * (1 - this.discount / 100)).toFixed(2); } // создадим новый объект const book = new Book({ name: 'Краткая история времени', author: 'Стивен Хокинг', isbn: '978-5-17102-284-6', totalPages: 232, price: 611.00, discount: 5 }); // цена книги со скидкой console.log(book.calcDiscountedPrice()); // 580.45

    Здесь мы Object.create используем для создания нового объекта, который будет использоваться в качестве прототипа для Book.prototype . При этом свою очередь этот новый объект будет иметь в качестве прототипа Product.prototype . Для этого мы передаем его в качестве аргумента методу Object.create . Кроме этого к этому объекту мы сразу же добавили два свойства: constructor и fullName . С помощью constructor мы восстановили конструктор, который был в прототипе пока мы ему не присвоили новое значение. А динамическое свойство fullName , представляющее собой геттер, будем использовать для получения имени книги с автором.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *