Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Last active June 14, 2022 14:37
Show Gist options
  • Select an option

  • Save dfkaye/6814571 to your computer and use it in GitHub Desktop.

Select an option

Save dfkaye/6814571 to your computer and use it in GitHub Desktop.
exporting prototypes instead of constructors

This a follow-up alternative to my constructor API proposal, with a possible implementation at my Constructor.js repo.

JavaScript function-first programmers argue that we should stop using constructor functions in JavaScript, for reasons that are incomprehensible to me.

They boil down to "use of new, prototype, and this is an anti-pattern."

There is nothing wrong with any of that, other than the inability to distinguish between between factory functions and constructors.

Examples that follow are taken/modified from (see Custom types (classes) using object literals in JavaScript

Custom Types

Constructors are really custom types. The well-understood pattern for creating type instances is of course the new keyword:

var white = new Color('#fff');

Or factories:

var white = Color.create('#fff');

But what if you forget new?

Well, actually it shouldn't matter if you add an instanceof check in the constructor function:

function Color(hex) {
  if (!(this instanceof Color)) {
    return new Color(hex);
  }
  
  ...
}

Doing that in every constructor protects clients from the bad mapping of this, but we're creating more work for ourselves in each case. We should delegate that to a helper function, or adopt a different invocation pattern.

Types as prototypes

Starting with a prototype, where color is an object:

var color = {

   r: 1, g: 1, b: 1,

   copy: function(color) {
       ...
   },

   setRGB: function(r, g, b) {
       ...
   },

   setHSV: function(h, s, v) {
       ...
   }
};

Instead of using Object.create(color), attach the constructor function directly to the color prototype:

color.constructor = function(hex) {
     ...
};

color.constructor.prototype = color;

Helper function

Now you can wrap that boilerplate step in a function call, like Nicholas Zakas' type:

function type(details){
    details.constructor.prototype = details;
    return details.constructor;
}

Which in action looks like:

var Color = type({
     constructor: function(hex) {
         ...
     },
     
     ...
});

var mycolor = new Color("ffffff");

Different invocation pattern

The only change I'd make here is for type to return the details argument:

function type(details){
    details.constructor.prototype = details;
    return details;
}

So we'd call:

var color = type({
  ...
});

var mycolor = new color.constructor("ffffff");

Because new vs. forgetting-to-use-new is just common enough to cause confusion, adding the .constructor to any new statement disambiguates the function being invoked.

Types in Commonjs

The Commonjs boilerplate used in Node.js looks like so:

var type = require('./path/to/type.js');

module.exports = Color;
function Color(hex) {
  ...
}

Color.prototype.r = ...

Rather than exporting the constructor, we could export the prototype instead:

var type = require('./path/to/type.js');

module.exports = type({
     constructor: function(hex) {
         ...
     },

     ...
});

That's usable as:

var color = require('./color.js');

var white = new color.constructor("ffffff");

Use this pattern with existing constructors

If ./color.js returns a constructor instead of a prototype, you can decorate your instantiation like this:

var color = require('./color.js').prototype;

var white = new color.constructor("ffffff");

For in-built constructors, such as String:

var string = String.prototype;

var white = new string.constructor("ffffff");

That feels exceedingly fake but does communicate the intention that new goes with prototype.constructor.

Superclass not needed (until it is)

Suppose we need to serialize the color object. We could add a serialize() method to the base color prototype, but what if you don't own the color.js module? That would expose it everywhere that color is used in your application.

If we really need to extend a given type's prototype, you could clone it and add new prototype properties.

var color = require('./color.js');

var fakeColor = Object.create(color);

fakeColor.serialize = function() {
  ...
}

Now, what if serialization depended on some identifier property for each instance? You'd need to augment the constructor to handle that. The straightforward, no-library required approach is to call the base object's constructor with the call() method:

var fakeColor = Object.create(color);

fakeColor.constructor = function (hex) {

    color.constructor.call(this, hex); // *super* call
    
    this.uuid == UUID++;
}

Follow that pattern to override any color method:

fakeColor.methodName = function (arg) {
    return this.uuid + '>>' + color.methodName.call(this, arg); 
}

__ super __

Here's where the superclass comes in - it replaces the baseName.methodName.call boilerplate with this.superclass.methodName.

Suppose that type() could take two arguments - a base object, and an inheriting object - that could amend it to support a super keyword.

Inside the inheriting constructor, replace dependencyName.constructor.call(arg) with this.super(arg).

Once that's been called, the this.super property is converted into a copy of the base object.

For example, if the inheriting object is a prototype, you can assign any methods in the specifier along with the constructor:

//module.exports = depUser;
var depUser = type(someDep, {

    constructor: function (data) {
        this.__super__(); // replaces someDep.constructor.call()
    
        this.data = data;
    },
    overriddenMethod = function () {            
        return this.__super__.overriddenMethod + ' >> ' + this.data; // __super__ is now a copy of someDep
    }
});

You can still modify the inheriting prototype:

depUser.anotherMethod = function () {        
    return this.__super__.anotherMethod + ' >> ' + this.data;
};

In either case, you'd still create a new depUser object as:

var user = new depUser.constructor(data);

user.__super__ => returns a copy of someDep

Yes, something.super.super is possible - and, yes, that is a real problem with inheritance - deep hierarchies.

The upside is that when you make copies of the base objects as prototypes for inheriting objects, you're not polluting the original base object's surface API.

[MORE TO COME... OR REMOVE]

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