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

Drylozav

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

2 ответа

Drylozav

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

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


Drylozav

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

// Базовый класс
class Foo {
    doSomething() {
        // ...
    }
}

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

licensed under cc by-sa 3.0 with attribution.