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

Honour class property values set by parents #242

Open brianmhunt opened 5 years ago

brianmhunt commented 5 years ago

It is a common pattern to have a parent class that sets the properties of children, but the current TC39 spec introduces a problem with this pattern: children properties are set to undefined.

Just one illustrative example (in Typescript, since the types aid the illustration):

abstract class Parent {
  constructor (params) {
     for (const prop of this.observables) {
        // for some observable e.g. ko.observable, mobx, etc., or other "decorator"
        this[prop] = observable()
     }
  }
  abstract get observables () : string[]
}

class Child {
  a: Observable<string>
  b: Observable<number>

  get observables () { return ['a', 'b'] }
}

c = new Child()
c.a === undefined // 🚨

The current spec 2.7 DefineField(receiver, fieldRecord) at 2.7.6 mandates that properties a and b be assigned at the construction of Child the value undefined.

Based on my own experience, this behaviour obscures bugs that can be subtle and difficult to debug. If introduced into e.g. Typescript this would have the potential to break a lot of code.

This problem would be avoided if a class does not assign undefined if the parent has already assigned a value (i.e. the property exists). I feel quite strongly that this is the behaviour most developers expect.

If one wishes to obscure properties from parents, it could be explicitly done with a new keyword such as protected or own i.e.

class Child {
   a: Observable<string>
   own b: string
   get observables() { return ['a', 'b'] }
}

c = new Child()
typeof c.a // Observable
typeof c.b // undefined

I noted that this is the behaviour honoured by @babel/plugin-proposal-class-properties, and that it's already been identified there as an issue https://github.com/babel/babel/issues/8280 (closed on the basis that it's in compliance with this spec).

rdking commented 5 years ago

@sepehr what you're describing isn't that difficult to implement in ES, but don't expect that the language supports it with syntax. The trick is to control the order of instances and prototypes while using accessors to keep data on the appropriate object. As an example, for an instance who's class was defined as roughly C extends B extends A, the instance structure would look like this:

let instance = { //C instance
  //C instance property accessors
  __proto__: { //B Instance
    //B instance property accessors
    __proto__: { //A instance
      //A instance property accessors
      __proto__: { //C prototype
        __proto__: { //B prototype
          __proto__: { //A prototype
            __proto__: Object.prototype
          }
        }
      }
    }
  }
};

If classes were structured like this, you could get what you wanted, but as you might guess, there are several problems with this design, not the least of which is the need to defeat copy-on-write semantics for properties behind a prototype interface by using accessors. That means the actual, per-class instance property storage is somewhere else. Not very memory efficient, or fast.

Another problem is that for classes extending native classes, the instance of the native object needs to be the top object. That alone interferes with inheritance, as any own property of the native object will always override all properties of subclasses.