6

Constructors Are Bad For JavaScript

 3 years ago
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.
Constructors Are Bad For JavaScript

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 no instanceof 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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK