Confused about JavaScript prototypal inheritance w

2020-07-27 04:52发布

问题:

I've read pages and pages about JavaScript prototypal inheritance, but I haven't found anything that addresses using constructors that involve validation. I've managed to get this constructor to work but I know it's not ideal, i.e. it's not taking advantage of prototypal inheritance:

function Card(value) {
    if (!isNumber(value)) {
        value = Math.floor(Math.random() * 14) + 2;
    }

    this.value = value;
}

var card1 = new Card();
var card2 = new Card();
var card3 = new Card();

This results in three Card objects with random values. However, the way I understand it is that each time I create a new Card object this way, it is copying the constructor code. I should instead use prototypal inheritance, but this doesn't work:

function Card(value) {
    this.value = value;
}

Object.defineProperty( Card, "value", {
    set: function (value) {
        if (!isNumber(value)) {
            value = Math.floor(Math.random() * 14) + 2;
        }

        this.value = value;
    }
});

This doesn't work either:

Card.prototype.setValue = function (value) {
    if (!isNumber(value)) {
        value = Math.floor(Math.random() * 14) + 2;
    }

    this.value = value;
};

For one thing, I can no longer call new Card(). Instead, I have to call var card1 = new Card(); card1.setValue(); This seems very inefficient and ugly to me. But the real problem is it sets the value property of each Card object to the same value. Help!


Edit

Per Bergi's suggestion, I've modified the code as follows:

function Card(value) {
    this.setValue(value);
}

Card.prototype.setValue = function (value) {
    if (!isNumber(value)) {
        value = Math.floor(Math.random() * 14) + 2;
    }

    this.value = value;
};

var card1 = new Card();
var card2 = new Card();
var card3 = new Card();

This results in three Card objects with random values, which is great, and I can call the setValue method later on. It doesn't seem to transfer when I try to extend the class though:

function SpecialCard(suit, value) {
    Card.call(this, value);

    this.suit = suit;
}

var specialCard1 = new SpecialCard("Club");
var specialCard2 = new SpecialCard("Diamond");
var specialCard3 = new SpecialCard("Spade");

I get the error this.setValue is not a function now.


Edit 2

This seems to work:

function SpecialCard(suit, value) {
    Card.call(this, value);

    this.suit = suit;
}

SpecialCard.prototype = Object.create(Card.prototype);
SpecialCard.prototype.constructor = SpecialCard;

Is this a good way to do it?


Final Edit!

Thanks to Bergi and Norguard, I finally landed on this implementation:

function Card(value) {
    this.setValue = function (val) {
        if (!isNumber(val)) {
            val = Math.floor(Math.random() * 14) + 2;
        }

        this.value = val;
    };

    this.setValue(value);
}

function SpecialCard(suit, value) {
    Card.call(this, value);

    this.suit = suit;
}

Bergi helped me identify why I wasn't able to inherit the prototype chain, and Norguard explained why it's better not to muck with the prototype chain at all. I like this approach because the code is cleaner and easier to understand.

回答1:

the way I understand it is that each time I create a new Card object this way, it is copying the constructor code

No, it is executing it. No problems, and your constructor works perfect - this is how it should look like.

Problems will only arise when you create values. Each invocation of a function creates its own set of values, e.g. private variables (you don't have any). They usually get garbage collected, unless you create another special value, a privileged method, which is an exposed function that holds a reference to the scope it lives in. And yes, every object has its own "copy" of such functions, which is why you should push everything that does not access private variables to the prototype.

 Object.defineProperty( Card, "value", ...

Wait, no. Here you define a property on the constructor, the function Card. This is not what you want. You could call this code on instances, yes, but note that when evaluating this.value = value; it would recursively call itself.

 Card.prototype.setValue = function(){ ... }

This looks good. You could need this method on Card objects when you are going to use the validation code later on, for example when changing the value of a Card instance (I don't think so, but I don't know?).

but then I can no longer call new Card()

Oh, surely you can. The method is inherited by all Card instances, and that includes the one on which the constructor is applied (this). You can easily call it from there, so declare your constructor like this:

function Card(val) {
    this.setValue(val);
}
Card.prototype...

It doesn't seem to transfer when I try to extend the class though.

Yes, it does not. Calling the constructor function does not set up the prototype chain. With the new keyword the object with its inheritance is instantiated, then the constructor is applied. With your code, SpecialCards inherit from the SpecialCard.prototype object (which itself inherits from the default Object prototype). Now, we could either just set it to the same object as normal cards, or let it inherit from that one.

SpecialCard.prototype = Card.prototype;

So now every instance inherits from the same object. That means, SpecialCards will have no special methods (from the prototype) that normal Cards don't have... Also, the instanceof operator won't work correctly any more.

So, there is a better solution. Let the SpecialCards prototype object inherit from Card.prototype! This can be done by using Object.create (not supported by all browsers, you might need a workaround), which is designed to do exactly this job:

SpecialCard.prototype = Object.create(Card.prototype, {
    constructor: {value:SpecialCard}
});
SpecialCard.prototype.specialMethod = ... // now possible


回答2:

In terms of the constructor, each card IS getting its own, unique copy of any methods defined inside of the constructor:

this.doStuffToMyPrivateVars = function () { };

or

var doStuffAsAPrivateFunction = function () {};

The reason they get their own unique copies is because only unique copies of functions, instantiated at the same time as the object itself, are going to have access to the enclosed values.

By putting them in the prototype chain, you:

  1. Limit them to one copy (unless manually-overridden per-instance, after creation)
  2. Remove the method's ability to access ANY private variables
  3. Make it really easy to frustrate friends and family by changing prototype methods/properties on EVERY instance, mid-program.

The reality of the matter is that unless you're planning on making a game that runs on old Blackberries or an ancient iPod Touch, you don't have to worry too much about the extra overhead of the enclosed functions.

Also, in day-to-day JS programming, the extra security from properly-encapsulated objects, plus the extra benefit of the module/revealing-module patterns and sandboxing with closures VASTLY OUTWEIGHS the cost of having redundant copies of methods attached to functions.

Also, if you're really, truly that concerned, you might do to look at Entity/System patterns, where entities are pretty much just data-objects (with their own unique get/set methods, if privacy is needed)... ...and each of those entities of a particular kind is registered to a system which is custom made for that entity/component-type.

IE: You'd have a Card-Entity to define each card in a deck. Each card has a CardValueComponent, a CardWorldPositionComponent, a CardRenderableComponent, a CardClickableComponent, et cetera.

CardWorldPositionComponent = { x : 238, y : 600 };

Each of those components is then registered to a system:

CardWorldPositionSystem.register(this.c_worldPos);

Each system holds ALL of the methods which would normally be run on the values stored in the component. The systems (and not the components) will chat back and forth, as needed to send data back and forth, between components shared by the same entity (ie: the Ace of Spade's position/value/image might be queried from different systems so that everybody's kept up to date).

Then instead of updating each object -- traditionally it would be something like:

Game.Update = function (timestamp) { forEach(cards, function (card) { card.update(timestamp); }); };
Game.Draw = function (timestamp, renderer) { forEach(cards, function (card) { card.draw(renderer); }); };

Now it's more like:

CardValuesUpdate();
CardImagesUpdate();
CardPositionsUpdate();
RenderCardsToScreen();

Where inside of the traditional Update, each item takes care of its own Input-handling/Movement/Model-Updating/Spritesheet-Animation/AI/et cetera, you're updating each subsystem one after another, and each subsystem is going through each entity which has a registered component in that subsystem, one after another.

So there's a smaller memory-footprint on the number of unique functions. But it's a very different universe in terms of thinking about how to do it.