使用原型的JavaScript继承 - '构造函数'属性?

时间:2014-03-11 03:32:18

标签: javascript oop prototype prototypal-inheritance

我见过很多像这样的东西,我正在寻找基本的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's 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's a prototype, 
                             // it's also an object in its own right,
                            // and deserves an accurate constructor property.

constructor谁应该真的是?

constructorinstanceof的结果有什么关系?

我发现自己在想,什么是正确的?我知道很多人会选择给每个食物类别(面包,寿司等)一个新的食物实例,而不是给它们都一样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?

我希望breadsushi都是食物的实例 - 而不是彼此。

如何在保持constructor属性以及instanceof运算符的预期行为的同时实现JavaScript继承?

4 个答案:

答案 0 :(得分:15)

让我们稍微检查你的代码。

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 from MDN

  

instanceof运算符测试对象在其原型链中是否具有构造函数的prototype属性。

所以我们做的事情就像

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

答案 1 :(得分:4)

您逻辑中唯一的问题是将同一个对象basicFood设置为Bread.prototypeSushi.prototype。尝试做这样的事情:

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

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

现在,instanceof breadsushi将为Food,但构造函数将为BreadSushi。 ;

答案 2 :(得分:2)

这是我个人的解决方案,我是从@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's 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,谁强调了能够在子环境中运行父构造函数的有用性。

我社区维基这个答案 - 如果您在此答案的代码中发现任何可疑的内容,请让我知道或编辑自己:)

答案 3 :(得分:1)

你做错了是将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