Master Objects in JS 🍨 (Part 3)

Master Objects in JS 🍨 (Part 3)

·

4 min read

Make your Constructors new-Agnostic 🏗️

When you create a constructor such as a User function, you rely on callers to remember to call it with the new operator. Notice how the function assumes that a receiver is a brand-new object:

function User(name, passwordHash) {
  this.name = name;
  this.passwordHash = passwordHash;
}

If a caller forgets the new keyword, then the function’s receiver
becomes the global object:

var u = User("baravelli", "d8b74df393528d51cd19980ae0aa028e");
u; // undefined
this.name; // "baravelli"
this.passwordHash; // "d8b74df393528d51cd19980ae0aa028e"

Not only does the function uselessly return undefined, it also disas- trously creates (or modifies, if they happen to exist already) the global variables name and passwordHash.

If the User function is defined as ES5 strict code, then the receiver
defaults to undefined:

function User(name, passwordHash) {
  "use strict";
  this.name = name;
  this.passwordHash = passwordHash;
}
var u = User("baravelli", "d8b74df393528d51cd19980ae0aa028e");
// error: this is undefined

In this case, the faulty call leads to an immediate error: The first line of User attempts to assign to this.name, which throws a TypeError. So, at least with a strict constructor function, the caller can quickly dis- cover the bug and fix it.

Still, in either case, the User function is fragile. When used with new it works as expected, but when used as a normal function it fails. A a more robust approach is to provide a function that works as a con- structure no matter how it’s called. An easy way to implement this is to check that the receiver value is a proper instance of User:

function User(name, passwordHash) {
  if (!(this instanceof User)) {
    return new User(name, passwordHash);
  }
  this.name = name;
  this.passwordHash = passwordHash;
}

This way, the result of calling User is an object that inherits from User.prototype, regardless of whether it’s called as a function or as a constructor:

let x = User("baravelli", "d8b74df393528d51cd19980ae0aa028e");
let y = new User("baravelli", "d8b74df393528d51cd19980ae0aa028e");
x instanceof User; // true
y instanceof User; // true

One downside to this pattern is that it requires an extra function call, so it is a bit more expensive. It’s also hard to use for variadic functions, since there is no straightforward analog to the apply method for calling variadic functions as constructors. A somewhat more exotic approach makes use of ES5’s Object.create:

function User(name, passwordHash) {
  let self = this instanceof User ? this : Object.create(User.prototype);
  self.name = name;
  self.passwordHash = passwordHash;
  return self;
}

Object.create takes a prototype object and returns a new object that inherits from it. So when this version of User is called as a function, the result is a new object inheriting from User.prototype, with the name and passwordHash properties initialized.

While Object.create is only available in ES5, it can be approximated in older environments by creating a local constructor and instantiat- ing it with new:

if (typeof Object.create === "undefined") {
  Object.create = function (prototype) {
    function C() {}
    C.prototype = prototype;
    return new C();
  };
}

Note that this only implements the single-argument version of Object.create. The real version also accepts an optional second argument that describes a set of property descriptors to define on the new object.

What happens if someone calls this new version of User with new? Thanks to the constructor override pattern, it behaves just like it does with a function call. This works because JavaScript allows the result of a new expression to be overridden by an explicit return from a constructor function. When the User returns self, the result of the new expression becomes self, which may be a different object from the one bound to this.

Protecting a constructor against misuse may not always be worth the trouble, especially when you are only using a constructor locally. Still, it’s important to understand how bad things can go wrong if a constructor is called in the wrong way. At the very least, it’s important to document when a constructor function expects to be called with new, especially when sharing it across a large codebase or from a shared library.

Things to Remember 🧠

  1. Make a constructor agnostic to its caller’s syntax by reinvoking itself with new or with Object.create.
  2. Document clearly when a function expects to be called with new.

🎉🎉🎉 Thank you for reading the third part of this article! 🎉🎉🎉

You can find me on twitter as well😃.