Страницы

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

понедельник, 25 ноября 2019 г.

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


Добрый День. Изучаю способы организации наследования в 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 t
be linked to. It just makes Bar.prototype be another reference to Foo.prototype, whic
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. Помогите разобраться пожалуйста.
    


Ответы

Ответ 1



Давайте начнем с отвлеченного примера: 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 modifyin 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 коде), полноценная реализация наследования должна его выставлять.

Ответ 2



Если вы используете ES6, то можно использовать для наследования стандартные средства языка: // Базовый класс class Foo { doSomething() { // ... } } // Дочерний класс, наследующий все поведение базового + // методы определенные ниже. class Bar extends Foo { doAnotherAction() { // ... } }

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

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