tc39 / proposal-call-this

A proposal for a simple call-this operator in JavaScript.
https://tc39.es/proposal-call-this/
MIT License
121 stars 5 forks source link

Import Gist that compares with Extensions proposal #9

Closed js-choi closed 3 years ago

js-choi commented 3 years ago

I need to integrate this old Gist that compares this proposal with @hax’s Stage-1 Extensions proposal. There are a few problems with the Gist, too.

The biggest problem is, as per the Matrix discussion from two days ago and also discussion in #8, the document focuses too much on the security use case.

I’m going to redo this document to focus more on the general frequency of bind/call and how clunky they currently are.

js-choi commented 3 years ago

@acutmore left a comment on the Gist, to which I’m replying here:

// The adversary’s code. delete Set.prototype.slice;

I think this was supposed to be delete Set.prototype.has :)

Yes, the delete Set.prototype.slice is a mistake; thanks for catching it!

There is currently no way to harden trusted code against an adversary’s mutation of Function.prototype.call or Function.prototype.bind.

This is not entirely true. It can be hardened against it just relies on being able to run code before anyone else (just like capturing any global/method before it can be modified).

// at start before other code runs:
const uncurryThis = Function.bind.bind(Function.call);
  // f => Function.call.bind(f)
  // f => (…args) => f.call(…args)

// or
const {apply} = Reflect;
const uncurryThis = f => (that, …args) => apply(f, that, args);

// then
const setHas = uncurryThis(Set.prototype.has);

// later
setHas(new Set([1]), 1); // true

It is true that guarding against Function.prototype.call mutation is possible with Function.bind.bind. This is another old mistake which I’ll need to fix. This guard requires wrapping every global method you use in an uncurried-this wrapper function, à la @ljharb’s call-bind library, which is transitively used by a ton of websites.

So the argument that I need to make is that (1) this intrinsic call-binding is just one common use case of a very commonly used pair of functions (bind/call), and (2) despite its frequency, using bind/call is currently clunky and its ergonomics should be optimized with an operator.

I do anticipate that the biggest argument against this will be “the method-wrapping is fine; just use the pipe operator |> if you want a fluent postfix expression”. I am still trying to think of ways to respond to that reasonable objection. But first I need to modify and import this Gist.

ljharb commented 3 years ago

to me it’s really not about the postfix part, i just need a way to syntactically change the receiver of a function - it’s just that once that exists, it’s kind of silly to force the awkward ordering of .call when we could easily have natural o.a(b) ordering.

js-choi commented 3 years ago

i just need a way to syntactically change the receiver of a function

@ljharb: People will probably say, “Why do you need syntax to change your function’s receiver? Why not just keep using Function.call or your uncurryThis/callBind wrapper?” It’s already kind of happened in that Matrix conversation, too.

The best argument against that anti-argument that I can think of is developer ergonomics. Using Function.call/uncurryThis/callBind is weird, clunky, and obscuring. o->fn(a) or whatever is more ergonomic than Function.call(fn, o) because it’s less wordy and because the ordering resembles o.fn(a).

If it weren’t for that ergonomic difference, then it’s like what @acutmore said: everything is already possible in userland without syntax. (Possible inherent performance problems with wrapped functions notwithstanding, cf. #8.)

ljharb commented 3 years ago

Part of it is robustness, and part of it is ergonomics. We have indisputable evidence that the current state of things is not ergonomic; nobody uses call/bind/apply when an arrow function or unsafe call suffices.