tc39 / proposal-bind-operator

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

::object.func !== ::object.func #17

Closed lukescott closed 8 years ago

lukescott commented 9 years ago

I'm concerned that this syntax can be very misleading. For example:

var object = {
  clickHandler() {}
}

element.addEventListener("click", ::object.clickHandler);
// later in the code...
element.removeEventListener("click", ::object.clickHandler); // doesn't work!

console.log(::object.clickHandler === ::object.clickHandler); // false

It's not immediately clear the two are not the same thing - something new is being created each time, which is being masked by the hidden bind.

zenparsing commented 9 years ago

@lukescott I think you want to bind to this, not target. See @sebmck 's implementation.

lukescott commented 9 years ago

@zenparsing this is undefined. As far as I can tell target is the prototype not the instance.

lukescott commented 9 years ago

Take a look at this REPL.

lukescott commented 9 years ago

This is the closest thing I can come up with with a getter, and inheritance is still broken:

class Person {
  get logthis() {
    let fn = () => {
      console.log(this.constructor.name); 
    };
    Object.defineProperty(this, "logthis",  fn);
    return fn;
  }
}

class Luke extends Person {
  logthis() {
    super.logthis();
  }
}

var luke = new Luke;
var logthis = luke.logthis;
logthis();

I get Object.defineProperty called on non-object.

My original solution works with inheritance though:

var Person = (function () {
  function Person() {
    _classCallCheck(this, Person);

    this.logthis = this.logthis.bind(this);
  }

  Person.prototype.logthis = function logthis() {
    console.log(this.constructor.name);
  };

  return Person;
})();

var Luke = (function (_Person) {
  function Luke() {
    _classCallCheck(this, Luke);

    if (_Person != null) {
      _Person.apply(this, arguments);
    }
  }

  _inherits(Luke, _Person);

  Luke.prototype.logthis = function logthis() {
    console.log(this.constructor.name);
    _Person.prototype.logthis.call(this);
  };

  return Luke;
})(Person);

var luke = new Luke();
var logthis = luke.logthis;
logthis();

http://jsfiddle.net/8t0ezphd/

I get Luke twice in console.log on the original method and the override. The above code suggared is:

class Person {
  logthis() => {
    console.log(this.constructor.name);
  }
}

class Luke extends Person {
  logthis() {
    console.log(this.constructor.name);
    super.logthis();
  }
}

var luke = new Luke;
var logthis = luke.logthis;
logthis();

Although it is a bit odd that the override doesn't have => as well. But it works without it.

Am I missing something here?

The alternative is going back to ::object.method() or object->method() and using call/apply. But then we have the memorization issue. Imagine if someone used that syntax in a loop without knowing any better.

zenparsing commented 9 years ago

@lukescott There may be something going on with babel's translation there.

zenparsing commented 9 years ago

@lukescott ah, I think I know what's going on there : )

The overriding function is just a plain value, not a getter. Need to think through this a bit more...

lukescott commented 9 years ago

@zenparsing If binding is slow, would something like this work better?

var Person = (function () {
  function Person() {
    _classCallCheck(this, Person);

      var _this = this;
      var _logthis = this.logthis;
      Object.defineProperty(_this, "logthis", {
          configurable: true,
          get: function() {
              var fn = _logthis.bind(_this);
              Object.defineProperty(_this, "logthis", {
                  configurable: true,
                  value: fn 
              });
              return fn;
          }
      });
  }

  Person.prototype.logthis = function logthis() {
    console.log(this.constructor.name);
  };

  return Person;
})();

var Luke = (function (_Person) {
  function Luke() {
    _classCallCheck(this, Luke);

    if (_Person != null) {
      _Person.apply(this, arguments);
    }
  }

  _inherits(Luke, _Person);

  Luke.prototype.logthis = function logthis() {
    console.log(this.constructor.name);
    _Person.prototype.logthis.call(this);
  };

  return Luke;
})(Person);

var luke = new Luke();
var logthis = luke.logthis;
logthis();

http://jsfiddle.net/8t0ezphd/1/

zenparsing commented 9 years ago

@lukescott OK, so if you autobind a method with a decorator on a parent class, that doesn't mean that overriding methods are autobound on the child class. It makes sense now that I've thought about it, but seems a bit surprising at first glance.

lukescott commented 9 years ago

@zenparsing Also with the decorator the target is the prototype, not the instance. So that's another issue with the decorator, at least with how it currently works. I think that's how the decorator is supposed to work, right?

zenparsing commented 9 years ago

