Home     Squared Rectangle     Walter Zorn Graphics

Advertisement

Can an Array In a JavaScript Prototype Produce Wrong Results?

Most JavaScript tutorials tell novices how to get correct results.

This one tells experienced programmers how to get incorrect results.   Smiley

Not difficult:  As soon as an object has a property whose value is an array, and that property resides in a proto­type chain, new array elements may show up in object instantiations where they have nothing to do.

In this example from the shipping industry, two persons get listed as crew members on a wrong ship.

Part of a question posted on CodingForums on Nov 24, 2011
Any ship needs a list of crew members.
The total number of persons onboard must be known too, for the case the ship sinks.

We place these two properties in a prototype, so that we won't have to repeat them for each kind of ship.

A passenger list is needed only if the ship is a passenger ship, so we decide not to place the passenger list in a prototype.


function Ship() {
    this.crew = [];
    this.persons = 0;
}

function PassengerShip(name) {
    this.name = name;
    this.passengers = [];
}

PassengerShip.prototype = new Ship;
Passengers and crew arrive one by one shortly before departure. The first thing they do onboard is to check in:

Ship.prototype.checkin = function(list, person) {
    this[list].push(person);
    ++this.persons;
}
(Instead of the push function, we could as well have used a statement such as

this[list][ this[list].length ] = person;
I have tried. Same wrong result.)

Let us launch two passenger ships and check some persons in.


var msFloat = new PassengerShip("M/S Float");

msFloat.checkin("crew", "Captain");
msFloat.checkin("crew", "Cook");

msFloat.checkin("passengers", "Alice");
msFloat.checkin("passengers", "Bob");

var msFlawed = new PassengerShip("M/S Flawed");

msFlawed.checkin("crew", "Capt'n");
msFlawed.checkin("crew", "Sailor");

msFlawed.checkin("passengers", "Charlie");
The shipping company wants a report when everybody has checked in.
But I'm not sure they will like it:

Ship.prototype.report = function() {
  function makeList(ship, list) {
    if (!ship[list]) return (ship.name + " has no list of " + list + ".\n");
    if (!ship[list].length) return (ship.name + "  has no " + list + ".\n");
    return (list + ":  " + ship[list].join(", ") + "\n");
  }
  alert("\nPre-Departure Report For  '" + this.name + "'\n\n" +
           makeList(this, "passengers") + makeList(this, "crew") +
            "\nNumber of persons onboard:  " + this.persons + "\n\n");
}

msFloat.report();  // OK: Alice, Bob as passengers; Captain, Cook as crew
msFlawed.report(); // WRONG CREW: Captain, Cook, Capt'n, Sailor

The “M/S Flawed” reports Captain, Cook, Capt'n, Sailor as its crew. Two of those never checked in on that ship.

Here is an "edit+execute" version of the concatenated code.

Click here to execute!    (You may edit the text first if you want.)
Code 1
Code 1
Click here to execute!    (You may edit the text first if you want.)

The code returns the right passenger lists and the right number of persons.

Only the crew for the M/S Flawed is wrong. Note that the crew is in an array in the prototype. The passenger list is an array but is not in the prototype. The number of persons is in the prototype but is not an array.

It’s time to look at some testoutput.

When a property is in the prototype, it may also exist as an “own property” of an instantiation. The hasOwnProperty method can tell us whether this is the case. Let us temporarily change the above code and replace our checkin function with an extended version:

Code 2
Code 2

You may copy and paste it in – but you don't have to, for here is how the testoutput looks:

Testoutput using Code 2
Testoutput using Code 2

There are several observations to be made.

The number of persons onboard a ship becomes an own property the first time a person checks in on that ship. The value in the prototype never changes.

The list of passengers is always an own property of a passenger ship, and new passengers on a ship are added to that ship’s own property correctly.

The crew never becomes an own property of any ship. Instead, new crew members are added to PassengerShip.prototype.crew.

It looks as if the ++ operator and the push method react differently when an own property is not present:

The ++ operator delivers its result in an own property. If this.persons does not already exist as an own property, the ++ operator creates one.

The push method operates on an own property if it is present. If not, push does not create an own property. Instead, push adds the new element to the array in the prototype, making the new element visible in all instantiations.

A workaround is not difficult. We can make the crew list an own property of a ship the first time a crew member checks in on that ship.

For instance, this way:  (No, it’s not meant to be a joke)
Code 3
The assignment operator creates an own property, and the slice method copies the crew list into it from the prototype.

It can be used either with the push function or with the assignment operator as mentioned in the beginning of this document, so we get two improved versions of our checkin function,
Code 4

or
Code 5

Copy the version you like best, and paste it into the example (Code 1) to overwrite the original checkin function. The example will work to everybody’s satisfaction, and you can safely go onboard and check in.

