Наследование JavaScript и свойство конструктора

Рассмотрим следующий код.

function a() {}
function b() {}
function c() {}

b.prototype = new a();
c.prototype = new b();

console.log((new a()).constructor); //a()
console.log((new b()).constructor); //a()
console.log((new c()).constructor); //a()
  • Почему конструктор не обновляется для b и c?
  • Я делаю наследование неправильно?
  • Каков наилучший способ обновления конструктора?

Далее, пожалуйста, рассмотрите следующее.

console.log(new a() instanceof a); //true
console.log(new b() instanceof b); //true
console.log(new c() instanceof c); //true
  • Учитывая, что (new c()).constructor равно a(), а Object.getPrototypeOf(new c()) - a{ }, как можно instanceof знать, что new c() является экземпляром c?

http://jsfiddle.net/ezZr5/

Ответ 1

Хорошо, пусть играет в игру с ума:

Из приведенного выше изображения видно:

  • Когда мы создаем функцию типа function Foo() {}, JavaScript создает экземпляр Function.
  • Каждый экземпляр Function (функция-конструктор) имеет свойство prototype, которое является указателем.
  • Свойство prototype функции конструктора указывает на его объект-прототип.
  • Объект prototype имеет свойство constructor, которое также является указателем.
  • Свойство constructor объекта-прототипа указывает на его конструкторскую функцию.
  • Когда мы создаем новый экземпляр Foo, например new Foo(), JavaScript создает новый объект.
  • Внутреннее свойство [[proto]] экземпляра указывает на прототип конструктора.

Теперь возникает вопрос, почему JavaScript не привязывает свойство constructor к экземпляру объекта вместо прототипа. Рассмотрим:

function defclass(prototype) {
    var constructor = prototype.constructor;
    constructor.prototype = prototype;
    return constructor;
}

var Square = defclass({
    constructor: function (side) {
        this.side = side;
    },
    area: function () {
        return this.side * this.side;
    }
});

var square = new Square(10);

alert(square.area()); // 100

Как вы можете видеть, свойство constructor - это еще один метод прототипа, например area в приведенном выше примере. Что делает свойство constructor особенным, так это то, что он использовал для инициализации экземпляра прототипа. В противном случае это точно так же, как и любой другой метод прототипа.

Определение свойства constructor на прототипе выгодно по следующим причинам:

  • Это логически правильно. Например, рассмотрим Object.prototype. Свойство constructor Object.prototype указывает на Object. Если свойство constructor было определено в экземпляре, то Object.prototype.constructor будет undefined, поскольку Object.prototype является экземпляром null.
  • Он не отличался от других методов прототипа. Это облегчает работу с new, поскольку для каждого экземпляра не требуется определять свойство constructor.
  • Каждый экземпляр имеет те же свойства constructor. Следовательно, он эффективен.

Теперь, когда мы говорим о наследовании, мы имеем следующий сценарий:

Из приведенного выше изображения видно:

  • Свойство производного конструктора prototype задается экземпляром базового конструктора.
  • Следовательно, внутреннее свойство [[proto]] экземпляра производного конструктора указывает и на него.
  • Таким образом, свойство constructor производного экземпляра конструктора теперь указывает на базовый конструктор.

Что касается оператора instanceof, вопреки распространенному мнению, он не зависит от свойства constructor экземпляра. Как видно из вышеизложенного, это приведет к ошибочным результатам.

Оператор instanceof является двоичным оператором (он имеет два операнда). Он работает с экземпляром объекта и функцией конструктора. Как пояснить на Mozilla Developer Network, он просто выполняет следующие действия:

function instanceOf(object, constructor) {
    while (object != null) {
        if (object == constructor.prototype) { //object is instanceof constructor
            return true;
        } else if (typeof object == 'xml') { //workaround for XML objects
            return constructor.prototype == XML.prototype;
        }
        object = object.__proto__; //traverse the prototype chain
    }
    return false; //object is not instanceof constructor
}

Проще говоря, если Foo наследует от Bar, то цепочка прототипов для экземпляра Foo будет:

  • foo.__proto__ === Foo.prototype
  • foo.__proto__.__proto__ === Bar.prototype
  • foo.__proto__.__proto__.__proto__ === Object.prototype
  • foo.__proto__.__proto__.__proto__.__proto__ === null

Как вы можете видеть, каждый объект наследуется от конструктора Object. Цепочка прототипа заканчивается, когда внутреннее свойство [[proto]] указывает на null.

Функция instanceof просто проходит цепочку прототипов объекта экземпляра (первый операнд) и сравнивает внутреннее свойство [[proto]] каждого объекта с свойством prototype функции конструктора (второй операнд). Если они совпадают, он возвращает true; и если цепь прототипа заканчивается, она возвращает false.

Ответ 2

По умолчанию

function b() {}

то b.prototype имеет свойство .constructor, которое автоматически устанавливается на b. Однако вы в настоящее время перезаписываете прототип и, таким образом, отбрасываете эту переменную:

b.prototype = new a;

Тогда b.prototype больше не имеет свойства .constructor; она была стерта с помощью перезаписи. Он наследует от a, хотя и (new a).constructor === a, и, следовательно, (new b).constructor === a (это относится к тому же свойству в цепочке прототипов).

Лучше всего просто установить его вручную:

b.prototype.constructor = b;

Вы также можете сделать небольшую функцию для этого:

function inherit(what, from) {
    what.prototype = new from;
    what.prototype.constructor = what;
}

http://jsfiddle.net/79xTg/5/

Ответ 3

constructor - это регулярное, неперечислимое свойство значения по умолчанию для свойства prototype объектов функции. Таким образом, присваивание prototype потеряет свойство.

instanceof будет работать, так как он не использует constructor, а скорее сканирует цепочку прототипов объекта для текущего значения свойства prototype, то есть foo instanceof Foo эквивалентно

var proto = Object.getPrototypeOf(foo);
for(; proto !== null; proto = Object.getPrototypeOf(proto)) {
    if(proto === Foo.prototype)
        return true;
}
return false;

В ECMAScript3 нет способа установить свойство constructor, которое ведет себя идентично встроенному, поскольку определяемые пользователем свойства всегда перечислимы (т.е. видимы для for..in).

Это изменилось с помощью ECMAScript5. Однако даже если вы установите вручную constructor, ваш код все еще имеет проблемы: в частности, это плохая идея установить prototype в экземпляр родительского класса - родительский конструктор не следует вызывать, когда child -class ', но, скорее, когда созданы дочерние экземпляры.

Вот пример примерного кода ECMAScript5 для того, как это сделать:

function Pet(name) {
    this.name = name;
}

Pet.prototype.feed = function(food) {
    return this.name + ' ate ' + food + '.';
};

function Cat(name) {
    Pet.call(this, name);
}

Cat.prototype = Object.create(Pet.prototype, {
    constructor : {
        value : Cat,
        writable : true,
        enumerable : false,
        configurable : true
    }
});

Cat.prototype.caress = function() {
    return this.name + ' purrs.';
};

Если вы застряли с ECMAScript3, вам нужно использовать clone() функцию вместо Object.create() и выиграть ' t сделать constructor неперечислимым:

Cat.prototype = clone(Pet.prototype);
Cat.prototype.constructor = Cat;