@lukescott It is, and that's the only way it can be really, since no instances exist at the time the decorator is applied. But for autobind you'd really need to bind to this.

lukescott commented 9 years ago

Would any of these w/ method() => {} be appropriate then?:

http://jsfiddle.net/8t0ezphd/ -- does binding unfront in constructor http://jsfiddle.net/8t0ezphd/1/ -- applies getter in constructor and does binding on-demand

Inheritance works on both.

erights commented 9 years ago

Both have multiplicative allocation costs.

lukescott commented 9 years ago

@erights can you elaborate on that? With those examples it only happens in the base class. When you override the method in a subclass the binding still sticks. The second one is on-demand, so it only happens when the method is actually used. And it's an opt-in feature for the base class - for event handlers.

erights commented 9 years ago

Are y'all using a plugin or something that I'm missing? These fiddles look like babel output, not es6.

With those examples it only happens in the base class.

  function Person() {
    _classCallCheck(this, Person);

    this.logthis = this.logthis.bind(this);
  }

The base class constructor is called per instance, so anything it does per instance is a per instance cost. The .bind call above allocates. Since you have a line like this for the logthis method, I presume you would have one for every method. Thus, allocation cost is numInstances*numMethods.

When you override the method in a subclass the binding still sticks.

I withdraw the objection about inheritance. When the super constructor returns with a bound logthis on the instance, the sub constructor will overwrite it with the bound form of the overriding logthis. The key is that all the methods themselves are still on the prototype in unbound form, so the overriding bound method can super-delegate to the overridden unbound method on the super prototype.

Because of my previous objects-as-closures based attempts, I overlooked that the per-instance bound methods can still use the unbound methods on the prototype. It is a bit weird that they exist in both places, but it does work.

The second one is on-demand, so it only happens when the method is actually used.

  function Person() {
    _classCallCheck(this, Person);

      var _this = this;
      var _logthis = this.logthis;
      Object.defineProperty(_this, "logthis", {
          configurable: true,
          get: function() {
              var fn = _logthis.bind(_this);
              Object.defineProperty(_this, "logthis", {
                  configurable: true,
                  value: fn 
              });
              return fn;
          }
      });
  }

This creates a separate getter per method per instance. It is still just as multiplicative.

lukescott commented 9 years ago

@erights I'm using babel repl output and then modifying the output for proof-of-concept.

The base class constructor is called per instance, so anything it does per instance is a per instance cost. The .bind call above allocates. Since you have a line like this for the logthis method, I presume you would have one for every method. Thus, allocation cost is numInstances*numMethods.

Are you assuming that numMethods is every method in the class? Given that this is an opt-in feature, that number can be 0. For example, the following would be numInstances * 1:

class Foo {
  handleEvent() => {
    // this method is always bound to the instance
  }

  someMethod() {
    // this is a normal method. this changes if copied.
  }
}

So numMethods should really be numBoundMethods. You only pay the cost for callback methods (event handlers and such). I'm not sure you can get around that cost.

Even with ::object.function you're paying that cost:

element.attachEventListener("change", ::this.handleChangeEvent);

And without memorization you pay that cost multiple times in certain situations:

for (let element of elements) {
  document.attachEventListener("change", ::this.handleChangeEvent);
}

And without memorization this doesn't work:

element.removeEventListener("change", ::this.handleChangeEvent);

(I'm using the existing prefix ::, but it could easily be this->handleChangeEvent -- makes no difference)

With method() => {} you can do just this:

element.attachEventListener("change", this.handleChangeEvent);
// and later
element.removeEventListener("change", this.handleChangeEvent);

The allocation cost is paid when emulating the feature in pre-ES7. Couldn't method() => {} be done differently in the JavaScript engine implementation in such a way where there the original method has a permanent context attached to it? Perhaps something like:

Object.defineProperty(Foo.prototype, "logthis", {
  autobind: true,
  value: function() {...}
});
erights commented 9 years ago

What's the problem again with the original @autobind annotation exactly as originally defined? In Babel I just tried

function autobind(target, key, descriptor) {
  return {
    get: function () {
      return descriptor.value.bind(this);
    }
  };
}

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName  = lastName;
  }

  @autobind
  getName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

class FormalPerson extends Person {
  constructor(title, firstName, lastName) {
    super(firstName, lastName);
    this.title = title;
  }

  @autobind
  getName() {
    return `${this.title}. ${super.getName()}`;
  }
}

class SillyPerson extends Person {
  constructor(title, firstName, lastName) {
    super(firstName, lastName);
    this.title = title;
  }

