JavaScript — это язык, основанный на прототипах, поэтому он не знает никаких классов — по крайней мере, реальных. Вместо этого все в JavaScript основано на объектах.
Почти каждый объект в JavaScript основан на прототипе. Исключения - тип Object (основа всех объектов) или объекты, прототип которых явно
установлен в null — не имеют прототипа. Каждый объект также может служить шаблоном, то есть прототипом другого объекта.
В этом случае новый объект наследует свойства и методы прототипа.
Прототип объекта хранится в свойстве __proto__, которое реализованно как псевдоним внутреннего свойства [[Prototype]].
Кроме того получить прототип объекта можно с помощью метода getPrototypeOf(). Например:
const tom = {name: "Tom", age: 39};
// получаем прототип
console.log(tom.__proto__); // Object
console.log(Object.getPrototypeOf(tom)); // Object
В обоих случаях мы получим один и тот же результат в виде определения типа Object:
Object
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: f isPrototypeOf()
propertyIsEnumerable: f propertyIsEnumerable()
toLocaleString: f toLocaleString()
toString: f toString()
valueOf: f valueOf()
__defineGetter__: f __defineGetter__()
__defineSetter__: f __defineSetter__()
__lookupGetter__: f __lookupGetter__()
__lookupSetter__: f __lookupSetter__()
__proto__: null
get __proto__: f __proto__()
set __proto__: f __proto__()
В прошлой теме были рассмотрены функции-конструкторы, который позволяют определить тип объекта и создать объект этого типа. Каждая такая функция-конструктор определяет свой прототип, который служит основой для создаваемых объектов. Этот прототип также можно получить с помощью свойства prototype. Например:
function Person(name, age) {
this.name = name;
this.age = age;
this.print = function(){
console.log(`Name: ${this.name} Age: ${this.age}`);
};
}
const tom = new Person("Tom", 39);
// получаем прототип
console.log(Person.prototype);
console.log(tom.__proto__);
console.log(Object.getPrototypeOf(tom));
Здесь получаем прототип функции-конструктора Person. Все три использованных способа получения прототипа аналогичны, и при выводе на консоль во всех трех случаях мы увидим что-то наподобие:
{constructor: ƒ}
constructor : ƒ Person(name, age)
[[Prototype]] : Object
Важно отличать конструктор и прототип. Прототип - это по сути план объекта, который может состоять из различных частей - методов и переменных, а собственно конструктор - только часть прототипа. Например, возьме выше определенную функцию Person:
function Person(name, age) {
this.name = name;
this.age = age;
this.print = function(){
console.log(`Name: ${this.name} Age: ${this.age}`);
};
}
console.log(Person.prototype);
Консольный вывод:
{constructor: ƒ}
constructor: ƒ Person(name, age)
[[Prototype]]: Object
Схематично мы можем представить прототип следующим образом:
Фактически прототип функции-конструктора Person состоит только из конструктора (в который неявно также входят унаследованные от типа Object методы типа toString()).
мы можем получить этот конструктор, использовав свойство constructor:
console.log(Person.prototype.constructor);
Консоль должна вывести что-то наподобие:
ƒ Person(name, age) {
this.name = name;
this.age = age;
this.print = function(){
console.log(`Name: ${this.name} Age: ${this.age}`);
};
Поскольку свойство constructor - это часть прототипа, то к нему обратиться можно и через имя объекта:
const tom = new Person("Tom", 39);
console.log(tom.constructor);
Теперь уберем метод print() из конструктора и определим его как часть прототипа:
function Person (name, age) {
this.name = name;
this.age = age;
}
// функция print определена как часть прототипа
Person.prototype.print = function(){
console.log(`Name: ${this.name} Age: ${this.age}`);
};
console.log(Person.prototype);
Консольный вывод браузера:
{print: ƒ, constructor: ƒ}
print: ƒ ()
constructor: ƒ Person(name, age)
[[Prototype]]: Object
Теперь прототип состоит из функции print и конструктора:
При этом вне зависимости от того, как мы определяем методы и свойства - внутри конструктора или как часть прототипа, мы их равным образом можем использовать для объектов данного типа:
function Person(name, age) {
this.name = name;
this.age = age;
this.print = function(){
console.log(`Name: ${this.name} Age: ${this.age}`);
};
}
const tom = new Person("Tom", 39);
const bob = new Person("Bob", 43);
// измененияем прототип
Person.prototype.sayHello = function(){
console.log(this.name, "says: Hello");
};
tom.print(); // Name: Tom Age: 39
tom.sayHello(); // Tom says: Hello
bob.print(); // Name: Bob Age: 43
bob.sayHello(); // Bob says: Hello
Причем мы можем определить одни и те же свойства и методы как внутри конструктора, так и как часть прототипа:
// конструктор пользователя
function Person (name, age) {
this.name = name;
this.age = age;
this.print = function(){
console.log(`[Конструктор] Name: ${this.name} Age: ${this.age}`);
};
}
Person.prototype.print = function(){
console.log(`[Прототип] Name: ${this.name} Age: ${this.age}`);
};
const tom = new Person("Tom", 39);
const bob = new Person("Bob", 43);
tom.print(); // [Конструктор] Name: Tom Age: 39
bob.print(); // [Конструктор] Name: Bob Age: 43
В этом случае методы, определенные внутри конструктора, будут скрывать одноименные методы прототипа.
Подобным образом можно добавлять и свойства. Например, добавим свойство company, которое представляет компанию:
const tom = new Person("Tom", 39);
const bob = new Person("Bob", 43);
// добавляем в прототип свойство company
Person.prototype.company = "SuperCorp";
console.log(tom.company); // SuperCorp
console.log(bob.company); // SuperCorp
Но важно заметить, что
значение свойства company будет одно и то же для всех объектов, это разделяемое статическое свойство. В отличие, скажем, от свойства
this.name, которое хранит значение для определенного объекта.
В то же время мы можем определить в объекте свойство, которое будет назваться также, как и свойство прототипа. В этом случае собственное свойство объекта будет иметь приоритет перед свойством прототипа:
const tom = new Person("Tom", 39);
const bob = new Person("Bob", 43);
Person.prototype.company = "SuperCorp";
bob.company = "MegaCorp"; // определяем свойство с тем же именем на уровне одного объекта
console.log(bob.company); // MegaCorp - берет свойство из объекта bob
console.log(tom.company); // SuperCorp - берет свойство из прототипа Person
И при обращении к свойству company javascript сначала ищет это свойство среди свойств объекта, и если оно не было найдено, тогда обращается к свойствам прототипа. То же самое касается и методов.