Exemplars: creating objects in JavaScript
Join the DZone community and get the full member experience.
Join For FreeThis post explores exemplars, factories for objects. The term exemplar has been proposed by Allen Wirfs-Brock to avoid the term class, which is not a good fit for JavaScript: Exemplars are similar to classes, but they are not classes.
Exemplars
An exemplar is a construct that creates objects. Each of the following kinds of exemplars produces instances of a single fixed type:- Regular expression literals: produce instances of RegExp:
> /abc/ instanceof RegExp true
- Array initializers: produce instances of Array.
> [1, 2, 3] instanceof Array true
- Object initializers: look as follows.
var jane = { name: "Jane", describe: function () { return "Person called "+this.name; } }
Each object produced by an object initializer (without non-standard extensions) is a direct instance of Object:> jane instanceof Object true
Function exemplars (constructors)
A function can be used as an exemplar if invoked via the new operator. Then it is called a constructor. We have previously seen how to create the single “person” jane via an object intializer. The following constructor is an exemplar for this kind of object.function Person(name) { this.name = name; } Person.prototype.describe = function () { return "Person called "+this.name; };This is how you use the new operator to create an object:
var jane = new Person("Jane");jane is considered in instance of Person. You can check that relationship via the instanceof operator:
> jane instanceof Person trueThere are two parts for setting up an instance of Person: The instance-specific properties are added via the constructor:
> jane.name 'Jane'Properties that are shared by all instances (mostly methods) are inherited from Person.prototype:
> jane.describe() 'Person called Jane' > jane.describe === Person.prototype.describe trueYou can verify that the single object Person.prototype is indeed the prototype of all instances of Person:
> Person.prototype.isPrototypeOf(jane) trueThe prototype-of relationship is also used to check whether an object is an instance of a constructor. The expression
jane instanceof Personis actually implemented as
Person.prototype.isPrototypeOf(jane)
Optional parameters
Often, one would like the same constructor to set up its instances in different ways. The following constructor for two-dimensional points can be invoked with either zero arguments or two arguments. In the former case, it produces a point (0,0), in the latter case, one specifies x coordinate and y coordinate.function Point(x, y) { if (arguments.length >= 2) { this.x = x; this.y = y; } else { this.x = 0; this.y = 0; } }This idea can be extended to more than two possible argument combinations. For example, the following constructor for points with a color can receive 3 arguments, 2 arguments, or 0 arguments.
function ColorPoint(x, y, color) { if (arguments.length >= 3) { this.color = color; } else { this.color = black; } if (arguments.length >= 2) { this.x = x; this.y = y; } else { this.x = 0; this.y = 0; } }There is a possibility to write this code more compactly: Missing parameters are undefined in JavaScript which means that we want to make assignments like
this.x = (x !== undefined ? x : 0); // this.x = (... ? value : default)The following function implements this kind of check:
function valueOrDefault(value, theDefault) { return (value !== undefined && value !== null ? value : theDefault); }Now ColorPoint is more compact:
function ColorPoint(x, y, color) { this.x = valueOrDefault(x, 0); this.y = valueOrDefault(y, 0); this.color = valueOrDefault(color, "black"); }You can also use the || operator:
function ColorPoint(x, y, color) { this.x = x || 0; this.y = y || 0; this.color = color || "black"; }Explanation:
left || rightevaluates to left if Boolean(left) is true and to right, otherwise. Caveat: With ||, the following values will all be interpreted as the argument being missing:
- undefined, null
- false
- +0, -0, NaN
- ""
> "" || "black" 'black'
Option objects
Positional optional parameters are limiting with regard to what parameters can be omitted: If a parameter that you want to omit isn’t at the end, you have to insert a placeholder value such as undefined or null. Furthermore, if there are many parameters, you quickly lose sight of the meaning of each one. The idea of an option object is to provide optional parameters as an object that is created via an object literal. For ColorPoint that would look as follows.> new ColorPoint({ color: "black" }) { color: 'black', x: 0, y: 0 } > new ColorPoint({ x: 33 }) { x: 33, y: 0, color: 'black' } > new ColorPoint() { x: 0, y: 0, color: 'black' }With a helper function, it is remarkably easy to transfer the optional values to the instance under construction and to fill in defaults for missing properties:
function ColorPoint(options) { _.defaults(this, options, { x: 0, y: 0, color: "black" }); }Above we have used the function defaults of the Underscore library [1]. It copies all of the properties of options to this that don’t exist there, yet. And similarly fills in the defaults via the third argument. [2] has more information on option objects.
Chainable setters
One can achieve a style that is very similar to an option object by creating a setter for each optional parameter. That looks as follows:> new ColorPoint().setX(12).setY(7) { x: 12, y: 7, color: 'black' } > new ColorPoint().setColor("red") { x: 0, y: 0, color: 'red' }Naturally, each of the setters must return this, so that setters can be chained and that the final result of such a chain is always the instance of ColorPoint.
function ColorPoint() { // Set default values this.x = 0; this.y = 0; this.color = "black"; } ColorPoint.prototype.setX = function (x) { this.x = x; return this; } ColorPoint.prototype.setY = function (y) { this.y = y; return this; } ColorPoint.prototype.setColor = function (color) { this.color = color; return this; }Given how mechanical writing such setters is, we can write a method withSetters that does it for us:
var ColorPoint = function (x, y, color) { this.x = 0; this.y = 0; this.color = "black"; }.withSetters( "x", "y", "color" );withSetters is a method that, if applied to a function, adds setters to that function’s prototype:
Function.prototype.withSetters = function (/*setter names*/) { var Constr = this; Array.prototype.forEach.call(arguments, function (propName) { var capitalized = propName[0].toUpperCase() + propName.slice(1); var setterName = "set" + capitalized; Constr.prototype[setterName] = function (value) { this[propName] = value; return this; }; }); return this; };Refining this approach, we can use an object literal to specify both setter names and default values at the same time:
var ColorPoint = function (x, y, color) { this.applyDefaults(); }.withSetters({ x: 0, y: 0, color: "black" });The defaults are added to the fresh instance of ColorPoint via the method applyDefaults. Apart from that method, not much changes – now the setter names are extracted from an object.
Function.prototype.withSetters = function (props) { var Constr = this; Constr.prototype.applyDefaults = function () { _.defaults(this, props); } Object.keys(props).forEach(function (propName) { var capitalized = propName[0].toUpperCase() + propName.slice(1); var setterName = "set" + capitalized; Constr.prototype[setterName] = function (value) { this[propName] = value; return this; }; }); return this; };
Initialization methods
Sometimes there are several mutually exclusive “modes” or ways of initializing an object. Using the same constructor for all modes is tricky. It would be better to have a clear indication of which mode is active. The problem of using the same name for different operations is nicely illustrated by a well-known quirk of JavaScript’s Array constructor. The first operation is to create an empty array with the given length:> new Array(3) [ , , ]The second operation is to create an array with given elements:
> new Array("a", "b", "c") [ 'a', 'b', 'c' ]However, the second operation is problematic, because if you want to create an array that has a single natural number in it, operation 1 takes over and doesn’t let you do it. A better solution is to again use chainable setters and give each operation a different name. As an example, let’s look at a constructor Angle whose instances can be initialized in either degrees or radians.
> new Angle().initDegrees(180).toString() '3.141592653589793rad'Note how we have separated instantiation (instance creation) via new Angle() from initialization via initDegrees(). Constructors usually perform both tasks; here we have delegated the latter task to a method. Angle can be implemented as follows.
function Angle() { } Angle.prototype.initRadians = function (rad) { this.rad = rad; return this; }; Angle.prototype.initDegrees = function (deg) { this.rad = deg * Math.PI / 180; return this; }; Angle.prototype.toString = function () { return this.rad+"rad"; };initRadians and initDegrees are not really setters, they are initialization methods. Initialization methods differ from setters in two ways: First, they can have more than one parameter or no parameter. In contrast, setters usually have exactly one parameter. Second, you often want to ensure that if a method is invoked on an instance, that instance has been initialized beforehand. And, possibly, one shouldn’t initialize more than once. The following code uses the boolean flag _initialized to check for these two checks.
function Angle() { this._initialized = false; } Angle.prototype._forbidInitialized = function () { if (this._initialized) { throw new Error("Already initialized"); } this._initialized = true; }; Angle.prototype.initRadians = function (rad) { this._forbidInitialized(); this.rad = rad; return this; }; Angle.prototype.initDegrees = function (deg) { this._forbidInitialized(); this.rad = deg * Math.PI / 180; return this; }; Angle.prototype._forceInitialized = function () { if (!this._initialized) { throw new Error("Not initialized"); } }; Angle.prototype.toString = function () { this._forceInitialized(); return this.rad+"rad"; };
Static factory methods
Static factory methods are an alternative to initialization methods. They are methods of the constructor that create instances. Given that a constructor is similar to a class in class-based languages, such methods are often called static. They are called factory methods, because they produce instances. Creating an angle with a static factory method instead of a constructor and an initialization method looks as follows.> var a = Angle.withDegrees(180); > a.toString() '3.141592653589793rad' > a instanceof Angle trueThis is an implementation:
function Angle(rad) { this.rad = rad; } Angle.withRadians = function (rad) { return new Angle(rad); }; Angle.withDegrees = function (deg) { return new Angle(deg * Math.PI / 180); }; Angle.prototype.toString = function () { return this.rad+"rad"; };The point of factory methods is to replace the constructor. We therefore want to make it impossible to accidentally invoke new Angle(), or even just Angle as a function. Simply hiding Angle won’t do, because we need it for instanceof.
Guarding the constructor, approach 1. The constructor throws an error if it isn’t called with a value that is only known to the factory methods. We keep the value secret by putting it inside an immediately-invoked function expression (IIFE, [3]).
var Angle = function () { var constrGuard = {}; function Angle(guard, rad) { if (guard !== constrGuard) { throw new Error("Must use a factory method"); } this.rad = rad; } Angle.withRadians = function (rad) { return new Angle(constrGuard, rad); }; Angle.withDegrees = function (deg) { return new Angle(constrGuard, deg * Math.PI / 180); }; Angle.prototype.toString = function () { return this.rad+"rad"; }; return Angle; }();Guarding the constructor, approach 2. An alternative is to not use the constructor to create an instance of Angle, but
Object.create(Angle.prototype)The factory methods now use the function createInstance that implements this approach.
var Angle = function () { function Angle() { throw new Error("Must use a factory method"); } function createInstance(rad) { var inst = Object.create(Angle.prototype); inst.rad = rad; return inst; } Angle.withRadians = function (rad) { return createInstance(rad); }; Angle.withDegrees = function (deg) { return createInstance(deg * Math.PI / 180); }; Angle.prototype.toString = function () { return this.rad+"rad"; }; return Angle; }();Interaction:
> var a = Angle.withDegrees(180); > a.toString() '3.141592653589793rad' > a instanceof Angle true
Creating instances of multiple types in a single location
Sometimes, there is a hierarchy of types and you want to create instances of those types in a single location. For example, expressions:Expression +-- Addition +-- IntegerThe type Expression has two subtypes: Integer and Addition (whose operands can be expressions). Assuming that Expression is abstract and will never be instantiated, parsing a text string will produce an instance of either Addition or Integer. The common way to implement such an operation is via a factory method:
Expression.parse = function (str) { if (/^[-+]?[0-9]+$/.test(str)) { return new Integer(str); } ... }However, JavaScript also lets you return an arbitrary object from a constructor. Hence, while the following assertion usually holds:
new C() instanceof Cit doesn’t have to:
> function C() { return {} } > new C() instanceof C falseThat allows you to implement expression parsing as follows:
function Expression(str) { if (/^[-+]?[0-9]+$/.test(str)) { return new Integer(str); } ... }Then the following assertion holds:
new Expression("-125") instanceof IntegerMost JavaScript engines perform an automatic optimiziation and don’t create an instance of the constructor, if this isn’t accessed and another object is (explicitly) returned.
The new operator
Invocation
new <function-valued expression>(arg1, arg2, ...)
var inst = new Constr(); var inst = new Constr;The function-valued expression above is usually either
- any expression, in parentheses, that evaluates to a function
- an identifier, optionally followed by one or more property accesses
function foo() { function bar(arg) { this.arg = arg; } return bar; }Calling the result of foo() as a function is easy, you simply append parentheses with arguments:
foo()("hello")To use the result as the operand of new, you have to add additional parentheses:
> new (foo())("hello") { arg: 'hello' }Without parentheses around foo(), you would invoke foo as a constructor and then call the result as a function, with the argument "hello". That the operand of new is finished with the first parentheses is an advantage when it comes to invoking a method on a newly created object. As an example, consider a constructor for colors.
function Color(name) { this.name = name; } Color.prototype.toString = function () { return "Color("+this.name+")"; }You can create a color and then immediately invoke a method on it:
> new Color("green").toString() 'Color(green)'The above expression is equivalent to
(new Color("green")).toString()However, property accesses before the parentheses are considered part of the operand of new. That is handy when you want to put a constructor inside an object:
var namespace = {}; namespace.Color = function (name) { this.name = name; };Interaction:
> new namespace.Color("red") { name: 'red' }
The new operator ignores a bound value for `this`
Functions have a method bind() that lets you create a new function with a fixed value for this, whose first 0 to n parameters are already filled in. Given the definitions.var obj = {}; function F() { console.log("this === obj? " + (this === obj)); }Then you can create a new function via bind:
var boundF = F.bind(obj);boundF works as expected when called as a function:
> boundF() this === obj? true undefinedThe result of boundF() is undefined. However, new overrides the bound this with a new instance:
> new boundF() instanceof F this === obj? false true
> function Constr() { console.log(this === obj) } undefined > (Constr.bind(obj))() true undefined > new (Constr.bind(obj))() false {}
The new operator doesn’t work with apply()
You can call the Date constructor as follows.new Date(2011, 11, 24)If you want to provide the arguments via an array, you cannot use apply:
> new Date.apply(null, [2011, 11, 24]) TypeError: function apply() { [native code] } is not a constructorThe reason is obvious: new expects Date.apply to be a constructor. More work is required to achieve for constructors what apply achieves for functions [4].
Object exemplars
One normally uses function exemplars to create objects:function Person(name) { this.name = name; } Person.prototype.describe = function () { return "Person called "+this.name; };An instance is created via the new operator:
new Person("Jane") instanceof PersonECMAScript 5 introduced a new way of creating instances: Object.create produces an object whose prototype is the given argument:
Object.create(Person.prototype) instanceof PersonThere are two more issues we would like to solve.
Working directly with the prototype object. Person was the name of the function exemplar. We want to make the prototype object an object exemplar and give it that name. This looks as follows.
var Person = { describe: function () { return "Person called "+this.name; } };We can’t work with instanceof, any more, because it is a function exemplar operator. However, isPrototypeOf() works nicely. The following assertion holds now.
Person.isPrototypeOf(Object.create(Person))Initialization. We still haven’t initialized our instances of Person. To do so, we introduce a method called init().
var Person = { init: function (name) { this.name = name; return this; }, describe: function () { return "Person called "+this.name; } };We use this object exemplar as follows:
> var jane = Object.create(Person).init("Jane"); > Person.isPrototypeOf(jane) true > jane.describe() 'Person called Jane'You can consult [5] for more information on object exemplars, including a library for working with them.
Topics not covered by this post
Two topics that are relevant to object creation were not covered by this post:- Subtyping [6] and how to subtype JavaScript’s built-ins [7].
- Keeping instance data private. You can read Crockford’s “Private Members in JavaScript” for more information.
References
- Trying out Underscore on Node.js
- Keyword parameters in JavaScript and ECMAScript.next
- JavaScript variable scoping and its pitfalls
- Spreading arrays into arguments in JavaScript
- Prototypes as classes – an introduction to JavaScript inheritance
- JavaScript inheritance by example
- Subtyping JavaScript built-ins
Opinions expressed by DZone contributors are their own.
Comments