Прототипы
Каждый объект в JavaScript может использовать любые методы, которые находятся в функции-конструкторе, с помощью которой данный объект и был создан. Например, обычный массив сам по себе не имеет ни одного метода, но функция конструктор Array
любезно предоставляет ему все методы из своего прототипа в использование.
1console.dir([].__proto__);2// То же самое, что и3console.dir(Array.prototype);
Разумеется, мы сами можем создавать функции конструкторы и задавать им прототипы. Итак, давайте представим зоопарк в ввиде javascript обьектов. Каждое животное можно предствить в ввиде обьекта где свойства обьекта это будут кличка, вид а звуки которые может издавать животное это будет методы
Функция-конструктор в данном случае будет выглядеть следующим образом:
1const Animal = function(name, species, sound) {2 this.name = name;3 this.species = species;4 this.sound = sound;5};67Animal.prototype.speak = function() {8 return this.species + ' ' + this.name + ' says ' + this.sound + '!';9};
Теперь мы можем создать один или несколько объектов с помощью функции конструктора Animal
и оператора new
:
1const cat = new Animal('Wizard', 'Cat', 'Meow');2const dog = new Animal('Pancho', 'Dog', 'Woof');3const fox = new Animal('Oliver', 'Fox', '????');
Каждый созданный нами объект не содержит своего метода speak
. В этом вы можете убедиться, просто выведя объект в консоль console.dir(cat
). Тем не менее, все созданные объекты могут использовать метод speak
:
1console.log(dog.speak()); // Dog Pancho says Woof!2console.log(cat.speak()); // Cat Wizard says Meow!3console.log(fox.speak()); // Fox Oliver says ????!
Что происходит на самом деле. Когда вы используете метод speak
с объектом, сначала происходит проверка того, есть ли у самого объекта этот метод. Если метода нет, то далее следует проверка на присутствие метода в прототипе. Если метода нет и в прототипе, то метод может быть найден в прототипе прототипа. И так далее, пока выполнение не дойдёт до последнего прототипа, который всегда содержит в себе все методы функции-конструктора Object
.
Чтобы в этом убедиться попробуйте выполнить в консоле браузера несколько строчек кода:
1// Прототипы для чисел2console.dir((10).__proto__); // Number3console.dir((10).__proto__.__proto__); // Object45// Прототипы для строк6console.dir('str'.__proto__); // String7console.dir('str'.__proto__.__proto__); // Object89// Прототипы для объектов10console.dir([].__proto__); // Array11console.dir([].__proto__.__proto__); // Object1213// Прототипы для созданной нами функции-конструктора Animal14console.dir(cat.__proto__); // Animal15console.dir(cat.__proto__.__proto__); // Object
Таким образом, созданный нами объект cat унаследовал все методы не только от функции Animal
, но и от Object
. В этом легко убедиться с помощью использования любого метода объектов, например, toString()
:
1console.log(cat.toString()); // "[object Object]"
Как я уже писал выше, при использовании любого метода сначала проверяется его наличие в самом объекте. Поэтому мы может сами переназначить тот же метод toString
для отдельного объекта cat
:
1const cat = new Animal('Wizard', 'Cat', 'Meow');2const dog = new Animal('Pancho', 'Dog', 'Woof');34cat.toString = function() {5 return this.species + ' ' + this.name;6};78console.log(cat.toString()); // Cat Wizard9console.log(dog.toString()); // "[object Object]"
Или же можно записать данный метод в прототип Animal
, чтобы все создаваемые с помощью этой функции-конструктора объекты использовали именно его:
1Animal.prototype.toString = function() {2 return 'This is ' + this.species + ' ' + this.name;3};45cat.toString = function() {6 return this.species + ' ' + this.name;7};89const cat = new Animal('Wizard', 'Cat', 'Meow');10const dog = new Animal('Pancho', 'Dog', 'Woof');11const fox = new Animal('Oliver', 'Fox', '????');1213console.log(cat.toString()); // Cat Wizard14console.log(dog.toString()); // This is Dog Pancho15console.log(fox.toString()); // This is Fox Oliver
Теперь объекты dog
и fox
обращаются к прототипу Animal
, а объект cat
имеет собственный метод и использует его.
Есть два способа устанавливать свойства в прототипы для объектов: плохой и хороший. Хороший способ вы уже видели — все примеры приведённые выше написаны с его помощью.
1Animal.prototype.speak = function() { /* code here */ };2Animal.prototype.toString = function() { /* code here */ };
В данном примере к прототипу просто добавляется ещё одно свойство, в независимости от того, что было в прототипе было раньше.
Пример плохого добавления свойств в прототип — непосредственная его перезапись новым объектом:
1Animal.prototype.toString = function() { /* code here */ };2// ...3Animal.prototype = {4 speak: function() {}5};
Почему так делать плохо? Всё просто. Перезаписывая прототип новым объектом, вы _полностью стираете все методы, которые были записаны в него ранее. Поэтому в приведённом выше примере метода toString
у всех объектов, созданных с помощью функции-конструктора Animal
, не будет.
Свойство __proto__
Абсолютно любой объект в JavaScript имеет свойство __proto__
. Это скрытое системное свойство, и не во всех реализациях языка оно доступно пользователю.
При обращении к любому свойству объекта, оно в первую очередь ищется в самом объекте:
1var obj = {2 ownProperty: 13};4console.log(obj.ownProperty);// 15// Но если его там нет, поиск происходит в свойстве __proto__:67obj.__proto__ = {propertyOfProto: 2};8console.log(obj.propertyOfProto);// 2910// Если его нет и там, оно ищется дальше по цепочке:1112obj.__proto__.__proto__ = {propertyOfProtosProto: 3};13console.log(obj.propertyOfProtosProto);// 3

