The Many Ways to Build Objects in JavaScript
In JavaScript, the dominant pattern to declare objects is to write a
constructor function and append each method to its prototype
property. To create an instance of the object, one must then use the
new
keyword on a call to the constructor function.
Here Set
is the constructor function; in its body, this
is a
reference to a newly-created object that will be returned after a call
to the constructor with the new
keyword. By convention, constructor
functions begin with a capital.
Note that Set
is just a regular function, i.e. we can call it
without the new
keyword. Though usually it is not what you want,
since Set
will return undefined
and will bind this
to the global
object in the absence of a caller.
The new
keyword applied to the Set
function has the following
effect:
- An empty object
o
is created. - The
[[Proto]]
property ofo
is bound toSet.prototype
. In other words, the prototype ofo
is set toSet.prototype
. - The function
Set
is called witho
is bound tothis
. - When the function returns, it returns
o
instead ofundefined
.
That is why methods of the object must be attached to the
Set.prototype
object: it is the prototype of every instance of
Set
.
I always found this pattern verbose and confusing. The new
keyword
was made to be familiar for Java programmers, but there is no mention
of classes, only prototypes. In fact, it can be misleading to relate
to Java, since the receiver of a method is not bound statically in
JavaScript.
The call to add(2)
has no explicit caller, so this
is bound to the
global object, which doesn’t have an items
object.
Also, functions have a unique prototype
property that is
unfortunately not the same thing as the [[Proto]]
property that
every object – including functions – have.
Languages that compile to JavaScript, like CoffeeScript, TypeScript, Dart and even the next version of EcmaScript all provide syntactic sugar to hide this pattern.
Luckily, there are alternatives to this pattern in vanilla JS. By not trying to mimic class-based languages, but harnessing the simplicity of the prototype model, one finds flexible and terse ways to create objects.
Object factories
JavaScript has objects literals, which are great for building singleton objects.
Attributes and methods are now just properties of the same object. In this respect, this form is more regular than the first pattern.
What if we want another instance of this object? In other prototype-based languages, we would just clone the first one; in JavaScript, there’s no built-in clone function. Instead, we can create a factory function.
The mkSet
function puts out new objects of identical initial state.
Compared to the previous example, we have lost a bit of flexibility.
The bag was usable right away, but here we must call the factory to
create a new object before beginning to use it.
This form also has another downside: the methods are recreated
separately for each object. The add
method of s1
and s2
are not
the same. Not only if this wasteful, but leaves us no way of adding
or altering methods for all objects of the same factory.
Sharing behavior with prototypes
We can correct the latter defect by putting the methods in a separate
setProto
object. Then, in the factory method we must set the
[[Proto]]
property to this setProto
object.
This way, all objects created by the factory mkSet
share the same
methods. We can easily extend the functionality of all sets by
appending methods to setProto
.
Note that we did not correct the first defect: we must still use the
factory to begin using sets. However, we essentially recreated the
first pattern: mkSet()
does what new Set()
did, and setProto
is
Set.prototype
. It is less verbose, but now the methods and
constructor are separate entities, which makes even less sense. As a
result setProto
, is not a usable object on its own. We would like
to reunite the two.
Reuniting prototype and constructor
Since the constructor is just a function, why not put it inside the prototype object itself?
Set
is still the prototype object, but the constructor is part of
its definition. Calling Set.new()
creates new objects inheriting
from Set
. In fact, new
is a method of all Set
objects.
While this is unusual for class-based languages like Java, where constructors are special methods, here we see they are just regular functions that return new objects.
There is a bit of redundancy there, namely the Set
in the line
__proto__: Set
. Since we will call the constructor with
Set.new()
, why not use this
instead?
Now Set.new()
and s1.new()
are no longer equivalent. The latter
create an object that have s1
as prototype, not Set
. This means
that we can specialize objects in branches, not only as a whole.
Here, s1
, s2
and s11
all have the clear
method, but only s1
and s11
have forEach
.
Prototypes as default objects
We should try to correct the first defect. As it stands, the only
method that makes sense to call on Set
is new
. Why not make Set
a fully functional instance of itself?
Just by calling new()
on the object defining the set
prototype, we
are giving it an items
array. Somehow we lost the direct reference
to the original prototype, but it does not matter since all set
objects will be derived from set
itself (and we can still access it
from set.__proto__
).
Cloning is just another method
Finally, when creating objects we might want to make a deep copy of
an existing copy – not start from a fresh state. This can be
achieved by adding a clone
method that defines the deep cloning of
each attribute. It also binds [[Proto]]
of course.
Here s1
begins as an identical copy of set
, but not sharing the
reference to the items
array. If we want shallow cloning, we can
just use Object.create(set)
. And if we want to branch, we can call
set.new
. We also can use set
right away as a set. All objects
derived from set
will inherit from changes made to their prototype,
which allow to specialize and extend their behavior as befit.
This last form is not only more flexible than the dominant pattern of the first example, it is more terse and regular. But more importantly, it illustrates the orthogonality of the core concepts of JavaScript: object literals, first-class functions and prototypes.