Some Javascript constructor patterns, and when to use them

by Sam Selikoff November 14, 2013


As I've been writing more and more JavaScript, I've learned something about the community: Javascripters like to do things differently. Ask two developers, or look at two different popular open-source projects, and you'll probably come across two different solutions for doing the same thing, whether it's as trivial as writing getter/setter methods or as complicated as loading modules. Sometimes I'm able to find a consensus around these solutions, but often I get lost among the alternatives, and find it difficult to determine which is most appropriate for my specific use case.

One example is writing constructors. I've come across several ways to do it - but which way is right? Why do some libraries choose one method, and some another? In this post, I'd like to explore some of the techniques I've encountered for writing constructor functions, their pros and cons, and when to use them.

Generic advice - using the new keyword (Mozilla)

If I had to describe the 'standard' constructor pattern I've seen, it's probably this:

// The constructor - like a class, but actually, not really
function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;

  this.fullName = function() {
    return this.firstName + ' ' + this.lastName;
  };
}

// Use it like this:
var sam = new Person('Sam', 'Selikoff');
sam.firstName;    // "Sam"
sam.fullName();   // "Sam Selikoff"

sam.firstName = 'Samuel';
sam.fullName(); // Samuel Selikoff

This looks a lot like something you'd see in other languages - but don't get too comfortable. If you instantiate two objects this way, they won't necessarily always have the same properties and methods. This is because Javascript lets you mutate objects at run-time:

// Add a gender property to this instance only
sam.gender = 'male';

You are also able to change existing methods and properties on your objects, and this may lead to behavior you don't expect. For example, if you changed the fullName method on a single object, those changes wouldn't be shared across other objects. This is because the fullName method is an anonymous function that's attached directly to an instance variable. Each object will get its own copy of the function.

If you wanted the function to be shared across all Person objects, you could do it like this:

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

Person.prototype.fullName = function() {
  return this.firstName + ' ' + this.lastName;
};

This constructor shares the fullName method across all instances. But how does it work?

Some Javascript eccentricities

Whenever we create objects using the new keyword (as we've been doing), those objects get a special reference to the Person.prototype dictionary. (In Javascript, dictionaries are synonymous with objects, but I'm using the term here to describe objects that are used mainly as key-value references). Prototypes in Javascript give objects a chain of dictionaries used to look up methods. This means that if we call a method on an object, but that method isn't defined directly on the object, Javscript will look for the method in the next level of the object's prototype chain. If it finds it, it will invoke the method; if not, it will keep looking up the chain.

In our first example, we didn't define a prototype on the Person constructor, so all instances of Person had prototype chains that were only one level deep, so to speak; that is, they only contained the default Object.prototype dictionary. This dictionary contains methods that all objects tend to have in other languages - toString, for instance.

In our second example, we explicitly created a dictionary on the prototype property of the constructor. The new keyword is aware of this property: it is designed to check if there's a dictionary at .prototype, and if there is, it adds it to the prototype chain of the object it's currently instantiating. This means that when we call sam.fullName(), Javascript first looks (unsuccessfully) for the method directly on the sam instance, and then moves on to the next level of sam's prototype chain, which is the dictionary we created. Because that dictionary does have the fullName method, that method gets invoked. And because all objects that get instantiated from the Person constructor have a reference to the same prototype dictionary, they all refer to the same method. So this is how we can share methods across objects.

Before moving on to the next pattern, we should cover our use of the this keyword. I'm sure you've come across many JavaScript posts outlining the dangers of being ignorant of this. We use it in the above construct - so what do we need to be careful of?

First, a brief overview. this is a keyword that's available in every Javascript function. It's really just a pointer. But what does it point to? The this keyword points to the object that's invoking the current function.

So, the this keyword is really just a pointer to an object. But the new keyword does something nice for us: it tells this to point to the object that's being instantiated. In this example from above,

var sam = new Person('Sam', 'Selikoff');

this pointed to sam whenever it was used inside the Person constructor function. If we hadn't used new, though, this would have referred to the global object, since that was the object executing the constructor function. For example:

var george = Person('George', 'Foreman'); // forgot 'new'
george; // undefined

Poor george is undefined! This is because when Person was invoked, this referred to the global object. That means these two lines within our constructor function

this.firstName = firstName;
this.lastName = lastName;

