Ранее у нас были
занятия по JavaScript стандарта ES6+ где, помимо
всего прочего, мы говорили об объектах. И отмечали, что если нам нужно описать
некую сущность: назначить ей разные свойства и поведение (методы), то это можно
сделать так:
"use strict";
let geom = {
name: "фигура",
sp: {x: 0, y: 0},
ep: {x: 100, y: 20}
};
Также мы
говорили, что для перебора всех свойств и методов объекта используется цикл for in:
for(let key in geom) {
console.log(key+": "+geom[key]);
}
А дальше сделаем
следующий, новый для нас ход: объявим новый объект, который бы расширял
функционал уже существующего geom. Это можно сделать с помощью
специального свойства
__proto__
которое есть у
каждого объекта JavaScript. Именно через него устанавливается
связь с базовым (расширяемым) объектом. И в нашем случае это будет выглядеть
так:
let rect = {
draw() {
console.log("Рисование прямоугольника: " +
this.sp.x+","+this.sp.y+","+this.ep.x+","+this.ep.y);
}
};
rect.__proto__ = geom;
В результате,
объект rect получает доступ
ко всем свойствам объекта geom и дополнительно объявляет метод draw. Это мы увидим
при следующем переборе:
for(let key in rect) {
console.log(key+": "+rect[key]);
}
А, вот если
закомментировать строку:
то останется
только один метод draw. Давайте уберем комментарий и выполним метод:
Увидим в консоли
сообщение:
Рисование
прямоугольника: 0,0,100,20
Здесь следует
отметить, что спецификация языка JavaScript предполагает существование
свойства __proto__ только в
браузерной среде. В других средах – это необязательное свойство но, тем не
менее, оно существует во всех популярных средах, в том числе, и серверных,
таких как Node.js.
Возвращаясь к
нашим объектам, можно сказать, что в них реализуется прототипный механизм
наследования. И эту цепочку можно продолжить, например, добавив вот такой
объект:
let info = {
getInfo() {
console.log(this.name);
},
__proto__: rect
}
info.getInfo()
Теперь у нас info наследуется от rect, а rect – от geom:
Разумеется,
когда мы используем свойство __proto__, то ему можно присваивать либо другой
объект, либо значение null. Все остальные типы будут попросту
игнорироваться. Также из этой схемы видно, что множественное наследование
реализовать не получится, т.к. свойство __proto__ ссылается
лишь на один определенный объект (либо ни на одного при значении null).
Далее, из объекта,
указанного в прототипе можно выполнять только чтение свойств и методов.
Например, если в объекте rect создать свойство name:
rect.name = "прямоугольник";
то это никак не
скажется на свойстве объекта geom:
console.log(geom.name) // фигура
То есть,
алгоритм такой: сначала происходит поиск атрибутов внутри текущего объекта и
если они не обнаруживаются, то поиск переходит к объекту, указанному в свойстве
__proto__.
Свойства-аксессоры set и get
В любом объекте
можно создавать специальные свойства, которые будут срабатывать в момент
присвоения (set) и чтения (get) данных из
объекта. Они определяются по следующему синтаксису:
-
set <имя метода>([параметры])
{…} – свойство для записи значений в объект;
-
get <имя метода>([параметры])
{…} – свойство для чтения значений из объекта.
Давайте в
качестве примера в объекте geom пропишем два таких свойства:
let geom = {
name: "фигура",
sp: {x: 0, y: 0},
ep: {x: 100, y: 20},
get nameGeom() {return this.name; },
set nameGeom(name) {this.name = name; },
};
И, далее, можно
с ними работать, следующим образом:
console.log(rect.nameGeom); // чтение свойства
rect.nameGeom = "Прямоугольник"; // запись свойства
console.log(rect.nameGeom);
Причем,
благодаря наличию свойств-аксессоров, при наследовании объекта в rect не создается
новое свойство nameGeom, когда ему присваиваются какие-либо
данные, так как JavaScript «понимает», что имеется сеттер с именем
nameGeom в базовом
объекте и именно его следует использовать в такой ситуации.
Поведение this при прототипном наследовании
Следующий момент
в этом примере, на который, возможно, кто-то из вас уже обратил внимание: на
какой из двух объектов (geom или rect) ссылается this при вызове
сеттера:
rect.nameGeom = "Прямоугольник";
Как мы с вами
говорили на предыдущих занятиях по JavaScript, указатель this является
динамическим и определяется контекстом вызова метода. В данном случае контекст
– это объект rect и именно на
него ссылается this при вызове сеттера из базового объекта geom. В результате,
состояние базового объекта не меняется, а в rect добавляется
свойство name:
И в этом легко
убедиться, если выполнить следующие строчки:
console.log(rect.nameGeom);
console.log(geom.nameGeom);
Как видите, для
объекта geom отображается имя
«фигура», тогда как для rect – имя
«Прямоугольник». Или, эту проверку можно визуализировать еще лучше, если
воспользоваться специальным методом:
obj.hasOwnProperty(key)
который
возвращает true, если ключ (key) принадлежит
объекту obj и false – в противном
случае. В нашем случае можно записать такую конструкцию:
for(let prop in rect)
if( rect.hasOwnProperty(prop) )
console.log(prop + ": " + rect[prop]);
На выходе
увидим:
draw: function () …
name: Прямоугольник
То есть,
свойство name действительно
было создано в дочернем объекте rect.
Но здесь есть
один тонкий момент. Если проделать ту же операцию с объектами, а не
примитивными типами данных, например с sp и ep:
let geom = {
name: "фигура",
sp: {x: 0, y: 0},
ep: {x: 100, y: 20},
get nameGeom() {return this.name; },
set nameGeom(name) {this.name = name; },
get coords() {
return [this.sp.x, this.sp.y, this.ep.x, this.ep.y];
},
set coords(coords) {
this.sp.x = coords[0]; this.sp.y = coords[1];
this.ep.x = coords[2]; this.ep.y = coords[3];
}
};
то операция:
изменит
состояние базового объекта geom, оставив дочерний без изменений:
console.log(rect.coords);
console.log(geom.coords);
Дело в том, что
мы здесь обращаемся сначала к свойству sp, которое определено
в базовом объекте (и мы автоматически переходим к нему), а затем уже, меняем
его свойства x и y. То есть, мы не
перезаписываем объект целиком, а работаем с его отдельными свойствами. А если
переписать, то оно добавится в дочернем объекте:
set coords(coords) {
this.sp = coords[0]; this.sp = coords[1];
this.ep = coords[2]; this.ep = coords[3];
}
Вот на это
следует обращать внимание.