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 сначала ищет это свойство среди свойств объекта, и если оно не было найдено, тогда обращается к свойствам прототипа. То же самое касается и методов.