Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
ES Class Property Declarations

When are variables dereferenced for initializer expressions?

class MyClass {
  myProp = this.generateMyProp();
  // what does `this` refer to?
  anotherProp = new Foo();
  // what is the first level scope Foo will be looked in for?
}

My assumptions: this would be the context in which the class was declared (not the instance of that class) and Foo would first attempt be resolved in the scope in which the class was declared getter scope, (which it won't be found in) then the class's parent scope in which it was defined.

The this case is the one I'm most confused on, because it would desugar incorrectly into the getter, which would change the context. (maybe I'm missing something?) Perhaps using this in these cases can be illegal, to avoid this extreme ambiguity altogether.

The scope lookup also will cause issues if nested classes are ever supported, a la Java.

For reference, traceur's implementation does all dereferencing lazily, so you can use this and it will reference the instance. demo

Edit: Looks like this is actually the same behavior that Java has. Perhaps that helps.

Owner

jeffmo commented Feb 10, 2015

@jayphelps: Right, you aren't missing anything. Access to this is a pitfall of what I currently have written up here so far. In the current form of this gist, it's pretty undefined what this or even super would mean in the middle of an initializer expression (hence the two bullets under 'caveats') since those expressions get moved around.

However, after running through this form of the proposal a few times with some people over the past few months, it occurred to me that the call to the getter function could easily just carry the this context (and [[HomeObject]]) that match up with the object currently being initialized. This would make this.generateMyProp() work almost identical to how it would work if you simply placed the expression into the constructor (except didn't have to worry about scope collision issues, etc). super.foo would also work very similarly (identically?) to how it would work if you used it in the constructor in the member-expression form.

I haven't updated the gist to reflect these updates yet, but I intend to do so soon-ish (definitely before the March TC39 meeting).

Nice start!

I think the properties on the prototype section isn't needed since anyone worth their salt would understand the difference anyway.

Owner

jeffmo commented Feb 12, 2015

@benjamingr: Probably so, but I didn't want to leave the context unmentioned so I just made it as short as possible.

Owner

jeffmo commented Feb 25, 2015

I've just brought this gist up to date with some of the latest ideas (and to match the current status of how allocation/initialization works in ES6).

There's still a lot of work to be done here, but I figure keeping this gist updated as progress happens is probably best.

arv commented Mar 4, 2015

It is not very clear what environment the expression is executed in. It looks like it is executed in no environment which of course is not possible.

Which environment is expr evaluated in below?

// EnvA
class C { 
  // EnvB
  x = expr;
  constructor() {
    // EnvC
  }
}

Hacking this and [[HomeObject]] is also problematic.

@arv @jeffmo From my private fields proposal:

Initializers are evaluated in a new lexical environment whose this value is undefined and
whose parent lexical environment is identified by the class body.

https://github.com/zenparsing/es-private-fields

I think it's crucial that this be undefined, since (given the fact that this can mean many different things in JavaScript) it's not clear from context what this would refer to.

arv commented Mar 5, 2015

@zenparsing I agree, that seems reasonable.

Next question: TS uses name : Type = expr; and name : Type;. Should this proposal allow a property name without an initializer? If so is that just a shorthand for undefined? Is it a TDZ?

arv commented Mar 5, 2015

ClassPropertyInitializer :
PropertyName = LeftHandSideExpression;

This does not look right. Do you really want to support:

class C {
  0 = 1;
  'abc' = 'def';
  true = false;
  [expr] = 42;
}

The computed one is particularly bad since it would get executed once per instance.

For simplicitys sake maybe only allow Identifier?

@arv (with that << would work in gists)

For private fields, I was originally thinking that no initializer is equivalent to " = undefined". That makes sense in a javascripty way, but that doesn't mesh so well with a type system. I'm not sure how to resolve that question right now. I think that's a longish discussion...

I'm curious why the initializer is restricted to LeftHandSideExpression, instead of AssignmentExpression (which would be consistent with variable initializers).

Owner

jeffmo commented Mar 5, 2015

Whoa, apparently I don't get emails for comments on gists :(

It is not very clear what environment the expression is executed in. It looks like it is executed in no environment which of course is not possible.

Do you agree it would be odd if the expression did not close over the scope immediately outside the class body? As in the case of:

function getStuff() { return 42; }
class MyClass {
  myProp = getStuff(); // 42
}

There's probably an argument to be made that it might be confusing if there were a 'getStuff()' method on this class (because it looks like its in the same block as the initializer). I'm relatively confident that this clarification could be learned fairly quickly...and only once.

Second, do you agree it would be odd if it executed in the context of any of the visually-sub-scopes? As in the case of:

function getStuff() { return 42; }
class MyClass {
  myProp = getStuff(); // 78

  constructor() {
    function getStuff() { return 78; }
  }

if those two cases can be agreed on, then I think we're left with only one remaining question -- which is whether each initializer should operate in its own closure (and if so, whether those scopes should be siblings of each other -- which is what I'm proposing here).

I think it's crucial that this be undefined

Can you clarify why you think this is crucial? Do you think it's too difficult to learn that the this value points at the instance object in all cases of class property initializers (no exceptions)? I'm not sure I agree that, because this is contextually overloaded in some places JS, we should stop adding uses for it. Obviously I have some bias (having written this up), but the semantic expectations of this in a property initializer here seems pretty clear to me... Maybe you can put forth and example (that's likely to be reasonably common) where it's less clear?

I think having access to this is quite important because it allows initializers to refer to each other (and parent-class properties) during initialization. I know that React, for example, is interested in such capability.

One last question on this topic: If this were undefined in these contexts, how would you propose to solve the problem of allowing property initializers to refer to each other?

Next question: TS uses name : Type = expr; and name : Type;. Should this proposal allow a property name without an initializer? If so is that just a shorthand for undefined? Is it a TDZ?

@wycats brought up a desire for allowing a property without an initializer (and I think it would make sense for that to be equivalent to = undefined). I'm down with this idea.

This does not look right. Do you really want to support: [...]

...nope, unintentional. Nice catch, I'll update this. I intended only to support identifier names

For private fields, I was originally thinking that no initializer is equivalent to " = undefined". That makes sense in a javascripty way, but that doesn't mesh so well with a type system

Depending on the type system, it's probably reasonable to deal with this as it would be a strict superset of JS compatibility. A type system should be able to support enforcement that either the value of the corresponding type is set in the constructor (in the strictest sense); Or, in a more flexible sense, it could understand the the type is effectively undefined|T

I'm curious why the initializer is restricted to LeftHandSideExpression

I just stuck with the simplest possible structure (a = b) to begin with. It might be reasonable to expand to AssignmentExpression, too

kosich commented Mar 6, 2015

A naive solution might suggest that properties should sit on the prototype, but this can prove a hazard for reference values like objects:

Aren't we defining methods on the prototype? If I get you right, you're suggesting to move these properties to an instance, which IMHO would be really misleading.

If we do move props to instances, following problems come up:

  • how would we define particularly prototype's properties ?
  • inheritance of such properties becomes non-obvious.

While having those properties defined on a prototype

  • we still got option to define instance-related-props in the constructor
  • properties, defined in class will be properly inherited.

And finally I believe, as JS developers, we understand well consequences of storing objects on a prototype chain.

Can you clarify why you think this is crucial?

Yeah - that was an overstatement. Where's my editor when I need him? : )

Need to think about this for a bit.

Is this disallowed within initializers for static properties?

kosich commented Mar 6, 2015

a little bit off topic: can you guys tell me why do es6 object literals differ from class definitions? (EDIT: there's a Brendan Eich's answer to similar question on mailing list) IMHO that has huge influence on how class-related syntax will evolve. Currently we shouldn't set comma delimiters in class methods, which eventually leads us to creating es7 class properties as name = ... ;

So we will have two different zones (if I may call them so) with different syntax/laws:
object declaration and class definition.

E.g.:

classes
class A {
  prop = 'hellow'; // properties are set with equal sign and a semicolon
  static prop = 'world';
  prop2 = this.prop; // ... ain't obvious what `this` should be resolved to: an instance, A or container
  method ( ) { /*...*/ }
  method2 = A.method; // again here we get more questions: is A resolvable?
}
objects
let a = {
  prop : a.method1(), // a is in TDZ so we get ref error
  prop2 : this.prop, // obvious resolving `this` to container
  method1 ( ) { /*...*/ }, // 'methods' are also separated with comma
  method2 ( ) { /*...*/ }
};

So, two different behaviors, while first is only a syntactic sugar for the second one.

...and
Once again about prototype/instance properties: how properties can be inherited/overridden?
If we accept logic, where props are set onto an instance, then:

class A {
  prop = 1;
}

var a = Object.create( A.prototype );
console.log( a.prop ); // -> `undefined`

class B extends A {
  prop = 1 + ( super prop ); // if such syntax is possible, there's no way to resolve `super prop`, without creating an instance
}

// also when we define a constructor
class C extends A {
  // super's constructor wont be called on `new...`
  constructor () {  }
}

let c = new C();
console.log( c.prop ); // -> `undefined`

P.S.: sorry for my interruption to your discussion.

Owner

jeffmo commented Mar 6, 2015

Using : instead of = to delimit property names from their initializer expressions seems fine to me (as long as it doesn't pose any unforeseen technical issues).

Arguments I've observed in the past on the topic of why and when classes can/should look different than objects seem to divulge into subjective opinion that just enrages lots of people; So while I'm happy to let that discussion happen at some point, I'd prefer if it not rathole the underlying topic just yet :)

For now I'll probably stick with = in my proposal for now just because it's what I have and I'm more interested in working out semantics and abstract syntax at the moment. Your suggestion here is definitely noted though, and I'll try to make sure it's brought up at some point.

Owner

jeffmo commented Mar 6, 2015

Ok, now that I've gone and said I don't want to dive into bikeshedding = vs : I've gotten even more feedback that : is preferred...so in the interest of least resistance (and since I don't have an opinion here) I'll just go ahead and switch to : in the proposal for now.

No promises this won't be revisited again in the future, but I'm ok with coming out of the gate with :.

Does : work with potential future ES type specifiers? There's no problem with "=":

class C {
    x: string = "abc";
}
Owner

jeffmo commented Mar 6, 2015

@zenparsing: Ah yes, that's a pretty good point

eyolas commented Mar 11, 2015

And := ?

class C {
    x := "abc";
}

sporto commented Mar 25, 2015

I don't get it, we are trying to mimic something else so hard. Looking at this I have no idea where myProp will end up.

class MyClass {
  myProp = 42;

  constructor() {
    console.log(this.myProp); // 42
  }
}

What is wrong with?

class MyClass {

  constructor() {
    this.myProp = 42;
  }
}

It is explicit

Owner

jeffmo commented Mar 26, 2015

@sporto: There's nothing generally wrong with your second example, and that needs to continue to work. This proposal is strictly not about specifying required properties or preventing expando, it's only about declaring expected properties.

This proposal is about enabling scenarios where declarative interpretation of expectations are useful. Some examples include editors so that they can make use of this info for typeaheads/inference, TypeScript/Flow can make use of this to allow their users to express intentions about the shapes of their classes, allowing general users to use this for just human-readable documentation about properties separate from potentially complex initialization logic, and possibly even allowing VMs to pre-emptively optimize objects created from a class with some of these hints on them.

To my mind, it's both reasonable (and probably even a requirement) that this proposal not require that you declare properties for a class before using them. It's meant to help enable a form of expressivity and for eliminating boilerplate in some circumstances where boilerplate can be avoided. (Here's an example from my proposal slides that show one scenario where boilerplate might be avoided: http://postimg.org/image/7m36gbzyh/ )

@sporto: The former is more friendly to documenting exposed properties and defaults. The ladder mixes defaults with init logic. Also the former can probably be better optimized (especially if the order doesn't matter). One last reason: static properties are currently (in ES6) a pain to define since they have to happen after the class definition. With this you can define them near the head of the class.

@jeffmo: Does the order the properties are defined in matter? Since a class is like an object (hash table) I'm thinking it shouldn't. And if that's the case, this should be undefined, shouldn't it?

As far as : vs = goes, if type hinting were added, would it need to be supported on objects as well? If so, wouldn't that disqualify : for being used for type hinting? And if that's the case wouldn't : be better for consistency with object?

Do you even need to type-hint when setting a default? With {foo: "bar"} can't we assume foo is string? So if that's the case, can we make type-hinting standalone? Like {foo::String} or {foo::ClassName}?

i dont understand why anybody need such behavior. why do we need another method to add properties to class instance?
i think this example is expected behavior:

var instA = new MyClass();
var instB = new MyClass();

instA.getValue(); // 42
instB.getValue(); // 42

instA.myProp.someValue = 100;

instA.getValue(); // 100 -- expected
instB.getValue(); // 100 -- wat!?

we added property to class and it should be the same in all objects of this class.

Transposing property expressions into the constructor? really? okay, it's not class property, its instance property. I see where can i use class properties - default values of my property, which i can override in constructor or any method. or i can change something in all objects of my class at once. but now we dont have class properties, we need to write ugly code like in your example:

class MyClass {
  getValue() {
    return this.myProp.someValue;
  }
}
MyClass.prototype.myProp = {someValue: 42}

adding class properties will be great sugar. but behavior in this proposal cant solve our problems, it even can not be called as "class" properties - its only second way to add something to object in construct phase. real class property should be stored in class (prototype)

What can i do if i need multiple class properties? I should write some ugly code to add it in my prototype. For example i need class to connect my db:

class MyClass {
  connectDB() {
    someDB.connect(this.dbConfig);
  }
}
MyClass.prototype.dbConfig = {url: "someurl"}
MyClass.prototype.cacheConfig = {url: "someurl"}
MyClass.prototype.otherStuff = {someValue: 42}

i can make it in some more object-oriented way:

class MyClass {
  connectDB() {
    someDB.connect(this.dbConfig);
  }
  static classProps(){
    this.dbConfig = {url: "someurl"}
    this.cacheConfig = {url: "someurl"}
    this.otherStuff = {someValue: 42}
  }
}
MyClass.classProps();

But this code still ugly and my class poluted by static method. I think it will be better to do it this way:

class MyClass {
  dbConfig = {url: "someurl"};
  cacheConfig = {url: "someurl"};
  otherStuff = {someValue: 42};

  connectDB() {
    someDB.connect(this.dbConfig);
  }
}

it's much readable! i have default config in all objects of my class. i can simply override it in object constructor.

+1 to @PinkaminaDianePie, class properties must be real "class" properties, defined on the prototype
if you need a mutable instance property, you should simply define it in the constructor
we need that flexibility, we need to be able to decide exactly what goes onto the prototype and what goes not
I really hope that the ES standard will not repeat the terrible awful mistakes of typescript (a really great idea and the worst possible implementation, typical microsoft), which personally for me made it completely unusable

jurca commented May 25, 2015

This looks nice, but I don't see how to define symbol properties. Have I missed anything? The private fields proposal does not seem to rely on symbols, so using @name won't help.

Also - JS uses automatic semicollon insertion (ASI). Wouldn't this proposal break this language feature by making semicollons required for class properties? I don't think we should suddenly force a new code style on people relying on ASI.

trusktr commented May 28, 2015

I'm in favor of class properties desugaring to prototype properties. So, this in favor of

var instA = new MyClass();
var instB = new MyClass();

instA.getValue(); // 42
instB.getValue(); // 42

instA.myProp.someValue = 100;

instA.getValue(); // 100 -- expected
instB.getValue(); // 100 -- wat!?

In the example, instA.myProp is a reference to an object. We haven't change the value of instA.myProp. It works just as expected, just like pointers do in C. In this example, if you set instA.myProp to another value (like make it a reference to another object), you haven't changed the reference for all instances, you've made a new instance property on instA. We are all then capable of adding constructor logic to modify referenced objects if we need.

Do you think people new to JavaScript will struggle with this? Would it be beneficial (less error prone for new people) to make class properties instance properties? Douglas you-know-who might say yes here and agree to the instance property route.

I prefer the prototype route so I have more control over what I'm doing (without extra prototype assignments after the class definition) like what @PinkaminaDianePie said, but obviously for people new to JavaScript the instance route will be easier to learn at first. I'm not sure which of those should win, I just know I like having the cleaner form of control that class props as prototype props would offer.

What about two syntaxes?

class MyClass {
  myProp = 42; // instance property, good for newbs
  otherProp := 56; // prototype property

  constructor() {
    console.log(this.myProp); // 42
  }
}

jabjab commented May 29, 2015

I also agree that class properties should desugar to prototype properties and instance properties should remain in the constructor. In my opinion, writing to instance.prop.subProp causing the entire property to be copied to the instance (if it wasn't copied already during construction, which seems counterproductive to the concept of class properties anyway) has more potential to be confusing than the prototypical behavior.

Python's class construction works similarly, where data members in the body of a class belong to the class while instance members are defined within the constructor by setting properties on self, the this-equivalent. (Python actually goes even further in terms of explicit context by forcing the first argument to every class method to be the context rather than having it as an implicit variable, but that's a different matter.)

The more I think about this, the more I agree with @PinkaminaDianePie, @trusktr, @jabjab, and others. Class properties should be on the prototype.

Reasons:

  • Static properties are on __proto__. Putting properties on .prototype would be more consistent.
  • Other languages with classes prohibit non-constant values as property defaults, but if they were allowed the reference would be copied, much like assigning directly to a prototype.

This can be done to fix the wat!?:

class MyClass {
  myProp
  constructor() {
        this.myProp = {someValue: 42};
   }
   getValue() {
     return this.myProp.someValue;
   }
}

Which makes me think... perhaps class properties should only allow constant values, such as string, number, undefined, and null? It would prevent foot stomping.

Defining the property outside of the constructor without a value, as in the example above, is still useful if you wanted to declare the type:

class MyClass {
  myProp Object
  constructor() {
        this.myProp = {someValue: 42};
  }
  getValue() {
    return this.myProp.someValue;
  }
}

And then we have the issue of wanting to declare a callback such as:

class MyClass {
  // using a `:` to define a property instead of `=` to be consistent with POJO
  handleMethod: () => {
    console.log(this); // always MyClass instance
  }
}

But that can be solved by extending methods a bit:

class MyClass {
  // => is simply added between () and { of the method
  handleMethod() => {
    console.log(this); // always MyClass instance
  }
}

@jeffmo could we perhaps get this made into a git repo with an issue tracker? Especially since the mentions don't seem to work.

So what about status of this proposal? @jeffmo we are waiting your answers =)

+1 on desugaring to simple prototype properties. +1 on allowing references (or anything) to be assigned.

Attaching the property to the prototype is the most obvious expectation and anything else is going to require strange magic below the surface. Seeing a property at the same conceptual / lexical level as a method, I think, would lead people to expect them to be structured similarly:

class Test {
  a: 3;
  g() { console.log('g'); }
}

I would expect a and g to be treated similarly and I think most programmers would.

I realize it is important to eliminate potential for error when possible, but hiding the simple semantics of prototypical inheritance very well may have the opposite effect. If the compiler rewrites what looks obvious into magic that is difficult to understand, it will hinder understanding and effective use of the language.

You can't keep everyone from making a mistake or you'll eventually have a language that can't do anything. The immensely complicated proposals seem to all stem from the idea that someone will not understand references in a language that only supports references. If things are not allowed, all that happens is people do the same thing in a clunkier way, often with the same potential for error (such as creating a 'var' in the class' module - same reference issue).

We definitely need a way to declare instance properties, rather than relying on imperative initialization in the constructor. There are several important reasons:

  1. They serve as the target in the syntax for type annotations.
  2. They serve as the target in the syntax for decorators.
  3. Tools (linters, static analyzers, code completion) often need to understand the shape of objects statically. Right now they tend to rely on encoding this structure in comments, which is duplicative and should be brought into the language syntax.
  4. VMs try to determine the shape of objects in order to allocate the right amount of memory. I'm told they do this by scanning the constructor, but that it's obviously an unreliable indicator of the ultimate shape.

Look at what Polymer has to do to let developer add some simple metadata about properties - it's had to invent its own declaration convention:

Polymer({
  properties: {
    a: {
      type: String,
      observer: 'aChanged',
      value: function() { return 'Hello' },
    },
    b: {
      type: Number,
      notify: true,
      value: function() { return 42; },
    },
  },
});

This is then turned into initialization logic at runtime by magic in the Polymer function. This is insanity in a world where we'd like to have powerful tools. We have to teach each tool about each library's technique for declaring properties.

Wouldn't this be much better?

class MyElement extends PolymerElement {
  @observer('aChanged')
  a : String = 'Hello';

  @notify
  b : Number = 42;
}

GothAck commented Jul 2, 2015

I really like the idea of class properties that can be defined outside of the constructor (and that can be desugared into the constructor using babel et al.

Could defining prototypal and instance properties perhaps look like:

class Demo {
  instance_prop = {a: 100};
  prototype proto_prop = 2;
  static meh = 'all the meh';
}

Anyways, my 2¢, it's been an interesting discussion :)

I'm with @lukescott, @PinkaminaDianePie, @trusktr, @jabjab on this one. No magic please. As I recall the point of classes was to make authoring constructors with prototypes easier, not to introduce new behaviour. The contents of the class block should always be assigned to the prototype (minus the constructor).

estk commented Jul 30, 2015

@lukescott I think you're on to something very high quality with your suggestion:

class MyClass {
  // => is simply added between () and { of the method
  handleMethod() => {
    console.log(this); // always MyClass instance
  }
}

If this eventually makes it into the standard you can be sure that it will turn up in almost every class definition. Personally it would scratch my last remaining js itch.

and what about computed property names? its the only way to use Symbols in class declaration

hax commented Aug 11, 2015

We need computed property name to override [Symbol.toStringTag] in the subclass...

mischkl commented Aug 31, 2015

IMHO, if your goal is to share properties across all instances, adding them to the prototype as @PinkaminaDianePie @RobertWHurst @lukescott et al suggest is not the way to go. Instead you should just use the static keyword (attaching to the constructor), it's that simple. (Unfortunately, ES6 doesn't support declaratively defining static properties, only static methods, but this is something that should be rectified ASAP as there are perfectly good, safe use cases for them.) There are really almost no arguments I can think of for attaching non-method properties to the prototype, other than maybe compatibility with some libraries that expect this.

On the other hand, there are very valid use cases for being able to define instance properties as part of the class definition, just look at any Java or TypeScript code if you can't imagine them. At the very least it helps with documentation, since it's not really desirable to put JSDoc in the middle of your constructor. Also this proposal opens the way for private instance properties, which provide a much more user-friendly syntax for hiding data than having to use the comparatively unfamiliar symbol syntax.

jmeas commented Aug 31, 2015

I know nothing, but one syntax that I like to support static/proto/instance props is...

class Base {
  // attached to the constructor
  static staticProp = 42;
  // attached to the prototype
  proto anotherProp = 43;
  // attached to the instance
  yetAnotherProp = 44;
}

I'm really confused about the class sugar syntax and the proposal here. Why do values need to be directly assigned to this? As @sporto already mentioned this is possible and very clearly expressed by assigning to this inside the constructor. What is missing is the ability to define properties directly on the prototype.

I'm just thinking in terms of what class is trying to accomplish and how it's currently not backwards compatible with things like React.createClass or Backbone.*.extend. Sure new developers are protected, but existing code will be a pain to mix between ES6 classes. Personally I think attaching to this is more confusing and less consistent than attaching directly to the prototype.

mischkl commented Sep 1, 2015

@tbranyen as was mentioned in the initial description, attaching directly to the prototype has the inherent problem that if you change the value it is reflected in all instances of that class. So it's not really very helpful to be able to do that. (In fact the uselessness/anti-patternness of this is exactly the reason it was excluded from the ES6 spec.) On the other hand, if your intention actually is to share across all instances, that's what static properties are for. What's missing is the ability to define instance fields declaratively, in a way that facilitates documentation and potentially helps with compiler optimizations about what an instance of a class "should" look like. This is a feature that is pretty much expected of the class pattern for people coming from other languages, so it's not like it's re-inventing the wheel.

Admittedly the terminology "class properties" is kind of confusing since it sounds like they are saved on the class as opposed to only being defined there. Maybe this should be re-termed something like "class-defined properties", "class members" or "fields" to make it clearer.

@jmeas I like it! Add private to that list and I'll sign up in a heartbeat. ;)

Hi, I'm a newbee on ES7, but would it be possible to declare an async property?

@jeffmo

Would you mind moving this to its own repo? An issue tracker would be really nice for this.

Owner

jeffmo commented Sep 14, 2015

And once again I'm duped by the lack of emails from gist comments (only this time I should've known better...) :(

I'm really sorry for the lack of interaction, everyone. Per @lukescott's and @impinball's suggestion I'm going to start moving this to it's own repo -- including various updates and bits of feedback. Hopefully then we can discuss by way of issues and have a good conversation here.

I'm planning to bring the proposal back to the TC39 meeting in a couple of weeks with everyone's feedback we've gotten thus far. After I've made some progress porting/"rebasing" the proposal to it's own repo, I'll post the link here.

Owner

jeffmo commented Sep 14, 2015

Ok, and before I move on too far I'd like to address some of the feedback given above. This is all really great and I very much appreciate everyone taking the time to discuss!

Hopefully you'll all actually manage to see this comment somehow... :p

@lukescott:

Does the order the properties are defined in matter?

The order only matters if the initializers have side effects (or if there are duplicate initializers for the same property name). Because initializers are evaluated in-order, later initializations will overwrite previous ones if they're the same name or generally have overlapping side effects. This is most consistent with the rest of the language (especially class methods -- which may have duplicates where the last version wins).

As far as : vs = goes, if type hinting were added, would it need to be supported on objects as well?

Though slightly beyond the scope of this proposal, I suppose this is worth considering since it's related in terms of forward consideration: Syntactic type-hints in TypeScript and Flow have worked really well using the regular : syntax for most things, but for things like objects you're right -- there's not a clear answer straight out of the ES-Grammar box.

In TypeScript the solution is to define a structural type for the full object (in an interface or type alias or inline annotation) and then hint that the entire object meets this structural type at assignment/init-annotation positions. In Flow you can do the same or you can specify the type inline using a parenthesized expression-annotation on the value a la:

var myObj = {
  someTypedProperty: (someVariable: number)
};

Given these two options I think this issue has already been worked around successfully, so I suspect if ES adopts type annotation syntax one day it will likely pull from the existing work of tooling like this.

@PinkaminaDianePie:

I suspect the way I worded the gist text might've added some confusion here (and I'll take this as a hint to clarify things in my impending updates). The contents of this gist run through a few possible solutions before eventually arriving at the final one which represents the actual proposed feature.

Indeed I am proposing both "class" (AKA "static") properties as well as "instance" properties -- two roughly separate features (the former is notated by a prefix static keyword on the property declaration). The two are only related by the fact that they can both be specified in a class declaration/expression -- so I'll probably just split them into two proposals so they can be discussed without confusion. Additionally, I'll be sure to disambiguate what I mean by "class" property in my updates going forward.

we added property to class and it should be the same in all objects of this class.

(cc also @trusktr, @jabjab, @mkleehammer, @lukescott regarding this vs prototype)

It's true that methods sit on the prototype and this works well, but one of the reasons it works well is because it's relatively uncommon to actually mutate a method object in a way that instances depend on. Most of the time, one simply calls a method rather than storing data in it.

For properties on an instance, the typical use case will almost certainly be storage read and used by many instances. If we knew the storage was both frozen and immutable, then it wouldn't even really matter if it hits the prototype vs instances -- but since there's no guarantee of immutability (and in fact a lot of code requires and relies on such mutability), there is a much more likely chance that unintentionally-shared instance properties will pose a hazard.

Moreover, if you run through a lot of code today (not just ES6, but also ES5 and prior) you see that the most common/idiomatic patterns that developers use for instance properties isn't usually to assign them to the prototype, but to initialize them on this in the constructor.

For cases where one wishes to share a reference across all instances of the objects, there are two very good means of doing this: (1) Have the class close over a shared variable. All instances of the class will be able to read/update/mutate/etc this singleton variable or (2) specify the property as a "static" property (i.e. it sits on the class itself rather than on this) and simply reference it by way of MyClass.sharedThing. Note that "static" properties are also part of this proposal (though, as mentioned above, I will probably split them into a sibling proposal since static properties are far simpler to spec than instance properties).

Honestly, though, I think @PinkaminaDianePie articulated this idea very well when clarifying that the properties we're describing here aren't really "class" properties, but they are "instance" properties.

its only second way to add something to object in construct phase

That's right, the "instance property" portion of this proposal is only about adding a declarative means to specify instance properties (and a place -- expressively separate from initialization logic -- to add documentation about the properties). These declarations will not be required, so there's no reason expando properties in constructors would cease to work in the same way they do today.

The purpose of the proposal is to (a) limit boilerplate in some cases*, (b) provide a syntactic target for decorators that operate on non-method entities (i.e. "properties"), (c) improve expressiveness in some cases (i.e. documentation for properties for both humans and tools -- distinctly separate from initialization logic in the constructor), and (d) to open potential optimization opportunities for VMs who wish to make a more educated prediction of the expected layout of class-generated objects.

* Example where we can limit boilerplate with class properties and move property documentation out of the middle of :

class BaseClass { constructor() { /* some common init logic here */ } }

// Without properties
class ChildClass1 extends BaseClass {
  constructor() {
    super();
    this.id = generateUniqueId();
  }
  getId() { return this.id; }
}

// With properties
class ChildClass2 extends BaseClass {
  id = generateUniqueId();
  getId() { return this.id; }
}

But this code still ugly and my class poluted by static method. I think it will be better to do it this way:

[...]

it's much readable! i have default config in all objects of my class. i can simply override it in object constructor.

I totally agree! And, in fact, this will be possible with the "instance property" portion of this proposal :)

