3

I'm a total Javascript newb, and I'm trying to wrap my head around OLN. What I'm encountering is that, when calling an object method from another method on the same object, the value of local value of 'this' in the called method is changing. Here's my code:

var generator = {
    generateForLevelSkillAndCount : function(level, skill, count) {
        var functionCall = this['generate_' + level + '_' + skill];
        return functionCall(count);
    },
    generate_0_4 : function(count) {
        return this.generate_generic_dots(count, 3);
    },
    generate_generic_dots : function(count, maxDots) {
        /* do cool stuff and return it */
    }
};

So, I call generator.generateForLevelSkillAndCount(0, 4, 20) and it works properly, calling generate_0_4(count). However, this is where it fails, with Chrome's Javascript console telling me "Uncaught TypeError: Object [object DOMWindow] has no method 'generate_generic_dots'."

I know enough to know that the problem is that the value of this in generate_0_4 is a DOMWindow object, rather than generator (which is what this is pointing to in generateForSkillLevelAndCount but I can't figure out why that would possibly be happening.

Update: I updated the example code per CMS's suggestion to get rid of eval, but the same error is being returned, so it's not just an eval bug.

John Biesnecker
  • 3,782
  • 2
  • 34
  • 43

6 Answers6

8

In JavaScript, the context object (this) is set to the "global object" (window, in browsers) unless the method is accessed as an object property. Therefore:

var foo = { bar: function() { alert(this.baz); }, baz: 5 };
var bar = foo.bar;
var baz = 3;

foo.bar();    // alerts 5, from foo
foo["bar"](); // alerts 5, from foo
bar();        // alerts 3, from the global object

Note that all three function calls are to the exact same function!

So, in your code, you're assigning the desired method to functionCall and calling it directly, which causes the function to use window as its context object. There are two ways around this: access the method as an object property or use .call() or .apply():

function generateForLevelSkillAndCount1(level, skill, count) {
    return this['generate_' + level + '_' + skill](count);
}

function generateForLevelSkillAndCount2(level, skill, count) {
    var functionCall = this['generate_' + level + '_' + skill];
    return functionCall.call(this, count);
}
Ben Blank
  • 54,908
  • 28
  • 127
  • 156
  • Excellent, this is exactly what I needed. The exact workings of JS's object syntax are still something of a mystery to me, but what you suggested worked perfectly. Thanks! – John Biesnecker Aug 25 '10 at 06:20
  • so foo.bar() is equivalent to bar.call(foo) ? – justin.m.chase Jan 13 '11 at 17:21
  • @justin — In my example, yes. (In practice, the function's name in the current scope may or may not match its name as a property on an object, but you get the idea.) – Ben Blank Jan 17 '11 at 07:35
3

First of all, I would encourage you to avoid eval where you don't need it, for example, in your fist function:

//...
generateForLevelSkillAndCount : function(level, skill, count) {
    var functionCall = this['generate_' + level + '_' + skill];
    return functionCall(count);
},
//...

You can use the bracket notation property accessor instead eval, it's unnecessary in this case.

Now, I guess you are trying your code on the Chrome's Console, and eval is failing because the console has a bug, when eval is invoked from a FunctionExpression (such as generateForLevelSkillAndCount), the evaluated code uses the Global context for its Variable Environment and Lexical Environment.

See this answer for more information on this bug.

Edit: After re-reading your code, the problem happens because you lose the base object reference when you assign the function to your functionCall variable, you can either:

Invoke the function directly, without using that variable:

//...
generateForLevelSkillAndCount : function(level, skill, count) {
    this['generate_' + level + '_' + skill](count);
},
//...

Or still use your variable, but persist the this value:

//...
generateForLevelSkillAndCount : function(level, skill, count) {
    var functionCall = this['generate_' + level + '_' + skill];
    return functionCall.call(this, count);
},
//...

More info on this...

Community
  • 1
  • 1
Christian C. Salvadó
  • 807,428
  • 183
  • 922
  • 838
  • Thanks for the info. I didn't realize I could use that notation (though it makes sense, given the nature of Javascript objects). Unfortunately, same error after updating the code (I'll update the example code in the question, too). – John Biesnecker Aug 25 '10 at 05:26
  • If using the bracket notation property accessor is failing the same way, is there another way to do dynamic function calls? – John Biesnecker Aug 25 '10 at 05:37
  • @John Biesnecker Yes, but do you really need them here? It seems like you could easily use regular functions and/or a lookup table instead of hard-coding `3` into `generate_0_4()`--unless there's a lot of redacted code. – cbednarski Aug 25 '10 at 06:01
  • @banzaimonkey That "do cool stuff and return it" was pretty complex and called many many times, but I certainly could have used a lookup table instead. Mostly, though, it was one of those "this has to be possible!" questions for the future. – John Biesnecker Aug 25 '10 at 06:19
  • @CMS, thanks much for the links to more information. `this` is a mysterious beast... :) – John Biesnecker Aug 25 '10 at 06:36
  • @John, you're welcome, yes it is one of the tricky concepts to gasp. Glad to help :) – Christian C. Salvadó Aug 25 '10 at 06:46
1

You can control the execution context of the method call by using call():

var generator = {
  generateForLevelSkillAndCount : function(level, skill, count) {
    return this['generate_' + level + '_' + skill].call(this, count);
  },
  generate_0_4 : function(count) {
    return this.generate_generic_dots.call(this, count, 3);
  },
  generate_generic_dots : function(count, maxDots) {
    /* do cool stuff and return it */
  }
};
Dave Ward
  • 59,815
  • 13
  • 117
  • 134
0

I'm as baffled as you are, but have you considered checking out what happens if you call generate_0_4 explicitly, instead of parsing it through eval()?

Richard Neil Ilagan
  • 14,627
  • 5
  • 48
  • 66
0

When you call generate_0_4 dynamically (using an implicit to_string()) it is returned to generateForLevelSkillAndCount as an ad-hoc function. Because it's in Window scope rather than Object scope, it can't reference this, and the internal call fails because this doesn't exist in that context.

Here's how to see what's happening:

generate_0_4 : function(count) {
    throw(this);
    return this.generate_generic_dots(count, 3);
},

With generator.generateForLevelSkillAndCount(0, 4, 1); you get [object Window] or [object DOMWindow].

With generator.generate_0_4(1); you get what you're expecting (and what works): [object Object] or #<an Object>.

cbednarski
  • 11,718
  • 4
  • 26
  • 33
0

This is a feature of Javascript: the value of this will depend on the object from which the function was called, not where it was defined. (Which makes sense when functions are first-class objects themselves.) this in most other contexts refers to the window object.

There are two common workarounds, using a wrapper function:

function bind(func, obj) {
    return function() {
        func.apply(obj, arguments);
    }
}

or using a closure:

var self = this;
function generate_blah() {
    // use self instead of this here
}

In your case, though, simply replacing

var functionCall = this['generate_' + level + '_' + skill];
return functionCall(count);

with

this['generate_' + level + '_' + skill](count);

would do the trick.

Tgr
  • 27,442
  • 12
  • 81
  • 118