tc39 / proposal-class-fields

Orthogonally-informed combination of public and private fields proposals
https://arai-a.github.io/ecma262-compare/?pr=1668
1.72k stars 113 forks source link

Function as a Class Property. Lack of description? #293

Closed dhilt closed 4 years ago

dhilt commented 4 years ago

There are tons of articles/docs mentioning Stage 3 Proposal in context of using Arrow functions as Class properties. Like, now we can use

class Foo {
  bar = () => console.log(this.baz);
  constructor(baz) { this.baz = baz; }
}

const foo = new Foo(1);
myElement.addEventListener('click', foo.bar);

instead of

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

const foo = new Foo(1);
myElement.addEventListener('click', foo.bar.bind(foo));

I'm just trying to get a confirmation if it's something legal from the Stage 3 Proposal perspective. I've walked through all Stage 3 related repos and seems this one is the most appropriate. And here we see only Primitive as a Class property example in the Readme:

class Counter extends HTMLElement { x = 0; ... } In the above example, you can see a field declared with the syntax x = 0

Does the Stage 3 Proposal really allow to use functions in that capacity or it's a kind of React/Babel/etc fantasy? If yes, it does, then maybe it would be a good idea to add a few words or even an example in the description?

ljharb commented 4 years ago

Yes, it’s allowed; any value is allowed in a class property.

a-ejs commented 4 years ago

why would you want that though? wouldn't that create a new function for every instance of your class, as well as break prototype inheritance?

(also weird how arrow functions that normally don't have their own this deviate from that behavior in this case, even though that's like almost their entire point)

nicolo-ribaudo commented 4 years ago

wouldn't that create a new function for every instance of your class

Not always:

function fn() {}

class A { x = fn };

Also, it's consistent with how this code creates a new object for every instance of the class:

class A { x = {} }

as well as break prototype inheritance?

Yes, it would be similar (modulo the defineProperty thing) to

class A {
  constructor() {
    this.fn = function () {};
  }
}

If you want it to respect the normal prototype chain you can use an "old-style" method. Also, having a runtime error when a value is a function is super inconsistent with the rest of the language.

also weird how arrow functions that normally don't have their own this deviate from that behavior in this case, even though that's like almost their entire point

Arrow functions in class fields behave like normal arrow functions: they get this from the outer scope.

class A {
  x = this;
  y = () => this;
}

let { x, y } = new A;
x === y();
a-ejs commented 4 years ago

guess i shouldn't have phrased that as a question.

littledan commented 4 years ago

Thanks for answering these questions, @ljharb and @nicolo-ribaudo .

a-ejs commented 4 years ago

my question wasn't answered, actually:

why would you want that though?

nicolo-ribaudo commented 4 years ago

The biggest usecase is to use arrow functions. Normal methods are like normal functions, thus don't capture this.

If you are looking for an example without arrow functions, here it is:

class ClickLogger {
  el;
  listener = function () { console.log("click!"); }

  constructor(el) {
    this.el = el;
  }

  attach() { this.el.addEventListener("click", this.listener) }

  detach() { this.el.removeEventListener("click", this.listener) }
}
const logger1 = new ClickLogger(document.body);
const logger2 = new ClickLogger(document.body);

logger1.attach();
logger2.attach();

// Click on the document. It will log "click!" twice, because you have two loggers

logger2.detach();

// Click on the document. It will log "click!" once, because you still have one logger

If you use a normal class method, it won't attach the second logger and after you detach the first one it won't log anything, even if there is still the first logger.

That said, functions are normal value in JavaScript, and disallowing some specific values would be inconsistent with the rest of the language. I cannot think of a valid use case to allow passing the exact "JavaScript is a horrible language!!! 2732" string as a parameter to an arrow function, but it doesn't mean that we should forbid it because, exactly like functions, it's a normal JavaScript value :shrug:.

Also, note that this proposal doesn't force you to use functions in class fields. If for your use case it's better to use a normal class method, you should still use it.

a-ejs commented 4 years ago

i never said anything about disallowing functions in 'fields'.

my point is any situation where a function 'field' can replace a prototype method is either an improper use of fields (after) or a weird use of methods (before).

you also mentioned inconsistency with the rest of the language multiple times, which is funny because it perfectly describes the entire idea of statements with delayed execution outside of functions (ie. this proposal).

rdking commented 4 years ago

my point is any situation where a function 'field' can replace a prototype method is either an improper use of fields (after) or a weird use of methods (before).

IMO, the opinion of @a-ejs is true regardless of whether the field is public or private. No doubt, this is an unpopular position.

ljharb commented 4 years ago

I share that opinion - functions shouldn’t go in class fields/own data properties, except for bound prototype methods.

However, it wouldn’t be consistent or appropriate for the language to enforce an arbitrary distinction between values, especially when it wouldn’t be enforceable in non-field data properties.

rdking commented 4 years ago

While I'm no longer arguing for a change (since I recognize it to be futile), I must note that it is the design of this proposal that precludes such a reasonable restriction, not the language itself. However, I also understand that this is itself based on a fundamental difference between our (mine vs the TC39 consensus) understandings of the nature of a class.