This proposal is a simplification of classes. It focuses on providing convenient syntax for the most common class patterns of constructors and prototypes: a constructor with a declarative set of shared prototype methods, and built-in syntax for accessing the super-class.
The most important goals of this proposal, which are directly addressed by minimal classes, are:
this-bindingthis-binding, lexically scoped to declared superclassThe following are not directly addressed by minimal classes, but are also not incompatible with them:
const, guards, public/private)superpublic, private, @ shorthandconst classesThese are all reasonable things to aim for. But they are not necessary in order to fulfill the goals above.
Do not want:
The three.js SkinnedMesh example cited in classes.
class SkinnedMesh extends THREE.Mesh { new(geometry, materials) { super(geometry, materials); this.identityMatrix = new THREE.Matrix4(); this.bones = []; this.boneMatrices = []; ... } update(camera) { ... super.update(); } }
The Monster example from classes. Classes do not provide any kind of special private record; private is simply achieved via private name objects.
// a private name used by the Monster class const pHealth = Name.create(); class Monster { // The keyword "new" followed by an argument list and a body // defines the body of the class’s constructor function. new(name, health) { this.name = name; this[pHealth] = health; } // An identifier followed by an argument list and body defines a // method. A “method” here is simply a function property on some // object. attack(target) { log('The monster attacks ' + target); } // The contextual keyword "get" followed by an identifier and // a curly body defines a getter in the same way that "get" // defines one in an object literal. get isAlive() { return this[pHealth] > 0; } // Likewise, "set" can be used to define setters. set health(value) { if (value < 0) { throw new Error('Health must be non-negative.') } this[pHealth] = value } } // The only way to create prototype data properties is by // modifying the prototype outside of the declaration. Monster.prototype.numAttacks = 0; // Immutable properties can be added with defineProperty. Object.defineProperty(Monster.prototype, "attackMessage", { value: 'The monster hits you!' });
As shown in classes, it’s possible to define static methods as well:
class Monster { // "static" places the property on the constructor. static allMonsters = []; // "static" is required for constructor methods static numMonsters() { return Monster.allMonsters.length; } }
Declaration :
ClassDeclaration
...
ClassDeclaration :
class Identifier ClassHeritage? { ClassBody }
MemberExpression :
ClassExpression
...
ClassExpression :
class Identifier? ClassHeritage? { ClassBody }
ClassHeritage :
extends AssignmentExpression
ExpressionStatement :
[lookahead ∉ { "{", "function", "class" }] Expression ;
// "..." means existing members defined elsewhere
ClassElement :
PrototypePropertyDefinition
ClassPropertyDefinition
PrototypePropertyDefinition :
IdentifierName ( FormalParameterList? ) { FunctionBody } // method
get IdentifierName ( ) { FunctionBody } // getter
set IdentifierName ( FormalParameter ) { FunctionBody } // setter
ClassPropertyDefinition :
static ExportableDefinition
ExportableDefinition :
VariableDeclarationList ; // data properties
IdentifierName ( FormalParameterList? ) { FunctionBody } // method
get IdentifierName ( ) { FunctionBody } // getter
set IdentifierName ( FormalParameter ) { FunctionBody } // setter
The semantics of minimal classes is described here via desugaring. I’ll use special variables of the form %x for fresh variables not exposed to user code. But desugarings of the contents of a class body can refer to these names bound by their containing class’s desugaring.
A class expression:
class C extends D { CE ... new(cargs) { cbody } CE ... }
is equivalent to:
(do { let %d = D, %p = %d.prototype, C = %d ◁ function C(cargs) { cbody }; C.prototype = Object.create(%p, { CE, ..., constructor: { value: C, enumerable: false, writable: true, configurable: true }, CE, ... }); C })
for the original definition of Object.create. Class declarations and anonymous class expressions are similar. Classes without an extends clause implicitly extend the original Object constructor.
In a class body, a static property declaration:
static X = E, ...;
is equivalent to:
X: { value: E, enumerable: true, writable: true, configurable: true }, ...
Static methods, getters, and setters are similar.
In a class body, a prototype method declaration:
m(args) { body }
is equivalent to:
m: { value: function m(args) { body }, enumerable: false, writable: true, configurable: true }
Prototype getters and setters are similar.
In this proposal, super works exactly the same as in object initialiser super, so it’s entirely compatible. But it’s described in separate terms to show that super can also stand alone as simply a part of the class desugaring to existing constructs.
In a class constructor (but not inside its nested functions), a super-constructor call:
super(args, ...)
is equivalent to:
%d.call(this, args, ...)
for the original meaning of Function.prototype.call.
In a class constructor or prototype method (but not inside its nested functions), a super-method call:
super.m(args, ...)
is equivalent to:
%p.m.call(this, args, ...)
for the original meaning of Function.prototype.call.
public for instance propertiespublic mean something if we use it@ shorthandvar for instance propertieslet is the new var – we want to kill var deadlet for instance propertiesprop or field for instance propertiesthis.x assignments in certain static contexts in constructor bodyinstance { foo, bar } for not-yet-initialized but undefined instance propertiesx; for not-yet-initialized but undefined instance propertiesclass instead of static for staticsstatic is already reserved and perfectly familiar, even common terminologyconstructor instead of new for the constructor syntax