Наследование JavaScript с прототипами - свойство "конструктор"?

Я видел много таких вещей, и я ищу подходящее решение для основного наследования JavaScript:

function Food(){}  // Food  constructor (class)
function Bread(){} // Bread constructor (class)

var basicFood = new Food();    // Food classes will inherit from this basicFood instance.

Bread.prototype = basicFood; // Bread now inherits from Food.

var bread = new Bread();     // We create some bread!
bread.constructor == Food;  // Though, now we feel very uneasy about how
                           // the constructor is wrong,

Bread.prototype.constructor = Bread; // So we explicitly set the prototype constructor
bread = new Bread();                // and when we create our new bread,
bread.constructor == Bread;        // we feel much better as the constructor appears correct.

// The issue? Suppose we have another food item, 
 // like in a real inheritance situation:

function Sushi(){};                    // We might be
Sushi.prototype = basicFood;          // tempted to do
Sushi.prototype.constructor = Sushi; // the same thing
var sushi = new Sushi();            // again

sushi.constructor == Sushi;  // Before we realize
bread.constructor == Sushi; // that we've ruined our bread.

basicFood.constructor != Food; // More importantly, we've really ruined all our basicFood,
                              // because while it a prototype, 
                             // it also an object in its own right,
                            // and deserves an accurate constructor property.

Кто constructor должен быть действительно?

И что constructor имеет какое-либо отношение к результатам instanceof?

Мне интересно, что правильно? Я понимаю, что многие предпочли бы дать каждому классу продуктов (хлеб, суши и т.д.) Новый экземпляр Food, а не дать им все тот же пример basicFood. Я хочу это более оптимальное решение (не делая ненужные экземпляры).

Учитывая нашу пищу, хлеб, суши и basicFood:

function Food(){}
function Bread(){}
function Sushi(){}
var basicFood = new Food();

Я подумал, что могу создать вспомогательный помощник,, который определял бы неперечислимый непереписываемый конструктор свойств без конфигурации для нового экземпляра:

Bread.prototype = basicFood; // We still simply inherit from basicFood
Sushi.prototype = basicFood;


// But we use this helper function when we make instances
function reconstructify(target, Constructor){
  return Object.defineProperty(target, 'constructor', {
    enumerable:   false,
    configurable: false,
    writable:     false,
    value:        Constructor
  });
}

var bread = reconstructify(new Bread(), Bread); // Like so
var sushi = reconstructify(new Sushi(), Sushi);

При тестировании этого я понял, что instanceof не ведет себя так, как я думал, это может:

// True expressions for testing -- all good
basicFood.constructor == Food;
bread.constructor     == Bread;
sushi.constructor     == Sushi;

basicFood instanceof Food; // good also
bread instanceof Food;
sushi instanceof Food;

sushi instanceof Bread; // uh oh, not so good that this is true
bread instanceof Sushi; // why is this?

Внимательно изучая его, я не могу заставить instanceof работать так, как я предполагал:

function Food(){}
function Bread(){}
function Sushi(){}

var basicFood = new Food();

Bread.prototype = basicFood;
Sushi.prototype = basicFood;

var bread = new Bread();
var sushi = new Sushi();

sushi instanceof Bread; // why true?
bread instanceof Sushi; // why true?

Я хочу, чтобы bread и sushi оба были экземплярами Food - не друг другу.

Как я могу достичь наследования JavaScript, сохраняя ожидаемое поведение для свойства constructor, а также оператора instanceof?

Ответы

Ответ 1

Давайте немного рассмотрим ваш код.

function Food(){}
function Bread(){}
function Sushi(){}
var basicFood = new Food();
Bread.prototype = basicFood;
Sushi.prototype = basicFood;

Примечание.. Когда вы устанавливаете тот же объект, что и прототип двух объектов, добавление в один прототип будет отражено и в другом прототипе. Например,

Bread.prototype = basicFood;
Sushi.prototype = basicFood;
Bread.prototype.testFunction = function() {
    return true;
}
console.log(Sushi.prototype.testFunction()); // true

Давайте вернемся к вашим вопросам.

var bread = reconstructify(new Bread(), Bread);
var sushi = reconstructify(new Sushi(), Sushi);
console.log(sushi instanceof Bread);    // Why true?
console.log(bread instanceof Sushi);    // Why true?

В соответствии с instanceof docs из MDN,

Оператор instanceof проверяет, имеет ли объект в своей прототипной цепочке свойство прототипа конструктора.

Итак, когда мы делаем что-то вроде

object1 instanceof object2

JavaScript попытается найти, если прототип object2 находится в цепочке прототипов object1.

В этом случае он вернет true только тогда, когда Bread.prototype находится в цепочке прототипов sushi. Мы знаем, что sushi строится из sushi. Таким образом, потребуется прототип sushi и проверьте, соответствует ли он прототипу Bread. Поскольку они оба указывают на тот же объект basicFood, который возвращает true. В том же случае для bread instanceof Sushi.

Итак, правильный способ наследования будет таким, как этот

function Food()  {}
function Bread() {}
function Sushi() {}

Bread.prototype = Object.create(Food.prototype);
Bread.prototype.constructor = Bread;
Sushi.prototype = Object.create(Food.prototype);
Sushi.prototype.constructor = Sushi;