  getName() {
    return `${this.title}. ${super.getName()}`;
  }
}

console.log(new FormalPerson("Mr", "John", "Doe").getName.call(null)); // "Mr. John Doe"
console.log(new SillyPerson("Honorable", "John", "Doe").getName()); // "Honorable. Jon Doe"

and got the results shown in the comments at the end.

erights commented 9 years ago

Let's see if this works:

https://babeljs.io/repl/#?experimental=true&evaluate=true&loose=false&spec=false&code=function%20autobind(target%2C%20key%2C%20descriptor)%20%7B%0A%20%20return%20%7B%0A%20%20%20%20get%3A%20function%20()%20%7B%0A%20%20%20%20%20%20return%20descriptor.value.bind(this)%3B%0A%20%20%20%20%7D%0A%20%20%7D%3B%0A%7D%0A%0Aclass%20Person%20%7B%0A%20%20constructor(firstName%2C%20lastName)%20%7B%0A%20%20%20%20this.firstName%20%3D%20firstName%3B%0A%20%20%20%20this.lastName%20%20%3D%20lastName%3B%0A%20%20%7D%0A%0A%20%20%40autobind%0A%20%20getName()%20%7B%0A%20%20%20%20return%20%60%24%7Bthis.firstName%7D%20%24%7Bthis.lastName%7D%60%3B%0A%20%20%7D%0A%7D%0A%0Aclass%20FormalPerson%20extends%20Person%20%7B%0A%20%20constructor(title%2C%20firstName%2C%20lastName)%20%7B%0A%20%20%20%20super(firstName%2C%20lastName)%3B%0A%20%20%20%20this.title%20%3D%20title%3B%0A%20%20%7D%0A%20%20%0A%20%20%40autobind%0A%20%20getName()%20%7B%0A%20%20%20%20return%20%60%24%7Bthis.title%7D.%20%24%7Bsuper.getName()%7D%60%3B%0A%20%20%7D%0A%7D%0A%0Aclass%20SillyPerson%20extends%20Person%20%7B%0A%20%20constructor(title%2C%20firstName%2C%20lastName)%20%7B%0A%20%20%20%20super(firstName%2C%20lastName)%3B%0A%20%20%20%20this.title%20%3D%20title%3B%0A%20%20%7D%0A%20%20%0A%20%20getName()%20%7B%0A%20%20%20%20return%20%60%24%7Bthis.title%7D.%20%24%7Bsuper.getName()%7D%60%3B%0A%20%20%7D%0A%7D%0A%0Aconsole.log(new%20FormalPerson(%22Mr%22%2C%20%22John%22%2C%20%22Doe%22).getName.call(null))%3B%20%2F%2F%20%22Mr.%20John%20Doe%22%0Aconsole.log(new%20SillyPerson(%22Honorable%22%2C%20%22John%22%2C%20%22Doe%22).getName())%3B%20%2F%2F%20%22Honorable.%20Jon%20Doe%22%0A

erights commented 9 years ago

Since

  1. @autobind has no multiplicative cost
  2. @autobind has a semantics I'd prefer just about everywhere compared to the current semantics
  3. Calling a method (foo.getName()) is vastly more frequent by both static and dynamic frequency than is extracting a method by name from an instance (foo.getName)
  4. With reasonable and local optimizations, @autobind need not add any costs to method calling
  5. @autobind adds allocation costs only on extracting a method by name, which is rare and does not need to be high performance
  6. @autobind's semantic cost is only that foo.getName !== foo.getName
  7. @autobind can also bundle in a workaround for the override mistake,

I will start to use @autobind essentially all the time. In retrospect, I dearly wish we had made it the default. Thus, I think we should evaluate it under the assumption that it is at least a common use case, and perhaps even the more common one.

lukescott commented 9 years ago

@erights this doesn't work:

var silly = new SillyPerson("Honorable", "John", "Doe");
var getName = silly.getName;
console.log(getName());

I get Cannot read property 'title' of undefined.

Neither does this:

console.log(new SillyPerson("Honorable", "John", "Doe").getName.call(null)); // "Honorable. Jon Doe"

This works because you're doing autobind on the subclass:

console.log(new FormalPerson("Mr", "John", "Doe").getName.call(null)); // "Mr. John Doe"

The autobind needs to be inherited without having to do it on the subclass.

erights commented 9 years ago

The autobind needs to be inherited without having to do it on the subclass.

