Базовые свойства Object, методы create, getPrototypeOf и setPrototypeOf

Продолжаем изучение наследования (расширения объектов) на JavaScript. Когда мы только начинали изучение объектов, то отмечали, что их можно создавать двумя способами, так:

let obj = {};

или так:

let obj = new Object();

В результате, любой объект 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 в клоне:

clone.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. И это, в общем-то логично. Как мы с вами отмечали на предыдущем занятии, свойства, которые ссылаются на объекты, а не примитивные типы данных, изменяются непосредственно в базовом классе. А, так как оба дочерних объекта наследуются от одного и того же базового, то и получаем такой результат.