Объекты в JavaScript связаны в цепочки прототипов. Если вы обращаетесь к свойству или методу какого-то объекта, но у него этого свойства или метода нет, то он обратится к своему прототипу с этим запросом. Если у прототипа тоже нет, он пойдет к своему прототипу и так далее.

Прототип объекта
Непосредственный прототип объекта указан в его служебном свойстве __proto__
.
let animal = { type: 'animal' };
let cat = {};
cat.__proto__ = animal;
Объект animal
становится прототипом объекта cat
. Теперь cat
наследует свойства animal
, если они не определены в нем самом.
console.log(cat.type); // "animal"
Доступ к прототипу объекта
Свойство __proto__
является служебным, поэтому не стоит обращаться к нему непосредственно, а тем более изменять его.
Для чтения значения свойства __proto__
есть специальный метод Object.getPrototypeOf
(obj):
Object.getPrototypeOf(cat); // {type: "animal"}
Он возвращает объект, который является прототипом данного объекта.
Установка прототипа
По умолчанию прототипом всех объектов в JavaScript является объект, который хранится в свойстве Object.prototype
:
let newObject = {};
Object.getPrototypeOf(newObject) === Object.prototype; // true
То есть, когда вы создаете новый объект, у него по умолчанию уже есть прототип. Поэтому даже пустой объект может использовать ряд методов, которых у него вроде бы нет:
newObject.toString(); // "[object Object]"
Можно создать объект с другим прототипом (отличающимся от Object.prototype
). Для этого предназначен метод Object.create:
Object.create(proto, descriptors)
В первом параметре proto
указывается прототип нового объекта. Прототипом может стать любой другой объект.
Второй параметр descriptors
необязателен, в нем можно указать встроенные свойства нового объекта.
let dog = Object.create(animal);
console.log(dog.type); // "animal"
Объект без прототипа
Можно создать объект совсем без прототипа, передав null
в метод Object.create
:
let emptyObject = Object.create(null);
Объект, созданный таким образом не наследует даже от Object.prototype
, он абсолютно чист и не имеет никаких методов и свойств.
console.log(emptyObject.toString); // undefined
Перезапись прототипа
Можно изменить прототип для уже существующего объекта:
let horse = {};
Object.setPrototypeOf(horse, animal);
console.log(horse.type); // "animal"
При создании у объекта horse
установлен прототип по умолчанию — Object.prototype
. Мы меняем его на animal
. Теперь horse
может обращаться к свойствам объекта animal
, если у него нет аналогичных собственных свойств.
Цепочка прототипов
Повторим еще раз.
У многих объектов в JavaScript есть прототипы. По умолчанию это Object.prototype
, но можно устанавливать в качестве прототипа и другие объекты. Также можно создать абсолютно чистый объект вообще без прототипа.
При обращении к свойствам и методам объекта сначала они ищутся в самом объекте:
let animal = { type: 'animal', canFly: false };
let bird = { canFly: true };
// Назначаем объект animal прототипом объекта bird
Object.setPrototypeOf(bird, animal);
console.log(bird.canFly); // true
Здесь мы обращаемся к свойству canFly
, которое определено у самого объекта bird
, так что обращения к прототипу не происходит.
Если свойство или метод в объекте не нашлось, он перенаправляет запрос к своему непосредственному прототипу.
console.log(bird.type); // "animal"
У объекта bird
нет свойства type, но оно есть у его прототипа animal
, поэтому в консоль выводится строка "animal"
, а не undefined
.
Запрос может идти дальше, вверх по цепочке прототипов, пока не найдется нужное свойство.
console.log(bird.toString()); // "[object Object]"
В объекте bird
нет метода toString
. В его прототипе animal
тоже нет. А вот в прототипе animal
— Object.prototype
— этот метод есть.
Важно понимать, что эти свойство type
и метод toString
не хранятся в объекте bird
и не принадлежат ему. Просто при необходимости он может найти их у своих прототипов и одолжить на время. При этом методы будут выполняться в контексте объекта bird
, как если бы они ему принадлежали.
Например, добавим объекту animal
новое свойство — количество ног и новый метод, который возвращает это количество:
animal.legs = 4;
animal.getLegsCount = function() { return this.legs; }
Объекту bird
тоже добавим количество ног, но не будем добавлять метод:
bird.legs = 2;
Теперь если вызвать у bird
метод getLegsCount
, он вернет значение 2
, так как будет выполнен именно для объекта bird
, то есть его this
будет ссылаться на bird
, как и положено:
console.log(bird.getLegsCount()); // 2
Для справки: Все, что вы хотели знать о this, но боялись спросить
Проверка принадлежности свойства
Иногда требуется узнать, принадлежит ли свойство (или метод) непосредственно объекту или наследуется по цепочке прототипов. Для этого используется метод object.hasOwnProperty(propName)
:
bird.hasOwnProperty("legs"); // true
bird.hasOwnProperty("getLegsCount"); // false
Обратите внимание, что этот метод определен в корневом прототипе — Object.prototype
. То есть он не будет доступен для объектов, у которых в цепочке прототипов нет Object.prototype
, например, для объектов, созданных через Object.create(null)
, и их прототипных потомков.
Функция-конструктор
Функции-конструкторы используются для создания объектов по определенному шаблону. Вызывать такую функцию нужно с ключевым словом new.
При вызове конструктор получает пустой объект, который доступен ему как this
, и может наполнить его любыми свойствами по своему усмотрению.
Этот объект возвращается из функции автоматически, поэтому инструкция return
не нужна.
Кроме того, функция-конструктор, как любая другая функция может принимать параметры.
// Функция-конструктор
function Rabbit(name) {
this.species = "Кролик";
this.name = name;
}
// Создание объектов с помощью конструктора
let rabbit1 = new Rabbit("Питер");
let rabbit2 = new Rabbit("Джон");
rabbit1
и rabbit2
— это обычные объекты. У каждого из них есть свойство species
, равное "Кролик"
, и свойство name
(имя у каждого свое).
console.log(rabbit1.name); // "Питер"
console.log(rabbit2.species); // "Кролик"
Очень часто шаблон, по которому конструктор создает объекты называется классом, а созданные объекты — экземплярами класса.
Function.prototype
У функции-конструктора есть свойство prototype
(вообще оно есть у любой функции, но для конструкторов оно имеет особый смысл). Изначально в нем лежит пустой объект (унаследованный как полагается от Object.prototype
).
console.log(Rabbit.prototype);
На самом деле, этот объект не совсем пустой. У него есть свойство constructor
, в котором находится ссылка на саму функцию-конструктор. Чуть позже еще поговорим о нем.
Когда мы вызываем конструктор с ключевым словом new
, он создает нам новый объект. Так вот, прототипом этого нового объекта будет тот объект, который лежит в свойстве prototype
функции-конструктора.
let rabbit = new Rabbit("Николай");
Object.getPrototypeOf(rabbit) === Rabbit.prototype; // true
То есть при создании нового объекта происходит примерно следующее:
Object.setPrototypeOf(rabbit, Rabbit.prototype);
Обычно в этот объект-прототип Function.prototype
добавляют методы, которые должны быть доступны новому созданному объекту. Таким образом, он сможет пользоваться ими по цепочке прототипов. А свойства определяют внутри самого конструктора.
В этом есть смысл, так как значение свойств у разных объектов может быть разным, то есть свойства должны принадлежать непосредственно созданному объекту. А вот методы одинаковые, это просто функция, которая что-то делает со свойствами. Поэтому целесообразно хранить эту функцию в прототипе и использовать ее совместно всеми объектами, наследующими этот прототип.
function Rabbit(name) {
this.species = "Кролик";
this.name = name;
}
Rabbit.prototype.hello = function() {
console.log("Привет! Я " + this.species + " " + this.name);
}
let rabbit1 = new Rabbit("Питер");
let rabbit2 = new Rabbit("Джон");
rabbit1.hello(); // Привет! Я Кролик Питер
rabbit2.hello(); // Привет! Я Кролик Джон
Function.prototype.constructor
Мы уже сказали, что в свойстве prototype
функции-конструктор по умолчанию уже есть поле constructor
, которое ссылается на саму функцию-конструктор.
Rabbit.prototype.constructor == Rabbit;
Это нужно, чтобы у новых объектов, созданных функцией Rabbit
осталась информация о конструкторе, который их создал.
let rabbit = new Rabbit();
rabbit.constructor === Rabbit; // true
Установка прототипа в конструкторе
Если мы хотим, чтобы объекты, созданные конструктором Rabbit
, имели в качестве прототипа объект animal
, то мы должны просто записать его в свойство Rabbit.prototype
.
function Rabbit(name) {
this.species = "Кролик";
this.name = name;
}
Rabbit.prototype = animal;
let rabbit = new Rabbit("Питер");
console.log(rabbit.type); // "animal"
В качестве прототипа для нового объекта rabbit
будет установлен тот объект, который лежит в Rabbit.prototype
, то есть animal
. Таким образом, мы можем обратиться к свойству rabbit.type
, хотя у самого rabbit
этого свойства нет.
Важно понимать, что изменение свойства prototype
функции-конструктора повлияет только на объекты, созданные уже после этого изменения:
let proto1 = { property: 1 };
let proto2 = { property: 2 };
function ClassConstructor() {
this.otherProperty = 3;
}
// Устанавливаем proto1 в качестве прототипа для создаваемых объектов
ClassConstructor.prototype = proto1;
// Создаем объект с прототипом proto1
let obj1 = new ClassConstructor();
// Меняем прототип для создаваемых объектов на proto2
ClassConstructor.prototype = proto2;
// Создаем объект с прототипом proto2
let obj2 = new ClassConstructor();
Object.getPrototypeOf(obj1) === proto1; // true
Object.getPrototypeOf(obj2) === proto2; // true
Для объекта obj1
прототипом является объект proto1
, и последующее изменение ClassConstructor.prototype
никак на это не повлияет.
Проблема #1: Потеря ссылки на конструктор
Если мы присваиваем какой-то объект в свойство prototype функции-конструктора, то теряем ссылку на сам конструктор. Поэтому рекомендуется установить ее вручную:
Rabbit.prototype = animal;
Rabbit.prototype.constructor = Rabbit;
Проблема #2: Изменение прототипа
Если мы устанавливаем в качестве прототипа непосредственно объект, то рискуем случайно изменить его.
let animal = {
type: "animal",
legs: 4
};
function Rabbit(name) {
this.species = "Кролик";
this.name = name;
}
Rabbit.prototype = animal;
Rabbit.prototype.constructor = Rabbit;
function Cow(name) {
this.species = "Корова";
this.name = name;
}
Cow.prototype = animal;
Cow.prototype.constructor = Cow;
let rabbit = new Rabbit("Питер");
rabbit.constructor === Rabbit; // false
rabbit.constructor === Cow; // true
Мы создали две функции-конструктора Rabbit
и Cow
. Одна создает кроликов, а другая — коров. Обе используют в качестве прототипа для своих экземпляров объект animal
.
Однако у объект rabbit
почему-то в свойстве constructor лежит функция-конструктор Cow
.
Дело в том, что и в Rabbit.prototype
, и в Cow.prototype
лежит ссылка на один и тот же объект animal
. Поэтому, когда мы устанавливаем свойство Rabbit.prototype.constructor
, оно устанавливается для объекта animal
. А когда мы устанавливаем Cow.prototype.constructor
— мы это свойство у animal
обновляем. Теперь в animal.constructor
лежит функция Cow
.
Чтобы такое не происходило, нужно каждому конструктору выделить свой собственный объект прототипа, чтобы его можно было менять без ущерба для исходного объекта animal
. Для создания такого объекта мы используем метод Object.create
, то есть создадим объект, прототипом которого является animal
, и используем его как прототип для функции-конструктора. Таким образом мы просто удлиняем цепочку прототипов на один элемент.
rabbit.prototype = Object.create(animal);
Rabbit.prototype.constructor = Rabbit;
Cow.prototype = Object.create(animal);
Cow.prototype.constructor = Cow;
Как работает Object.create(proto)
Обобщив знания о конструкторах и прототипах, мы можем лучше представить логику работы метода Object.create
.
function inherit(proto) {
function F() {}
F.prototype = proto;
let object = new F();
return object;
}
Функция inherit
эмулирует метод Object.create(proto)
.
Сначала создаем пустую функцию-конструктор F
. Она никак не изменяет создаваемый объект, поэтому на выходе у нас не будет лишних свойств или методов.
В качестве прототипа для создаваемых объектов (метод F.prototype
) устанавливаем объект proto
, переданный как параметр.
Создаем новый объект, вызывая конструктор F
с ключевым словом new
. Его прототипом окажется proto
, что и нам и было нужно.
Теперь просто возвращаем новый объект из функции.
Наследование классов
Как установить объект в качестве прототипа, мы разобрались. Но что если у нас нет конкретного объекта, а только конструктор для него.
Мы можем легко наследовать один класс (конструктор) от другого.
Создадим для начала функцию-конструктор Animal
:
function Animal(name) {
this.name = name;
this.type = "Животное";
this.species = "Животное";
}
Animal.prototype.hello = function() {
console.log("Привет! Я " + this.species + " " + this.name);
}
Этот конструктор может создавать объекты с собственными свойствами name
, type
и species
, а также с доступным из прототипа методом hello
.
Теперь мы хотим создать конструктор Rabbit
, который будет наследовать все методы и свойства от Animal
с некоторыми небольшими изменениями.
Для этого нам нужно сделать две вещи:
1) В самом конструкторе Rabbit
нужно заполнить новый объект (this
) теми же свойствами, которыми свои объекты заполняет конструктор Animal
.
function Rabbit(name) {
// отправляем this для заполнения свойствами в конструктор Animal
Animal.apply(this, arguments);
// переопределяем нужные свойства
this.species = "Кролик";
}
2) Установить для новых объектов класса прототип с тем же набором методов, который есть в Animal.prototype
.
// Используем для основы прототипа Animal.prototype
Rabbit.prototype = Object.create(Animal.prototype);
Rabbit.prototype.constructor = Rabbit;
// Добавляем дополнительные методы
Rabbit.prototype.jump = function() {
console.log(this.species + " " + this.name + " прыгает");
};
Теперь создаем нового кролика:
let rabbit = new Rabbit("Питер");
console.log(rabbit.type); // "Животное" - унаследовано от Animal
console.log(rabbit.species); // "Кролик" - переопределено в Rabbit
console.log(rabbit.name); // "Питер" - задано входящим параметром
rabbit.hello(); // "Привет! Я Кролик Питер" - метод из класса Animal
rabbit.jump(); // "Кролик Питер прыгает" - метод из класса Rabbit
rabbit.constructor === Rabbit; // true
Проверка принадлежности к классу
В JavaScript есть оператор instanceof, который позволяет узнать, принадлежит ли объект к определенному классу (то есть создавал ли его определенный конструктор).
rabbit instanceof Rabbit; // true
Эта проверка учитывает всю цепочку прототипного наследования:
rabbit instanceof Animal; // true
Здесь тоже будет true
, так как конструктор Animal
тоже участвовал в создании объекта rabbit
.
Алгоритм проверки выглядит так:
- Сначала свойство
__proto__
объектаrabbit
сравнивается со свойствомprototype
функции-конструктораAnimal
. Если бы они были равны, это означало бы, что rabbit создан конструктором Animal. Но они не равны. - Вместо
rabbit
берется его прототип (rabbit.__proto__
) и теперь его свойство__proto__
сравнивается сAnimal.prototype
. А вот они уже равны, ведь прототипrabbit
(Rabbit.prototype
) действительно наследует напрямую отAnimal.prototype
. Проверка пройдена.
rabbit instanceof Animal
rabbit.__proto__ === Animal.prototype; // false
rabbit.__proto__.__proto__ === Animal.prototype; // true
Проверка может продолжаться до тех пор, пока прототипы не закончатся.
1 комментарий