Why? Even implementation aside, this is not what I would expect. The subclass is overriding it, so the subclass is in control. If the subclass says the method is not auto-bound, I would not expect it to be autobound. IMO, the behavior of your "counter-example" is exactly correct and anything else would be broken.

lukescott commented 9 years ago

@erights That is broken behavior. If a method is meant to be a callback for an event handler and someone overrides that method in a subclass the method is still a callback. I would find that behavior very surprising.

For example:

class Foo {
  constructor() {
      // this is broken in Foo2!
      element.attachEventListener("change", this.handleChangeEvent);
  }

  @autobind
  handleChangeEvent() {
    // ...
  }
}

class Foo2 extends Foo {
  handleChangeEvent() {
      // ... this is no longer auto bound!
  }
}

new Foo2(document.getElementsByClassName("foo")[0]);

I'm not ok with that. Breaking inheritance is broken.

The ::object.method proposal did not have this problem:

class Foo {
  constructor() {
      // still works, still passes a bound function
      element.attachEventListener("change", ::this.handleChangeEvent);
  }

  handleChangeEvent() {
    // ...
  }
}

class Foo2 extends Foo {
  handleChangeEvent() {
      // ...
  }
}

new Foo2(document.getElementsByClassName("foo")[0]);

What I suggested is doing is basically what ::object.method does, it just moves the binding to the class definition and changes ::object.method !== ::object.method into object.method === object.method.

IMO, numInstances * numBoundMethods allocation is acceptable, especially when we're talking about single methods ES5-ES6. I would be more worried about function call performance w/ a bind. ES7 could optimize that allocation away.

erights commented 9 years ago

I have run out of time tonight. I hope to get to the rest of it some other time. For now I will respond only to

That is broken behavior. If a method is meant to be a callback for an event handler and someone overrides that method in a subclass the method is still a callback. I would find that behavior very surprising.

If the subclass overrides it to no longer be a callback for an event handler, then it isn't. If the subclass overrides it to be a dog catcher, then it is a dog catcher. We do not enforce that subclasses must honor the contract associated with the superclass. The "being a callback" aspect of a method is not "final" -- we have no "final".

lukescott commented 9 years ago

If the subclass overrides it to no longer be a callback for an event handler, then it isn't. If the subclass overrides it to be a dog catcher, then it is a dog catcher. We do not enforce that subclasses must honor the contract associated with the superclass. The "being a callback" aspect of a method is not "final" -- we have no "final".

Can you provide a use-case where a callback isn't going to be a callback in a subclass? What % of this use-case makes up the majority of what users are going to want it for? Can you provide justification of forcing someone to re-declare autobind for every subclass?

As per the example in my previous post, knowledge about the method being a callback is in the superclass:

class Foo {
  constructor() {
      // this is broken in Foo2!
      element.attachEventListener("change", this.handleChangeEvent);
  }
//...
}

What is the likelihood that someone is going to forget to @autobind their method in the subclass?

erights commented 9 years ago

The number of use cases where a subclass should override something from the superclass in a way that violates all expectations associated with the superclass is approximately zero. Along with much of the rest of the world, I believe that the Liskov substitution principle (LSP) is a good design rule that should almost always be followed.

For what % of use cases would you not autobind a method, once autobinding is available and cheap? I think this is also approximately zero. Autobinding should have been the default. Since we missed this window for plain classes, our options are

  1. Add a checking rule to jshint that any method on a plain class not annotated with @noautobind be annotated with @autobind.
  2. Introduce other non-plain classes, like const classes or StrongScript classes, where we can get this default right.
  3. Invent a class annotation with similar effect: on such an annotated class, all methods not annotated with @noautobind default to autobound.

All of these have the effect of making a silent non-autobinding by mistake into a detectable hazard. This should include accidentally forgetting to autobind an override of an autobound inherited method.

When would you ever use @noautobind on purpose? I am sure there are cases, but I currently can't think of any.

Can you provide justification of forcing someone to re-declare autobind for every subclass? What is the likelihood that someone is going to forget to @autobind their method in the subclass?

Can you provide justification for forcing someone to say "const", "let", "var", or "function" on every variable declaration? What is the likelihood that someone will forget to say "const", "let", "var", or "function" when declaring a variable, but instead inadvertently write a plain assignment? I know I have done this more times than I can count. I am very happy that strict mode and now modules have made this hazard into a reported error.

lukescott commented 9 years ago

Class methods should have been bound by default: In principle, I agree. The reason this was not done by default is because it is not cheap, especially when emulating in ES5.

  1. Add a checking rule to jshint that any method on a plain class not annotated with @noautobind be annotated with @autobind.

