Constructors Are Bad For JavaScript
source link: https://tsherif.wordpress.com/2013/08/04/constructors-are-bad-for-javascript/comment-page-1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
JavaScript is a language that has always been at odds with itself. It derives its core strength from a simple, flexible object model that allows code reuse through direct object-to-object prototypal inheritance, and a powerful execution model based on functions that are simply executable objects. Unfortunately, there are many aspects of the language that obscure this powerful core. It is well-known that JavaScript originally wanted to look like other popular languages built on fundamentally different philosophies, and this trend in its history has lead to constructs in the language that actively work against its natural flow.
Consider the fact that although the prototypal, object-to-object inheritance mentioned above is a core aspect of the language, there was no way, until ECMAScript 5, to simply say “I want to create an object whose prototype is this other object”. You would have to do something like the following:
var
proto = {protoprop:
"protoprop"
};
function
C() {}
//Dummy throwaway constructor.
C.prototype = proto;
var
obj =
new
C();
proto.isPrototypeOf(obj);
=>
true
Thankfully, using Object.create()
, this can now be simplified to:
var
proto = {protoprop:
"protoprop"
};
var
obj = Object.create(proto);
proto.isPrototypeOf(obj);
=>
true
The latter construct is much more in tune with JavaScript’s core structure. The constructor mechanism used in the former example is a clunky indirection of the simple object-to-object inheritance model the language actually uses, but until recently there was no other way for objects to inherit from one another. They were forced to use constructors as mediators. This gave JavaScript the veneer of a typed, class-based language, while obfuscating and actively interfering with its natural structure and flow.
Constructors
In JavaScript: The Good Parts, Douglas Crockford warns against using the new
keyword for constructor invocation. I’ll admit that when I first read this, I believed his argument to be based mainly on the dangers of forgetting new
and polluting the global namespace:
function
C() {
this
.instance_member =
"whoops!"
}
var
c = C();
// Forgot "new"
c;
=> undefined;
window.instance_member;
// Property added to global namespace!
=>
"whoops!"
But in JavaScript Patterns, Stoyan Stefanov suggests using the following pattern to safeguard against accidentally polluting the global namespace:
function
C() {
if
(! (
this
instanceof
C)) {
return
new
C();
}
this
.instance_member =
"This is ok."
;
}
var
c = C();
// Is this a constructor?
c;
=> {instance_member:
"This is ok."
}
window.instance_member;
// No global namespace pollution.
=> undefined
Allowing a constructor to be used without new
looks a little strange, and one might argue that it makes the code less clear, but this pattern does make the constructor much safer. And using the constructor mechanism allows us to organize our code in a class-like manner around constructor-object links:
c
instanceof
C;
// It's like a class, right?
=>
true
c.constructor === C;
=>
true
This seemed like a perfect solution, and it gave me the impression that perhaps Crockford was being overcautious. But digging a little deeper, I stumbled upon the following bit of odd behaviour. I’ll use standard constructor invocation to make things clear:
// Constructor
function
C() {}
// Create object.
var
c =
new
C();
c
instanceof
C;
=>
true
c.constructor === C;
=>
true
// Change prototype
C.prototype = { prototype_prop :
"proto"
};
c.constructor === C;
=>
true
c
instanceof
C;
// instanceof no longer works!
=>
false
Changing the prototype breaks instanceof
! On a conceptual level, I suppose one could argue that if you change the prototype a constructor uses to create objects, it can’t really be considered the template for objects created before the change. And that’s essentially all instanceof
does: it checks prototypes. But that means the result of c instanceof C
has nothing to do with what C
actually is! This is especially clear in the following example:
// Create two constructors with the
// same prototype.
var
proto = {protoprop:
"protoprop"
};
function
C() {
this
.cprop =
"cprop"
};
C.prototype = proto;
function
F() {
this
.fprop =
"fprop"
};
F.prototype = proto;
var
f =
new
F();
f.protoprop;
// Has prototype properties
=>
"protoprop"
f.fprop;
// Has F properties
=>
"fprop"
f.cprop;
// Doesn't have C properties
=> undefined
f
instanceof
C;
// Is an instance of C!?!
=>
true
So the instanceof
operator is extremely misleading about what it’s doing. c instanceof C
does not mean that c
was created by C
or that c
has anything to do with C
. It basically just means “at this moment, the prototype C
will use if it’s invoked as a constructor (even if it’s never actually invoked as a constructor) appears somewhere in the chain of prototypes of c
”. Essentially, it’s equivalent to C.prototype.isPrototypeOf(c)
, but the latter is far more upfront about what it’s actually doing.
Also consider the rarely used constructor
property that’s set for all objects. The information it provides is of questionable value:
function
C() {}
C.prototype = { prototype_prop :
"proto"
};
// Changing the prototype breaks the constructor
// property for all objects created after the change.
c =
new
C();
c
instanceof
C;
=>
true
c.constructor === C;
=>
false
A full treatment of this behaviour can be found here, but essentially, the constructor
property of an object is set by the engine exactly once. When a function is defined, its prototype
property is initialized to an object with a constructor
property pointing back to the function itself. If you set the function’s prototype
property to some other object, without explicitly setting that new object’s constructor
property, all objects created by the function will have their constructor properties set to Object
(which is the default).
So why do instanceof
and the constructor
property even exist in JavaScript? They create false links between objects and the functions that create them in a manner so brittle and misleading that they only serve to obfuscate how the code actually works. I believe they exist simply to prop up a fundamentally broken construct: the constructor. To be clear, constructors, at their core, are useful: they are simply functions for creating objects. The key problem is all the extra baggage that comes with that creation. Why should an object be considered an instanceof
the function that creates it? Why the mysterious invocation of new
to magically create the new object?
What we need is a way to take advantage of the reuse patterns of constructors, while at the same time writing code that is more explicit about what it’s actually doing. This can be done by pushing the object creation and inheritance code directly into the constructor, essentially turning it into a factory function.
Factory Functions
Writing explicit factory functions involves relatively minor changes to the code of constructors. Say, for example, that we have a constructor like the following:
function
MyObject(data) {
this
.data = data;
}
MyObject.prototype = {
getData:
function
() {
return
this
.data;
}
}
var
o =
new
MyObject(
"data"
);
This can be replaced by the following equivalent factory function:
function
myObject(data) {
var
obj = Object.create(myObject.proto);
obj.data = data;
return
obj;
}
myObject.proto = {
getData:
function
() {
return
this
.data;
}
}
var
o = myObject(
"data"
);
The objects created by the constructor and the factory function are equivalent, but the factory function construct has the following advantages:
- There’s no risk of using it in the “wrong” way. It doesn’t require
new
, as it isn’t meant to be used as a constructor. Nor is it a constructor that forces proper invocation, essentially hiding errors. The factory function is meant to be used in exactly one way: as a regular function. - There’s no pretense of creating a “class” of objects by capitalizing the name or otherwise trying to make it look like the classes of other languages. The
prototype
property isn’t used, so there will be noinstanceof
link between the function and the objects it creates. It is simply a function that happens to create objects.
So the advantages aren’t just theoretical. Code written in this way will be more maintainable and less error-prone.
Taking this idea a step further, if we want to go all the way and not use new
at all in our code, the following generic factory can be used to invoke constructor functions in a more explicit manner:
function
genericFactory(Ctr) {
var
obj = Object.create(Ctr.prototype);
var
args = Array.prototype.slice.call(arguments, 1);
Ctr.apply(obj, args);
return
obj;
}
Using it to invoke the MyObject
constructor described earlier in this section would look like the following:
var
o = genericFactory(MyObject,
"data"
);
Whether this makes the code clearer or not might be debatable, but one definite advantage is that this construct allows us to invoke constructors dynamically, something not possible with invocations that use new
. This example also shows more generally that constructor invocation can be done away with quite easily with the tools now afforded us in ECMAScript 5.
Conclusions
JavaScript at its core, is a simple, elegant, expressive language that has unfortunately been weighed down by its confused history. Constructors, as an example, were clearly introduced to give the JavaScript the appearance of other popular, class-based, object-oriented languages, but as was discussed here, they at best mislead, and at worst actively interfere with our ability to engage with the core structure of the language. Constructors run contrary to the prototypal, functional, object-based nature from which JavaScript draws its strength. I believe that by putting them aside, by recognizing them as artifacts of a time when JavaScript was trying to look like something other than itself, we’ll be able to embrace the language for what it actually is and write clearer, safer, more elegant and more maintainable code.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK