tc39 / proposal-bind-operator

This-Binding Syntax for ECMAScript
1.75k stars 30 forks source link

Support a syntax for calling `apply`? #11

Closed krilnon closed 9 years ago

krilnon commented 9 years ago

I was thinking about this proposal in the context of existing code like this:

let array1 = [1,2,3]
let array2 = [4,5,6]

array1.push.apply(array1, array2)

console.log(array1) // [1,2,3,4,5,6]

...where the goal is to append elements from one existing array to another existing array (without creating a new one like concat would.)

I'd want to write something like ::console.log('hi'), but today that generates console.log.call(console, 'hi'). So I wonder if it'd be useful to have a similar operator that calls Function.prototype.apply, like :::console.log(['hi']), which would translate to console.log.apply(console, ['hi']).

I would have used my original example of appending to an array, but the :: implementation, at least based on my somewhat limited knowledge of it, seems a bit flawed in the case below, since it gives array2 as an argument to call before array2 is ever initialized:

let array1 = [1,2,3]
let array2 = eval(prompt()) // enter [4,5,6]

::array1.push(array2)

console.log(array1)
"use strict";

var _context;

var array1 = [1, 2, 3];
var array2 = (_context = eval(prompt()), array1.push).call(_context, array2);

console.log(array1);

Anyway, just a thought. Happy to hear criticisms or reasons why I don't get the point of your proposal.

zenparsing commented 9 years ago

Hi @krilnon , thanks for the feedback!

Let's start with you first snippet. This function bind proposal doesn't really address that use case. Instead, you can use spread arguments:

let array1 = [1,2,3];
let array2 = [4,5,6];
array1.push(...array2); // Note the spread argument
console.log(array1);

Generally, you can use spread arguments where you might previously have used Function.prototype.apply.

Your second snippet actually has an ASI problem. Remember, :: is also a binary operator. When you type:

let array2 = eval(prompt()) // enter [4,5,6]
::array1.push(array2)

you are saying the same thing as:

let array2 = eval(prompt())::array1.push(array2)

Let's back up a bit though.

There are two use cases for this proposal. The first is autobinding the "this" value in methods.

Consider this code:

let obj = {
    x: 100,
    foo() { console.log("My value of x is: " + this.x) },
};

setTimeout(obj.foo, 100);
// Logs 'My value of x is: undefined'

Why does it log "undefined"? Well, when we pass obj.foo to setTimeout, we're passing a function with an unbound this value. The this value will be whatever the caller calls the function with, and setTimeout calls it with [Window]. With the unary operator ::, we can auto-bind the method and get the desired behavior.

setTimeout(::obj.foo, 100);
// Logs 'My value of x is: 100'

But :: can also be used as a binary operator. In that case, all we are doing is taking the function on the right hand side, and creating a bound function from it where the this value is bound to whatever is on the left.

let obj = { x: 100 };
function foo() { console.log("My value of x is: " + this.x) }
let fn = obj::foo; // Bind foo, using "obj" as the `this` value
fn(); // Logs 'My value of x is: 100'

The main use case for this is "extension methods". We can create functions that act and feel like methods but don't have to be actually "on" the object.

// Here is a domain-specific Array method
function pushValuesFromPrompt() {
    let x = promptForInput(); // Get a line of CSV from standard in...
    this.push(...x.split(/\s*,\s*/g)); // Push those values onto the array
    return this; // Return the array
}

// Using "::", we can use it like a method, without having to hang it off
// of Array.prototype.
["a", "b", "c"]::pushValuesFromPrompt();

Hopefully that helps? Let me know what you think.

krilnon commented 9 years ago

Instead, you can use spread arguments: Ah, right. I forgot about that operator. Yeah, it totally covers my use case.

Your second snippet actually has an ASI problem. Remember, :: is also a binary operator. When you type:

Yep. I should have seen that too. My intuition for when ASI is going to be a problem hasn't adjusted to some of the newer operators.

Hopefully that helps? Let me know what you think.

It does! I read through all of your examples and the value of the operator seems pretty clear to me now. In particular, the case of setTimeout(obj.foo, 100); was elucidating, because it made clear how the callsite-binding nature of JS makes the case annoying. (Because if you called setTimeout(_ => obj.foo(), 100) instead, you'd get the expected method-like behavior.)

(Having used ActionScript years ago, it's sometimes jarring to have to remember that even ES6+ doesn't have many cases where functions used as methods are actually bound like one might expect methods to be.)

Thanks for explaining all of this to me. I'll go ahead and close this issue since you resolved my question about apply with the spread operator.