Data in Javascript Classes (Part 2)

Ranando King
Oct 8, 2019 · 4 min read

I didn’t originally intend to write a second part to this article, but I thought I’d be being remiss if I didn’t come back with more information. It’s about that foot-gun: the object-on-the-prototype problem. In that last article, my advice was simply “Don’t Do That!”. I feel like that doesn’t help very much. This time, let me show you a good way to follow that advice without having to chance too much about how you would use the code in the last article.

Shoot The Foot-Gun

If you haven’t already, you’re going to want to go back and read “Data in Javascript Classes”. What’s below might not make much sense unless you’re good at figuring things out with only a few clues.

Let’s start by re-introducing , but with a few new changes.

const PublicData = (function() { //New
let initFns = new WeakMap; //New
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'`);
}
//New
let p = retval.prototype;
let keys = Object.getOwnPropertyNames(p).concat(
Object.getOwnPropertySymbols(p)
);
_this = Reflect.construct(base, args, new.target); //Moved
for(let key of keys) {
let obj = p[key];
if (obj && (typeof(obj) === "object") && initFns.has(obj)) {
_this[key] = initFns.get(obj).call(_this);
}
}
return _this;
};//Section 3
Object.defineProperties(retval.prototype, Object.getOwnPropertyDescriptors(proto));
Object.setPrototypeOf(retval.prototype, base.protoype);
Object.setPrototypeOf(retval, base);
return retval;
}
//New
Object.defineProperty(PublicData, init, {
enumerable: true,
value: function init(fn) {
if (typeof(fn) !== "function") {
throw new TypeError("init() requires a function.");
}
let retval = {};
initFns.set(retval, fn);
return retval;
}
});
return PublicData;
})();

It’s still quite small, but a few things have changed:

  1. is wrapped in an IIFE so that there is private data associated with it.
  2. A new , has been created to hold initialization functions.
  3. Code has been added to the constructor to find and execute the initialization functions
  4. A function property has been added to to add the initialization functions.

Ok, so I said it 3 times: “Initialization Function”. That’s because this is the key to beating the foot-gun. I’ll show you what I mean in a moment. First, here’s some example code.

class Ex extends PublicData({
obj: {
a: "alpha",
b: "beta",
c: "gamma"
}
});
let a = new Ex;
let b = new Ex;
console.log(`a.d = ${a.d}`); //a.d = undefined
console.log(`b.d = ${b.d}`); //b.d = undefined
a.d = "fubar";
console.log(`a.d = ${a.d}`); //a.d = "fubar"
console.log(`b.d = ${b.d}`); //b.d = "fubar"

This is the foot-gun that everyone wants to avoid. For most cases, this is obviously not how things are supposed to work. Yet, this is what ES has to give us for objects on the prototype. Now let me show you how to avoid it.

The Nuts & Bolts

class Ex extends PublicData({
obj: PublicData.init(() => ({
a: "alpha",
b: "beta",
c: "gamma"
})
});
let a = new Ex;
let b = new Ex;
console.log(`a.d = ${a.d}`); //a.d = undefined
console.log(`b.d = ${b.d}`); //b.d = undefined
a.d = "fubar";
console.log(`a.d = ${a.d}`); //a.d = "fubar"
console.log(`b.d = ${b.d}`); //b.d = undefined

So what happened? Why is it working so nicely? Notice that takes a function as its parameter. This is the initialization function that I was talking about before. Its sole job is to return an instance of the data we want to put on some property, but it’s not going to be run immediately.

How does this help? It’s not that hard to understand. If you look at the definition of the function above, you’ll see that it:

  1. creates a new object
  2. uses that new object as the key to an entry in with the passed function as the value
  3. returns the new object.

This means that instead of our target object with 3 keys, on the prototype is given an empty object that is the key to finding the initialization function. Once we create a new instance of , the constructor is invoked. After creating the instance object, this constructor searches its own prototype for object properties that are keys in . When it finds one, it runs the corresponding initialization function and stores the resulting value at the corresponding property on the instance. This means each instance of has its own copy of the object on .

So there we have it. Declarative data in our definitions with foot-gun avoidance in under 70 lines of code. In all honesty, I’m too lazy to keep typing whenever I want to add an object to my data. I’d probably just set a constant like this:

const INIT = PublicData.init;

…and use everywhere instead, but I thought writing it out like this would help to make it clear what’s happening and how terribly simple it is to implement.

The Startup

Get smarter at building your thing. Join The Startup’s +794K followers.

Sign up for Top 10 Stories

By The Startup

Get smarter at building your thing. Subscribe to receive The Startup's top 10 most read stories — delivered straight into your inbox, once a week. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Ranando King

Written by

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +794K followers.

Ranando King

Written by

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +794K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store