__proto__
любого значения (кроме null
и undefined
) ссылается на prototype
соответствующего ему типу данных:
1(0).__proto__ === Number.prototype &&2false.__proto__ === Boolean.prototype &&3"string".__proto__ === String.prototype &&4(new Date).__proto__ === Date.prototype &&5(function(){}/* new Function */).__proto__ === Function.prototype67// Все типы данных наследуются от Object, это означает что к примеру:8Number.prototype.__proto__ === Object.prototype // true910// Завершение цепочки:1112Object.prototype.__proto__ === null
__proto__
vs prototype
в JavaScript
__proto__
- это фактический объект, который используется в цепочке поиска для разрешения методов и т.д.
prototype
- это объект, который используется для построения __proto__
, когда вы создаете объект с помощью new
1function Foo(){}2console.log( (new Foo).__proto__ === Foo.prototype); // true3console.log((new Foo).prototype === undefined); // true
Можно добавлять методы в прототип, даже если объект уже создан с помощью new
1function Tree(name) {2 this.name = name;3}4Tree.prototype.sayName = function() {5 console.log(this.name);6}78const tree1 = new Tree('Maple');9const tree2 = new Tree('Bamboo');1011tree1.sayName(); // Maple12tree2.sayName(); // Bamboo1314// add another method later15Tree.prototype.sayBye = function() {16 console.log(`Bye bye, ${this.name}`);17}1819tree1.sayBye(); // Bye bye, Maple20tree2.sayBye(); // Bye bye, Bamboo
Важно
Можно добавлять или удалять переменные, или функции в прототипе в любое время. Все его экземпляры будут динамически ссылаться на обновленный прототип, когда они попытаются получить к нему доступ.
Можно также переназначить прототип с помощью нового объекта-прототипа.
1function Tree(name) {2 this.name = name;3}4Tree.prototype.sayName = function() {5 console.log(this.name);6}78const tree1 = new Tree('Maple');9const tree2 = new Tree('Bamboo');1011tree1.sayName(); // Maple12tree2.sayName(); // Bamboo1314Tree.prototype = {15 sayName: function() {16 console.log(`Bye bye, ${this.name}`)17 }18}1920const tree3 = new Tree('Ginkgo');2122tree1.sayName(); // Maple23tree2.sayName(); // Bamboo24tree3.sayName(); // Bye bye, Ginkgo
Как мы можем видеть в результате, прототип Tree
был заменен другим, когда был сделан tree3
.
Но самое интересное - tree1
и tree2
по-прежнему указывают на предыдущую функцию sayName
, которая печатает this.name
, а tree3
указывает на новое sayName
.
Вот в чем вы должны убедиться, прежде чем заменять существующий прототип новым.
Поскольку существующие экземпляры по-прежнему относятся к исходному прототипу, они не будут работать должным образом.
Кроме того, сборщик мусора (garbage collection) не очищает исходный прототип, поскольку на него ссылается кто-то другой, в данном случае tree1
и tree2
.
Поэтому менять прототип не рекомендуется.
Прототип - очень мощный механизм в JavaScript, поскольку он позволяет множеству объектов обмениваться данными друг с другом.
JavaScript позволяет расширять собственные объекты, такие как Array
, Number
, String
, путем расширения их прототипов.
Если вы хотите добавить метод к прототипу массива, можно сделать следующее:
1Number.prototype.plus = function (num) {2 return this + num3}45Number.prototype.minus = function (num) {6 return this - num7}89// После выполнения кода выше любой сможет вызывать метод `plus` and minus для любого числа созданного через конструктов Number.10// Нет необходимости создавать экземпляр специального класса или вносить какие-либо изменения в код.1112var x = Number(10).plus(1).plus(10).minus(2).plus(20)13console.log(x) // 391415var y = Number().minus(1) // -1161718String.prototype.addDot = function( ){19 return this.concat('.')20}2122var text = 'Hello world'.addDot() // "Hello world."
Практика 👨💻
Задание :one:
Создать следующие функции для строки:
string reverse
1"hello".reverse() // olleh
string isPalindrome
1"hello".isPalindrome() // false2"level".isPalindrome() // true3"Madam".isPalindrome() // true
Важно: isPalindrome должна же быть независима от регистра те Level, LeVeL, leVEL все эти варианты должны вернуть true
Задание :two:
Реализовать следующие функции для массива
Array.first
1var firstNum = [1,2,3].first() // 123var firstWord = ['Hello','world'].first() / 'Hello' +4[].first() // undefined
Array.last
1var lastNum = [1,2,3].last() // 323var lastWord = ['Hello','world'].last() / 'world' +4[].last() // undefined
Array.diff
1var result = [1,2,3].diff([2, 1])2console.log('result',result)// [3]