1
class TestClass {
    constructor() {
        this.prop = 5;
    }
    MethA() {
        console.log(this);
        console.log(this.prop);
    }
    MethB() {
        (true ? this.MethA : null)();
    }
}
Test = new TestClass();
Test.MethB();

Why does this code fail when reaching (this.prop) in MethA from the ternary operator in MethB?

VM:6 undefined
VM:7 Uncaught TypeError: Cannot read property 'prop' of undefined
    at MethA (<anonymous>:7:26)
    at TestClass.MethB (<anonymous>:10:35)
    at <anonymous>:14:6

To preempt any further comments about the null in what is clearly a demonstrative example:

In the real code, the condition of the ternary operator is a variable flag, and the false value is another method. The benefit to this structure is that the two functions can share a single argument list that only has to be written once and doesn't need a name.

No, moving the calls to inside the ternary operator doesn't answer this question. It loses the benefit of only needing to write the arguments once, and it's also side effect abuse.

The real solution is to replace the ternary with an if {} else {} for side effects, or wrap the methods in arrow functions for a value expression. But that doesn't answer this question either: The point isn't what code could achieve a similar result, as that would be easy; The point is why this code doesn't work and what underlying language quirks are the cause of that.

Will Chen
  • 482
  • 4
  • 12
  • 1
    Have you tried to change `(true ? this.MethA : null)();` to `(true ? this.MethA() : null);`? – B001ᛦ Jul 05 '21 at 14:30
  • Why is there a ternary there anyway? If there were a real condition instead of a hard-coded true, you'd get an error about `null` not being a method. You're better off collecting the method reference, then executing it if it's good (e.g., `const ref = (true ? this.MethA : null); if (typeof ref === 'function') ref.call(this);`) – Heretic Monkey Jul 05 '21 at 14:33
  • @B001ᛦ That would be side effect abuse. In my actual project, there's an argument list that I want to only write once for both methods, and pass to a different method depending on the condition. – Will Chen Jul 05 '21 at 14:36
  • `this.MethA` returns the function without the context (`this`) and when it's invoked without the context we lose `prop` – Nir Alfasi Jul 05 '21 at 14:36
  • Well you could turn `MethA` and `MethB` into [arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) to get around the issue. But you still run into the problem that `null` isn't a method when your condition fails – Reyno Jul 05 '21 at 14:38
  • 1
    @HereticMonkey or even more simply `if (cond) this.MethA();` – VLAZ Jul 05 '21 at 14:43
  • @WillChen "*That would be side effect abuse.*" more than using a conditional operator to get a function reference and invoke it?! If you're held at a gunpoint and *absolutely* have to do a conditional method execution as part of an expression, then executing the method immediately is the more correct thing. Hopefully, nobody holds a gun to your head, so you don't actually have to write this kind of code, though. – VLAZ Jul 05 '21 at 14:46
  • "*what underlying language quirks are the cause of that*" it's just how the value of `this` works. Check the duplicate links. – VLAZ Jul 05 '21 at 14:48
  • @VLAZ Yes, it would. Getting references to different properties based a condition is the entire point of a conditional operator— [It doesn't matter whether the values are functions or numbers or whatever](https://en.wikipedia.org/wiki/First-class_citizen), and as the conditional operator is itself just an expression, it doesn't matter whether you add multiply or call or do anything else to them either. Calling the functions inside the conditional operator, however, breaks that model (and means writing the argument list twice)... Plus relying on lazy evaluation to avoid running both paths. – Will Chen Jul 05 '21 at 14:49
  • @WillChen I'm well aware that functions are first-class objects. I'm also well aware that you shouldn't be using this with *methods*. For reasons that you've found out by trying it and getting an error. It's *functions* that are first-class, methods are functions with a `this` value. Said value is determined at call time. Which feats neatly into the whole ecosystem, as you then need *less* function objects floating around, instances can share a single one and calling `a.m()` or `b.m()` determines whether you get `this = a` or `this = b` when the function is called. – VLAZ Jul 05 '21 at 14:55
  • @VLAZ Sure. That would have been a much more useful and decent response than a rant about me being held at gunpoint. In some dynamic languages (namely Python), methods *are* wrapped into first class objects— hence the question, and the "language quirks". – Will Chen Jul 05 '21 at 15:06

1 Answers1

4

It has to do with the way scope works in JavaScript, specifically how the value of this is bound.

The main thing to understand is that (in general) when you invoke a function, this is set to the object on which the method was invoked.

wookie.eatBanana(); // inside the eatBanana method, this === wookie

But if you invoke the method separately from the object, this ends up being undefined:

const detachedFn = wookie.eatBanana;

// same function, but now this === undefined
detachedFn(); 

And what you're doing with this.MethA in the ternary effectively detaches it from "this". It's effectively doing this:

MethB() {
  const method = true ? this.MethA : null;
  method(); // invoked standalone, so "this" is undefined inside MethA.
}

There are a variety of ways around this if that's not the desired behavior.

Arrow functions inherit the current scope, so if you change MethA to an arrow function you'll notice this issue goes away:

MethA = () => {
  console.log(this);
  console.log(this.prop);
}

const method = this.MethA;
method(); // this is fine, because arrow functions are bound to the scope in which they are declared

Or you can use bind, call, or apply to specify the scope explicitly:

const method = this.MethA.bind(this); // new function pre-bound to 'this'
MethA.call(this); // invoke with a specific scope
MethA.apply(this); // same as above; handles args a bit differently
Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
ray
  • 26,557
  • 5
  • 28
  • 27