DEV Community

Cover image for Master Objects in JS ๐Ÿจ (Part 3)
Ben Matt, Jr.
Ben Matt, Jr.

Posted on • Updated on • Originally published at code-rainbow.web.app

Master Objects in JS ๐Ÿจ (Part 3)

Make your Constructors new-Agnostic ๐Ÿ—๏ธ

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

function User(name, passwordHash) {
  this.name = name;
  this.passwordHash = passwordHash;
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
more robust approach is to provide a function that works as a con-
structor 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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 variadi c 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;
}
Enter fullscreen mode Exit fullscreen mode

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();
  };
}
Enter fullscreen mode Exit fullscreen mode

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 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 badly 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! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰

And if you want more in depth knowledge about your favorite programming languages checkout my personal blog to become an on demand developer ๐Ÿ˜‰, and you can find me on twitter as well๐Ÿ˜ƒ.

Discussion (2)

Collapse
walfredocarneiro profile image
Walfredo Carneiro

Thats's great! Thank you!

Collapse
jrmatanda profile image
Ben Matt, Jr. Author

You welcome, and thanks for taking your time to read my articles ๐Ÿ˜‰