That's crazy. Most methods are not passed as callbacks. Binding every method would double the call time of for all methods.

An event handler is a very specific use case where you have to bind. A use case we're actually trying to solve. I don't think it's unreasonable to say that any method defined as bound should stay bound in a subclass that chooses to override that method.

  1. Introduce other non-plain classes, like const classes or StrongScript classes, where we can get this default right.

And what does that look like with class Foo {}?

Can you provide justification for forcing someone to say "const", "let", "var", or "function" on every variable declaration? What is the likelihood that someone will forget to say "const", "let", "var", or "function" when declaring a variable, but instead inadvertently write a plain assignment? I know I have done this more times than I can count. I am very happy that strict mode and now modules have made this hazard into a reported error.

Apples and oranges. A variable context is in the same function - easier to spot. A superclass implementation is in another file or library. Overriding a superclass method should not break the method.

erights commented 9 years ago

The reason this was not done by default is because it is not cheap, especially when emulating in ES5.

I was there. I don't remember whether we considered it and if we did, I don't remember why we rejected it. It is indeed possible that it was a performance concern that I'm not getting.

However, I am confident that the performance of a transpiled-to-ES5 implementation was not a reason. We do not make decisions on that basis.

That's crazy. Most methods are not passed as callbacks. Binding every method would double the call time of for all methods.

It is crazy if it actually doubles the call times for methods. I bet that implementations, by recognizing their own getter, can keep the call times to essentially what they are now. In fact, implementations should eventually treat the bound case as the expected case, and if necessary make the unbound case pay for being the odd duck.

I don't think it's unreasonable to say that any method defined as bound should stay bound in a subclass that chooses to override that method.

Let's say we had gotten the default right and you had to say @noautobind to get the current behavior. In that case, if a superclass writes an unmarked (and therefore autobound) method and the subclass overrides it with an @noautobind, should the subclass' annotation be ignored since the superclass said (by silence) that the method should be autobound?

And what does that look like with class Foo {}?

const class Foo {}

Apples and oranges. A variable context is in the same function - easier to spot. A superclass implementation is in another file or library. Overriding a superclass method should not break the method.

Now that we have the option, choosing not to autobind a method is bad practice and should generally be assumed to break things. Once autobinding is cheap, why would you ever @noautobind? This is the central question that is so far unanswered.

BrendanEich commented 9 years ago

I hate OOP. It's all Java's fault that JS isn't this- and new-free, first-class functions only :-P.

Going down the OOP rabbit holes (plural), I see why @lukescott wants @autobind to be inherited even when overriding. Or really, everyone wants StrongScript classes. ES4 classes (AS3 in Flash/Flex), IIRC, worked the desired way.

@erights, we didn't add auto-binding to ES6 classes because we did not want to impose a hidden cost not found in prototypal-pattern (desugared) classes in ES5.

If a new mode a la strong/sound can help, great. Otherwise I think @erights and @zenparsing are on surer footing arguing for less OOP happiness as the price of straightforward (as if by desugaring) if not pellucid semantics: @autobind not inherited, and obj.foo !== obj.foo.

/be

erights commented 9 years ago

@BrendanEich This sounds plausible, but do you remember if we ever explicitly considered it?

pellucid

I had to look it up. Nice word. Thanks ;).

erights commented 9 years ago

@BrendanEich To clarify, I know we explicitly considered and rejected doing it automagically in the implementation, in such a way that obj.foo === obj.foo. This was part of my early class proposals based on making objects-as-closures or traits efficient. This is also closer to what I gather was in ES4. This would indeed have required new orthogonal implementation machinery that implementers were right to balk at. I understand why it died.

However, the getter technique of @autobind, done at the price of obj.foo !== obj.foo, is the idea that strikes me as new. It seems free of the problems that killed everything else I remember, and it does it within the ES5 object model with no new semantic state. Do you remember if we had ever explicitly considered it?

BrendanEich commented 9 years ago

We discussed it over the many years, at least a couple of times. I don't have time to search meeting notes on esdiscuss.org right now :-P.

/be

erights commented 9 years ago

Not asking for a record, just your memory. Thanks!

BrendanEich commented 9 years ago

The getter idea wasn't framed via decorators, but some of us have been around this corner of the design space and (I recall) we still didn't like the implicit cost. Also not observed in the wild in class desugarings or idiomatic prototypal pattern instances.

Decorators help. Opt-in FTW.

lukescott commented 9 years ago

@erights

