Введение ⇡
В этой лекции мы познакомимся с таким понятием как цепочка прототипов. Это мощный инструмент, используемый в JavaScript и позволяющий строить сложные программы.
Рассмотрим пример. Допустим, у нас есть необходимость хранить сущности студентов и лекторов.
Мы выделим сущность студента:
var student = {
age: 20,
name: 'John',
sleep: function () {},
getAge: function () {},
getName: function () {}
}
А также сущность лектора:
var lecturer = {
age: 33,
name: 'Iwan',
talk: function () {},
getAge: function () {},
getName: function () {}
}
У этих сущностей очень много общего, например, свойства getAge
, getName
. Но также есть и специфичные: sleep
у студента и talk
у лектора.
Обычно для выделения таких абстракций используются классы и их наследование друг от друга. Это отлично, за исключением одного маленького но. В JavaScript нет такого понятия, как классы. В нашем распоряжении только объекты и функции, которые тоже объекты.
Решить же эту задачу нам позволяет механизм цепочки прототипов. Цепочка прототипов позволяет делегировать свойства одного объекта другому. Мы выделяем абстракции, а потом делегируем свойства от частного объекта более общему.
Лекция будет состоять из двух основных блоков. Вначале мы познакомимся с тем, где нам могут встретиться цепочки прототипов и как работает этот механизм. Во второй части мы научимся строить эти цепочки, а также выделять абстракции, вынося в них общий повторяющийся код и общую функциональность.
Получение свойства объекта ⇡
Впервые, где мы сталкиваемся с работой прототипов, – это при обращении к свойствам или методам объекта. Рассмотрим простой пример:
var student = { name: 'John' };
console.log(student.name);
Здесь мы хотим получить имя пользователя.
Когда мы обращаемся к свойству объекта, вызывается внутренний метод [[Get]]
, отвечающий за поиск и возврат значения.
В простом случае метод находит свойство на объекте и возвращает значение данного свойства. Например, в нашем примере у объекта student
есть свойство name
, поэтому вернется его значение и результат будет John
.
Если же свойства нет, поиск на этом не останавливается. Тут-то на сцену и выходит цепочка прототипов.
[[Prototype]]
⇡
У каждого объекта в JavaScript есть внутренее свойство [[Prototype]]
, которое обычно указывает на другой объект или равно null
. Это скрытое свойство и прямого доступа к нему нет.
var student = { age: 20 };
// student.[[Prototype]] -> Object.prototype
Как мы видим, по умолчанию при создании объекта свойство [[Prototype]]
ссылается на объект Object.prototype
, у которого в свою очередь тоже есть скрытое свойство [[Prototype]]
, равное null
.
var student = { age: 20 };
// student.[[Prototype]] -> Object.prototype
// Object.prototype.[[Prototype]] -> null
Чтобы было проще, приму следующее обозначение. Если свойство [[Prototype]]
объекта ссылается на другой объект, то я буду это показывать стрелкой с двумя дефисами:
var student = { age: 20 }; // student --> Object.prototype --> null
Есть несколько способов получить доступ к этому объекту.
__proto__
⇡
Ранее в спецификации ES3 не было описано прямого способа достучаться до этого свойства, поэтому некоторые браузеры изобрели не документированное свойство __proto__
. Пользоваться им не рекомендуется, так как это свойство не поддерживается спецификацией и может отсутствовать в какой-нибудь среде.
Тем не менее, для отладки в консоли оно вполне подойдет.
var student = { age: 20 }; // student --> Object.prototype --> null
console.log(student.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
console.log(student.__proto__.__proto__); // null
Этот метод также является сеттером:
var person = { type: 'none' }; // person --> Object.prototype --> null
var student = { age: 20 }; // student --> Object.prototype --> null
student.__proto__ = person; // student --> person --> Object.prototype --> null
console.log(student.__proto__ === person); // true
Object.getPrototypeOf
⇡
В спецификации ES5 появился метод Object.getPrototypeOf
для получения прототипа объекта:
var student = { age: 20 }; // student --> Object.prototype --> null
console.log(Object.getPrototypeOf(student) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
Object.setPrototypeOf
⇡
А вот сеттер прототипа для объекта появился только в ES6, поэтому доступен не везде и пользоваться им не рекомендуется. Однако, пока мы не познакомились с другими и более распротраненными способами управлять цепочкой прототипов, я буду использовать данный метод в примерах.
var person = { type: 'none' }; // person --> Object.prototype --> null
var student = { age: 20 }; // student --> Object.prototype --> null
Object.setPrototypeOf(student, person); // student --> person --> Object.prototype --> null
console.log(Object.getPrototypeOf(student) === person); // true
Получение свойства из цепочки ⇡
Итак, вернемся к методу [[Get]]
и получению свойства объекта. Простой случай, когда свойство существует у объекта, мы уже рассмотрели. Теперь обратим внимание на случай, когда свойства нет.
Как я уже сказал, поиск не останавливается. И, наверно, раз уж мы столько времени уделили свойству [[Prototype]]
, как-то связан с ним.
Так и есть. Когда метод [[Get]]
не находит свойства на объекте, то переходит к объекту, на который ссылается свойство [[Prototype]]
, и повторяет поиск. Проверяется, есть ли свойство на объекте (уже другом!), если есть – возвращается его значение, если нет – происходит переход к новому объекту через свойство [[Prototype]]
.
Процесс этот завершается либо когда свойство находится на каком-нибудь объекте, либо когда очередное свойство [[Prototype]]
указывает на null
. Если свойство не находится, то есть доходим до конца, метод [[Get]]
возвращает undefined
Такая цепочка из ссылок [[Prototype]]
называется цепочкой прототипов.
Примеры:
var person = { age: 0, type: 'none' }; // person --> Object.prototype --> null
var student = { age: 20 }; // student --> Object.prototype --> null
Object.setPrototypeOf(student, person); // student --> person --> Object.prototype --> null
console.log(student.age); // 20 (свойство найдено на самом объекте student)
console.log(student.type); // "none" (свойство через цепочку объекте person)
console.log(student.some); // undefined (свойство не найдено ни на объекте, ни в цепочке)
Object.prototype
⇡
Верхняя граница цепочки прототипов обычно объект Object.prototype
. Свойство [[Prototype]]
этого объекта указывает на null
.
Объект Object.prototype
имеет множество полезных методов: .toString()
, .valueOf()
, .hasOwnProperty(..)
, .isPrototypeOf(..)
. Если цепочка прототипов объекта содержит Object.prototype
(а в большинстве случаев это именно так), то все эти методы доступны для объекта.
var student = { age: 20 }; // student --> Object.prototype --> null
var students = []; // students --> Array.prototype --> Object.prototype --> null
console.log(student.hasOwnProperty('age')); // true
console.log(students.hasOwnProperty('length')); // true
В этом примере ни у объекта student
, ни у students
нет метода hasOwnProperty
. Но они им доступны за счет того, что у них в цепочке есть объект Object.prototype
. Это очень удобно!
Итерация по объекту, проверка свойства, enumerable ⇡
С правилами получения свойств объекта связаны такие операции, как проверка свойства через оператор in
или итерация по ключаем объекта через цикл for..in
Оператор in
⇡
Для проверки наличия свойства у объекта используется оператор in
. Данный оператор проверяет наличие свойства не только на самом объекте, но и в его цепочке прототипов.
var person = { type: 'none' }; // person --> Object.prototype --> null
var student = { age: 20 }; // student --> Object.prototype --> null
Object.setPrototypeOf(student, person); // student --> person --> Object.prototype --> null
console.log('age' in student); // true (из student)
console.log('type' in student); // true (из person)
console.log('hasOwnProperty' in student); // true (из Object.prototype)
Object.prototype.hasOwnProperty()
⇡
Как же проверить наличие свойства именно на самом объекте? Для этого служит функция Object.prototype.hasOwnProperty()
. Она доступна объектам через цепочку прототипов.
var person = { type: 'none' }; // person --> Object.prototype --> null
var student = { age: 20 }; // student --> Object.prototype --> null
Object.setPrototypeOf(student, person); // student --> person --> Object.prototype --> null
console.log(student.hasOwnProperty('age')); // true
console.log(student.hasOwnProperty('type')); // false
console.log(student.hasOwnProperty('hasOwnProperty')); // false
Цикл for..in
⇡
Чтобы получить все свойства и методы объекта, используется for..in
.
var person = { type: 'none' }; // person --> Object.prototype --> null
var student = { name: 'John', age: 20 }; // student --> Object.prototype --> null
Object.setPrototypeOf(student, person); // student --> person --> Object.prototype --> null
for(var key in student) {
console.log(key); // name, age, type
}
Как мы видим из примера, данный цикл также проходится по всей цепочке прототипов. Но почему же в выводе нет свойств из объекта Object.prototype
? Ответ прост. Да, цикл for..in
проходится по всей цепочке, но итерируется только по перечисляемым свойствам. Помните из лекции про объекты? Это те свойства, которые обладают свойством enumerable
.
var student = { name: 'John', age: 20 }; // student --> Object.prototype --> null
// Объявляем неперечисляемое свойство
Object.defineProperty(student, 'type', {
enumerable: false,
value: 'admin'
});
for(var key in student) {
console.log(key); // name, age
}
Свойство type
не вывелось, так как оно неперечисляемое.
Итерация по собственным ключам ⇡
Если смиксовать цикл for..in
и метод Object.prototype.hasOwnProperty()
, то можно добиться итерации только по собственным свойствам объекта:
var person = { type: 'none' }; // person --> Object.prototype --> null
var student = { name: 'John', age: 20 }; // student --> Object.prototype --> null
Object.setPrototypeOf(student, person); // student --> person --> Object.prototype --> null
for(var key in student) {
if (student.hasOwnProperty(key)) {
console.log(key); // name, age
}
}
Для облегчения этой задачи в ES5 появился метод Object.keys
:
var person = { type: 'none' }; // person --> Object.prototype --> null
var student = { name: 'John', age: 20 }; // student --> Object.prototype --> null
Object.setPrototypeOf(student, person); // student --> person --> Object.prototype --> null
console.log(Object.keys(student)) // ["name", "age"]
Object.keys(student).forEach(function (key) {
console.log(key); // name, age
});
Установка свойства объекта, затенение свойств ⇡
Как работает механизм получения свойства, мы разобрались. Теперь разберемся с тем, как происходит установка значения. Присваиванием свойства занимается внутренний метод [[PUT]]
.
var student = { age: 20 };
student.age = 21;
console.log(student); // {age: 21}
В простом случае метод [[PUT]]
находит свойство на объекте и устанавливает новое значение. Конечно, если это свойство не является read-only
(writable: false
).
var student = { name: 'John', age: 20 }; // person --> Object.prototype --> null
// Объявляем свойство read-only
Object.defineProperty(student, 'type', {
writable: false,
value: 'none'
});
student.type = 'admin';
console.log(student); // {name: "John", age: 20, type: "none"}
Как видим в примере, свойство type
не изменилось.
В строгом же режиме при присваивании значения read-only
свойству выбросится исключение:
TypeError: Cannot assign to read only property 'type' of #<Object>
Если же свойства нет ни у объекта, ни в цепочке прототипов, то это свойство просто создается на объекте:
var student = { age: 20 }; // student --> Object.prototype --> null
student.name = 'John';
console.log(student); // {age: 20, name: "John"}
Затенение свойств ⇡
Мы уже встречались с термином затенения. Сейчас познакомимся с ним в контексте цепочек прототипов. Рассмотрим пример, когда свйоство есть одновременно и у объекта, и в цепочке прототипов.
var person = { age: 0, type: 'none' }; // person --> Object.prototype --> null
var student = { age: 20 }; // student --> Object.prototype --> null
Object.setPrototypeOf(student, person); // student --> person --> Object.prototype --> null
console.log(student.age); // 20
В примере выше результатом будет 20
. Это произошло, потому что метод [[GET]]
возвращает первое найденое свойство. Получается, что свойство age
объекта student
собой заслоняет такое же свойство объекта person
из цепочки прототипов. Это явление обычно называется затенением. Говорят, что свойство student.age
затеняет свойство person.age
.
Установка нового значения для свойства из цепочки ⇡
Вернемся к установке свойств. Мы рассмотрели два простых варианта, а именно: когда свойства не существует совсем и когда оно есть на объекте. Теперь рассмотрим более сложный случай, когда свойство есть в цепочке прототипов. Возникает три возможных сценария.
Свойство имеет стандартный сеттер и не помечено как read-only
.
В этом случае создастся затеняющее свойство на объекте.
var person = { age: 0, type: 'none' }; // person --> Object.prototype --> null
var student = {}; // student --> Object.prototype --> null
Object.setPrototypeOf(student, person); // student --> person --> Object.prototype --> null
console.log(student.age); // 0 (свойство объекта person, полученное по цепочке)
student.age = 20;
console.log(student.age); // 20 (свойство объекта student)
console.log(person); // {age: 0, type: "none"} (объект person не изменился)
console.log(student); // {age: 20} (у student создалось затеняющее свойство)
Свойство помечено как read-only
Поведение будет аналогичным тому, которое мы рассматривали c read-only
свойством на объекте. В строгом режиме выбросится исключение, а в нестрогом просто ничего не произойдет.
var person = { type: 'none' }; // person --> Object.prototype --> null
var student = {}; // student --> Object.prototype --> null
Object.setPrototypeOf(student, person); // student --> person --> Object.prototype --> null
// Объявляем свойство read-only
Object.defineProperty(person, '__name__', {
writable: false,
value: 'person'
});
student.__name__ = 'non-person'; // В cтрогом режиме исключение TypeError
console.log(student); // {}
console.log(student.__name__); // "person"
В примере мы попытались изменить свойство __name__
. Но у нас ничего не вышло: на объекте никаких свойств не появилось и объект в прототипе не изменился.
Свойство имеет кастомный сеттер В этом случае вызовется кастомный сеттер, а свойства на объекте не создастся.
var person = { type: 'none' }; // person --> Object.prototype --> null
var student = {}; // student --> Object.prototype --> null
Object.setPrototypeOf(student, person); // student --> person --> Object.prototype --> null
// Объявляем кастомный сеттер
(function () {
var NAME = 'person';
Object.defineProperty(person, '__name__', {
get: function() { return NAME; },
set: function(newName) { NAME = newName }
});
}());
student.__name__ = 'student';
console.log(student); // {}
console.log(person.__name__); // student
В примере выше мы попытались установить новое значение в свойство __name__
. Вызвался наш кастомный сеттер, который исправил свойство __name__
у объекта person
. Свойства же на объекте student
не создалось.