Прототип объекта
В JavaScript объекты имеют специальное скрытое свойство [[Prototype]] (так оно названо в спецификации), которое либо равно null, либо ссылается на другой объект, который называется «прототипом». Свойство [[Prototype]] в JS используется для реализации наследования (прототипирования).
Прототипирование - это механизм, с помощью которого объекты JavaScript наследуют свойства друг от друга.
JavaScript часто описывают как язык прототипного наследования — каждый объект имеет объект-прототип, который выступает шаблоном (родительским объектом) и от которого объект наследует методы и свойства.
Объект-прототип также может иметь свой прототип и наследовать его свойства и методы и так далее (цепочка прототипов объясняет доступность объектам свойств и методов, которые определены в других объектах).
ВАЖНО! Методы и свойства в цепочке прототипов не копируются из одного объекта в другой: к ним обращаются, поднимаясь вверх по цепочке.
Устаревшее свойство __proto__ объектов:
- является производным от свойства prototype конструктора и обеспечивает в JavaScript связь между экземпляром объекта и его прототипом;
- является геттером/сеттером для [[Prototype]] (таким образом делая его изменяемым).
В настоящее время свойство __proto__ заменяют функции Object.getPrototypeOf() и Object.setPrototypeOf().
Оператор instanceof проверяет, принадлежит ли объект к определённому классу (проверяет, присутствует ли объект constructor.prototype в цепочке прототипов object).
Синтаксис:
1 |
object instanceof constructor |
Например:
1 2 3 4 5 6 7 |
function Car(brand = "none", model = "none", year = 2000) { (this.brand = brand), (this.model = model), (this.year = year); } let myCar = new Car("Nissan", "", 2008); console.log(myCar instanceof Car); // true |
Важно: instanceof выполняет проверку типов совсем не так, как это происходит в строго типизированных языках. Вместо этого, он проверяет на идентичность прототипу, и его легко обмануть.
Например, он не будет работать в разных контекстах выполнения (распространённый источник ошибок и ненужных ограничений). Для справки, пример с библиотекой bacon.js. Поскольку это проверка идентичности свойства prototype целевого объекта, это может приводить к неочевидным вещам, например:
1 2 3 4 5 |
function foo() {} let bar = { a: "a" }; foo.prototype = bar; // Object {a: "a"} let baz = Object.create(bar); // Object {a: "a"} baz instanceof foo; // true (???) |
Этот результат полностью соответствует спецификации JavaScript: просто instanceof не может дать никаких гарантий относительно безопасности типов.
Свойство prototype
Наследуемые свойства и методы объекта, созданного с помощью конструктора, могут быть определены в свойстве конструктора prototype.
Значение свойства prototype - это объект, который в основном представляет собой контейнер для хранения свойств и методов, которые мы хотим наследовать объектами, расположенными дальше по цепочке прототипов.
Таким образом, например, Object.prototype.valueOf() и т. п. доступны для любых типов объектов, которые наследуются от Object.prototype, включая новые экземпляры объектов, созданные из конструктора.
Object.is(), Object.keys() и другие члены, не определённые в контейнере prototype, не наследуются экземплярами объектов или типами объектов, которые наследуются от Object.prototype: это методы (свойства), доступные только в конструкторе Object().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// функция-конструктор function User(name = "noname", age = 0) { (this.name = name), (this.age = age); } let newUser = new User("Alex", 30); console.log(User.prototype); // {constructor: ƒ} console.log(newUser.valueOf()); // {constructor: ƒ} console.log(newUser.is()); // TypeError: newUser.is is not a function console.log(Object.is()); // true |
Различие между прототипом объекта и свойством prototype функции-конструктора:
- прототип объекта является свойством каждого экземпляра (и доступен через Object.getPrototypeOf(obj) или через устаревшее свойство __proto__);
- свойство prototype в функциях-конструкторах является свойством конструктора (т.е. Object.getPrototypeOf(new Foobar()) относится к тому же объекту, что и Foobar.prototype).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// функция-конструктор function User(name = "noname", age = 0) { (this.name = name), (this.age = age); } let newUser = new User("Alex", 30); console.log(newUser); // User {name: 'Alex', age: 30} console.log(User == newUser); // false console.log(newUser.prototype); // undefined console.log(newUser.__proto__); // {constructor: ƒ} console.log(Object.getPrototypeOf(newUser)); // {constructor: ƒ} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
let User = { name: "Alex", age: 50, car: "Nissan", getThis() { return "Результат метода объекта User"; }, }; console.log(User.prototype); // undefined console.log(Object.prototype); // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, …} console.log(User.__proto__); // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, …} // создадим новый экземпляр объекта User let newUser = Object.create(User); console.log(User == newUser); // false console.log(newUser); // {} console.log(newUser.prototype); // undefined console.log(newUser.__proto__); // {name: "Alex", age: 50, car: "Nissan", getThis: ƒ} console.log(Object.getPrototypeOf(newUser)); // {name: "Alex", age: 50, car: "Nissan", getThis: ƒ} // свойство prototype функции-конструктора let string = new String("Это строка"); console.log(string); // String {"Это строка"} console.log(string.prototype); // undefined console.log(String.prototype); // String {"", constructor: ƒ, anchor: ƒ, big: ƒ, blink: ƒ, …} console.log(string.__proto__); // String {"", constructor: ƒ, anchor: ƒ, big: ƒ, blink: ƒ, …} |
Изменение прототипа [[Prototype]] объекта является очень медленной операцией, это справедливо для любого браузера и движка JavaScript. Изменение прототипов может распространяться на любой код, который имеет доступ к любому объекту, чей прототип [[Prototype]] был изменён. Если вы заботитесь о производительности, вы никогда не должны изменять прототип [[Prototype]] объекта. Вместо этого создайте объект с нужным прототипом [[Prototype]] с помощью метода Object.create().
Метод Object.getPrototypeOf() возвращает прототип (то есть внутреннее свойство [[Prototype]]) указанного объекта.
Синтаксис метода Object.getPrototypeOf():
1 |
Object.getPrototypeOf(obj) |
Параметры метода Object.getPrototypeOf():
obj - объект, прототип которого будет возвращён.
1 2 3 4 |
const car = { brand: "Nissan" }; let newCar = Object.create(car); Object.getPrototypeOf(newCar); // {brand: "Nissan"} |
Метод Object.setPrototypeOf() изменяет прототип (внутреннее свойство [[Prototype]]) указанного объекта на другой объект или на null.
Синтаксис метода Object.setPrototypeOf():
1 |
Object.setPrototypeOf(obj, prototype); |
Параметры метода Object.setPrototypeOf():
obj - объект, которому устанавливается новый прототип prototype.
Возвращаемое значение: новый прототип объекта (объект или null).
1 2 3 4 5 6 7 8 9 |
const car = { brand: "Nissan" }; const airplane = { brand: "Su" }; let newCar = Object.create(car); Object.getPrototypeOf(newCar); // {brand: "Nissan"} Object.setPrototypeOf(newCar, airplane); // установим новый прототип Object.getPrototypeOf(newCar); // {brand: "Su"} |
ВАЖНО! Изменение прототипа [[Prototype]] объекта является очень медленной операцией, это справедливо для любого браузера и движка JavaScript. Изменение прототипов может распространяться на любой код, который имеет доступ к любому объекту, чей прототип [[Prototype]] был изменён. Если вы заботитесь о производительности, вы никогда не должны изменять прототип [[Prototype]] объекта. Вместо этого создайте объект с нужным прототипом [[Prototype]] с помощью метода Object.create().
Свойство constructor
Каждая функция-конструктор имеет свойство prototype, значением которого является объект, содержащий свойство constructor и указывающий на исходную функцию-конструктор. Cвойства, определённые в свойстве obj.prototype становятся доступными для всех объектов экземпляра, созданных с помощью функции-конструктора. Конструктор - это функция, поэтому её можно вызвать с помощью круглых скобок; вам просто нужно включить ключевое слово new, чтобы указать, что вы хотите использовать эту функцию в качестве конструктора.
Для создания другого экземпляра объекта из конструктора необходимо поместить круглые скобки, содержащие любые требуемые параметры, в конец свойства constructor().
1 2 3 4 5 6 7 8 9 10 |
// функция-конструктор function User(name = "noname", age = 0) { (this.name = name), (this.age = age); } let newUser = new User("Alex", 30); let newestUser = new newUser.constructor("Serg", 20); console.log(newestUser); // User {name: 'Serg', age: 20} |
Свойство constructor позволяет также, например, вернуть имя конструктора экземпляра объекта:
1 |
console.log(newUser.constructor.name); // User |
Значение constructor.name может измениться (из-за прототипического наследования, привязки, препроцессоров, транспилеров и т. д.), Поэтому для более сложных примеров можно использовать оператор instanceof, который проверяет, принадлежит ли объект к определённому классу (присутствует ли объект constructor.prototype в цепочке прототипов object).
Работа ключевого слова new
- создаёт новый экземпляр класса;
- связывает this с новым экземпляром класса;
- создаёт ссылку нового объекта [[Prototype]] на объект, на который ссылается свойство функции конструктора prototype;
- создаёт ссылку нового свойства constructor объекта на конструктор, который был вызван;
- именует тип объекта после конструктора, который вы сможете заметить в консоли отладки (например, [Object Foo] вместо [Object object]);
- позволяет оператору instanceof проверить, является ли ссылка на прототип объекта тем же самым объектом, на который ссылается свойство prootype конструктора.
Изменение прототипов
Методы, добавленные в прототип, становятся доступны для всех экземпляров объектов, созданных конструктором:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function User(name = "noname", age = 0) { (this.name = name), (this.age = age); } // добавим свойство, которое хотим передать объектам-наследникам User.prototype.car = "nissan"; let newUser = new User("Alex", 30); console.log(newUser); // User { // age: 30 // name: "Alex" // __proto__: // car: "nissan" // унаследованное свойство, добавленное вне кода функции-конструктора // constructor: ƒ User(name = "noname", age = 0) // __proto__: Object // } |
Особенности определения свойств и методов в prototype
Свойства, определённые в prototype, не очень гибки при таком определении. Например, вы можете добавить свойство следующим образом:
1 |
User.prototype.fullName = 'Alex NAV'; |
Однако, было бы намного лучше создать fullName из name.first и name.last:
1 |
User.prototype.fullName = this.name.first + ' ' + this.name.last; |
Однако это не работает, поскольку в этом случае this будет ссылаться на глобальную область, а не на область функции. Вызов этого свойства вернёт undefined undefined. Таким образом, внутри конструктора целесообразно определять постоянные свойства прототипа (т. е. те, которые никогда не нуждаются в изменении).
Довольно распространённый шаблон, упрощающий чтение кода - это:
- определение свойств внутри конструктора (конструктор содержит только определения свойств) и
- определение методов в прототипе (методы вынесены в отдельные блоки и не хранятся в каждом экземпляре класса; обращение к ним происходит по цепочке прототипирования).
Например (отсюда):
1 2 3 4 5 6 7 8 9 10 11 12 |
// Определение конструктора и его свойств function Test(a, b, c, d) { // определение свойств... } // Определение первого метода Test.prototype.x = function() { ... }; // Определение второго метода Test.prototype.y = function() { ... }; //... и так далее |