The prototype object that is created together with the function is indeed making the difference. When you call the function with new, the this object will get that prototype object as its prototype. So the prototype chain for that new object is one step longer than what you get with the factory function.
instanceof
The language offers the instanceof operator and the constructor property, which are useful when you use constructors, as they give information about the instance's constructor. In the case of an object literal (as used in your box function), that determines that the constructor is Object and not box.
To bring the two versions of the functions a bit closer together, so that only the essential differences are highlighted, you could write them as follows:
function box(x,y,z) {
let obj = {};
obj.x = x;
obj.y = y;
obj.z = z;
// Better reference the properties here, ...in case they are changed:
obj.volume = function () { return obj.x * obj.y * obj.z }
return obj;
}
The constructor could look like this:
function Box(x,y,z) {
// `this` is already initialised as a new object with a specific prototype
this.x = x;
this.y = y;
this.z = z;
this.volume = function () { return this.x * this.y * this.z }
// `this` is returned by default, so no `return` is needed
}
Notice the difference in the prototype chains (run the snippet) and the usefulness of instanceof:
function box(x,y,z) {
let obj = {};
obj.x = x;
obj.y = y;
obj.z = z;
obj.volume = function () { return obj.x * obj.y * obj.z }
return obj;
}
function Box(x,y,z) {
this.x = x;
this.y = y;
this.z = z;
this.volume = function () { return this.x * this.y * this.z }
}
let a = box(1,2,3);
let b = new Box(1,2,3);
// Print prototype chains
console.log("a-->" + Object.getPrototypeOf(a).constructor.name);
let protB = Object.getPrototypeOf(b);
console.log("b-->" + protB.constructor.name + "-->" +
Object.getPrototypeOf(protB).constructor.name);
// Print use of `instanceOf`
console.log("a instanceof box?", a instanceof box);
console.log("b instanceof Box?", b instanceof Box);
Methods
So far I ignored the fact that the volume function could be reused instead of defined separately for each instance. You can in both cases define the volume function as a (more) global function. Note that you now could use the this keyword, since normally you would call volume as instance.volume():
function volume { return this.x * this.y * this.z }
function box(x,y,z) {
let obj = {};
obj.x = x;
obj.y = y;
obj.z = z;
obj.volume = volume;
return obj;
}
Still, you have the explicit obj.volume = volume assignment. In the constructor pattern, you don't need to create that instance property. It is enough the define the method on the prototype object:
function Box(x,y,z) {
this.x = x;
this.y = y;
this.z = z;
// No need here to define `volume`
}
Box.prototype.volume = function { return this.x * this.y * this.z };
So, actually you only have one reference to the method.
Class syntax
Finally, there is the class syntax, which is an alternative to use constructors:
class Box(x,y,z) {
constructor(x,y,z) {
this.x = x;
this.y = y;
this.z = z;
}
volume() {
return this.x * this.y * this.z;
}
}
In this syntax volume is also defined on the prototype, not on the instance.
Downsides to using constructors
So far, the comparison was favourable to using constructors (in my opinion).
But:
- The use of the
this object can be avoided completely by using factory functions. Using this can be confusing (as the many questions on that topic on Stack Overflow demonstrate). Some coders feel this is good reason to abandon constructors.
- Creating longer prototype chains can impact performance negatively and sometimes make simple tasks look complicated. For the above example there is no such problem, but the typical examples for using Object Oriented Programming, where you have a
Being from which Animal is derived, from which Mammal is derived, from which ...etc, etc, can get confusing. A flat data structure can then feel like a relief.