tc39 / proposal-class-public-fields

Stage 2 proposal for public class fields in ECMAScript
https://tc39.github.io/proposal-class-public-fields/
487 stars 25 forks source link

Evaluation order for static properties vs class declaration #40

Closed loganfsmyth closed 8 years ago

loganfsmyth commented 8 years ago

Raised over in https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy/issues/17 but I figured I'd file a proper bug to get a solid answer. What is the expected behavior of

class Foo {
  static me = Foo;
}

with the current spec, it looks like this would be a TDZ error because the Foo property assignment would be evaluated as step 24.v.c.a of ClassDefinitionEvaluation, and the Foo binding does not get initialized until step 26.i.

Babel 6's class property implementation currently evaluates me = Foo after Foo has been initialized, but my transform-decorators-legacy plugin will result in Foo being uninitialized at this point, and the difference between the two can cause trouble for users since code like this will work with standard Babel 6 until they enable decorators, at which point it will break.

uMaxmaxmaximus commented 8 years ago
class User {
  static schema = {lol: User} // User is undedined (is a bug)
}

It seems to me improper specification and should not follow it. And I think that in this way many believe. but where to go. I have to write a decorator such as:

@schema({
 foo: User
})
class User {}

but this not works

works just ugly way

class User {}
User.schema = {lol: User}

But what if I want to create a class in expression?

return (function(){
  class User {}
  User.schema = {lol: User}
  return User
})()

It is obvious that this is a bug, and the specification is not true There is no reason to make such behavior.

jeffmo commented 8 years ago

Copying the following from the referenced issue for posterity:

The class properties spec states (Step 24.ii.a) that the initializer scope inherits from the class-body scope; And the ES6 spec states (Step 4) that the class body scope does have access to the class binding (no TDZ). So indeed, the code above should work as expected.

I am planning to write out proper spec text for the next TC39 meeting at the end of July, so hopefully this will reduce confusion around some of these things.

jeffmo commented 8 years ago

Oh, and you're right in pointing out that 24.v.c.a is flawed!

Things have changed a bit such that static class property initializers don't actually evaluate until the end of the class definition evaluation process -- so the spec text over in the README is a little outdated there. That's my fault, apologies!

loganfsmyth commented 8 years ago

@jeffmo I think you may be misunderstanding what Step 4 does. That creates the binding in the class body scope, absolutely, but it does not give it a value, it leaves it uninitialized. The value of the class constructor F isn't known until step 14. Attempting to access the value of the binding before it is initialized results in an exception. The step

is the one that initializes the binding, and currently in the spec text that happens after the static values are evaluated. That line would have to be moved to be above the static property evaluation for it to be able to access the class name binding.

jeffmo commented 8 years ago

That creates the binding in the class body scope, absolutely, but it does not give it a value, it leaves it uninitialized.

You're right and this is why I followed up with the clarification that we've moved the step described in 24.v.c.a to the end of the procedure (i.e. after Step 23 that you're pointing out). Apologies for taking for granted that you knew about that tweak to the proposal.

loganfsmyth commented 8 years ago

Okie doke, cool. I couldn't quite tell from the post above, my bad.

loganfsmyth commented 8 years ago

An edge case came to mind that I don't think the decorator spec specifies well, but I'm curious for your thoughts. Given a case like this:

@(cls => class New extends cls {})
class Base {
  static get current(){ return Base; }
  static original = Base;
}

Ignoring the class property for a second, the decorator spec doesn't make it clear if Base.current === Base (from outside). Clearly the Base binding outside the class declaration would resolve to the New child class, but is Base inside New or the original Base?

If the expectation is that Base inside the class body would be the decorator return value, this Base.current === Base, this would potentially mean that Base.original !== Base.current, because the class properties will have been evaluated before the decorator. This might be counter-intuitive to developers.

The other tough part there being that the current spec defines the internal class-name binding to be const, whereas this would require it to be set once and then updated again after decorator evaluation.

This is all moot if the decorator return value doesn't update the class body binding, but that has its own edge cases for users.

jeffmo commented 8 years ago

I think @wycats has some solutions for this scenario. He'd included general thoughts in the previous TC39 meeting, so I'll defer to him here for details