Proposal for a static constructor and static member variables
So we're on the same page, in the active proposal list ( tc39/proposals) you'll find
- Public Fields: tc39.github.io/proposal-class-public-fields
- Private Fields: tc39/proposal-private-fields
Public Fields includes a syntax for static properties that would convert your example to something like
class Multiton {
static instances = {};
constructor(name) {
if (Multiton.instances.hasOwnProperty(name)) throw `Multiton name
already used`;
Multiton.instances[name] = this;
}
}
though I don't know I'd personally do this in this instance, rather than putting it outside the class, like
const instances = {};
class Multiton {
constructor(name) {
if (instances.hasOwnProperty(name)) throw `Multiton name already
used`;
instances[name] = this;
}
}
Keep in mind that if something is a "static" in the ES6 class method sense,
you will never be able to do this.instances
to get it because the
property does not live on this
, it lives on the constructor. I'm unsure
if the private fields proposal would allow static #instance = {};
as a
static with #instance[name] = this;
inside the constructor.
The public one is the one I was thinking of when I said it shouldn't conflict. (I don't see the private spec going anywhere to be honest with that syntax, but if it does that would be interesting).
As for the conversion, yes that would be a good conversion for the first example. In the second example putting things outside places them in a different scope. Imagine you had multiple classes defined and they had their own instances member.
As for static constructors a more contrived example would be a memoization operation where one does initial work or generation in the static constructor rather than initialization.
class MathLibrary
{
static table = {};
static constructor()
{
// Common cases
for (let i = 0; i < 100; ++i)
{
MathLibrary.table[i] = // something complex and time consuming
}
}
static Calculate(x)
{
if (MathLibrary.table.hasOwnProperty(x))
{
return MathLibrary.table[x];
}
else
{
let complexCalculation = // something complex and time consuming
MathLibrary.table[x] = complexCalculation;
return complexCalculation;
}
}
}
Keep in mind that if something is a "static" in the ES6 class method sense, you will never be able to do
this.instances
to get it because the property does not live onthis
, it lives on the constructor.
I'll have to think about that. Might need another proposal. I always like to think about static methods and variables as being shared among all instances. Would be nice to get that kind of meaning also. Essentially aliases. Accessing static methods inside of a instance methods requires using the type name which I find less than ideal. Probably a personal preference though.
Having a static constructor makes things more confusing, since now static
and instance constructors would need to be differentiated, and its value is
kind of unclear. Your example also doesn't make sense, since you're
accessing the static properties from the instance constructor using this
,
which would refer to the instance, not the constructor.
It's an interesting idea and certainly some other languages (Java, C#), have static constructors; others (C++) have lived without. I don't recall the runtime details for C#, but certainly Java defers calling static constructors until actually initializing the class, which is indeed later than JavaScript's class definition time, at least for main scripts.
Given modules, though, I wonder if static constructors of the style
you're describing (where they aren't triggered until the first access
to the class) are worth the overhead? Implementing would either mean
trapping access to the class constructor's binding and doing init on
first post-definition access (but I suspect that would be prone to
being triggered early a lot), or effectively proxying the class
constructor and using the get
and some other traps (under the
covers, granted, but the overhead is still there). That sounds heavy
to me. Whereas using a module gives us the lazy init by the nature of
modules -- if they're never imported, they're never run:
export default class MathLibrary {
// (Assumes the public class properties proposal)
static table = (() => {
const t = [];
// ...time-consuming work initializing t here...
return t;
})();
static calculate() {
// ...can use MathLibrary.table here...
}
}
(I wouldn't make table
a static property at all -- exposes internal
details of the class -- but I wanted to stay close to your example.)
I wouldn't mind some syntactic sugar to make that prettier, but I'm not sure we need it to be deferred to first use of the class; I'm happy for it to run at class definition time. Using a module very nearly does that deferral already. The use case for deferred static construction becomes a module that imports the module above but then conditionally doesn't use it (and we only want to do the time-consuming work in the branch that does), which starts getting a bit...narrow?
Putting modules to the side for now:
From the current proposal:
The Singleton and Multiton patterns are two instances where this feature can be utilized.
I assume you mean a singleton that isn't just a simple object, e.g., a
class for which new X
returns the same instance every time, or an
object with a property or getter that always returns the same
instance, with lazy initialization. Although a static constructor
could be used for that, I'm not sure it brings a lot to the table vs.
existing mechanisms, for instance:
let instance = null;
class Singleton {
constructor() {
if (!instance) {
instance = this;
console.log("initialization");
}
return instance;
}
}
// Logs "initialization" during first construction, and not the second
const s1 = new Singleton();
const s2 = new Singleton();
console.log(s1 === s2); // true
or
let instance = null;
const Singleton = {
get instance() {
if (!instance) {
instance = {}; // or whatever
console.log("initialization");
}
return instance;
}
};
// Logs "initialization" once, then logs true
console.log(Singleton.instance === Singleton.instance);
or
const Singleton = {
get instance() {
const instance = {}; // or whatever
console.log("initialization");
Object.defineProperty(Singleton, "instance", {
value: instance,
enumerable: true
});
return instance;
}
};
// Logs "initialization" once, then logs true
console.log(Singleton.instance === Singleton.instance);
Granted that last one is several lines, but if it's a pattern you use often, a helper function solves that.
Similarly for Multiton. Can you elaborate how a static constructor applies?
On this
and static properties:
[From Logan Smyth]
Keep in mind that if something is a "static" in the ES6 class method sense, you will never be able to do
this.instances
to get it because the property does not live onthis
, it lives on the constructor.I'll have to think about that. Might need another proposal.
More accurately, you can refer to static properties via this
, but
only from within static methods called in the normal way:
class Example {
// (Assumes public class fields proposal goes forward)
static a = 42;
static announce() {
console.log(`${Example.a}, ${this.a}`);
}
}
Example.announce(); // "42, 42"
Example.announce.call({}); // "42, undefined"
const a = Example.announce;
a(); // Error in strict mode
// "42, undefined" (probably) in loose mode
So for instance, you could have used this
within your static
MathLibrary.Calculate
function. You can't from within a class
constructor as in your current proposal text. (Side note: I'd
recommend sticking to standard naming in these discussions -- e.g.,
calculate
rather than Calculate
-- to avoid confusion.)
In a method where this
is an instance of the class, you can of
course use this.constructor.x
to refer to a static property x
, if
you want to avoid repeating the class name.
Best,
-- T.J.
There is this proposal for static
blocks, which IMHO would fit
better with the intent:
littledan/proposal-class-static-block
Class initialization isn't the same as object initialization. Classes are only initialized once, and parameters don't make sense, so it doesn't make sense to make it a method-like construct. Getters are still function-like, and they're readable via descriptors. So such a "static constructor" would in practice be closer to a special IIFE that's run in the context of the class.
In addition, does it make sense to re-run the static
block with
subclasses? Generally, it doesn't, because subclasses prototypically
inherit their parent (you already get those for free), and it could
break the intuitive behavior of using them with the private field
proposal to expose private properties to specific classes and
functions outside it. (Those functions would be created once per class
- subclass, so it'd waste a lot of time for no otherwise good reason.)
So given it's best to only run it once ever, and parameters don't make sense, might as well just make it a declarative block.
Isiah Meadows me at isiahmeadows.com
The initial proposal for discussion is below:
sirisian/ecmascript-static-constructor
I don't believe this conflicts with other proposals yet, nor any future proposals. Essentially it adds static members in a very compact syntax with minimal grammar changes required.
What I foresee happening years from now is public, private, and static member syntax will be added. Much like how public members are defined in the constructor for the moment the static constructor would be the way to define static members of the class for now.
Thoughts?