Now that we have the option, choosing not to autobind a method is bad practice and should generally be assumed to break things. Once autobinding is cheap, why would you ever @noautobind? This is the central question that is so far unanswered.

I agree. You would never want @noautobind. If it was cheap, could you change classes in ES7 to @autobind by default? How much code would break? Does anyone rely on @noautobind?

This benchmark is not very encouraging though: http://jsperf.com/bind-vs-emulate/13.

Chrome 43.0.2357.124 on OS X 10.10.3
bind - 5,594,117 - 99% slower
emulate - 92,910,404 - 79% slower
simple - 93,837,139 - 79% slower
self - 441,522,153

At least under the assumption that every method was @autobind by default and emulated in ES5 (ES6?). That penalty is easier to swallow with opt-in on a per-method basis.

Re: @autobind on both superclass and subclass methods, can you take a look at this issue? I tried to memorize .bind as @sebmck suggested, but the method ends up being overwritten by the super-class implementation. Regardless, you end up with a .bind on each implementation ("get bind" logs twice). If doing .bind in the constructor, it only happens once and the correct method is always used.

Brendan Eich: Or really, everyone wants StrongScript classes.

Here here!

erights commented 9 years ago

Changing plain classes to autobind by default will never happen. I never suggested that. If you think I did, please reread my suggestions.

erights commented 9 years ago

Re look at this issue, as already discussed, that code does not work on frozen instances.

erights commented 9 years ago

If doing .bind in the constructor, it only happens once and the correct method is always used.

If doing .bind in the constructor, it still happens twice, but the subclass constructor overwrites the binding from the superclass constructor so the correct method is indeed always used. This was the "does not work with inheritance" objection that I already withdrew. You were right about that.

lukescott commented 9 years ago

Changing plain classes to autobind by default will never happen. I never suggested that. If you think I did, please reread my suggestions.

I did not. I was just asking the question: Who relies on @noautobind behavior? I know that making a change like that would be a breaking change. But, if as you say, such use cases are approximately zero, how much harm would such a change be? That is, writing every class as strict class Foo {} vs fixing class Foo {}.

Re look at this issue, as already discussed, that code does not work on frozen instances.

Ok, so without memorization on @autobind the cost per call to getName is:

n = number of times @autobind is used on the same method.

If doing .bind in the constructor, it still happens twice, but the subclass constructor overwrites the binding from the superclass constructor so the correct method is indeed always used. This was the "does not work with inheritance" objection that I already withdrew. You were right about that.

Just want to make sure I'm on the same page: Are you saying that .bind happens twice in the constructor w/ opt-in? With the example I provided it only happens once:

http://jsfiddle.net/8t0ezphd/2/

Assuming that the super-class does the binding and the subclass does not.

erights commented 9 years ago

how much harm would such a change be?

I don't know. It is a question worth asking. Data would be great. TC39 loves data, and data has a huge influence on our decisions.

In the absence of data, we rely on our intuitions informed by experience. We do occasionally make a bet, even without data, that a technically breaking change is not actually breaking, and that it is worth the cost of trying and seeing what happens. We have been very successful at that -- we have gotten away with many technically breaking changes without actually breaking the web. Of those we've decided to try, we have had to backtrack very few.

However, looking at the bets that we have and have not decided to try without data, I am rather sure this one is under threshold. The payoff is actually rather high, which is great, but my intuition about the probability of breakage is that it is also too high for us to take a chance on. Again, data could completely change the situation for the better.

n getter calls

What is n?

Just want to make sure I'm on the same page ... Assuming that the super-class does the binding and the subclass does not.

Sorry, I was responding to the other case -- the one where I consider your proposal to be doing the right thing, where both the subclass and superclass say they want autobinding. You are correct that for your only-super-autobinds case the binding only happens once and it still binds the right method.

However, I consider this a bug, not a feature. Looking at the subclass by itself, let's say we see a class whose getName method does not invoke super.getName. By normal understanding its getName behavior should then be self contained -- I should be able to figure out what it is locally. I look for other delegations from sub to super. Of course I necessarily see one in the constructor. I look at the super constructor. I see no mention of getName. Thus, it is surprising to me that the superclass has still intervened in the meaning of my getName.

For both of your cases, Object.getOwnPropertyNames(foo) has extra members. This is weird. It is at least as weird as foo.getName !== foo.getName. Since each proposal has such a weirdness, we can consider these a draw on this criteria.

lukescott commented 9 years ago

