Расширение объектов. Прототипы

Последнее обновление: 18.11.2023

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

Схематично мы можем представить прототип следующим образом:

Прототипы функций-конструкторов в JavaScript

Фактически прототип функции-конструктора 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 и конструктора:

Схема прототипов функций-конструкторов в JavaScript

При этом вне зависимости от того, как мы определяем методы и свойства - внутри конструктора или как часть прототипа, мы их равным образом можем использовать для объектов данного типа:

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

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850