Person on sailboat (animated smiley from smileys.smilchat.net)
Should we accept such a workaround as a final solution?  Absolutely not, in my opinion.

A cleaner approach could be to try and keep arrays away from prototypes unless it serves a specific purpose to have them there. This was done in a Squared Rectangle Calculator elsewhere on this site, but there might be other cases where such a solution would not be ideal.

And – prototypes exist in order to be used, don’t they?

(Author:  Bjarne Pagh Byrnak)




Follow-Up on Answer From  CodingForums:

I am not an expert in the deeper theory of JavaScript, so I posted a question at  CodingForums.

DaveyErwin, who presents himself as a Regular Coder living in Forth Worth, Texas, in a reply dated Thursday November 24, 2011, at 7:19 PM CST – that is, during busy Thanks­giving hours if I under­stand time zones right – proposed a solution of the following kind.

One may let PassengerShip call Ship each time an instance is created; then, each instance gets a crew array of its own.

Code 6
Code 6

The first statement in PassengerShip causes Ship to be called with the new PassengerShip instantiation as the this object. The first statement in Ship will then create an own property for this.crew in the new instantiation, with an empty list of crew members.

The second statement in Ship will create an own property for this.persons. The only effect is that the ++ operator will not have to create one when checkin is called some time later. (The statement that creates this.persons could as well be removed from Ship and placed in Ship.prototype. This will be done from now on.)

The properties crew, persons, and passengers are introduced in only one place in the source text, as should be the case in a well-written code.

The fundamental problem – the need for an own property for an array – is addressed deep in the con­structor functions in a clean and transparent manner. It is no longer mixed into any method.

DaveyErwin’s recommendation looks very much like the kind of solution I hoped for. Let us try it out with testoutput switched on.

Let us also add a CruiseShip or two, with a PassengerShip as a prototype. We let CruiseShip call PassengerShip which in turn calls Ship. With a little fine-adjustment in order to keep testoutput informative – and with an updated name for one of the other ships – the code could look this way:

Edit and click to execute (31 alerts!) – or simply look at the testoutput far below.
Code 7
Code 7
Edit and click to execute the above (31 alerts) – or simply look at the testoutput below.

And here is the testoutput. Everything now works to perfection. Look how the constructor functions call one another:

Testoutput from Code 7
Testoutput from Code 7

If the call of PassengerShip had been omitted from the CruiseShip constructor in Code 7, the reports would have looked as if Dorothea and Ernest had been passengers on two ships simultaneously, just like the two crew members in Code 1. (You may edit Code 7 and try the experiment, if you want.)

Automatic Inheritance and Creation of Own Properties:

In the constructors for CruiseShip and PassengerShip, it was written out explicitely which other constructor to call. That information could as well be computed.

The example below shows one of many ways to do this.

An array property containing owner and hometown, which will be assumed here to be the same for all the company’s ships, will be added to the prototype of PassengerShip – just to show that arrays and other properties can be handled correctly even if they are not copied into the instantiations for individual ships.

Another two subclasses, ResearchShip with CruiseShip as its prototype, and BioResearchShip with ResearchShip as its prototype, will be added too.

The terms "Class" and "Subclass" were borrowed from object-oriented languages other than JavaScript. For instance, CruiseShip is a subclass of PassengerShip.

Try automatic inheritance!     (9 alerts. You may edit the text.)
Code 8
Code 8
Try automatic inheritance!     (9 alerts. You may edit the above text.)
All logic related to inheritance and own properties is now kept inside two relatively small functions.
Does that mean the code is perfect?

Not quite.

It cannot handle a ship that has an empty name. This is because insertOwnProperties uses the name argument internally. It also inserts two properties “running” and “callee” without checking for name conflict.

The function insertOwnProperties gets called recursively, because a callee may call it again before it has returned from a previous call. The recursion stops at the bottom of the prototype chain (because Ship does not call insertOwnProperties), after which the incarnations return successively one by one.


Conclusion
If you want correct results, it is not enough to understand your problem. It seems you must also under­stand your programming language in some depth – at least when you are using JavaScript.




POSTSCRIPT

Do you know what?  At an embarrassingly late time, I made a discovery.

In addition to push, the Array object has a method called concat. It is used to concatenate two arrays. Unlike push, concat leaves the this array unaffected and returns the result of concatenation, so you have to do an assignment.

That is good news, because the assignment operator (an equal sign) will create an own property if it does not already exist. So, if in Code 1 you replace the checkin function with

Code 9

then the example works correctly and all problems disappear. (The square brackets around person can be omitted.)

But – once again – to add workarounds for JavaScript peculiarities here and there in the definitions of higher-level methods such as checkin might not be the best way to produce reliable software. Workarounds are best placed at the deepest possible level, as was done above.


Home     Squared Rectangle     Walter Zorn Graphics     Who?