ClassyObjects: A JavaScript Class-y Inheritance Example
I recently released a screencast that covers JavaScript Objects & Prototypes on my WatchMeCode site. In this screencast, I go through all of the basics of working with objects in JavaScript, including prototypal inheritance. Toward the end of the video, I also create an example framework that sort of brings a class-like or “class-y” inheritance system in to JavaScript. I’ve decided to open source that code and you can find it in my ClassyObjects repository at Github.
DANGER, WILL ROBINSON!
Before I go any further, I want you to know that I do not endorse this type of inheritance scheme in JavaScript, and more explicitly, I do not recommend the use of this ClassyObjects framework for any purpose other than learning. It is not suitable for any other use. There are bugs, design and implementation limitations, and in general, class-y inheritance frameworks should be avoided in JavaScript. We’ll see why in a moment.
An Example
There’s a small example in the Readme for the project, which illustrates the use of the framework:
In this example, I’m using the `define` method to create a new “class” (re: constructor function), using `extend` to inherit from that object and create yet another “class” (re: constructor function), and then use those objects to do some work – including the ability to call the `super` object of the one that I’ve extended in to.
It works fairly well. The few bits of functionality that you see in this case show the proper results. But there’s some pretty serious problems with writing code this way.
Classy Frameworks: A Mistake
From Douglas Crockford:
“Five years ago I wrote Classical Inheritance in JavaScript. It showed that JavaScript is a class-free, prototypal language, and that it has sufficient expressive power to simulate a classical system. My programming style has evolved since then, as any good programmer’s should. I have learned to fully embrace prototypalism, and have liberated myself from the confines of the classical model.“
He also says at the bottom of his Classical Inheritance page:
“I have been writing JavaScript for 8 years now, and I have never once found need to use an uber function. The super idea is fairly important in the classical pattern, but it appears to be unnecessary in the prototypal and functional patterns. I now see my early attempts to support the classical model in JavaScript as a mistake.“
Frankly, I agree with him. I think there’s some potential value in a framework like this, in specific scenarios. But the uses of it seem to be more and more limited, the more I learn about good prototypal inheritance patterns.
From my Objects & Prototypes screencast:
“More than just the remaining bugs [in the ClassyObjects framework], though, wether or not a class-y framework is a good idea to begin with is a subject of intense debate.
A class-y framework like this is powerful, indeed, and there are times when it can come in handy. Backbone is a good example, again. Jeremy Ashkenas – the creator of Backbone – recognized the need to provide a simple inheritance mechanism for the objects in Backbone so he provided one. But at the same time, he didn’t split the inheritance framework out in to it’s own library. I remember reading a comment at one point where he said he didn’t want to impose that style or it’s limitations on anyone outside of Backbone.
For all of the convenience that we created, the class-y objects framework imposes a lot of overhead and brings it’s own limitations and issues. So I say we should embrace prototypes and prototypal inheritance and relegate the class-y frameworks, like the one we just wrote, to the special cases where the advantages may outweigh the disadvantages.”
Overhead And Other Concerns
Ok, enough of the nebulous rhetoric… there are a few real problems that I see in code like this. Chief among them are:
- Implying a “class” definition, which can be dangerous for inexperienced JS devs
- The overhead of defining the multiple layers of inheritance
- The overhead of managing the “super” context correctly
Implying A Class Definition
This is probably the worst of the problems that I’ve mentioned – at least in my opinion. I consistently run in to questions on StackOverflow where the person asking the question is looking at something like Backbone, assuming that they have a class definition because of the way it looks, and winding up with problems that are directly caused by not understanding object literal syntax. Now I’m not blaming Backbone or saying that these developers should know better. Doing either of those would get us nowhere. The point of using this as an example is to show that a class-like inheritance structure in JavaScript can be very deceiving.
When a developer brings years of experience with a language like Java, C#, C++ or other class-based systems, class-y JavaScript frameworks can be very deceptive. They look so much like class definitions that it’s easy to fall in to the trap of thinking that they are classes. Of course, there are no classes, so we are really looking at object literals.
Overhead Of Inheritance Layers
When a JavaScript object has a method called on it, that method might not exist on the object itself. It may exist on a prototype in the inheritance chain. When that is the case, the runtime must search up the prototype chain to find the method and call it from the prototype. In a small inheritance chain, this happens so fast that you’ll likely never notice it. But when we introduce a class-y inheritance framework like ClassyObjects, we add a lot more overhead for each layer of inheritance in order to protect the object we are extending from the one we have extended in to.
Look at it this way: when you have a standard prototypal inheritance chain going on, you have at most the number of objects that you are directly working with:
In this example, there are two objects that we defined and used: MyObject and InheritingObject. InheritingObject’s prototype is MyObject, directly. Any change we make to MyObject will be directly reflected in InheritingObject.
Now look at the the “inherits” function from the ClassyObjects framework, and a very simple usage of it:
In the example usage, it might look like we are only dealing with two objects. But the truth is we are dealing with no less than 5 objects: MyObject, ConstructorFunction, ConstructorFunction.prototype, the “definition” object literal, and finally the object instance that we create from the resulting “class-y” object.
We’ve added 2.5 times the number of layers to our system, so that we can create a class-like structure.
But there are some benefits to this. We’re not adding all of these layers for the sake of adding them. In the prototype example where we only have two objects in use, modifying MyObject will result in changes being available to InheritingObject. This might not be the desired behavior, and the ClassyObjects framework solves that with the additional overhead.
When we call “inherits” to create a new constructor function (“class”), we add the extra “inhertingInstance” object as the prototype of our ConstructorFunction specifically so that we can isolate the prototype of the new objects from the original object we extended. This means we can directly modify the “MyClass.prototype” object and have it affect all of our MyClass instances, while still isolating the original MyObject from those changes.
Overhead Of Managing “this” in “super”
Both Backbone and ClassyObjects have a problem with context, directly caused by the way JavaScript respects the context of the called function. If you have a setup like this:
You’re going to end up with an infinite loop and a stack overflow problem. The problem is caused the use of “this.super”. In the call to “bar.baz”, the context of the call is set to the “bar” object. But the “baz” function doesn’t exist on “bar”, so it looks at “foo” for the function. Now the method definition for “foo.baz” calls “this.super.baz()”, which looks like it should call “root.baz”, right? After all, the “super”-class of “foo” is “root”. But since the function execution context has been set to “bar”, “this” still refers to “bar”. Therefore, “this.super.baz” will effectively call “bar.baz” – which is the original call that we made to start this whole thing off, thus resulting in an infinite loop.
You can fix this, though. You can add the overhead of wrapping “super” as a function and then wrapping the “super” function of each object in a bound context function. EmberJS does this, for example, and there’s a tremendous amount of overhead involved again. It’s akin to the way we created additional inheritance layers in order to isolate the prototype of a “class-y” constructor from the object that it extends.
More layers, more overhead, more potential for slowing down your application and your framework. No thanks.
Still, It Has it’s Uses (I Think)
For all the class-y bashing that I’m doing here, I still think there’s some valid use of a class-like inheritance structure in JavaScript. Specifically, when creating a larger framework with it’s own need for behavior re-use and simplified object extension. That is, I don’t think a class-y framework should be built for the sake of itself. Instead, I think frameworks like Ember and Backbone have it right when they take advantage of a class-like infrastructure in order to make the larger purpose of the MV* style framework and library easier to use.
Sure, there are likely ways in which Backbone and Ember could facilitate their inheritance without the use of a class-like infrastructure. I don’t know that it would serve the needs of the end-user as well as a class-like framework, though. But then, I haven’t seen an MV* framework or library that takes this approach, yet. Maybe they are out there – and I’d love to see one. If one doesn’t exist, though, maybe it’s time to write one.
For More Info On Prototypes…
If you’re interested in learning more about JavaScript objects and prototypes, check out my 40 minute screencast on the subject (paid). I walk through the basics of object literals, functions, constructor functions, prototypes, the inheritance chain, building the ClassyObjects framework, and type checking in JavaScript.