@jurca & @hax:

This looks nice, but I don't see how to define symbol properties. Have I missed anything?

Oops! This was oversight. Barring any unforeseen grammar issues, I don't see any reason not to include declarative computed properties rather than just IdentifierName. Thanks for pointing that out!

JS uses automatic semicollon insertion (ASI). Wouldn't this proposal break this language feature by making semicollons required for class properties?

Well, ASI only happens in some places...not all. In some (many?) places semicolons are already required just for purposes of disambiguation (the same reason we need them here). Consider, for example:

var b = 1, c = 2;

var a = b + c
([1, 2, 3]).length

This is a syntax error because it is parsed as var b = 1, c = 2; var a = b + c([1,2,3]).length -- but c is not a function, so it can't be "called"!.

@GothAck:

Could defining prototypal and instance properties perhaps look like:

class Demo {
  instance_prop = {a: 100};
  prototype proto_prop = 2;
  static meh = 'all the meh';
}

Assuming one can make the case for prototype properties, that certainly seems like a viable option to explore for the future (and it's also nice because it means the current proposal doesn't preclude this idea). However, I'd like to keep the scope of this proposal small right now since we need to get the fundamentals baked and tested. Since the pattern of setting properties on this in the constructor (rather than on prototype) is already most prevailing, I think this is a good place to start.

