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

803
04 января 2017, 04:19

Добрый День. Изучаю способы организации наследования в 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. Помогите разобраться пожалуйста.

Answer 1

Давайте начнем с отвлеченного примера:

var a = {test: 11}
    b = a;
b.test = 12;
console.log(a.test); // Выведет 12!

Это происходит потому, что объекты в JS присваиваются и передаются по ссылке а не по значению.

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

Answer 2

Если вы используете ES6, то можно использовать для наследования стандартные средства языка:

// Базовый класс
class Foo {
    doSomething() {
        // ...
    }
}
// Дочерний класс, наследующий все поведение базового +
// методы определенные ниже.
class Bar extends Foo {
    doAnotherAction() {
        // ...
    }
}
READ ALSO
Javascript. Как передать аргументом в функцию css selector?

Javascript. Как передать аргументом в функцию css selector?

Напишем функцию changeCss, которая принимает селектор selector, свойство CSS property и значение для этого свойства value:

941
Как сделать, что бы при каждом ajax все функции запускались заново

Как сделать, что бы при каждом ajax все функции запускались заново

Как сделать, чтобы при срабатывание функции ajax, которая добавляет новые элементы на страницу, запускались функции из других скриптовСкрипты...

946
jQuery DataTables - добавить строку в таблицу

jQuery DataTables - добавить строку в таблицу

Дано: MySQL, Java - сервлеты, jQuery DataTables

1179