Skip to content

Instantly share code, notes, and snippets.

@rbuckton
Last active March 19, 2018 21:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rbuckton/d6a0d1618d388d6d2258602c3a9805bf to your computer and use it in GitHub Desktop.
Save rbuckton/d6a0d1618d388d6d2258602c3a9805bf to your computer and use it in GitHub Desktop.

Introduction

This is a strawman for the inclusion of a new ClassProperty syntax:

class C {
  static f() { ... }
  
  g() {
    class.f();
  }
}

Grammar

MemberExpression[Yield, Await] :
  ClassProperty[?Yield, ?Await]

ClassProperty[Yield, Await] :
  `class` `[` Expression[+In, ?Yield, ?Await] `]`
  `class` `.` IdentifierName

Runtime Semantics

  • Function Environment Records have a new field in Table 15:
    Field Name Value Meaning
    [[ClassObject]] Object | undefined If the associated function has class property access and is not an ArrowFunction, [[ClassObject]] is the class that the function is bound to as a method. The default value for [[ClassObject]] is undefined.
  • Function Environment Records have a new method in Table 16:
    Method Purpose
    GetClassBase() Return the object that is the base for class property access bound in this Environment Record.
  • ECMAScript Function Objects have a new internal slot in Table 27:
    Interanl Slot Type Description
    [[ClassObject]] Object | undefined If the function has class property access, this is the object where class property lookups begin.
  • During ClassDefinitionEvaluation, the class constructor (F) is set as the [[ClassObject]] on the method.
  • During NewFunctionEnvironment, the [[ClassObject]] is copied from the method (F) to envRec.[[ClassObject]].
  • Arrow functions use the [[ClassObject]] of their containing lexical environment (similar to super and this).
  • A new Class Reference type is added with properties similar to Super Reference.
  • When evaluating ClassProperty: `class` `.` IdentifierName we return a new Class Reference with the following properties:
    • The referenced name component is the StringValue of IdentifierName.
    • The base value component is the [[ClassObject]] of GetThisEnvironment().
    • The actualThis component is either:
      • If the containing method is static, the current this binding.
      • Else, the [[ClassObject]] of GetThisEnvironment().
  • When evaluating ClassProperty: `class` `[` Expression `]` we return a new Class Reference with the following properties:
    • The referenced name component is the result of calling ?ToPropertyKey on the result of calling GetValue on the result of evaluating Expression.
    • The base value component is the [[ClassObject]] of GetThisEnvironment().
    • The actualThis component is either:
      • If the containing method is static, the current this binding.
      • Else, the [[ClassObject]] of GetThisEnvironment().
  • GetThisValue(V) would be modified to add an optional calling argument that is set to true during EvaluateCall.
  • GetThisValue(V, true) returns the thisValue component of a Class Reference in the same way that it does for a Super Reference.
  • GetThisValue(V, false) returns the base value component of a Class Reference.

Examples

Property Access

In class methods or the class constructor, getting the value of class.x always refers to the value of the property x on the containing lexical class:

class Base {
    static f() {
        console.log(`this: ${this.name}, class: ${class.name})`);
    }
}
class Sub extends Base {
}

Base.f();                           // this: Base, class: Base
Sub.f();                            // this: Sub, class: Base
Base.f.call({ name: "Other" });     // this: Other, class: Base

This behavior provides the following benefits:

  • Able to reference static members of the containing lexical class without needing to repeat the class name.
  • Able to reference static members of an anonymous class declaration or expression:
    export default class {
        static f() { ... }
        g() { class.f(); }
    }

Property Assignment

In class methods or the class constructor, setting the value of class.x always updates the value of the property x on the containing lexical class:

function print(F) {
    const { name, x, y } = F;
    const hasX = F.hasOwnProperty("x") ? "own" : "inherited";
    const hasY = F.hasOwnProperty("y") ? "own" : "inherited";
    console.log(`${name}.x: ${x} (${hasX}), ${name}.y: ${y} (${hasY})`);
}

class Base {
    static f() {
        this.x++;
        class.y++;
    }
}

Base.x = 0;
Base.y = 0;

class Sub extends Base {
}

print(Base);                        // Base.x: 0 (own), Base.y: 0 (own)
print(Sub);                         // Sub.x: 0 (inherited), Sub.y: 0 (inherited)

Base.f();

print(Base);                        // Base.x: 1 (own), Base.y: 1 (own)
print(Sub);                         // Sub.x: 1 (inherited), Sub.y: 1 (inherited)

Sub.f();

print(Base);                        // Base.x: 1 (own), Base.y: 2 (own)
print(Sub);                         // Sub.x: 2 (own), Sub.y: 2 (inherited)

Base.f();

