Продолжаем
изучение наследования (расширения объектов) на JavaScript. Когда мы
только начинали изучение объектов, то отмечали, что их можно создавать двумя
способами, так:
или так:
В результате,
любой объект JavaScript содержит
множество предопределенных свойств и методов, которые описаны в объекте, на
который ссылается свойство prototype функции-конструктора Object.
В этом легко
убедиться, если вывести это свойство в консоль:
console.log(Object.prototype)
Что из этого
следует? Как мы с вами отмечали на предыдущем занятии, все эти свойства
базового объекта становятся доступными в каждом объекте JavaScript. Например,
можно выполнить такую команду:
console.log(obj.toString())
которая не
приведет к ошибке, т.к. метод toString объявлен в
базовом объекте. А, учитывая, что в JavaScript объектами
являются массивы и функции:
Array, Function
то все они могут
оперировать методами базового объекта или, переопределять эти методы в своем
классе, как это делает объект Array:
let obj = Array(1,2,3);
console.log( obj.toString() )
который выводит
в консоль строку со списком значений элементов массива.
Поведение примитивных типов данных
В JavaScript строки, числа и
булевые переменные:
String, Number, Boolean
относятся к примитивным
типам и, строго говоря, не являются объектами. Однако, для них также можно
вызывать методы базового объекта, например:
console.log("Hello World".toString());
Конечно, эта
запись не имеет особого смысла, т.к. она просто возвратит ту же самую строку,
но она показывает, что строка, литерал, здесь превращается в объект. Как такое
может быть? В действительности, все очень просто. Если виртуальная машина JavaScript «видит», что к
литералу идет обращение как к объекту, то она временно создает объект
соответствующего типа, для этого объекта вызывает указанный метод и, затем,
временный объект уничтожается. В частности, строка обертывается в объект типа String и мы можем для
литерала вызвать, например, метод toUpperCase:
console.log("Hello World".toUpperCase());
Раз это так, то
для любого такого объекта, в нашем случае строки, можно менять и добавлять
новые свойства и методы, используя объект prototype. Как мы
отмечали на предыдущем занятии, если добавить в prototype свойство (или
переопределить существующее), то новый созданный объект будет его содержать.
Например, мы легко можем переопределить метод toUpperCase:
String.prototype.toUpperCase = function() {
return this;
}
В результате, он
попросту вернет исходную строку. Или, можно добавить новый метод:
String.prototype.len = function() {
return this.length;
}
console.log("Hello World".len());
И так далее.
Однако, этим функционалом следует пользоваться с большой осмотрительностью,
т.к. если в разных программных модулях будут переопределяться одинаковые
методы, но с разной логикой работы, то в основной программе работать будет тот,
что импортируется в последнюю очередь. Это может привести к непредвиденным
ошибкам. В современном программировании есть только один случай, в котором
одобряется изменение встроенных прототипов. Это создание полифилов.
Полифил – это эмуляция метода, который существует в спецификации JavaScript, но ещё не
поддерживается текущим движком JavaScript.
Во всех
остальных случаях лучше не прибегать к этому механизму.
Методы create, getPrototypeOf и setPrototypeOf
На сегодняшний
день свойство __proto__ считается устаревшим и формально
поддерживается только в браузерной среде. Хотя, почти все остальные среды по-прежнему
позволяют им пользоваться. Ему на смену пришли новые методы объекта Object:
-
Object.create(proto,
[descriptors]) – используется для создания нового объекта с указанием базового
(proto) и
необязательным набором дополнительных дескрипторов свойств – descriptors;
-
Object.getPrototypeOf(obj)
– возвращает ссылку на базовый объект, либо null, если его нет;
-
Object.setPrototypeOf(obj,
proto) – назначает базовый объект proto для уже
существующего объекта obj.
Например,
используя объекты из предыдущего занятия:
let prop = {
sp: {x: 0, y: 0},
ep: {x: 100, y: 20},
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];
}
};
function Rect() {
this.name = "прямоугольник";
this.draw = function() {
console.log("Рисование фигуры: "+this.name);
}
this.__proto__ = prop;
}
Перепишем
объявление объекта Rect с использованием метода create:
let rect = Object.create(prop, {
name: {value: "прямоугольник", writable: true},
draw: {value: function() {
console.log("Рисование фигуры: "+this.name);
}
},
});
console.log( rect.coords );
rect.draw();
Чтобы получить
ссылку на базовый класс, можно воспользоваться методом getPrototypeOf:
console.log( Object.getPrototypeOf(rect) === prop );
Наконец, для
замены базового объекта на другой, выполним метод setPrototypeOf:
Object.setPrototypeOf( rect, {} );
Мы здесь указали
пустой объект вместо прежнего prop и теперь свойство coords возвращает
значение undefined.
Способ клонирования объекта
Часто в скриптах
описанные методы используют для клонирования объекта с сохранением ссылки на
базовый объект:
let clone = Object.create(Object.getPrototypeOf(rect),
Object.getOwnPropertyDescriptors(rect));
И если,
например, поменять свойство name в клоне:
то это никак не
отразится на объекте rect:
console.log( rect.name );
console.log( clone.name );
Однако,
свойства-объекты ведут себя иначе и при изменении состояния базового объекта:
clone.coords = [1,2,3,4];
console.log( rect.coords );
console.log( clone.coords );
Увидим изменение
и у второго объекта rect. И это, в общем-то логично. Как мы с вами отмечали
на предыдущем занятии, свойства, которые ссылаются на объекты, а не примитивные
типы данных, изменяются непосредственно в базовом классе. А, так как оба
дочерних объекта наследуются от одного и того же базового, то и получаем такой
результат.