Owner

jeffmo commented Sep 15, 2015

FYI: I've begun porting things over to this repo: https://github.com/jeffmo/es-class-properties/
Lets try to continue discussion on specific topics using issues there.

vedmant commented Jan 2, 2016

I'm totally for class properties, I'm not JavaScript developer, but PHP developer, and first time I wanted to specify initial state in ES6 I tried to use: state = { count: 0 }; which is the first obvious thing to do, furthermore for static properties. This is easy and logical. I think it's good Idea to borrow some classes logic from different languages, like Traits for mixing from PHP, interfaces for inverting dependencies, so on.

GGAlanSmithee commented Aug 18, 2016

@jeffmo Even if this have been moved, you might want to update the gist to show that this is currently in stage 2, not stage 0. (Or atleast it says stage 2 here, but this lists it as a stage 1 draft...)

*EDIT: it does look like it is indeed in stage 2, according to this list

Accepting

class Whatever{
  someSymbolThatIsAFunction(){
    // yeah they call me a method but I am of the Function type
  }
}

but not

class Whatever{
  someSymbolThatIsNotAFunction = 1;
}

is to violate JS syntax basics.

bounceme commented Jun 2, 2017

so are semicolons required after each instance property set using this proposed syntax? otherwise a criticism i have is that the class body is messier and looks too much like any other set of statements of a regular function, and parsing things like *ident (){} is much harder

No, semicolons are not required; they follow normal ASI rules and may come automatically. The grammar contains semicolons, just as it does at the end of normal statements which are also subject to ASI.

dawsbot commented Jun 29, 2017

Should be updated from "stage 0" in the description of this gist to "stage 2" @jeffmo. Thanks for the information 👌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment