|
| 1 | +## The Problem |
| 2 | + |
| 3 | +You have the classic JavaScript problem known as the *incorrect `this` context*. |
| 4 | +The [`this` keyword in JavaScript][1] behaves differently than in does in other languages like C# and Java. |
| 5 | + |
| 6 | +<!-- Call out the exact line here --> |
| 7 | + |
| 8 | +### How `this` works |
| 9 | + |
| 10 | +The `this` keyword, in a function, is determined as follows: |
| 11 | + * If the function was created through a call to `.bind`, the `this` value is the argument provided to `bind` |
| 12 | + * If the function was *invoked* through a method call, e.g. `expr.func(args)`, then `this` is `expr` |
| 13 | + * Otherwise |
| 14 | + * If the code is in [*strict mode*][2], `this` is `undefined` |
| 15 | + * Otherwise, `this` is `window` (in a browser) |
| 16 | + |
| 17 | +Let's look at how this works in practice: |
| 18 | + |
| 19 | + class Foo { |
| 20 | + value = 10; |
| 21 | + doSomething() { |
| 22 | + // Prints 'undefined', not '10' |
| 23 | + console.log(this.value); |
| 24 | + } |
| 25 | + } |
| 26 | + let f = new Foo(); |
| 27 | + window.setTimeout(f.doSomething, 100); |
| 28 | + |
| 29 | +This code will print `undefined` (or, in strict mode, throw an exception). |
| 30 | +This is because we ended up in the last branch of the decision tree above. |
| 31 | +The `doSomething` function was invoked, the function wasn't a result of a `bind` call, and it wasn't invoked in a method syntax position. |
| 32 | + |
| 33 | +We can't see the code for `setTimeout` to see what its invocation looks like, but we don't need to. |
| 34 | +Something to realize is that all `doSomething` methods point to *the same function object*. |
| 35 | +In other words: |
| 36 | + |
| 37 | + let f1 = new Foo(); |
| 38 | + let f2 = new Foo(); |
| 39 | + // 'true' |
| 40 | + console.log(f1.doSomething === f2.doSomething); |
| 41 | + |
| 42 | +We know that `setTimeout` can only see the function we passed it, so when it invokes that function, |
| 43 | + there's no way for it to know which `this` to provide. |
| 44 | +The `this` context has been lost due to our *referencing* the method without *invoking* it. |
| 45 | + |
| 46 | +### The Red Flag |
| 47 | + |
| 48 | +Once you know about `this` problems, they're easy to spot: |
| 49 | + |
| 50 | + class Foo { |
| 51 | + value = 10; |
| 52 | + method1() { |
| 53 | + doSomething(this.method2); // DANGER, method reference without invocation |
| 54 | + } |
| 55 | + method2() { |
| 56 | + console.log(this.value); |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | +## The Solution |
| 61 | + |
| 62 | +You have a few options here, each with its own trade-offs. |
| 63 | +The best option depends on how often the method in question is invoked from differing call sites. |
| 64 | + |
| 65 | +### Arrow Function in Class Definition |
| 66 | + |
| 67 | +Instead of using the normal method syntax, use an [arrow function][3] to initialize a per-instance member. |
| 68 | + |
| 69 | + class DemonstrateScopingProblems { |
| 70 | + private status = "blah"; |
| 71 | + |
| 72 | + public run = () => { |
| 73 | + // OK |
| 74 | + console.log(this.status); |
| 75 | + } |
| 76 | + } |
| 77 | + let d = new DemonstrateScopingProblems(); |
| 78 | + window.setTimeout(d.run); // OK |
| 79 | + |
| 80 | + * Good/bad: This creates an additional closure per method per instance of your class. If this method is usually only used in regular method calls, this is overkill. However, if it's used a lot in callback positions, it's more efficient for the class instance to capture the `this` context instead of each call site creating a new closure upon invoke. |
| 81 | + * Good: Impossible for external callers to forget to handle `this` context |
| 82 | + * Good: Typesafe in TypeScript |
| 83 | + * Good: No extra work if the function has parameters |
| 84 | + * Bad: Derived classes can't call base class methods written this way using `super.` |
| 85 | + * Bad: The exact semantics of which methods are "pre-bound" and which aren't create an additional non-typesafe contract between your class and its consumers. |
| 86 | + |
| 87 | +### Function Expression at Reference Site |
| 88 | + |
| 89 | +Shown here with some dummy parameters for explanatory reasons: |
| 90 | + |
| 91 | + class DemonstrateScopingProblems { |
| 92 | + private status = "blah"; |
| 93 | + |
| 94 | + public something() { |
| 95 | + console.log(this.status); |
| 96 | + } |
| 97 | + |
| 98 | + public run(x: any, y: any) { |
| 99 | + // OK |
| 100 | + console.log(this.status + ': ' + x + ',' + y); |
| 101 | + } |
| 102 | + } |
| 103 | + let d = new DemonstrateScopingProblems(); |
| 104 | + // With parameters |
| 105 | + someCallback((n, m) => d.run(n, m)); |
| 106 | + // Without parameters |
| 107 | + window.setTimeout(() => d.something(), 100); |
| 108 | + |
| 109 | + * Good/bad: Opposite memory/performance trade-off compared to the first method |
| 110 | + * Good: In TypeScript, this has 100% type safety |
| 111 | + * Good: Works in ECMAScript 3 |
| 112 | + * Good: You only have to type the instance name once |
| 113 | + * Bad: You'll have to type the parameters twice |
| 114 | + * Bad: Doesn't easily work with variadic parameters |
| 115 | + |
| 116 | + [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this |
| 117 | + [2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode |
| 118 | + [3]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions |
0 commit comments