really just created two new global variables, firstName and lastName. When Person finished executing, it returned undefined (which happened by default, since it didn't return anything). So george is undefined, and you've got two new globals - most certainly not what you intended!

Therefore, it's important to remember to use the new keyword when using this pattern. Typically, constructors are capitalized (and instances lower-cased) to remind developers of this rule.

Closures with getter-setters (D3.js, jQuery)

We just saw that when using the above pattern, you need to manage the context of this by using the new keyword. If this troubles you, there is another technique which avoids this altogether.

Here's an example of how it's used:

function Person(firstName, lastName) {
  var _firstName = firstName,
      _lastName = lastName;

  var my = {
    firstName: _firstName,
    lastName: _lastName
  };

  my.fullName = function() {
    return _firstName + ' ' + _lastName;
  };

  // Getter/setters
  my.firstName = function(value) {
    if (!arguments.length) return _firstName;
    _firstName = value;

    return my;
  };

  my.lastName = function(value) {
    if (!arguments.length) return _lastName;
    _lastName = value;

    return my;
  };

  return my;
}

// Use it like this:
var mark = Person('Mark', 'Twain'); // note: no `new` keyword!

mark.firstName('Samuel');
mark.lastName('Clemens');

mark.fullName(); // Samuel Clemens

I was confused about this pattern when I first encountered it; the first pattern seems much more intuitive and familiar. But notice that since we never use this, we simply don't have to worry about managing its context. No matter where we call mark.fullName(), or mark.firstName('Mark'), the instance variables we're manipulating (_firstName and _lastName) will be the correct ones.

It turns out that the biggest benefit of this technique has nothing to do with this, though. Rather, it's the ability to give your objects private properties and methods. In the example above, as you may have guessed, _firstName and _lastName are not publically accessible:

mark._firstName;
// undefined

How does this work?

Typically, when a function finishes executing, the variables declared within that function fall out of scope and get destroyed. But there's an exception: if any code outside the function holds a reference to these variables after the function finishes executing, they won't be cleaned up. Instead, a closure will be formed, and those variables will remain in memory.

In the example above, since the Person constructor function returns the inner object my, and since my refers to the local variables _firstName and _lastName (both in the getter/setters and in the fullName method), the variables are not destroyed. When the getter/setters are invoked, they are operating on those same local variables.

But why do this? Why go through this bizarre set of steps just to create what are essentially instance variables on an object - something we already know how to do? The difference between the local variables that the closure has access to and the instance variables on the objects we made earlier is that the outside world does not have direct access to the local variables.

In the objects from our first pattern, anybody with a reference to the sam object could change the firstName property to anything he pleases:

sam.firstName = 'Peter';

But the only way to access the variables using the current technique are through the getter/setter methods that we attached to my. By attaching those methods to my, we made them public; but the local variables themselves remain private. We can now add validation and other logic to our getter/setters, giving us more control over our object's interfaces.

So we see that closures with getter/setters give us the ability to have private properties and methods, a feature of OOP we've come to expect from other languages.

The best of both worlds?

As we've seen, prototypes (from the first pattern) give us method sharing, and closures (from the second pattern) give us private properties. A natural follow-up question is: can we have both?

It turns out the that there's no way for shared methods that live on the constructor's prototype to access the object's hidden, private properties. This means that you effectively have to choose one or the other. But which one should you choose, and why?

If performance is crucial - for example, if you plan on making thousands of objects - you'll want to go with prototypes and shared methods. Removing redundant copies of methods is going to be a suggestion that any consultant or book on performance will make.

On the other hand, if the object you're making is large and complex, and is designed mainly for public consumption, the flexibility that comes with having private properties and methods will probably win out, and you'll want to take advantage of closures. This is why libraries like jQuery and D3 tend to use closures: users will almost certainly only be using one instance of the library, which means that method sharing becomes less of an issue.

As a clarifying note, you can certainly use prototypes and closures on the same object. But only the closures - which are not shared across instances - will have access to your object's private variables. The shared methods on the object's prototype will not.

Custom constructors (Ember.js, Backbone.js, etc.)

Sometimes when making objects within a framework or library, you'll avoid native Javascript constructors altogether. This allows the library to have more control over the objects in your application.

In Ember, for example, you define objects by extending the base Ember.Object object, and then calling the create method on that object. You set properties by passing them into the constructor.

Here's how it works:

Person = Ember.Object.extend({
  fullName: function() {
    return this.get('firstName') + ' ' + this.get('lastName');
  }
});

var tim = Person.create({
  firstName: "Tim",
  lastName: "Tebow"
});

tim.fullName();
// Tim Tebow

Custom methods like this may feel unnecessary or cumbersome, but they give a lot of power to library authors.

For example, one of Ember's core features is its data bindings system, which requires that each object in an application have the ability to observe the properties of other objects. By giving developers a custom constructor function, Ember can implement this functionality in a consistent and controlled way, without requiring developers to write additional code just to mix in the behavior. So in this case, the custom constructor simplifies use of the framework for Ember developers. Underneath the hood, these custom constructors are simply wrappers around one of the two methods we covered.

So depending on what you're designing and who you're designing it for, a custom constructor function may be the way to go.

No constructor, a.k.a. the object literal

Of course, you can always create an object using no constructor at all:

var sam = {
    firstName: "Sam",
    lastName: "Selikoff",
    fullName: function() {
        return this.firstName + ' ' + this.lastName;
    }
}

This is known as an object literal. But you probably are interested in writing constructors because you plan on making many objects. Object literals won't help you there - but they do have plenty of uses, including storing configuration settings or raw data, or reducing the number of globals in your library.

Summary

The takeaway is that there are many ways to create objects in Javascript, each with its own costs and benefits. It's good to know the kinds of things these patterns let you accomplish, and how other developers have used them, so you can decide for yourself when a technique is appropriate in your own work.

Here's a recap of what we discussed. Hopefully the next time you need to make some objects, you'll be better equipped to choose the appropriate method:

  • Prototypes let you share public methods across objects. (They also let you use inheritance).

  • Closures let you have private properties and methods. They are often used in libraries. (Closures are part of a larger pattern called the Module pattern).

  • Custom constructors can be useful when building specialized APIs.

  • Object literals are often used to store and pass around isolated chunks of data, like configuration settings or the parameters to an AJAX request. They can also help reduce the number of globals in your code.

Learn more

Comments

comments powered by Disqus