Страницы

Поиск по вопросам

вторник, 2 октября 2018 г.

Прототипное наследование

Добрый День. Изучаю способы организации наследования в JavaScript и написал небольшой пример :
function Foo(name) { this.name = name; }
Foo.prototype.myName = function() { return this.name; };
function Bar(name, label) { Foo.call(this, name); this.label = label; }
Bar.prototype = Foo.prototype;
Bar.prototype.myLabel = function() { return this.label; };
var a = new Bar("a", "obj a");
a.myName(); a.myLabel();
Вопрос возник на строке :
Bar.prototype = Foo.prototype;
Пытаясь понять разницу между
Bar.prototype = new Foo()
и
Bar.prototype = Foo.prototype;
набрел на статью, в которой говориться
Bar.prototype = Foo.prototype doesn't create a new object for Bar.prototype to be linked to. It just makes Bar.prototype be another reference to Foo.prototype, which effectively links Bar directly to the same object as Foo links to: Foo.prototype. This means when you start assigning, like Bar.prototype.myLabel = ..., you're modifying not a separate object but the shared Foo.prototype object itself, which would affect any objects linked to Foo.prototype.
Вопрос заключается в последнем предложении. Почему при добавлении прототипу свойства Bar, мы автоматически меняем и прототип объекта Foo ? Если я правильно понял, то как раз при добавлении свойства или метода в объект Foo, должен измениться и объект Bar, т.к. он ссылается на прототип Foo. Помогите разобраться пожалуйста.


Ответ

Давайте начнем с отвлеченного примера:
var a = {test: 11} b = a;
b.test = 12; console.log(a.test); // Выведет 12!
Это происходит потому, что объекты в JS присваиваются и передаются по ссылке а не по значению.
Свойство .prototype - это объект. Когда вы выполняете код:
Bar.prototype = Foo.prototype;
вы присваиваете свойству Bar.prototype ссылку на объект Foo.prototype. Как следствие, любое изменение свойства Bar.prototype приводит к изменению Foo.prototype, о чем и говорится в приведнной цитате:
This means when you start assigning, like Bar.prototype.myLabel = ..., you're modifying not a separate object but the shared Foo.prototype object itself, which would affect any objects linked to Foo.prototype.

Небольшое лирическое отступление
Вообще говоря, я бы рекомендовал вам никогда не использовать конструкцию:
Bar.prototype = new Foo();
а всех тех, кто вам это советует -- смело отправляйте учить основы JS. Вся соль в том, что вызывая new Foo() вы вызываете конструктор объекта. При этом сам конструктор может с одной стороны накладывать ограничения на передаваемые аргументы, а с другой иметь побочные действия. Разберем каждый из этих случаев отдельно.
Предположим, у вас есть вот такой конструктор, накладывающий ограничения на свои аргументы:
Foo = function(a) { if (typeof a === 'undefined') { throw new Error('You have to set the first argument.'); }
this.a = a; }
В этом случае вы уже не можете просто взять и выполнить:
Bar.prototype = new Foo();
т.к. вам нужно в явном виде предать аргумент в конструктор, который полностью лишен смысла в момент описания иерархии наследования. Самое интересное, что значение параметра a все равно будет затерто при вызове конструктора Foo в дочернем конструкторе Bar. Поэтому конструкция new Foo() еще и лишена смысла.
Теперь предположим, что родительский конструктор имеет побочные эффекты:
Foo = function(a) { console.log('Here I am!'); }
При использовании:
Bar.prototype = new Foo();
и дальнейшем:
var Bar = function() { Foo.call(this); }
строка "Here I am!" будет выведена даважды. Согласитесь, это не всегда желаемое поведение системы.
Ну и еще один любопытный факт: даже если в сейчас родительский конструктор не имеет ни побочных эффектов ни ограничений на аргументы, это не значит, что он останется таким навсегда. Лучше уж сразу сделать все правильно, чем нервно отлаживать код в поисках ошибки, когда все сломается.

Приведу, для справки, правильную реализацию наследования в JS:
// Базовый конструктор var Foo = function() { // ... };
Foo.prototype.doSomething = function() { // ... };
// Дочерний конструктор var Bar = function() { // Вызываем базовый конструктор для текущего объекта. Foo.call(this); // ... };
// Устанавливаем правильное значение в цепочке прототипов. Bar.prototype = Object.create(Foo.prototype, { // Выставляем правильную функцию-конструктор для всех создаваемых // объектов. constructor: { value: Bar, enumerable: false, writable: true, configurable: true } });
// Расширяем прототип дочернего "класса". Этот шаг должен идти // СТРОГО ПОСЛЕ установки значения Bar.prototype. Bar.prototype.doAnotherAction = function() { // ... };
В случае, когда вы не можете использовать Object.create (старые барузеры) вы можете либо использовать один из существующих полифилов, либо сделать все ручками(через анонимный конструктор):
var inherits = function(ctor, superCtor) { // Временный конструктор, который не делает ничего и нужен // только для разрыва прямой связи между прототипами ctor // и superCtor. Его использование позволяет менять прототип // дочернего конструктора, не боясь сломать родительский. var Tmp = function() {}; Tmp.prototype = superCtor.prototype;
// Обратите внимание, вызов new Tmp() не имеет АБСОЛЮТНО // никаких побочных эффектов и не накладывает ограничений // на передаваемые значения. ctor.prototype = new Tmp(); // Выставляем правильную функцию-конструктор для всех // создаваемых объектов. ctor.prototype.constructor = ctor; };

С учетом всего выше сказанного универсальная функции наследования может иметь вид:
var inherits = (function() { if (typeof Object.create === 'function') { // Используем более простой вариант, если Object.create существует. return function(ctor, superCtor) { ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true } }); }; }
// Используем временный конструктор для старых браузеров return function(ctor, superCtor) { var Tmp = function() {}; Tmp.prototype = superCtor.prototype; ctor.prototype = new Tmp(); ctor.prototype.constructor = ctor; }; })();
UPD:
В реализациях выше, после присваивания прототипа, задается свойство Function.prototype.constructor. Хотя это свойство редко используется на практике (лично я ни разу не видел в production коде), полноценная реализация наследования должна его выставлять.

Комментариев нет:

Отправить комментарий