17

I am trying to get a recursion method to work in a class context. Within my class I have the following method:

    countChildren(n, levelWidth, level) {
    if (n.children && n.children.length > 0) {
        if (levelWidth.length <= level + 1) {
            levelWidth.push(0);
        }
        levelWidth[level + 1] += n.children.length;
        n.children.forEach(function (n) {
            this.countChildren(n, levelWidth, level+1);
        });    
    }
    // Return largest openend width
    return levelWidth;
}

However, when I use this method (which worked before when I just used it as function countChildren = ...) it can't... find (?) itself: Cannot read property 'countChildren' of undefined at the recursion.

Does anyone have any ideas?

David Montgomery
  • 473
  • 1
  • 4
  • 15
  • 2
    That's because the callback for `forEach` has it's own scope, and sets the value of `this` to something other than the class. – adeneo Jul 17 '17 at 15:09

5 Answers5

19

The problem arises because within your loop, this gets redefined to the inner function scope.

countChildren(n, levelWidth, level) {
    var self = this; // Get a reference to your object.

    if (n.children && n.children.length > 0) {
        if (levelWidth.length <= level + 1) {
            levelWidth.push(0);
        }
        levelWidth[level + 1] += n.children.length;

        n.children.forEach(function (n) {
            // Use "self" instead of "this" to avoid the change in scope.
            self.countChildren(n, levelWidth, level+1);
        });    
    }
    // Return largest openend width
    return levelWidth;
}
krillgar
  • 12,596
  • 6
  • 50
  • 86
  • 2
    Good catch @krillgar. – colecmc Jul 17 '17 at 15:11
  • 4
    I'm not a fan of the `that = this` trick. There are constructs that are part of the language for this particular use case, mainly `.bind()` and arrow functions in ES6. – Sumi Straessle Jul 17 '17 at 15:16
  • 1
    @SumiStraessle I'm not a fan of `this` at all. OP seems to be newer to Javascript, so I didn't want to confuse them. I see you answered with that, so I won't append to my answer. – krillgar Jul 17 '17 at 15:18
  • 1
    No worries. I like arrow functions to solve the scoping hell problem, it also makes the code more readable. – Sumi Straessle Jul 17 '17 at 15:19
  • @SumiStraessle I haven't spent much time with them as my last project was server side. I'll take a look at using them with TypeScript next time I'm doing client side work as my enterprise work often still unfortunately needs to target IE. – krillgar Jul 17 '17 at 15:20
  • Good luck with IE ! :-D – Sumi Straessle Jul 17 '17 at 15:21
  • every single array function takes a last argument called this, which decides what the value of this will be inside the function. that's more useful than self =this. just don't use arrow function, cause they override the 'this'. – amitava mozumder Jan 08 '20 at 09:15
14

Try binding the method in the constructor.
Also, by using an arrow function for your forEach, you keep the scope of the class' this.

export class MyClass {
    constructor(){
        this.countChildren = this.countChildren.bind(this);
    }

    countChildren(n, levelWidth, level){ ... }


    countChildren(n, levelWidth, level) {
        if (n.children && n.children.length > 0) {
            if (levelWidth.length <= level + 1) {
                levelWidth.push(0);
            }
            levelWidth[level + 1] += n.children.length;
            n.children.forEach( n => { // arrow function do not need to rebind this
                this.countChildren(n, levelWidth, level+1);
            });    
        }
        // Return largest openend width
        return levelWidth;
    }
}
Sumi Straessle
  • 1,116
  • 12
  • 23
1

the variable this Gets redefined within:

  1. the inner scope of a for loop
  2. within a inline function declariation
  3. within asynchronous function calls.

I agree with krillgar with the declaration of self. it fixed my problem with an asynchronous call.

obj.prototype.foo = function (string){
   var self = this;
   if(string){ do something }
   else
   setTimeout(function(){
     self.foo("string");
     }, 5000);
}
0

The this inside the foreach than the this in the class. In your case, this refers to the current element being iterated.

you need to bind the scope.

n.children.forEach(function (n) {
   this.countChildren(n, levelWidth, level+1);
}.bind(this));   
ThomasK
  • 300
  • 1
  • 8
-1

Try using .call() to invoke the function. That way you can specify the context directly.

Like this:

this.countChildren.call(this, n, levelWid);th, level+1

Edit: Noticed my error, what you should really do is bind the anonymous function:

like this:

  n.children.forEach(function (n) {
    this.countChildren(n, levelWidth, level+1);
  }.bind(this)); 
Nathan Montez
  • 461
  • 3
  • 8