Skip to content

Execution order for class fields & constructor default parameters is surprising and inconsistent #3572

Open
@brad4d

Description

@brad4d

Description:

The order of execution for constructor function default parameter initializers and instance field initializers is surprising for base classes.
The instance field initializers execute before the constructor's default parameters are initialized.

Also, the order is reversed for derived classes.
The constructor's default parameters are initialized first.
The field initializers execute when the super() call is reached.

class BaseClass {
  baseClassField = (
      console.log('init baseClassField'),
      'prints before defaultParam is initialized');

  constructor(defaultParam = (
        console.log('init BaseClass defaultParam'),
       'prints after field is initialized'
      )) {
    // The spec places execution of the class instance field initializers
    // first, followed by the operation, OrdinaryCallEvaluateBody, which
    // executes both the processing of default parameters in the argument list
    // and the execution of the function body.
    //
    // This order of execution is likely surprising to the programmer,
    // who would expect the values of the parameters to have all been calculated
    // before the class field initializers execute.
    //
    // To emulate this order of execution transpilers must move the default parameter
    // initialization into the constructor body, so it can happen after the field
    // initializers. This is a non-obvious thing for the transpiler to have to do,
    // effectively transpiling a feature that "shouldn't" need to be transpiled.
  }
}

class DerivedClass extends BaseClass {
  derivedClassField = (
      console.log('init derivedClassField'),
      'prints after the default param is initialized');

  constructor(
      defaultParam = (
          console.log('init DerivedClass defaultParam'),
          'prints before derivedClassField is initialized')) {
    // Since the `this` value isn't calculated until the `super()` call is
    // reached, the spec cannot execute the instance field initializers before
    // starting the OrdinaryCallEvaluateBody operation, so the default
    // parameter initializers do run before instance field initializers for
    // a derived class.
    //
    // The action invoked by `super()` triggers execution of the BaseClass
    // constructor, followed by the instance field initializer for
    // derivedClassField.
    super();
  }
}

console.log(`
BaseClass constructor call
========
`);
new BaseClass();
// prints:
// init baseClassField
// init BaseClass defaultParam

console.log(`
DerivedClass constructor call
========
`);
new DerivedClass();
// prints:
// init DerivedClass defaultParam
// init baseClassField
// init BaseClass defaultParam
// init derivedClassField

This inconsistent behavior seems to be an artifact of how the spec was written rather than an intentional choice. If initializing a function's parameters were defined as a separate operation from executing the function's body, this could be resolved.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions