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

Feedback: why no arguments in initializers? #289

Closed patrick-soquet closed 4 years ago

patrick-soquet commented 4 years ago

Preventing field initializers to contain arguments required special care in the specification and the implementations. A developer asked me why this code is wrong:

function f() {
  return class {
    constructor() {}
    field = arguments
  };
}
const C = f(0, 1, 2);
const o = new C;
print(o.field);

while this code is right:

function f(...args) {
  return class {
    constructor(...args) {}
    field = args
  };
}
const C = f(0, 1, 2);
const o = new C;
print(o.field); // 0,1,2

I searched for an explanation but did not find one. What are we trying to prevent here? Thank you

rdking commented 4 years ago

The hang-up is a matter of semantics. Here's a translation of the class you gave that should explain why:

function f() {
  return class {
    constructor() {
      //super goes here if needed
      (() => {
        Object.defineProperties(this, "field", {
          enumerable: true,
          configurable: true,
          writable: true,
          value: arguments        //"strict mode" doesn't allow arguments
        });
      })();
      //other initializer functions here if they exist
      //constructor body code goes here if it exists
    }
  }
}

As you can see, the issue is that each initializer is essentially wrapped in a separate function and called from the scope of the constructor. That's also the reason that you can't use new.target in an initializer.

littledan commented 4 years ago

I can't speak for the whole committee's reasoning, but here's why I think it makes sense to ban arguments in initializers.

First, at a surface level: We decided to ban arguments because arguments was not going to give people what they expected. Following the general principles of lexical scoping, arguments was either going to either be an empty arguments list (if we considered each initializer to be its own method, scope-wise), or the enclosing binding of arguments (if we just fell back to the default, as computed property keys and the extends clause do).

But I guess the real question is, why not give the arguments value that would be more useful, the one passed into the constructor? I think the general idea of lexical scoping would be that these things are only set within the particular curly braces/parameter list where the function is defined. It's weird enough that arguments is set without being textually present; if we also defined it in a few other places that aren't even linearly adjacent, it'd be even weirder.

In the course of developing the class fields proposal, we did lament how constructor arguments are unavailable. We looked into several different syntactic alternatives, but eventually concluded that the syntax used in TypeScript and Babel (field = initializer in the class body) was for the best. (Well, this is only part of the syntax TS supports, and I do like some of the other parts, but it's hard to see how to carry them over here.)

As an aside, arguments is considered a legacy construct, and rest arguments are preferred instead for more intuitive coding patterns. It'd really be too bad if arguments were the only way to get at constructor parameters. But it doesn't seem like that was really an option anyway.

patrick-soquet commented 4 years ago

@littledan Thank you much for your detailed explanation. FWIW, fields, especially private fields have been really welcomed in XS. Hopefully the proposal will be stage 4 soon.