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:
1function Square (side) {
2 this.side = side;
3};
4
5Square.prototype.area = function () {
6 return this.side * this.side;
7};
8
9Square.prototype.perimeter = function () {
10 return this.side * 4;
11};
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:
1function ContainerSquare (side, contents) {
2 this.superclass(side);
3 this.contents = contents;
4};
5
6ContainerSquare.prototype = new Square();
7ContainerSquare.prototype.superclass = Square;
8ContainerSquare.prototype.constructor = ContainerSquare;
9
10ContainerSquare.prototype.getContents = function () {
11 return this.contents;
12};
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
.
1var firstSquare = {
2 side: 5,
3 area: function () {
4 return this.side * this.side;
5 },
6 perimeter: function () {
7 return this.side * 4;
8 }
9};
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:
1if (typeof Object.beget !== 'function') {
2 Object.beget = function (o) {
3 var F = function () {};
4 F.prototype = o;
5 return new F();
6 }
7}
8
9var secondSquare = Object.beget(firstSquare);
10
11secondSquare.side = 6
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.
1var squareMaker = function(side) {
2 return {
3 getSide: function() {
4 return side;
5 },
6 area: function () {
7 return side * side;
8 },
9 perimeter: function () {
10 return side * 4;
11 }
12 };
13};
14
15var anotherSquare = squareMaker(5);
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.
1var containerMaker = function(side, contents) {
2 var container = squareMaker(side);
3 container.getContents = function () {
4 return contents;
5 };
6 return container;
7};
8
9var anotherContainer = containerMaker(6, "O");
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.