Shh, come here! Wanna hear a secret?
Classical inheritance is a tested and tried approach.
It is useful to implement it in JavaScript often. Classes are a nice concept to have and having templates for modeling our world after objects is awesome.
Classical inheritance is just a pattern. It's perfectly OK to implement classical inheritance in JavaScript if it's the pattern you need for your use case.
Prototypical inheritance focuses on sharing functionality and that's awesome (dinasaur drumstick awesome), but in some cases you want to share a data-scheme and not functionality. That's a problem prototypical inheritance does not address at all.
So, you're telling me classes are not evil like everyone keeps telling me?
No, they are not. What the JS community frowns upon is not the concept of classes, it's limiting yourself to just classes for code reuse. Just like the language does not enforce strong or static typing, it doesn't enforce schemes on object structure.
In fact, behind the scene clever implementations of the language can turn your normal objects to something resembling classical inheritance classes.
So, how do classes work in JavaScript
Well, you really only need a constructor:
function getVehicle(engine){
return { engine : engine };
}
var v = getVehicle("V6");
v.engine;//v6
We now have a vehicle class. We didn't need to define a Vehicle class explicitly using a special keyword. Now, some people don't like to do things this way and are used to the more classical way. For this JS provides (silly imho) syntactic sugar by doing:
function Vehicle(engine){
this.engine = engine;
}
var v = new Vehicle("V6");
v.engine;//v6
That's the same thing as the example above for the most part.
So, what are we still missing?
Inheritance and private members.
What if I told you basic subtyping is very simple in JavaScript?
JavaScript's notion of typing is different than what we're used to in other languages. What does it mean to be a sub-type of some type in JS?
var a = {x:5};
var b = {x:3,y:3};
Is the type of b
a sub type of the type of a
? Let's say if it is according to (strong) behavioral subtyping (the LSP):
<<<< Begin technical part
- Contravariance of method arguments in the subtype - Is fully preserved in this sort of inheritance.
- Covariance of return types in the subtype - Is fully preserved in this sort of inheritance.
- No new exceptions should be thrown by methods of the subtype, except where those exceptions are themselves subtypes of exceptions thrown by the methods of the supertype. - Is fully preserved in this sort of inheritance.
Also,
All of these are again, are up to us to keep. We can keep them as tightly or loosly as we want, we don't have to, but we surely can.
So matter of fact, as long as we abide to these rules above when implementing our inheritance, we're fully implementing strong behavioral subtyping, which is a very powerful form of subtyping (see note*).
>>>>> End technical part
Trivially, one can also see that structural subtyping holds.
How would this apply to our Car
example?
function getCar(typeOfCar){
var v = getVehicle("CarEngine");
v.typeOfCar = typeOfCar;
return v;
}
v = getCar("Honda");
v.typeOfCar;//Honda;
v.engine;//CarEngine
Not too hard, was it? What about private members?
function getVehicle(engine){
var secret = "Hello"
return {
engine : engine,
getSecret : function() {
return secret;
}
};
}
See, secret
is a closure variable. It's perfectly "private", it works differently than privates in languages like Java, but it's impossible to access from the outside.
What about having privates in functions?
Ah! That's a great question.
If we want to use a private variable in a function we share on the prototype we need to firrst understand how JS closures and functions work.
In JavaScript functions are first class. This means you can pass functions around.
function getPerson(name){
var greeting = "Hello " + name;
return {
greet : function() {
return greeting;
}
};
}
var a = getPerson("thomasc");
a.greet(); //Hello thomasc
So far so good, but we can pass that function bounded to a around to other objects! This lets you do very loose decoupling which is awesome.
var b = a.greet;
b(); //Hello thomasc
Wait! How did b
know the person's name is thomasc? That's just the magic of closures. Pretty awesome huh?
You might be worried about performance. Let me tell you how I learned to stop worrying and started to love the optimizing JIT.
In practice, having copies of functions like that is not a big issue. Functions in javascript are all about well, functionality! Closures are an awesome concept, once you grasp and master them you see it's well worth it, and the performance hit really isn't that meaningful. JS is getting faster every day, don't worry about these sort of performance issues.
If you think it's complicated, the following is also very legitimate. A common contract with other developers simply says "If my variable starts with _
don't touch it, we are both consenting adults". This would look something like:
function getPerson(name){
var greeter = {
greet : function() {
return "Hello" +greeter._name;
}
};
greeter._name = name;
return greeter;
}
Or in classical style
function Person(name){
this._name = name;
this.greet = function(){
return "Hello "+this._name;
}
}
Or if you'd like to cache the function on the prototype instead of instantiate copies:
function Person(name){
this._name = name;
}
Person.prototype.greet = function(){
return "Hello "+this._name;
}
So, to sum it up:
You can use classical inheritance patterns, they are useful for sharing types of data
You should also use prototypical inheritance, it is just as potent, and much more in cases you want to share functionality.
TheifMaster pretty much nailed it. Having privates private is really not a big deal as one might think in JavaScript, as long as your code defines a clear interface this should not be problematic at all. We're all concenting adults here :)
*The clever reader might think: Huh? Weren't you tricking me there with the history rule? I mean, property access isn't encapsulated.
I say no, I was not. Even if you don't explicitly encapsulate the fields as private, you can simply define your contract in a way that does not access them. Often like TheifMaster suggested with _
. Also, I think the history rule is not that big of a deal in a lot of such scenarios as long as we're not changing the way property access treats properties of the parent object. Again, it's up to us.