Прототипное наследование Javascript (+ видео с примером)
В этом видео мы разберемся - что такое прототипы и как работает прототипное наследование в JS.
Давайте рассмотрим небольшой пример:
В JS есть нативный или встроенный объект под названием Date, который используется для работы с датами и временем.
Чтобы воспользоваться функционалом объекта Date - нам сначала нужно создать наш собственный экземпляр этого объекта. Для этого мы используем специально слово new:
1const myDate = new Date();2console.log(myDate);3// Date Fri Nov 12 2021 15:05:52 GMT+0300 (Moscow Standard Time)
В переменной myDate мы получаем новый экземпляр объекта Date, который содержит текущий момент времени. Далее, мы можем использовать различные встроенные методы объекта Date, обращаясь к нашему экземпляру.
Например, мы можем получить только текущий год:
1myDate.getFullYear();2// 2021
или текущее время:
1myDate.getHours();2// 15
То есть теперь, для нашего экземпляра объекта Date (переменная myDate) - доступно много различных методов, которые живут в головном объекте Date.
То же самое касается других нативных объектов, таких как Object, Number, Array и так далее.
Если мы напишем название любого из этих объектов в консоли и нажмемenter, мы увидим, что все они являются функциями.
1Date;2// function Date()
1Object;2// function Object()
Если запускать эти функции, используя слово new, то в ответ мы будем получать объекты (экземпляры головного объекта). Давайте это проверим:
1const myDate = new Date();2typeof myDate;3// "object"
1const myDate = new Date();2myDate instanceof Date;3// "true"
Создаем собственный головной объект
Теперь давайте создадим собственный аналог головного объекта, который будем использовать для создания собственных отдельных экземпляров.
Для этого мы будем использовать так называемую функцию конструктор . Название таких функций принято писать с большой буквы.
1function Auto() {2 console.log('наша машина');3}
Создаем экземпляры головного объекта
Теперь мы можем использовать эту функцию конструктор для создания отдельных экземпляров машин.
1const tesla = new Auto();2console.log(tesla);3// Object { }
Таким образом, мы получили экземпляр нашего головного объекта Auto - пустой объект, присвоенный нашей переменной tesla. Давайте используем метод constructor, чтобы удостовериться, что это действительно так:
1console.log(tesla.constructor);2// function Auto()
1tesla instanceof Auto;2// true
Добавляем свойства к экземплярам
Теперь, давайте добавим какие-то свойства в каждый из наших экземпляров головного объекта Auto. Для этого мы используем нашу функцию конструктор:
1function Auto(brand, color, gas) {2 this.brand = brand;3 this.color = color;4 this.gas = gas;5}
Внутри функции Auto, значение this относится к каждому конкретному экземпляру, который мы будем создавать:
1const bmw = new Auto('bmw', 'white', 100);2const nissan = new Auto('nissan', 'black', 100);
Мы создали 2 экземпляра нашего головного объекта Auto. Каждый экземпляр обладает своими собственными свойствами.
Добавляем методы к экземплярам
Теперь давайте попробуем добавить какие-то методы к нашим машинкам. Метод drive будет расходовать бензин каждой машины (свойство gas):
1function Auto(brand, color, gas) {2 this.brand = brand;3 this.color = color;4 this.gas = gas;5 this.drive = function () {6 if (this.gas > 0) {7 this.gas = this.gas - 10;8 return this.gas;9 console.log(`Уровень бензина = ${this.gas}`);10 } else {11 console.log(`Бензин закончился!`);12 }13 };14}1516const bmw = new Auto('bmw', 'white', 100);17const nissan = new Auto('nissan', 'black', 100);1819bmw.drive();
Все отлично работает - но есть небольшой ньюанс!
Каждый раз, cоздавая новый экземпляр машины, мы также создаем новую функцию drive (для каждого экземпляра)!
Как в этом убедиться?
Давайте сравним функцию drive у разных экземпляров машин:
1bmw.drive === nissan.drive;2// false
Так что же в этом плохого?
В текущем примере, где у нас всего 2 экземпляра машин - проблем никаких нет. Но если количество экземпляров возрастет до 1,000 или десятков тысяч, это окажет негативное влияние на производительность.
Используем прототип (js prototype)
Чтобы не создавать каждый раз новый метод drive() для каждого экземпляра машины, мы можем поместить этот метод в, так называемый, прототип (prototype) нашего головного объекта.
1function Auto(brand, color, gas) {2 this.brand = brand;3 this.color = color;4 this.gas = gas;5}67Auto.prototype.drive = function () {8 if (this.gas > 0) {9 this.gas = this.gas - 10;10 return this.gas;11 console.log(`Уровень бензина = ${this.gas}`);12 } else {13 console.log(`Бензин закончился!`);14 }15};1617const bmw = new Auto('bmw', 'white', 100);18const nissan = new Auto('nissan', 'black', 100);
Таким образом мы создадим всего один метод drive(), который будет использоваться всеми экземплярами наших машин.
В этом и заключается суть прототипного наследования. Мы получаем метод, к которому имеют доступ все экземпляры нашего головного объекта. При этом мы не создаем большого количества копий этого метода для каждого экземпляра.
Теперь давайте посмотрим на экземпляр нашего объекта в переменной nissan:
1nissan;2// Object { brand: "nissan", color: "black", gas: 100 }
Мы видим, что метод drive пропал из списка свойств. Это происходит потому, что теперь метод drive "живет" в прототипе объекта.
Теперь, когда мы будем запускать метод drive, используя какой-либо экземляр, Javascript будет искать этот метод сначала в свойствах экземпляра, а потом прототипе.
Давайте убедимся в этом на примере:
Давайте добавим еще одно свойство в прототип нашего объекта:
1Auto.prototype.discount = '20%';
Если мы в консоли напишем:
1bmw.discount;2// получаем '20%'
Теперь давайте дополнительно добавим свойство discount в наши экземпляры:
1function Auto(brand, color, gas) {2 this.brand = brand;3 this.color = color;4 this.gas = gas;5 this.discount = '70%';6}
Если мы повторно в консоли проверим свойство discount конретного экземпляра, то получим значение "70%":
1bmw.discount;2// получаем '70%'
То есть, первым делом идет поиск в свойствах экземпляра. Если свойство отсутствует - JS делает поиск в прототипе!
Обновляем логику работы метода в прототипе для всех экземпляров
Часто требуется обновить логику работу какого-либо метода. Используя прототипное наследование, обновленный метод становится доступным для всех экземпляров!
Рассмотрим пример
Создаем метод info и ниже меняем его, в соответствии с новыми требованиями:
1function Auto(brand, color, gas) {2 this.brand = brand;3 this.color = color;4 this.gas = gas;5}67Auto.prototype.drive = function () {8 if (this.gas > 0) {9 this.gas = this.gas - 10;10 return this.gas;11 console.log(`Уровень бензина = ${this.gas}`);12 } else {13 console.log(`Бензин закончился!`);14 }15};1617// Оригинальная версия метода18Auto.prototype.info = function () {19 return `${this.brand} color is ${this.color}`;20};2122// Обновленная версия метода23Auto.prototype.info = function () {24 return `${this.brand} - уровень топлива: ${this.gas}`;25};
На этом этапе, для всех экземпляров нашего объекта Auto доступна обновленная версия метода info.