JavaScript is a very misunderstood language. It has gotten a bad rap because most people that use it don't look for its brighter side. Instead they will often look online for some code to copy paste, or they will hack something together using tiny snippets and in-lines to get a drop down menu.
It is also a very conflicted language. By nature, JavaScript is a Prototypal language where nearly everything is an Object and there are no classes. It is also somewhat Functional since all the functions are First Class Functions. They are just more objects that can be passed into and returned from other functions.
The issue is that many people try to use JavaScript like they would use Java or C++. They want a classical language, where they strictly define all their types and then create instances of those types.
Even though JavaScript can be jammed, beaten, or even mutilated into classical structures (in fact the book JavaScript: The Definitive Guide spent a chapter on exactly that), this is betraying its nature and ignoring all the benefits that come with Prototypes.
The Classical Form
Lets take a look at a basic classical implementation of a square using JavaScript:
You will notice that JavaScript even has a new
key word in an attempt to match a Classical implementation. The function Square()
defines a class with a side attribute. It also has two instance methods, area
and perimeter
.
We can then create mySquare
in almost the same way we might do it in Java. This is JavaScript though, and the new
works differently. It creates an empty object and then calls the Square()
function (or the constructor) passing the empty object into this
.
The constructor then populates that object with the specified behavior before returning it. This is particularly scary, because if you forget to put new
in front of the constructor, this
will refer to the Global Object, as will mySquare
(meaning any further changes made to mySquare
would also be on the Square
“class”).
This is why, if you want to use JavaScript in the classical way, you should always capitalize the first letter of a constructor.
Ok, so we have our Square
, but say we want to create a square that holds a X
or an O
. Well, we would want to inherit the properties of our current Square
class and then add to them. We might do something like this:
We create a new class for the ContainerSquare
which uses the Square's constructor to define the side
attribute, along with the two instance methods. We then define our new contents
attribute, along with an accessor method to go with it.
Line 6 sets up a prototypal inheritance structure so that a ContainerSquare
object will be linked to the proper prototype chain.
The prototype chain is the hierarchy of objects that a method or attribute call will traverse until it finds what it is looking for. In other words, when I call myContainer.size()
, JavaScript will first check to see if the myContainer
object has that function.
If not, it will proceed to check the object's prototype, which is the ContainerSquare
. Since ContainerSquare
doesn't have the function, next in line is the ContainerSquare
prototype, which is just Square
. Square
does in fact have a size()
function, and since JavaScript will use the first function it finds, it will use Square's size()
function.
Thus, by setting the ContainerSquare
prototype to Square
, we inherit any of the attributes or methods of Square
.
Although this all works, and we have successfully mashed a prototypal language into a classical scheme, we should try this in the way JavaScript wants to be used.
The Prototypal Form
We can start with the square
. There are few pretty simple ways to achieve the same functionality using regular objects and their prototypes, and we will look at two of them. The first will create a square
object we can use.
Then if we need more squares
, we can make a copy of that object using its prototype. The second will create a squareMaker
function, which can then be used to produce new squares
.
Here, we already have a new and usable square
at our disposal. We don't need to call a constructor and define any types. We can just take an object and mold it into the form we want.
Say we want another square
though. We can't just write something like var secondSquare = firstSquare;
because JavaScript passes objects by reference, and thus the secondSquare
would just point to the firstSquare
.
What we can do, and this is a technique developed by Douglas Crockford, is make a copy of our object by calling a new constructor with a prototype
that points to our object. This new constructor creates a new empty object and assigns the values of our old object to the new empty object.
You can do this yourself, or you can use Crockford's technique as follows:
You can see that we are actually defining an on-the-fly constructor F
, using our object o
to define F.prototype
. Then we create a new object using that constructor. This will give us a new copy of our object, with all its attributes and functions. If we now called secondSquare.area();
we would get 36.
If you wanted to then make a ContainerSquare
, you could simply add a contents attribute to the secondSquare
, and then make copies of the secondSquare
if you need more Containers.
Keep in mind that since firstSquare
is the prototype of secondSquare
, if you were to add a contents attribute to firstSquare
, you would then have that attribute on secondSquare
. However, adding attributes to the secondSquare
does not place them on the firstSquare.
Spawn More Protolords
The other way to get squares would be to make a squareMaker
function. This function will return a new object with whatever attributes you define. I am also going to show you some closure so that the attributes are private, and only accessible through accessor methods.
You will notice that the return value of the squareMaker
is almost exactly like how we defined our firstSquare
object. We are just returning the definition square
, and thats quite awesome.
Another thing you will notice is the bit of closure. We pass a value into the squareMaker
, but it isn't stored anywhere. It is held in the scope of the squareMaker
function, allowing those internal methods to use it, but hiding it from the outside (unlike the side attribute defined in the firstSquare
).
This sort of closure is another great tool you can use in JavaScript. In a way, this use of closure is like defining private variables to a class.
To get the container functionality we create a containerMaker
using the squareMaker
, and some more closure, to make a new square object. We then dynamically add a getContents
method to provide access to our private contents attribute.
Conclusion
So we have now seen two ways to use some of the better parts of JavaScript to get the same functionality that we could get using the classical scheme.
We can also see some advantages in using JavaScript in its natural form like: getting some closure (which can be exceedingly powerful), very dynamic objects ready to change in anyway you can think of, quickly defined objects which can be used and multiplied, and no need to predefine types. Pretty sweet right?
I will admit one slight inefficiency with the two solutions I showed you (although there are ways around this). Using the squareMaker
, or making copies of the objects will make full copies, including the function objects defined inside.
If you use the classical scheme I showed, you are defining functions on the prototype rather than the object itself, thus there will only be one copy of the function.
You can, of course, do something similar in the JavaScript scheme, but the optimization doesn't count for all that much in most cases (unless you are doing mobile development where you want to save everything you can!).
JavaScript isn't a classical language, but instead a prototypal language that is powerful enough to mimic a classical language without breaking a sweat. There is much to be gained by recognizing this fact and changing your mindset to work with it.
Trying to fit a square into a circular hole will just get you stuck. All languages are unique with a variety of their own advantages, and as good developers we should recognize these differences and reap all the benefits they have to offer.