print(Base);                        // Base.x: 2 (own), Base.y: 3 (own)
print(Sub);                         // Sub.x: 2 (own), Sub.y: 3 (inherited)

This behavior provides the following benefits:

  • Assignments always occur on the current lexical class, which should be unsurprising to users.

Method Invocation

Invoking class.x() in a static method uses the current this as the receiver (similar to the behavior of super.x()):

class Base {
    static f() {
        console.log(`this.name: ${this.name}, class.name: ${class.name})`);
    }
    static g() {
        class.f();
    }
}
class Sub extends Base {
}

Base.g();                           // this: Base, class: Base
Sub.g();                            // this: Sub, class: Base
Base.g.call({ name: "Other" });     // this: Other, class: Base

This behavior provides the following benefits:

  • Method invocation preserves the this receiver to allow for overriding static methods in a subclass.
  • Invocation behavior is similar to super.x(), so should be less surprising to users.

Invoking class.x() in a non-static method or the constructor uses the value of containing lexical class as the receiver:

class Base {
    static f() {
        console.log(`this.name: ${this.name}, class.name: ${class.name})`);
    }
    g() {
        class.f();
    }
}
class Sub extends Base {
}

let b = new Base(); 
let s = new Sub();

b.g();                                      // this: Base, class: Base
s.g();                                      // this: Base, class: Base
Base.prototype.g.call({ name: "Other" });   // this: Base, class: Base

This behavior provides the following benefits:

  • Since instances will not have the lexical class constructor in their prototype hierarchy (other than through narrow corner cases), users would not expect the lexical this to be passed as the receiver from non-static methods. This behavior is the most intuitive and least-surprising behavior for users.

Relationship to Other Proposals

Class Fields

This proposal can easily align with the current class fields proposal, providing easier access to static fields without unexpected behavior:

class Base {
    static counter = 0;
    id = class.counter++;       // Assignment, so `Base` is used as `this`
}

class Sub extends Base {
}

console.log(new Base().id);     // 0
console.log(new Sub().id);      // 1
console.log(Base.counter);      // 2
console.log(Sub.counter);       // 2

Class Private Methods

This proposal can also align with the current proposals for class private methods, providing access without introducing TypeErrors due to incorrect this while preserving the ability for subclasses to override behavior:

class Base {
    static a() {
        console.log("Base.a()");
        class.#b();
    }
    static #b() {
        console.log("Base.#b()");
        this.c();
    }
    static c() {
        console.log("Base.c()");
    }
}

class Sub extends Base {
    static c() {
        console.log("Sub.c()");
    }
}

Base.a();   // Base.a()\nBase.#b()\nBase.c()
Sub.a();    // Base.a()\nBase.#b()\nSub.c()

Class Private Fields

In addition to private methods, this proposal can also align with the current proposals for class private fields, providing access to class static private state without introducing TypeErrors due to incorrect this:

class Base {
    static #counter = 0;
    static increment() {
        return class.#counter++;
    }
}

class Sub extends Base {
}

console.log(Base.increment());  // 0
console.log(Sub.increment());   // 1
console.log(Base.increment());  // 2

Known Issues

  • ClassProperty does not provide a way to access the static members of an outer class:
    class Outer {
        static f() {}
        static g() {
            class Inner {
                static g() { } // cannot use `class` to access `Outer.f`
            }
        }
    }
    However, the same issue exists for SuperProperty and can be resolved in the same way:
    class Outer {
        static f() {}
        static g() {
            const callOuterF = () => class.f();
            class Inner {
                static g() { callOuterF(); }
            }
        }
    }
@rbuckton
Copy link
Author

rbuckton commented Mar 19, 2018

Another option might be a class.static meta-property. We could also consider a class.outer meta-property chain as well:

class Outer {
  static f() { return 1; }
  static g() {
    class.static; // Outer
    class.static.f(); // 1
    class.outer.static; // error
    class Inner {
      static f() { return 2; }
      static g() {
        class.static; // Inner
        class.static.f(); // 2
        class.outer.static; // Outer
        class.outer.static.f(); // 1
        class.outer.outer.static; // error
      }
    }
  }
}

However, with this approach it is a lot trickier to handle the receiver for a call to support subclassing. We might have to do something like this:

ClassPrefix:
  `class`
  ClassPrefix `.` `outer`

ClassStatic:
  ClassPrefix `.` `static`

ClassProperty[Yield, Await]:
  ClassStatic `.` IdentifierName
  ClassStatic `[` Expression[+In, ?Yield, ?Await] `]`

ClassReference:
  ClassStatic [lookahead != { `.`, `[` }]

Where ClassReference allows you to do f(class.static) (or new class.static()), but we still could special case class.static.x and class.static[x].

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