Data in JavaScript Classes

Ranando King
The Startup
Published in
7 min readOct 5, 2019

--

I’m going to follow the pattern I’ve seen in many other articles of this type and give you a little peek into my experiences that led to this article before we get into the meat and potatoes. However, if that sounds like a bad case of TL;DR to you, just jump down to the next section and spare yourself the drowsiness. I promise, I don’t mind.

A Little History

Before the release of ES6, I had grown accustom to using ES5-style pseudo classes. It was as simple as writing a constructor function, setting up a prototype with all the default data and functions I wanted, and re-assigning the prototype’s constructor back to the constructor function. Even inheritance was relatively simple (albeit a bit involved).

Like many, I’d grown tired of all the boiler plate and crafted utility functions to do the staging quickly. After a short while, though, I wanted more. I wanted the ability to create private data inside a class. A bit of research and the WeakMap solution landed in my lap. Beautiful…. and yet somehow awkward and clumsy.

And once again I found myself rolling up some boilerplate. Then I found myself wanting something truly difficult: protected properties. By that time, there were several libraries around that were arguably superior to my own. I tried many of them, but they all fell short of what I wanted.

Around the same time, I’d heard rumors that Javascript would soon gain support for the “class” keyword. That got my hopes up. Maybe a little too far, because when ES6 and “class” landed, my hopes crashed with it. No support for declared data members whatsoever. So while working on some code for my then present job, I studied up on Proxy, and WeakMap, and settled on my first design for a fully privileged class library. That was a grand experiment. It worked so well that production code was released using it.

Since then, I’ve sharpened my skills and created several newer and (arguably) better designs. While I was doing this, I learned of TC39 and their data proposal that is now the infamous proposal-class-fields. My initial hopes were high, seeing what looked like declared properties in the class definition, but once again, those hopes were quickly dashed.

After years of putting default values on the prototype, it didn’t make sense to me to put them in some hidden, inaccessible space only to have them magically reappear when running the constructor. Then came private-fields and its u̶g̶l̶y h̶o̶r̶r̶i̶b̶l̶e d̶i̶s̶t̶u̶r̶b̶i̶n̶g peculiar syntax. I could understand the rational for it so, ok. Not every new thing is going to look great or be comfortable to type.

But then they did it. They merged the two proposals, and made a choice that completely broke how I intended to use it. If you look in the proposal, it’s the Set vs. Define argument. I won’t go into details here because I wrote

https://medium.com/@kingmph/the-new-feature-on-the-horizon-in-es-cd0015158ceb

when I hit my limit on how much disappointment I could stomach. So back to work I went, creating yet another class library, even if only to show that the approach that was taken in that proposal was more damaging than necessary.

And now the part you were waiting for…

In case you haven’t seem them, ES6 classes look like this:

class Ex {
somefunc(...args) {
//Some code here
}
get prop() { return something; }
set prop(v) { something = v; }
...
constructor(...args) {
//Initialize the class here
}
}

Unfortunately, there’s no facility for data. To add data properties, you either still find yourself manipulating the prototype after the class definition or you have to create the ones you need on the instance in the constructor. To me this seems to be a waste.

To solve this problem, I turned back to wrappers. First I tried wrapping the class definition and making modifications that way, but that got complicated real quick due to the fact that I had no control over what instance object was received by the constructor. That’s exacerbated by the existence of super.

In the end, I dispensed with the use of the class keyword altogether. The result ended up being a very small and fairly simple library I named ClassicJS. That’s what’s in the image at the top of the article. I’m not going to go into detail about that library. Instead, I’m going to tell you about how the basic principle can be used to add public data properties to normal ES6 class prototypes.

Take a look at this code:

class Point2D extends PublicData({
x: 0,
y: 0,
}) {
translate(dx, dy) {...}
rotate(xAngle, yAngle) {...}
}
class Point3D extends PublicData(Point2D, {
z: 0
}) {
translate(dx, dy, dz) {
super.translate(dx, dy);
...
}
rotate(xAngle, yAngle, zAngle) {
super.rotate(xAngle, yAngle);
...
}
}