I don't know. It is a question worth asking. Data would be great. TC39 loves data, and data has a huge influence on our decisions. ... However, looking at the bets that we have and have not decided to try without data, I am rather sure this one is under threshold. The payoff is actually rather high, which is great, but my intuition about the probability of breakage is that it is also too high for us to take a chance on. Again, data could completely change the situation for the better.

How would we collect data on this?

What is n?

Number of levels @autobind is applied. So in the case of getName(), this would be 2. In the code sample I provided earlier "get bind" was emitted 2 times per call. Could be 3 if FormalPerson was subclassed and getName() was again reimplemented w/ autobind().

However, I consider this a bug, not a feature. Looking at the subclass by itself, let's say we see a class whose getName method does not invoke super.getName. By normal understanding its getName behavior should then be self contained -- I should be able to figure out what it is locally.

I suppose this is true for the getName() case. But getName() is also not a use-case where @autobind would be useful. No one will notice whether or not the method is bound or not. The method will likely be called off the class instance. This will be true for a majority of class methods.

I look for other delegations from sub to super. Of course I necessarily see one in the constructor. I look at the super constructor. I see no mention of getName. Thus, it is surprising to me that the superclass has still intervened in the meaning of my getName.

An inherited callback handler, such as handleChangeEvent(), would not be surprising.

The more we discuss this the more of an odd duck any form of @autobind is to me, inherited or not. It's surprising to me that any method would lose binding to the instance, whether it was an inherited method with @autobind or a brand new method.

I would rather pursue fixing class methods. We could test that theory with a babel/traceur plugin/option.

erights commented 9 years ago

How would we collect data on this? I would rather pursue fixing class methods. We could test that theory with a babel/traceur plugin/option.

Excellent. That seems like a great way to start.

ssube commented 9 years ago

@lukescott

Who relies on @noautobind behavior? I know that making a change like that would be a breaking change. But, if as you say, such use cases are approximately zero, how much harm would such a change be?

If I understand this correctly (bind methods to their instance by default, decorate to keep variable this?), a lot of older DOM-related libraries like jQuery use that behavior as the default. One of the examples for on is:

$( "#dataTable tbody tr" ).on( "click", function() {
  console.log( $( this ).text() );
});

How often that crops up in as a class method (formal ES6 class or IIFE pseudo-class), I'm not sure. I suspect most class methods used as event handlers are bound in some fashion to access their class, so this may not be a huge problem.

lukescott commented 9 years ago

@ssube

This would only be for ES6 classes. JavaScript objects would not change.

If I understand this correctly (bind methods to their instance by default, decorate to keep variable this?), a lot of older DOM-related libraries like jQuery use that behavior as the default. One of the examples for on is:

jQuery wraps DOM elements. A jQuery wrapper is returned from $( "#dataTable tbody tr" ) containing 0 or more elements. on is being called from the wrapper, so it is bound to the wrapper.

I'm not sure if this works:

var result = $( "#dataTable tbody tr" );
result.on( "click", function() {
  console.log( $( this ).text() );
});

But if it does they are likely doing their own binding. Regardless, they aren't using ES6 classes anyway.

The method binding is related to the ::object.method part of the original proposal.

The object::function part concerns jQuery. In #18 I proposed something like this:

import {on, text} from "jquery";

document.querySelectorAll("#dataTable tbody tr").(on, "click", () => {
  console.log(this.(text));
});

That would eliminate jQuery's need for the wrapper.

ssube commented 9 years ago

@lukescott I wasn't worried about jQuery wrapping elements, but folks passing class methods to jQuery events. For example:

class Foo {
  bar() {
    console.log(this);
  }

  attach(jq) {
    jq.on('click', this.bar);
  }
}

Today, that will log the element that triggered the event rather than the surrounding class (see this fiddle).

With @noautobind as the decorator and autobinding as the default, that behavior will change, no? Or am I misunderstanding the idea?

Naddiseo commented 9 years ago

@ssube, IIRC that happens without jquery as well, normal addEventListener will bind the element as this, so auto-binding class methods would also change that behaviour. However, as @erights mentioned, getting evidence as to whether that would be a problem is probably what needs to happen before making a decision one way or another.

lukescott commented 9 years ago

@ssube Gotcha. Yep, that is correct. Although I would find one of these to be more likely:

class Foo {
  bar = (e) => {
    console.log(e.toElement);
  }

  attach(jq) {
    jq.on('click', this.bar);
  }
}
class Foo {
  constructor() {
    this.bar = this.bar.bind(this);
  }

  bar(e) {
    console.log(e.toElement);
  }

