Ivan Drinchev
Ivan Drinchev

Reputation: 19581

Javascript classes and variable references

I'm trying to solve this puzzle minded Javascript OOP problem.

So I have the following class :

var ClassA = function() {
    this.initialize();
}

ClassA.prototype = {

    methods : ['alpha','beta','gama'],

    initialize : function() {
        for ( var i in this.methods ) {
            this[this.methods[i]] = function() {
                console.log(this.methods[i]);
            }
        }
    }
}

var a = new ClassA();

When I call every method I expect to print the name of it, right? But here is what i get :

a.alpha(); // returns gama ?!?
a.beta();  // returns gama ?!?
a.gama();  // returns gama

But when my class looks like this :

var ClassB = function() {
    this.initialize();
}

ClassB.prototype = {

    methods : ['alpha', 'beta', 'gama'],

    initialize: function() {
        for ( var i in this.methods ) {
            this.addMethod(this.methods[i]);
        }
    },

    addMethod: function(method) {
        this[method] = function() {
            console.log(method);
        }
    }

}

var b = new ClassB();

b.alpha(); // returns alpha
b.beta();  // returns beta
b.gama();  // returns gama

Why is this happening ?

Upvotes: 6

Views: 475

Answers (4)

gen_Eric
gen_Eric

Reputation: 227190

for ( var i in this.methods ) {
      this[this.methods[i]] = function() {
          console.log(this.methods[i]);
       }
}

Your problem lies here. When this loop ends, i is the last element. Each function uses the same i, so they are all the last element.

When you use addMethod you are making a closure to "capture" the correct value.

EDIT: When you call addMethod you are "copying" the value, instead of using the i value, which changes with each loop iteration.

Upvotes: 6

Tomas
Tomas

Reputation: 59435

You can fix your first example by adding an anonymous closure:

initialize : function() {
    for ( var i in this.methods ) {
        (function (i) { // anonymous closure
            this[this.methods[i]] = function() {
                console.log(this.methods[i]);
            }
        }).call(this, i); // use .call() if you need "this" inside
    }
}

Now it will work the same way as your second example. "Anonymous" means that the closure is made by function which doesn't have a name and is called instantly as it is "created".

Note sideways: use .call(this, ...) to preserve this inside the called function, or you can do var that = this, use that instead of this and call the function normally:

for ( var i in this.methods ) {
    var that = this;
    (function (i) { // anonymous closure 
        that[that.methods[i]] = function() {
            console.log(that.methods[i]);
        }
    })(i); // Called normally so use "that" instead of "this"!
}

Upvotes: 1

nnnnnn
nnnnnn

Reputation: 150010

In your first version:

initialize : function() {
    for ( var i in this.methods ) {
        this[this.methods[i]] = function() {
            console.log(this.methods[i]);
        }
    }
}

The methods that you create within initialize all refer to the same i variable from initialize - and after initialize runs i has the value "gama", so regardless of which of the methods you call that's the value of i that they'll log to the console. JS doesn't store the current value of i at the time the method is created.

JS creates a "closure" for each function - variables declared in your initialize function (i.e., i) continue to be in scope for the nested function(s) even after initialize has finished.

The second version calls addMethod to add each method:

addMethod: function(method) {
    this[method] = function() {
        console.log(method);
    }
}

...and so when they run they'll refer to their own "copy" of the method parameter because then there is a separate closure for each of the methods.

Edit: See also this question: How do JavaScript closures work? (several answers there explain this more clearly than I did).

Upvotes: 3

Erik  Reppen
Erik Reppen

Reputation: 4635

Well, first of all stop using for (property in object) loops on Arrays. It's all fun and games until somebody prototypes to the Array object which is both a perfectly reasonable and very useful/popular thing to do. This will result in custom methods getting added to your for x in array loops.

As for the problem, it's doing exactly what you told it to do in version 1. The problem is that by the time you get around to firing it, i is the last thing i was, 'gamma'. When you pass a reference into a function as an argument, the function holds on to the value's state as it was passed.

Upvotes: 0

Related Questions