views:

457

answers:

7

This seems inconsistent, but probably is due to the fact that I'm new to javascript's prototype inheritance feature.

Basically, I have two base class properties, "list" and "name". I instantiate two subclasses and give values to the properties. When I instantiate the second subclass, it gets the list values from the first subclass instance, but only for the "list" and not for "name". What's going on?? Of course, I would prefer that any subsequent subclass instances not get values from other instances, but if that's going to happen, it should be consistent!

Here is a code snippet:

    function A()
    {
        this.list = [];
        this.name = "A";
    }
    function B()
    {
    }
    B.prototype = new A();

    var obj1 = new B();
    obj1.list.push("From obj1");
    obj1.name = "obj1";

    var obj2 = new B();

    //output:
    //Obj2 list has 1 items and name is A
    //so, the name property of A did not get replicated, but the list did
    alert("Obj2 list has " + obj2.list.length + " items and name is "+ obj2.name);
+4  A: 
  • You're saying in this code B is an implementation of A.
  • Then you're saying obj1 is a new instance of B and so should grab all of A's values.
  • Then you add an element to the referenced location of the list in obj2 which in turn adds an element to the referenced location of the list in A (because they are referencing the same thing).
  • Then you change the value of the name in obj1.
  • Then you're saying obj2 is a NEW instance of B and so should grab all of A's values.

You changed the values of the list referenced in A but not the reference itself. obj2.name should be "A".

NickLarsen
+3  A: 

When you push a new object into obj1.list you are mutating the existing list on the prototype. When you change the name, you are assigning a property on obj1, the instance, not the prototype. Note the same thing would happen if you had done:

obj1.list = ["from obj1"]
...
console.log(obj2.list) // <--- Will log [] to console
ironfroggy
+1  A: 

You are re-assigning the name variable. What is happening is that you are re-assigning a new name variable to obj1. Your declaration of name is overriding the declaration on the prototype object(so you don't see it). With the list, you are mutating the list in place not changing it's reference.

Kekoa
+2  A: 

The thing with prototypes, is that you can do reads all day long from them, and it wont change the underlying structure of what is pointing to what. However, the first time you do an assignment, you are replacing, on that instance, what that property points to.

In your case, you didn't actually reassign the prototyped property, you modifed the value of the underlying structure that was found at in that property.

What this means is that all objects that share a prototype of A actually share the implementation of A. That means any state carried in A will be found on all instances of B. The moment you do an assignment to that property on an instance of B, you have effectively replaced what that instance (and only that instance) points to (I believe this is due to the fact there is a new property "name" on B that gets hit in the scope chain, before it reaches the "name" implementation on A).

EDIT:

To give a more explicit example of whats going on:

B.prototype = new A();
var obj1 = new B();

Now, the first time we do a read, the intepreter does something like this:

obj1.name;

Interpreter: "I need property name. First, check B. B does not have 'name', so lets continue down the scope chain. Next up, A. Aha! A has 'name', return that"

However, when you do a write, the interpreter doesn't actually care about the inherited property.

obj1.name = "Fred";

Interpreter: "I need to assign to property 'name'. I'm within the scope of this instance of B, so assign 'Fred' to B. But I leave everything else farther down the scope chain alone (i.e., A)"

Now, next time you do a read...

obj1.name;

Interpreter: "I need property 'name'. Aha! this instance of B has the property 'name' already sitting on it. Just return that - I don't care about the rest of the scope chain (i.e. A.name)"

So, the first time you write to name, it inserts it as a first class property on the instance, and doesn't care anymore about what is on A. A.name is still there, it's just further down the scope chain, and the JS interpreter doesn't get that far before it found what it was looking for.

If "name" had a default value in A (as you have, which is "A"), then you would see this behaviour:

B.prototype = new A();
var obj1 = new B();
var obj2 = new B();

obj1.name = "Fred";

alert(obj1.name); // Alerts Fred
alert(obj2.name); // Alerts "A", your original value of A.name

Now, in the case of your array, you never actually replaced list on the scope chain with a new array. What you did is grab a handle on the array itself, and added an element to it. Hence, all instances of B are affected.

Interpreter: "I need to get the 'list' propery, and add an element to it. Checking this instance of B ... nope, does not have a 'list' property. Next in the scope chain: A. Yep, A has 'list', use that. Now, push onto that list"

This would not be the case if you did this:

obj1.list = [];
obj1.push(123);
Matt
Thank you all for the explanations. So, even though it says:B.prototype = new A();and obj2 = new B();The "list" property of A() is not getting created anew, it's only getting recycled?Anyway, I can accept this fate :) So, what is the best way to have a base class that contains an array? I'm building a tree and need a generic node-with-children class.
med
A: 

(reposting because the comment formatting is horrible)

Thank you all for the explanations. So, even though it says:

B.prototype = new A();

and

obj2 = new B();

The "list" property of A() is not getting created anew, it's only getting recycled?

Anyway, I can accept this fate :) So, what is the best way to have a base class that contains an array? I'm building a tree and need a generic node-with-children class.

med
Yes, that is correct. "Inheritance" in JavaScript is different than in languages like Java or C#. JavaScript has something called a scope chain. When you request a property on an object, it starts walking down that scope chain looking for the property. When you set the prototype of your object to create inheritance, what you are really doing is inserting A into that scope chain. Since it doesn't find 'list' in B, it checks A. See my answer above for more details. Or search javascrpit scope chain on the 'net. It's a complicated subject, but well worth understanding.
Matt
to answer the second part - you can do: var obj2 = new B().prototype = new A();
Matt
@Matt, it's prototype chain, not scope chain. Scope has implications in closures.
Ionuț G. Stan
A: 

This behavior happens because you're not assigning a value to "list", you're modifying it.

This is perfectly sensible behavior when you realize how the prototype chain works. When you reference obj1.list, it first looks if "list" exists in obj1, uses that if found, and otherwise uses the one found on obj1.prototype (which is MyClass.prototype.list).

So:

obj1.list.push("test"); // modifies MyClass.prototype.list
obj1.list = ["new"]; // creates a "list" property on obj1
obj1.list.push("test"); // modifies obj1.list, not MyClass.prototype.list
delete obj1.list; // removes the "list" property from obj1
// after the delete, obj1.list will point to the prototype again
obj1.list.push("test"); // again modifies MyClass.prototype.list

The most important take-away is this: "prototypes are not classes". Prototypes can do a reasonably good job of faking classes, but they are not classes, so you should not treat them as such.

Joeri Sebrechts
+1  A: 

Of course, I would prefer that any subsequent subclass instances not get values from other instances, but if that's going to happen, it should be consistent!

Then don't inherit from a new instance of A, inherit from A's prototype. This will ensure that you don't inherit state, you only inherit behaviour. That's the tricky part with prototypal inheritance, it inherits state too, not only behaviour as in classic OOP inheritance. In JavaScript, constructor functions should only be used for setting up the state of the instance.

var A = function (list) {
    this.list = list;
    this.name = "A";
};

A.prototype.getName = function () {
    return this.name;
};

var B = function (list) {
    this.list = list;
    this.name = "B";
};

B.prototype = A.prototype;   // Inherit from A's prototype
B.prototype.constructor = B; // Restore constructor object

var b = new B([1, 2, 3]);

// getName() is inherited from A's prototype
print(b.getName()); // B

And by the way, if you want to change something in A, through B, you have to do this:

B.prototype.name = "obj1";
Ionuț G. Stan