13

I'm using the extremely useful local fat arrow to preserve this context in callbacks. However, sometimes I need to access the value that this would've had if I hadn't used the fat arrow.

One example are event callbacks, where this has the value of the element that the event happened on (I'm aware that in this particular example you could use event.currentTarget, but lets assume you can't for the sake of an example):

function callback() {
    // How to access the button that was clicked?
}

$('.button').click(() => { callback() });

Note: I've come across this question which deals with this exact same issue, but in CoffeeScript.

Community
  • 1
  • 1
fstanis
  • 5,234
  • 1
  • 23
  • 42
  • 2
    Use a normal function, and store the outer `this` in a variable. – Oriol Aug 15 '15 at 13:57
  • "...and lexically binds the this value" https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Functions/Arrow_functions – coma Aug 15 '15 at 16:47
  • Don't forget about good ol' `event.currentTarget` to get the event target regardless of the `this` binding (note that it will be an `HTMLElement`, not a jQuery object). – Jared Smith Aug 15 '15 at 21:36

3 Answers3

3

You could write a decorator function that wraps a fat-arrow function inside another function which allows the access to the usual this and passes that value to the fat-arrow function as an additional argument:

function thisAsThat (callback) {
    return function () {
        return callback.apply(null, [this].concat(arguments));
    }
}

So when you call thisAsThat with a fat-arrow function, this basically returns a different callback function that, when called, calls the fat-arrow function with all the arguments but adds this as an argument in the front. Since you cannot bind fat-arrow functions, you can just call bind and apply on it without having to worry about losing the value.

You can then use it like this:

element.addEventListener('click', thisAsThat((that, evt) => console.log(this, that, evt)));

This will log the this of the current scope (as per fat-arrow rules), the this of the callback function as that (pointing to the element for event handlers), and the event itself (but basically, all arguments are still passed on).

poke
  • 369,085
  • 72
  • 557
  • 602
  • 1
    Thanks - this is such a simple & clever idea. One question though - is `.bind` really needed? I could (as I see it) achieve the same effect with `.apply` alone, like this: `function thisAsThat(callback) { return function() { return callback.apply(null, [this].concat(arguments)); }; }`. Any problems with this approach in your opinion? – fstanis Aug 16 '15 at 12:25
  • Yes, that should work just as well. I just used `bind` so I wouldn’t have to play around with `arguments` (since it’s not an array but just “array-like”). But yeah, using `concat` like that seems like a good idea. I’ll adjust my answer to use that instead :) – poke Aug 16 '15 at 12:28
1

You don't need to use arrow functions for that purpose.

You can simply use the Function's bind to change the function's scope:

function callback() {
  // How to access the button that was clicked?
  $("span").text(this.text());
}

var b = $('.button'); // in Typescript you should be using let instead of var
b.click(callback.bind(b));
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<button class='button'>Hello, world!</button>
<span></span>

For other complex scenarios where you want to use both arrow functions and call other functions in the same context, you can use Function's call or apply:

// let's suppose your callback function expects a Date and a Number param
$('.button').click(() => callback.call(this, new Date(), 23));
// or
$('.button').click(() => callback.apply(this, [new Date(), 23]));
Buzinas
  • 11,597
  • 2
  • 36
  • 58
  • 3
    OP wants to access both `this` (the one of the outer scope, and the one of the callback) at the same time, ideally from just a fat-arrow function. What you are doing is just rebinding `this` for a “full” callback function. – poke Aug 16 '15 at 12:12
  • Indeed, this solution works *only* if, at the time you register the event handler, you have access to the value you want `this` to have when the event is triggered. Consider the case of delegated events, such as `$('#button-container').on('click', '.button', callback)`. In that case, you couldn't know in advance which button was clicked and you can't `.bind` it to an argument (in fact, `this` would actually have many possible values, depending on the buttons that exist). – fstanis Aug 16 '15 at 12:39
  • Oh, sorry, I've misunderstood the question. Thanks for clarifying! – Buzinas Aug 16 '15 at 17:44
0

@poke's answer is the right idea, but there are a couple issues:

function thisAsThat (callback) {
    return function () {
        return callback.apply(null, [this].concat(arguments));
    }
}

First, arguments is not a true array, so calling concat() as shown above will result in a nested array:

[0] = this
[1] = [ [0] = param1, [1] = param2, ... ]

To fix this, use slice() to convert arguments to a true array:

        return callback.apply(null, [this].concat(Array.prototype.slice.call(arguments)));

Result:

[0] = this
[1] = param1
[2] = param2
...

Second, passing null for the first parameter to apply() didn't work for me; I had to pass class Foo's this explicitly. Here's a complete example:

class Foo {

    public setClickHandler(checkbox: JQuery): void {
        checkbox.click(this.captureThis(this.onCheckboxClick));
    }

    protected onCheckboxClick(checkbox: HTMLInputElement, event: Event) {
        // 'this' refers to class Foo
    }

    protected captureThis(callback) {
        var self = this;
        return function () {
            return callback.apply(self, [this].concat(Array.prototype.slice.call(arguments)));
        }
    }

}

With this approach, the onCheckboxClick() callback has access to both the class this and the checkbox element (as the first parameter) that would have been this in a typical click event callback. The downside to using captureThis() is that you lose TypeScript's type safety for the callback, so Typescript can't stop you from messing up the callback function signature.

Community
  • 1
  • 1
cbranch
  • 4,709
  • 2
  • 27
  • 25
  • It appears that something changed in the implementation, as I remember the `[this].concat(...)` approach worked in the past. Also, I assume that passing `null` as the first argument didn't work for you since your approach replaces the fat arrow functions altogether (as you are basically replicating their functionality). If you use fat arrow functions, you can keep the type safety of it. – fstanis Feb 22 '16 at 18:04
  • But you _can't_ use fat arrow because it would rewrite `this` in `[this].concat(...)` to refer to class Foo instead of the checkbox. – cbranch Feb 22 '16 at 19:42