var bread = new Bread();
var sushi = new Sushi();

console.log(sushi instanceof Bread);  // false
console.log(bread instanceof Sushi);  // false
console.log(sushi.constructor);       // [Function: Sushi]
console.log(bread.constructor);       // [Function: Bread]
console.log(sushi instanceof Food);   // true
console.log(bread instanceof Food);   // true
console.log(sushi instanceof Sushi);  // true
console.log(bread instanceof Bread);  // true

Ответ 2

Единственная проблема в вашей логике - установить те же объекты basicFood как для Bread.prototype, так и для Sushi.prototype. Попробуйте сделать что-то вроде этого:

Bread.prototype = new Food();
Bread.prototype.constructor = Bread;

Sushi.prototype = new Food();
Sushi.prototype.constructor = Sushi;

Теперь instanceof bread и sushi будут Food, но конструкторы будут bread и sushi для каждого из них в частности:

Ответ 3

Это мое личное решение, которое я разработал из объединенных набросков мудрости @thefourtheye, @FelixKling, @SeanKinsey и даже выходки из @helly0d:


Простейшее решение:

/** Food Class -- You can bite all foods **/
function Food(){ this.bites = 0 };
Food.prototype.bite = function(){ console.log("Yum!"); return this.bites += 1 };

/** All Foods inherit from basicFood **/
var basicFood = new Food();

/** Bread inherits from basicFood, and can go stale **/
function Bread(){
  Food.apply(this); // running food constructor (defines bites)
  this.stale = false;
};
Bread.prototype = Object.create( basicFood );
Bread.prototype.constructor = Bread; // just conventional
Bread.prototype.goStale = function(){ return this.stale = true };

/** Sushi inherits from basicFood, and can be cooked **/
function Sushi(){
  Food.apply(this);
  this.raw = true;
};
Sushi.prototype = Object.create( basicFood );
Sushi.prototype.constructor = Sushi;
Sushi.prototype.cook = function(){ return this.raw = false };

<ч/ " >

Расширенная методология:

Это лучше, потому что свойство прототипа constructor не перечислимо.

/** My handy-dandy extend().to() function **/
function extend(source){
  return {to:function(Constructor){
    Constructor.prototype = Object.create(source);
    Object.defineProperty(Constructor.prototype, 'constructor', {
      enumerable:   false,
      configurable: false,
      writable:     false,
      value:        Constructor
    });
    return Constructor;
  }}
};


function Food(){ this.bites = 0 };
Food.prototype.bite = function(){ console.log("Yum!"); return this.bites += 1 };
var basicFood = new Food();


var Bread = extend(basicFood).to(function Bread(){
  Food.apply(this);
  this.stale = false;
});
Bread.prototype.goStale = function(){ return this.stale = true };


var Sushi = extend(basicFood).to(function Sushi(){
  Food.apply(this);
  this.raw = true;
});
Sushi.prototype.cook = function(){ return this.raw = false };

<ч/ " >

Обе методологии выше дают одни и те же результаты:

var food  = new Food();
var bread = new Bread();
var sushi = new Sushi();

console.log( food instanceof Food );   // true
console.log( food instanceof Bread );  // false
console.log( food instanceof Sushi );  // false

console.log( bread instanceof Food );  // true
console.log( bread instanceof Bread ); // true
console.log( bread instanceof Sushi ); // false

console.log( sushi instanceof Food );  // true
console.log( sushi instanceof Bread ); // false
console.log( sushi instanceof Sushi ); // true

console.log( food.constructor );       // Food
console.log( bread.constructor );      // Bread
console.log( sushi.constructor );      // Sushi

<ч/ " >

Особая благодарность @FelixKling, чей опыт помог отточить мое понимание в чате за пределами этой темы - также к @thefourtheye, который первым показал мне правильный путь, а также @SeanKinsey, который подчеркнула полезность возможности запуска родительского конструктора в контексте дочерних элементов.

Сообщество wiki'd ответьте - пожалуйста, дайте мне знать или отредактируйте себя, если вы найдете что-нибудь в этом ответе, который является подозрительным:)

Ответ 4

То, что вы делаете неправильно, - это повторно использовать объект basicFood для нескольких дочерних классов. Вместо этого новый новый. Таким образом, когда вы добавляете членов в прототип (новый экземпляр родителя), вы добавляете его в экземпляр, который не используется совместно с другими наследующими классами.

Теперь есть одна вещь, которой не хватает ваш код, и это конструкторы без побочных эффектов. Многим конструкторам нужны аргументы, и они будут бросать без них, но как вы можете создать прототип для нового нисходящего класса без создания родительского элемента? Ну, нас действительно не интересует родительская функция, только в прототипе родителей. Итак, что вы можете сделать, это

function Parent() { /*some side effect or invariant */ }
Parent.prototype.foo = ...
function Child() { Parent.call(this); }

// the next few lines typically go into a utility function
function F() {} // a throw-away constructor
F.prototype = Parent.prototype; // borrow the real parent prototype
F.prototype.constructor = Parent; // yep, we're faking it
Child.prototype = new F(); // no side effects, but we have a valid prototype chain
Child.prototype.bar = ... // now continue adding to the new prototype