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