Javascript Guide - Advanced Patterns
You can now also navigate the guide through the table of contents.
Now that we know how prototypes work, at least on the basic level, I think it's time we learn a bit about the concept of objects and inheritance.
In the programming world, we have an entire paradigm of Object Oriented programming. Although JS is not object oriented, it is Object Based, and in fact, because it is such a powerful language, almost everything that can be done with Object-Oriented languages can be mimicked with JS.
About the concept of Encapsulation
WikipediaA language construct that facilitates the bundling of data with the methods operating on that data.
Well, actually, in Wikipedia that part is a secondary definition, but for us it's more important. The concept of encapsulation (as we are going to handle it) means that a behavior should be attached to the object that performs it. We already encountered that concept when we used Array.push - the push action is a part of the array prototype, and that means that every array has it. It is an action an array performs on itself.
So, when we create our code, it is often useful to notice if certain part of our code interact with the same parts, or that their behavior it interconnected, and wrap them within an object. Since we still haven't manipulated our web pages yet, the examples here are going to be abstract, but stay with me on this.
Creating a Person
Lets take our Person Example from before, but extend it a little. Here's how we do it:
- First thing we should ask ourselves is -
What properties do we want our Object to have?
- in our example, it's going to be - name,age, location and energy. - Now we need to decide if any of these should have a default value, and if some of these can be changed. So in our case - location and energy should start the same for all persons, but age should be changeable on construction.
- We now need to decide what actions it will perform - our person will walk, and talk. Both will consume energy, and so he should also eat.
With these in mind, we are ready to write our code. In this example (and the ones that will follow), I will use some patterns that are a bit more advanced than what we learned so far, so you can learn from them.
function Person (first,last,age){
this.name = name;
this.last = last;
this.age = age || 20;
//if age wasn't set, it will be undefined, which is "falsy". when JS encounters this notation,
//it will attempt to assign the first argument, and if it is false it will use the second one.
//this pattern allows us to set default values to our properties.
//In this case, what I'm saying is "use provided age. if non provided set to 20".
}
Person.prototype.energy = 100;
Person.prototype.location = 0;
Person.prototype.walk = function(steps){
//receives how many steps the person walked. advances the person's movement.
//when a person is too young or too old he only moves half the distance, and uses twice that energy.
var speed = 1
, energy = 1
, energy_needed = 0;
if (this.age < 10 || this.age > 50){ // when our person is too young or too old
speed = 0.5; // he will walk half speed
energy = 2; //and consume twice the energy
}
energy_needed = steps * energy;//how much energy is needed for this walk
if (this.energy < energy_needed){
console.log('I don\'t have enough energy for this task...');
return;
}
this.energy -= energy_needed;
this.location += steps * speed;
};
Person.prototype.talk = function(){
if (this.energy <= 1) {//even talking takes energy
console.log('I don\'t have enough energy for this task...');
return;
}
this.energy--;
console.log('My Name Is '+this.first+' '+this.last+' and I am '+this.age+' Years old');
};
Person.prototype.eat = function(calories){
this.energy += calories;//yummm
};
var bob = new Person('Bob','Alice',25);
bob.walk(20);
console.log(bob.location); //will be 20, because bob is young, and moves one tile per step
bob.talk(); //will log My Name is Bob Alice and i am 25 Years old
This was a lot more code than what we're used to, so take the time to see you get it. It's not that complicated in fact.
Extending our Person
But, in real life, although we have many persons, many of them perform much more functions than eat, walk and talk. Lets take for example, a Warrior - he can also attack people.
Here the concept of inheritance kicks in. In object oriented programing, we can create an object (or, in most programing languages - class) that inherit another. In JS, this is done by assigning one object to another' prototype.
But problems start to kick in when we want to inherit Classes that have constructors. You see, although a Function can inherit it's parent's method, it does not inherit it's construction code. So, when we do this:
Warrior = function(){};
Warrior.prototype = Person.prototype;
var war = new Warrior("Bob","Alice");
war.talk(); // will log My Name is undefined undefined and i am undefined Years old
Person's constructor won't run, although Warrior did inherit it's talk method. To make it run, we need to call it in a way that will make it's this point to our current warrior. There are 2 ways to do this, and both will be done through manipulation of scope.
-
Using Scopes
As you recall, the
thiskeyword is very sensitive to the scope. As it is now,Personis located on the global scope, so when calling it as is (withoutnew), it'sthiswill point to the global scope.But, if we assign
Personto a method ofWarrior, calling that method will point to theWarriorinstance:function Warrior(first,last){ this.constructor = Person; this.constructor(first,last); }; Warrior.prototype = Person.prototype; var war = new Warrior("Bob","Alice"); war.talk(); //will log My Name is Bob Alice and i am 25 Years oldrun code This might seem a little hackish, but it works.
-
Apply
The second method is to use one of every
function's method -apply.function.applyis a way for us to run a function, specifying it what scope to use. It accepts 2 arguments:- A scope to use. If non is supplied (
null) it will use the global scope. - An array of arguments to pass to the function.
So, if we pass
Person.applythe Warrior'sthisand it's arguments (which already is Array-like), we can receive the previous result, without the use of a special argument:function Warrior(first,last,age){ Person.apply(this,arguments); }; Warrior.prototype = Person.prototype; var war = new Warrior("Bob","Alice"); war.talk();//will log My Name is Bob Alice and i am 25 Years oldrun code applyis actually an extremely powerful tool, for many other uses, so you should keep it in the back of your head. - A scope to use. If non is supplied (
I personally like the second one better, so that's the one we'll use.
Now let's profile our Warrior class. As I said, I want it to be able to attack. For it to attack, I want us to be able to set it's attack power.
The way I want the attack method to work is that it will receive a Person, and remove some of it's energy. We want to make sure that attack actually received a Person, and so we will use the instanceof operator.
instanceof has a bit weird syntax - a sentence like one. They way it works is like this: sub_class instanceof sup_class - is sub_class an instance of sup_class. What this actually does is to check whether one class has another's prototype in it's prototype-chain. This means that any object that has it's prototype point to Person will be an instance of Person. That also means that any instance of Warrior will also be an instance of Person and so forth.
Just so we can get the hang of it:
console.log({} instanceof Object); //true
console.log(new String('') instanceof String); //true
console.log(1 instanceof Object); //false
So, to go on with our code, the attack method:
Warrior.prototype.power = 10;
Warrior.prototype.attack = function(person){
if (!(person instanceof Person)){
console.log('Provided person is invalid');
return;
}
person.energy -= this.power;
}
var bob = new Person("Bob","Alice")
, john = new Warrior("John","Smith");
john.attack(bob);
console.log(bob.energy); //since the attack took off 10 point of energy, it will log 90 ()
To wrap it up
We've learned quite a few tricks today, and some have them were probably a bit confusing. If that's not enough, we wrote quite a lot of code (I even used an external file this time...). So here's a quick list of the stuff we learned:
- We've learned how to set default but dynamic values to our properties, using this syntax:
this.age = age || 20. - We've learned how to use
prototypesandfunctionsto encapsulate the behavior of our objects. - We've learned how to use the prototype to extend our objects into new ones.
- We've learned how to play with the scope to reuse other object's constructors. While we were at it, we learned of the very cool
applymethod. - Finally, we've learned how to use the
instanceofoperator to check if a certain object is an instance of another.
Now that we've learned quite a lot of the bases of ECMAScript, I feel it is now safe to start playing with the DOM.