In a very simple and straight forward-looking way, we have added the capability to include declared data in our classes. So what’s going on here? Everything you need to know is in that `PublicData` function.

Most of us have heard of the “super return” trick, right? You know, it’s where you have a class extend a function that returns a pre-manipulated object instead of the usual newly created one. This isn’t that, but it’s similar. Instead of simply having PublicData create a function doing the super return trick, it creates a new class in the prototype chain and uses the data you specified as the prototype. When you give it 2 parameters, like with Point3D in the example above, the first parameter is used as a base class for the generated data class.

The function is surprisingly easy to write:

function PublicData(base, proto) {
//Section 1
switch(arguments.length) {
case 0:
base = Object;
proto = {};
break;
case 1:
if (typeof(base) === "function") {
proto = {};
}
else {
proto = base || {};
base = Object;
}
break;
default:
if (typeof(base) !== "function") {
throw new TypeError("Invalid 'base' argument");
}
if (!proto || (typeof(proto) !== "object")) {
throw new TypeError("Invalid 'proto' argument");
}
break;
}
//Section 2
let retval = function PublicDataShim(...args) {
if (!new.target) {
throw new TypeError(`${retval.name} requires 'new'`);
}
return Reflect.construct(base, args, new.target);
};
//Section 3
Object.defineProperties(retval.prototype, Object.getOwnPropertyDescriptors(proto));
Object.setPrototypeOf(retval.prototype, base.protoype);
Object.setPrototypeOf(retval, base);
return retval;
}

Section 1 is just some dynamic argument validation. Since you can pass in either just a prototype or a base class and a prototype, that code just ensures that what you passed in makes sense. Not much to explain there.

Section 2 is fairly close to the default definition of a constructor for a class that extends another class. Since this is a function and not a class, Reflect.construct has to be used to get the same effect as calling super. The throw is added for consistency and as a validation that new.target actually exists. If it didn’t, Reflect.construct would not return an instance object with the correct prototype.

Section 3 is the tried-and-true ES5 method of creating a pseudo-classical class that extends another one. The only minor difference is in that the proto parameter is shallow copied onto the prototype of the newly generated function. That keeps us from using whatever was passed in as proto as a means to back-door manipulate the intervening prototype.

If we create an instance of Point3D, the resulting instance looks like this:

The data doesn’t end up on the same object as the one being declared, but it does preserve everything you’d expect from data properties on the prototype.

The Down Side

Due to an unfortunate decision made in the development of JavaScript, we’re stuck with a nasty little “foot-gun” regarding the placement of objects on the prototype. However, after many years of development experience by many developers, there is now a well known solution to this problem:

DON’T DO THAT!

In general, if you want your class to have a data object on the prototype, you should probably either deep freeze it, or Proxy-wrap it so that it is immutable unless you want all of your class instances to edit properties of the same object instance. If instead you want each instance to have its own copy of that object, assign the object in the constructor.

As long as you do that (before any code that may edit that object), then you won’t encounter any issues, even if you do put a mutable copy of the instance on the prototype. However, it’s still advisable to follow the 3-word rule above. As long as it’s not an object, data properties on the prototype chain are no issue at all, and preserve all the semantics you’ve come to expect from your friendly neighborhood prototype-oriented language. So have fun putting data in your class definitions while we wait for a final solution from TC39.

If by any chance, you found yourself curious about the code in the image at the top of the article, it’s an extension of this same idea that allows for private and protected data on instance and the constructor function. However, it’s not yet complete. There are a few features left to write, but it functions well as is. I’ll be writing another article when it’s complete.

Edit:

I wasn’t completely satisfied with my “Don’t Do That” declaration above, as it really doesn’t give you any work-around for declaratively adding objects as data in your class definitions. To remedy this, I’ve written a follow-up article.

With this, you should now be able to create data in your classes without needing to depend on Babel, TypeScript, or proposals that aren’t yet fully a part of the language. Enjoy!

--

--

Ranando King
The Startup

I am: a father of 3, a software engineer with design and development experience in many languages, an amateur musician/producer, and a published poet.