  attach(jq) {
    jq.on('click', this.bar);
  }
}

As soon as you need the instance you have to do something like the above.

At this stage we're only talking about experimenting with the idea of making the change.

BrendanEich commented 9 years ago

For future reference:

https://esdiscuss.org/topic/methods-automatic-binding-on-read (3 years old)

https://github.com/gbracha/generalizedTearOffs/blob/master/proposal.md (fresh Dart work)

/be

WebReflection commented 9 years ago

I've spot this conversation only recently via ES mailing list, I wonder why a simple mixin-like approach hasn't been discussed yet: it solves inheritance gotcha, it could work without extra costs (cost is at runtime only if the utility is accesed)

// the mixin
var boundable = (function (wm) {
  return function boundable(Class) {
    Object.defineProperty(
      Class.prototype,
      'boundTo',
      {
        writable: true,
        configurable: true,
        value: function boundTo(method) {
          var
            callback = typeof method === 'function' ?
              method : this[method],
            m = wm.get(this) || (wm.set(this, m = {
              original: [],
              bound: []
            }), m),
            i = m.original.indexOf(callback)
          ;
          if (i < 0) {
            i = m.original.push(callback) - 1;
            m.bound[i] = callback.bind(this);
          }
          return m.bound[i];
        }
      }
    );
  };
}(new WeakMap));

// the test
@boundable
class Test {
  constructor(name) {
    this.name = name;
  }
  getName() {
    return this.name;
  }
}

var me =  new Test('Andrea');
console.log(
  me.boundTo('getName') ===
  me.boundTo(me.getName)
); // true

var getName = me.boundTo('getName');
console.log(getName()); // Andrea

Above method has been successfully used for long time in eddy through the global Object.prototype but since these decorators are apparently a thing I think the suggested snippet might be a solution to the very common issue?

Otherwise I think I would vote +1 for the ::obj.method to return the same bound method each time, which is the least surprise in my opinion and yes, many developers keep loosing tracks of their listeners with arrow function and bind since about ever :-(

Just my little contribution, if any. Best Regards

jussi-kalliokoski commented 9 years ago

I made weakbind as a proof of concept for this (and to be possibly used by the babel transform for this syntax) and if you look at the benchmarks, it looks very promising - and the benchmarks are much simpler, especially the React one in terms of render tree, than real world applications, so the benefit is likely to be even greater in the real world. It uses a two-dimensional WeakMap which allows it to nicely provide idempotency without causing memory leaks or additional bookkeeping.

zenparsing commented 9 years ago

@jussi-kalliokoski Cool. As discussed upthread, for the syntactic use case we'd want the memoized bound functions to be frozen (in order to prevent communication channels).

WebReflection commented 8 years ago

Since this has been brought up again in es-discuss ML, I'd like to ask @erights and others a few questions:

  1. I've never ever needed two different version of a bound function/method for the same object. The only reason that happened is because that's how .bind was specified, and it's the No. 1 footgun when it comes to assign at runtime a method to a generic event ( without using hadnleEvent that indeed doesn't need that, but that's just one story, not the whole scenario, and nodejs has no such thing as handleEvent, for example ). Accordingly, does anyone in here have a valid use case for having two different versions of the same bound method to the same object, and a reason about why that is desired?
  2. using ::obj.fn just to shortcut fn.bind(obj) seems to me a huge missed opportunity. The amount of saved chars and the explicit intent and well known behavior is not worth trapping forever :: as shortcut that won't fix concretely any problem. Are we sure it's actually a good idea to move on this way instead of bringing in something new, something better, something needed way more than just bind?
  3. the security implication thing looks like a bit overlooked. Every method, inherited or not, of every object has the same problem. Why would we block something developers keep expecting and asking for the :: syntax? If we can access the object any other form of communication channel would be possible and developers these days use Symbols for these things thinking these are somehow private or protected. Can we please be realistic on what :: would like to solve as real need for developers, and avoid pushing back anything that is not fully frozen down to its inherited properties? If not, why isn't a new kind of reflection method such Reflect.petrify(obj) considered or proposed, where the object gets promoted to a Proxy version of itself that won't accept any change to the object itself and any of its inherited properties and methods? I feel like Object.freeze went out with a huge handicap nobody wants to fix, and the side effect is that we cannot think forward about utilities truly needed and wanted, things like ::obj.method === ::obj.method that would make developers life easier and free from wrong expectations. Then they'll have every other way they want to footgun themselves.

Thanks foor any sort of outcome, reply, consideration, and/or enlightenment.

Best Regards