Прототипы

Каждый объект в 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};
6
7Animal.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__); // Number
3console.dir((10).__proto__.__proto__); // Object
4
5// Прототипы для строк
6console.dir('str'.__proto__); // String
7console.dir('str'.__proto__.__proto__); // Object
8
9// Прототипы для объектов
10console.dir([].__proto__); // Array
11console.dir([].__proto__.__proto__); // Object
12
13// Прототипы для созданной нами функции-конструктора Animal
14console.dir(cat.__proto__); // Animal
15console.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');
3
4cat.toString = function() {
5 return this.species + ' ' + this.name;
6};
7
8console.log(cat.toString()); // Cat Wizard
9console.log(dog.toString()); // "[object Object]"

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

1Animal.prototype.toString = function() {
2 return 'This is ' + this.species + ' ' + this.name;
3};
4
5cat.toString = function() {
6 return this.species + ' ' + this.name;
7};
8
9const cat = new Animal('Wizard', 'Cat', 'Meow');
10const dog = new Animal('Pancho', 'Dog', 'Woof');
11const fox = new Animal('Oliver', 'Fox', '????');
12
13console.log(cat.toString()); // Cat Wizard
14console.log(dog.toString()); // This is Dog Pancho
15console.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: 1
3};
4console.log(obj.ownProperty);// 1
5// Но если его там нет, поиск происходит в свойстве __proto__:
6
7obj.__proto__ = {propertyOfProto: 2};
8console.log(obj.propertyOfProto);// 2
9
10// Если его нет и там, оно ищется дальше по цепочке:
11
12obj.__proto__.__proto__ = {propertyOfProtosProto: 3};
13console.log(obj.propertyOfProtosProto);// 3
prototype chain Эта цепочка называется цепочкой прототипов (prototype chain).

__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.prototype
6
7// Все типы данных наследуются от Object, это означает что к примеру:
8Number.prototype.__proto__ === Object.prototype // true
9
10// Завершение цепочки:
11
12Object.prototype.__proto__ === null

__proto__ vs prototype в JavaScript

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

1function Foo(){}
2console.log( (new Foo).__proto__ === Foo.prototype); // true
3console.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}
7
8const tree1 = new Tree('Maple');
9const tree2 = new Tree('Bamboo');
10
11tree1.sayName(); // Maple
12tree2.sayName(); // Bamboo
13
14// add another method later
15Tree.prototype.sayBye = function() {
16 console.log(`Bye bye, ${this.name}`);
17}
18
19tree1.sayBye(); // Bye bye, Maple
20tree2.sayBye(); // Bye bye, Bamboo
ℹ️Важно

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

Можно также переназначить прототип с помощью нового объекта-прототипа.

1function Tree(name) {
2 this.name = name;
3}
4Tree.prototype.sayName = function() {
5 console.log(this.name);
6}
7
8const tree1 = new Tree('Maple');
9const tree2 = new Tree('Bamboo');
10
11tree1.sayName(); // Maple
12tree2.sayName(); // Bamboo
13
14Tree.prototype = {
15 sayName: function() {
16 console.log(`Bye bye, ${this.name}`)
17 }
18}
19
20const tree3 = new Tree('Ginkgo');
21
22tree1.sayName(); // Maple
23tree2.sayName(); // Bamboo
24tree3.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 + num
3}
4
5Number.prototype.minus = function (num) {
6 return this - num
7}
8
9// После выполнения кода выше любой сможет вызывать метод `plus` and minus для любого числа созданного через конструктов Number.
10// Нет необходимости создавать экземпляр специального класса или вносить какие-либо изменения в код.
11
12var x = Number(10).plus(1).plus(10).minus(2).plus(20)
13console.log(x) // 39
14
15var y = Number().minus(1) // -1
16
17
18String.prototype.addDot = function( ){
19 return this.concat('.')
20}
21
22var text = 'Hello world'.addDot() // "Hello world."

Практика 👨‍💻

Задание :one:

Создать следующие функции для строки:

string reverse

1"hello".reverse() // olleh

string isPalindrome

1"hello".isPalindrome() // false
2"level".isPalindrome() // true
3"Madam".isPalindrome() // true

Важно: isPalindrome должна же быть независима от регистра те Level, LeVeL, leVEL все эти варианты должны вернуть true

Задание :two:

Реализовать следующие функции для массива

Array.first

1var firstNum = [1,2,3].first() // 1
2
3var firstWord = ['Hello','world'].first() / 'Hello' +
4[].first() // undefined

Array.last

1var lastNum = [1,2,3].last() // 3
2
3var lastWord = ['Hello','world'].last() / 'world' +
4[].last() // undefined

Array.diff

1var result = [1,2,3].diff([2, 1])
2console.log('result',result)// [3]
Hello