Jonline
Jonline

Reputation: 1747

How to properly create Javascript prototypes such that class variables don't persist between instantiations?

I'm struggling a lot with Javascript prototyping; I don't seem to understand when I should be declaring an attribute in the prototype vs instantiation. Particularly, I don't understand, why in my code attached (a basic exercise in building a little Router class to treat a document more like an app than a page), attributes set at instantiation are persisting and thus accumulating in what I intend to be separate objects entirely.

Mostly this is a learning exercise, but I've scaled back some of the original code to help with contextual obfuscation*.

Code is here: http://codepen.io/anon/pen/xGNmWM

Basically, each time you click a link, the data-route attribute on the element should be picked up an event listener, a new Route object should be instantiated (and passed information about the intended route); finally the Router class should actually "launch" the Route, ie. make an ajax request or do some in-document stuff, whatever.

Right now, the Route.url.url attribute should be, in my obviously wrong understanding, created anew each time and then informed by passed data. Somehow, this attribute is persisting and thus accumulating passed information from each click.

I truly don't understand why.

**I have not removed anything that would effect my question; really it could be trimmed back even more but I realize the integrity of the question relies on a reasonable facsimile of the original code.

Upvotes: 1

Views: 115

Answers (2)

Skarllot
Skarllot

Reputation: 754

You have two problems.

By Value vs By Reference

In Javascript primitive types, as numbers, booleans and strings, are passed to another functions or set to another variable by value. That means that its value is copyed (cloned).

The object types, as objects, arrays and functions, are passed to another functions or set to another variable by reference. That means that variables of this type are just references to a content to memory. Then when you set an object to a variable, just its memory address is being copied.

When you pass the "route_data" its reference is copied. Then the Route constructor is working on same variable hold by Router. If you clone your object before pass it the problem is solved.

...
var route_data = this.route_data[ route_name ];
route_data = $.extend(true, {}, route_data);  // Clone object using jQuery
var route = new Route( route_name, route_data, request_obj);
...

Prototype

The Javascript has a prototypal inheritance, that means that each object points to its prototype that is another object.

var obj = { name: 'John' };
console.log(obj.__proto__);

All objects root prototype is Javascript Object by default. As noted by example above.

function Person(name) {
    this.name = name;
}

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

var obj = new Person('John');

console(obj.getName());
console(obj.__proto__);
console(obj.__proto__.__proto__);

When you use new a new empty object is created and binded as this to specified function. Additionally its object prototype will point to the object specified on prototype of called function.

On get operations the Javascript engine will search on entire prototype chain until specified field is found.

But on set operations if the specified field does not exist on current object, a new field will be created.

When you define the url field on Route this should be static, because when you try to change it a new field is created.

If you verify your Route instance you will note that you have created duplicated url fields. One on object itself and another on prototype.

Upvotes: 1

slebetman
slebetman

Reputation: 113906

I would have really appreciated a minimal code example posted here on SO rather than codepen. It would have saved me some time reading your code (you're not paying me for this after all).

Anyway, this is problematic:

Route.prototype = {
    // etc..
    url : {url: false, type: "get", defer: false}
    // etc..
}

Basically, what you're doing is this:

var global_shared_object = {url: false, type: "get", defer: false};
Route.prototype.url = global_shared_object;

Do you see the problem? Now when you do:

var route1 = new Route();
var route2 = new Route();

Both the .url property of route1 and route2 point to the same object. So modifying route1.url.url will also modify route2.url.url.

It should be noted that route1.url and route2.url are different variables. Or rather different pointers. Modifying route1.url will not modify route2.url. However, the way they've been initialized makes them both point to the same object so modifying that object can be done from either pointer.

The key to making this work is to create a new object for .url every time you create a new object. That can be done either in the constructor or in your case your .init() method:

Route = function (name, route_data, request_obj) {
    this.url = {url: false, type: "get", defer: false};
    this.init(name, route_data, request_obj);
}

Implied Lesson:

The lesson to take from this is that the object literal syntax is actually an object constructor. Not just a syntax.

// this:
{};
// is the same as this:
new Object();

So every time you see an object literal, in your mind you should be thinking new Object() then see if it makes sense to call it just once or do you need a new instance of it every time.

Upvotes: 1

Related Questions