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

Instance and prototype fields #265

Closed a-ejs closed 5 years ago

a-ejs commented 5 years ago

This is an updated summary of the problems I see with new syntax. I might update this with more points later.

It is unintuitive

Given that this:

class foo {
  bar() {}
}

is practically identical to:

function foo() {}
foo.prototype.bar = function() {};

it is not entirely unreasonable to expect

class foo {
  bar = null;
}

to be the equivalent of

function foo() {}
foo.prototype.bar = null;

but it isn't.

Likewise, in this class definition:

class B extends A {
  foo() {}
}

method foo of B.prototype expectedly takes priority over inherited method foo from A.prototype:

class A {
  foo() {}
}

But if A were defined like this:

class A {
  foo = 1;
}

behavior would be radically different, which is not apparent.

It is inconsistent

It is also inconsistent with similar constructs in the language or this very proposal. For example, if you add the keyword static, this:

class foo {
  static bar = function() {};
}

expectedly does become identical to

class foo {
  static bar() {}
}

Object literals use somewhat similar syntax, so this:

{
  foo() {}
}

is also practically same as

a = {
  foo: function() {}
}

Proposed class fields on instances is not consistent with any of this behavior.

It does not add any clarity

Properties on class instances are initialized in the constructor, like so:

class {
  constructor() {
    this.listOfThings = [];

    // do other work
  }

  /* ... */
}

Everything about this example is clear. It defines a constructor function that, when called, sets the property listOfThings of this to a new array. There's also zero ambiguity to when this assignment is done and how it works with inheritance.

Proposed syntax simplifies it to:

class {
  constructor() {
    // do the work
  }

  // The following line can go anywhere in the class body
  listOfThings = [];
}

This is, supposedly, somehow an improvement. But where it saves the programmer the need to type this. in front of the definition, it makes the behavior much more implicit and less apparent.

It changes the meaning of class syntax

Classes in JavaScript is just an abstraction, syntactic sugar, whatever you want to call it, over its prototype-based inheritance. A "class" is defined by a constructor function and its prototype. So, class syntax is just an easier way to define that function and the prototype.

Body of class definition describes properties of the prototype or the function itself, only the constructor property being somewhat special since it describes the function itself. But it still exists as a method property (syntactically) and even goes into the prototype (as A.prototype.constructor) - that's also pretty neat. (Although that behavior is unrelated to class syntax, it's just a nice touch. And it's consistent!)

class A {
  // Everything here describes the properties of A and A.prototype
  constructor() {
    // This defines the body of A
  }
}

New syntax invalidates all of that. Now, the class body describes both what the constructor is and what it does, in one place. And since it doesn't replace the constructor() {} syntax, what it does is now defined both inside and outside the function body, something extremely unusual and not achievable with other syntax.

It mixes function code with normal code

As far as I know, this behavior is not present anywhere else in the language.

A function's body is a sequence of statements that's executed when the function is called, as opposed to non-function object literals that are evaluated right away.

Class body is not a function body. It describes the function and its prototype as an object. With instance fields, class body becomes mixed code, where some parts are evaluated right away, and some are evaluated with the constructor:

class {
  // definition
  constructor() {}

  // definition
  method() {}

  // function code
  a = 1;

  // definiton
  static a = 1;
}

This is very unusual for JS. Just because similar behavior exists in other languages like C++ doesn't necessarily mean it would work perfectly fine in JS, especially considering how C++ classes and JS prototype inheritance have very little in common besides general purpose.

Imagine if function properties could be defined within its body:

function foo() {
  doAThing();
  bar: baz();
  doAnotherThing();
  return 123;
}
// as sugar for
function foo() {
  doAThing();
  doAnotherThing();
  return 123;
}
foo.bar = baz();

This isn't much different from class fields.

Summary

This syntax provides very little, if any, benefit for all the downsides that it has.

I don't see how current way of initializing instance properties is so bad that it needs to be simplified or changed in the first place.

New syntax is deceiving, confusing, does not promote good practice. Someone who doesn't know better could easily create more instances than needed of the same thing without realizing, as an example. It can easily become another "ugly" part of the language, for the lack of a better word.

It's very strange to me that apparently very few people seem to see an issue with all this.


original issue text below


The following code:

class foo {
  bar() {}
  baz() {}
}

defines a "class" foo with "methods" bar and baz - "class" and "methods" being quoted because that's just syntactic sugar over defining a constructor function and a prototype, like so:

function foo() {}
foo.prototype.bar = function() {};
foo.prototype.baz = function() {};

So if I were to swap out a method definition for another value, I would expect something like this:

class foo {
  bar = 1;
  baz() {}
}

to become (identical to):

function foo() {}
foo.prototype.bar = 1;
foo.prototype.baz = function() {};

but instead it becomes:

function foo() { this.bar = 1; }
foo.prototype.baz = function() {};

which is counter-intuitive, and frankly not that useful (saves typing the this. part in constructor, whereas adding properties to the prototype remains a pain in the ass), but I think the former should be the focus.

So why does it work that way? Seems much more confusing than it is useful.

Likewise, the whole concept of "private fields" brings in too much complications while not being a very useful addition in general to say the least, and I don't think it belongs in the language - but that's just my opinion (and isn't the subject of this issue).

a-ejs commented 5 years ago

Another use case would be defining class methods in different files:

import {someMethod} from "somewhere";

export class Thing {
  method = someMethod;
}

I don't think there's a 'nice' way to do this as of now.

trusktr commented 4 years ago

3jh5ba

;)