In the previous chapter, we learned about JavaScript's object implementation and discovered how ideas from both prototypal and classical languages were incorporated into the language. This merging of approaches is somewhat unique to the language and sometimes makes it harder for new developers to learn how object-oriented programming in JavaScript works.
MooTools, with its focus on modularity and simplicity, provides an elegant alternative to the native implementation. To put it succinctly, MooTools turns JavaScript into a classical language. In this chapter, we'll learn how this class system works, and we'll examine some of the techniques used by MooTools to add a major language feature to JavaScript.
Suppose we need to create several JavaScript objects to represent people. These objects need to have two properties, name and age, and a method called log to print out the values of these properties. Recalling what we learned from the last chapter, we come up with this snippet:
var Person = function(name, age){ this.name = name; this.age = age; }; // Properties Person.prototype.name = ''; Person.prototype.age = 0; // Methods Person.prototype.log = function(){ console.log(this.name + ', ' + this.age); }; // Creating a new Person instance var mark = new Person("Mark", 23); mark.log(); // 'Mark, 23'
First, we created a Person
constructor by assigning a function to our variable. We then defined the name, age
, and log
members by augmenting our constructor's prototype object. Finally, we created a new instance object using new Person()
. We know that the new keyword creates a new object that inherits from Person.prototype
, and we stored our newly created object in our mark
variable.
So the whole process—without thinking too much of the details—can be summed up in three steps: create a constructor function, modify its prototype property, and instantiate it using new
. Of course, this is from a JavaScript perspective—in true prototypal languages (like our Io example in the last chapter), the process is much simpler: create an object, and clone it to create a new object.
Unlike their prototypal counterparts, though, classical programming languages don't use objects to define other objects but instead rely on a special construct called a class to define objects. A class—much like a prototype—defines the structure of an object by specifying its properties and methods. For example, here's an implementation of a Person class in a classical language called ooc:
Person: class { // Properties name := "" age := 0 // Methods init: func (name: String, age: Int) { this name = name this age = age } log: func { printf("%s, %d", this name, this age) } } // Creating a new Person instance mark := Person new("Mark", 23) mark log() // "Mark, 23"
First, we created a new class called Person
and then we defined several members: the properties name
and age
, which are a string and an integer respectively, and two methods, init
and log
. We then created a new object instance of Person
called mark
and called its log()
to print out the name and age of our instance. You'll notice that we didn't have a constructor function, although we did have an init
method that looks similar to our original constructor. We'll learn more about this difference later. The important thing to note right now is that both our JavaScript and ooc examples are doing the same task of defining objects to represent people, even though they're using different constructs to do it.
While they are both defining the structure of objects, classes and prototypes are different within the languages themselves. In a true prototypal language (which JavaScript is not), prototypes are always objects and any object can be a prototype. In contrast, classes can be objects or primitive types, depending on the language implementation. Classical languages also use special syntax to define classes, as in our ooc example, while prototype languages define prototypes like regular objects. This special syntax for classes makes the structure of instances immutable in most cases: you won't be able to add new members to objects without defining them first through the class—something you can freely do in prototypal language. For instance, you won't be able to do mark occupation := 'JavaScript Developer'
in our ooc example above because the property occupation wasn't defined in our class.
But the biggest difference is in terms of inheritance and types. Objects in classical languages inherit from classes and, because the word class implies a group of things, these objects are seen not only as instances of those classes but also members of that group defined by class. In the case of our ooc example, the object mark
not only inherits from the Person
class but it's also a member of a group that is composed of all Person
objects. We commonly refer to this grouping as a type. It's the job of a class not only to define the structure of objects but also define the type of those objects.
However, we can't have the same grouping in true prototypal languages. Since objects are cloned and inherit directly from other objects, there's no separate construct that defines the type of the objects. For example, we have a Person
object and we clone it to create a mark
object. We can say that the mark
object inherits from Person
and that it's a copy of the Person
object. But this doesn't mean that the mark
object is of the type Person
because Person
itself is just an object and it doesn't define a grouping.
So where does JavaScript stand in this? Interestingly, it's somewhere in the middle. JavaScript is technically a prototypal language but it also contains classical influences. For one, a JavaScript object doesn't inherit from just any plain object but from the special prototype
object defined as a property of the constructor. In essence, JavaScript prototypes are similar to classes because they're special language constructs. The existence of these special language constructs—prototypes and constructor functions—means we could have "types" in JavaScript because they create groupings for our objects. But because the prototype is still an object, JavaScript still implements prototypal rather than classical inheritance.
Take a look at the previous JavaScript and ooc examples. Clearly, the class declaration in ooc is cleaner than the declarations in JavaScript. In our ooc example, all the properties and methods of our object were declared inside the class {...}
declaration and you could easily see what members our instances would have. Our JavaScript example, on the other hand, is more verbose and repetitive: there's no visual grouping for our constructor and prototype
members and we had to write Person.prototype
several times.
This becomes even more apparent as you start writing large JavaScript applications where you need to declare many different kinds of objects. I don't know about you, but typing stuff over and over again seems counterproductive and tedious to me.
Developers were quick to see this problem and they developed many techniques to simplify object-oriented programming in JavaScript. The best solution, of course, was to implement a class system in JavaScript. There are many class systems available from different frameworks, each with its own feature set, but arguably the best among all of them is the MooTools class system.
That's quite a big assertion on my part, so let me give you a few reasons:
The syntax of MooTools classes is similar to what's found in actual classical languages.
The MooTools class system is built on the native JavaScript object implementation—all the magic is already in the language itself.
The class system abstracts and extends the native object implementation, adding useful features that simplify object-oriented programming.
Inheritance is handled by the class itself—including direct inheritance and "multiple inheritance" using "mixins" (more on these later and in the next chapter).
The system itself is extensible, which means you can add new features easily.
We'll discuss these points more thoroughly throughout this chapter, but here's the most important thing to consider: MooTools doesn't give you a half-baked class system that's nothing more than syntax changes. Instead, the MooTools class system is a complete implementation of class-based programming for JavaScript.
The first point is actually easy to prove. To give you a taste, here's how we implement the previous examples using a MooTools class:
var Person = new Class({ // Properties name: '',
age: 0, // Methods initialize: function(name, age){ this.name = name; this.age = age; }, log: function(){ console.log(this.name + ', ' + this.age); } }); // Creating a new Person instance var mark = new Person('Mark', 23); mark.log(); // 'Mark, 23'
Pretty, isn't it? In fact, it looks strikingly similar to our ooc example. But even with the syntax change, using the class is no different from how we'd use a constructor: we still used the new
keyword, we still passed arguments to our constructor function, and we still got a new object that inherits from Person
.
In MooTools, we create a class using the Class
constructor. This constructor takes a single object argument called params
, the key-value pairs that will become the members of the class. The object passed to the class constructor is your blueprint, and it defines how you want your instances to look. It then returns a constructor function. Let's take a look at another class:
var Animal = new Class({ name: 'Animal', eat: function(){ console.log(this.name + ' is eating.'), }, walk: function(){ console.log(this.name + ' is walking.'), } }); var myAnimal = new Animal(); myAnimal.eat(); // 'Animal is eating.' myAnimal.walk(); // 'Animal is walking.' console.log(myAnimal instanceof Animal); // true
Here we create a new class called Animal
by passing an object to the Class
constructor. Our object literal has three members: a property called name
and two methods, eat
and walk
. The Class
constructor returns a new constructor function and then we create an instance of the class called myAnimal
using new Animal()
. Because we declared the eat
and walk
methods for our class using the object literal, we are able to call these methods on our myAnimal
object. Finally, we use the instanceof
operator to verify that our new myAnimal
object is indeed an instance of Animal
.
I mentioned that the MooTools class system is built upon the native object implementation, and we saw that the Class
constructor takes an object that describes the members of the class we're creating and then returns a constructor. If we connect these two pieces of information, we get the gist of how the Class
constructor works: it creates a new constructor function, goes through the keys we specified in the object argument, appends these keys to the new constructor's prototype
property, and returns the constructor.
It might sound too simple, but that's exactly how the core of the MooTools class system works. A MooTools class is nothing more than a simple abstraction of the constructor-prototype pair with additional extensions to make working with them simpler. This means that everything we learned in the previous chapter about objects and prototypes still applies to classes.
Of course there are major differences, particularly in how we'll look at things from now on. Because MooTools abstracts the native prototypal implementation, there's almost no need for us to work directly with prototypes—and in most cases, you won't even notice you're working with prototypes.
Another thing we have to change is how we talk about object-oriented programming itself. Since we'll be dealing with classes rather than prototypes, we'll be using classical terms from here on. It's a weird shift to talk about JavaScript in a classical manner, but because MooTools provides a complete system for classical programming, we'll need to use proper terms when working with the class system.
In the MooTools class system, there's no distinction between the constructor and the prototype because we have only one blueprint object: the class. Before, we had to create a constructor function and then augment its prototype
property to add new methods. But since we're only going to deal with classes from now on, we'll only have to worry about the class itself and the members we'll be defining in our class.
One shift we'll have to make when switching from the prototypal model to the classical one is to forget about the actual constructor function. In the prototypal model, we used the constructor function to prepare the object. In the case of our Person
constructor, we set the name
property of the instance to the value of the argument passed. When it comes to more complex programs, constructor functions tend to be more complex than this, performing lots of operations that set up the new instance.
But we didn't declare a constructor function in our implementation of the Person
class using the MooTools class system. Instead, the stuff we usually place inside the constructor's body was placed in a special method called initialize
in our class declaration. This method is called the initializer and it effectively replaces the constructor function from the prototypal model.
In essence, both the constructor and the initializer are used for the same thing: to set up the new instance. The main difference is that the constructor in the prototypal model is technically separate from its prototype and is not inherited by its instances, while an initializer is an actual method that's inherited by objects:
var Person = new Class({ // Properties name: '', age: 0, // Methods initialize: function(name, age){ this.name = name; this.age = age; }, log: function(){ console.log(this.name + ', ' + this.age); }
}); var mark = new Person('Mark', 23); console.log(typeof mark.initialize); // 'function' console.log(typeof Person); // 'function' console.log(typeof Person.prototype.initialize); // 'function'
However, having an initialize
method doesn't mean we don't have an actual constructor. In our Person
class example, Person
is our constructor and Person.prototype.initialize
is the initializer. Since the MooTools class system still uses the native prototype-based object implementation, we still need constructor functions to augment their prototype properties and to create new objects using the new
keyword.
One misconception is that the initialize
function we declare in our class will become the constructor function for the class, but this isn't the case. All classes actually use a common constructor function. We say "common constructor" not because all classes use the exact same function, but because all of them have a constructor that looks like this:
// the common constructor function function(){ reset(this); if (newClass.$prototyping) return this; this.$caller = null; var value = (this.initialize) ? this.initialize.apply(this, arguments) : this; this.$caller = this.caller = null; return value; }
Most of the stuff inside this common constructor might look like gibberish right now, but for the moment let's focus on the line that starts with var value
. This is the most important line in the common constructor and it invokes the instance's initialize
method, passing any arguments given to the common constructor. Also, note that the return value of the initialize
method is stored in the variable value
and returned at the end of our common constructor.
Applying this to our previous example, the call to new Person('Mark', 23)
first invoked the Person
function (which is the common constructor) and then the instance's initialize
method was called, passing the argument 'Mark'
to it. These invocations are transparent: when looking at it from the outside, it seems that the initialize method was invoked directly.
The separation of the constructor function and the initialize method might seem redundant—after all, we could just remove the common constructor and use the initialize
function as the constructor itself. This distinction, however, is important because of the difference between instantiation and initialization.
Instantiation is the process of creating a new object, while initialization is the process of setting up the object. With our original prototypal model, both the instantiation and initialization phases happen: first instantiation via the new
keyword and then initialization by calling the constructor.
But there are times when we want to create an object without initializing it. It's probably hard to think of why we'd want to do this, but we'll see how it's useful later on. What we do need to realize right now is that with our original prototypal model, we can't separate instantiation and initialization, because the constructor always gets called after creating a new object. Having a separate method for initialization solves this problem because we can now bypass the initialization phase if necessary.
The common constructor also enforces some workarounds for the language, like removing object references and adding private methods. Since we already have a function automating these tasks for us, we can focus our attention on the class itself rather than having to write these workaround codes every time.
In the prototypal model, we had to worry about the separation of the constructor and the prototype. While the constructor is still invoked to process the instance, it's not part of the instance itself—which can lead to some confusion later on. In contrast, the MooTools class system eliminates this distinction by making the constructor "irrelevant" and instead turning to the initialize
method. With a class, the only things you have to worry about are the actual members of your object.
The MooTools class system still uses the native prototypal implementation, and members are still added to the prototype
of the constructor of the class. Because properties and methods still behave the same way, we can easily apply what we already learned about them from the last chapter.
The process of adding members to the prototype of the class isn't straightforward, though. It may seem that the Class
constructor simply takes the members of the object argument passed and adds them directly to the prototype
, but there's a more complex process happening underneath.
Before we go any further, let's revisit a problem we encountered in the last chapter. Suppose we have the following class:
// person.js var Person = new Class({ name: '', age: 0, initialize: function(name, age){ this.name = name; this.age = age; }, log: function(){ console.log(this.name + ', ' + this.age); } });
This class is found in a file called person.js
, which is an external file that's not accessible to us. Now we want to add new methods to this class: getName, setName, getAge
, and setAge
. We don't have access to the class declaration itself, so we can't edit it directly. What we did in the last chapter was to augment Person.prototype
to add the new members. Since classes use the native prototypal implementation, we could use the same technique to augment our Person
class.
That also works with classes, but it's not considered proper MooTools style. Classes abstract prototypes, so there's no need to directly augment the prototype
object. The proper way is to use a class method known as implement
to add these new members:
// program.js Person.implement({ getName: function(){ return this.name; }, setName: function(name){ this.name = name; }, getAge: function(){
return this.age; }, setAge: function(age){ this.age = age; } });
We call the implement
method of our Person
class and then pass it an object literal containing the methods we want to add to our class. The result is that all instances of Person
will now have those methods, just like the result we'd get if we augmented the prototype
object directly.
All classes have the implement
method: it's a part of the simplified abstraction of the MooTools class system. I said that it's not proper MooTools style to directly augment the prototype of a class, but that's an understatement. What I should have said was this: always use the implement method. Unless you know what you're doing and unless you have a real need to do so, never directly augment the prototype of a class.
Some might think that this is too strict a rule. If MooTools classes just abstract the native prototypal implementation, augmenting class prototypes should work. This isn't the case, though: part of the abstraction provided by classes is ensuring proper object behavior. The implement
method doesn't just add members to the prototype directly, but instead processes the members first to make sure that everything will work as expected.
You might have noticed that the implement
syntax is similar to the Class
constructor's syntax, with both requiring an object literal describing the properties and methods to be added to the prototype
of the class. That's because the Class
constructor itself uses implement
to add the members. All class members go through the implement
method—even the ones passed through the Class
constructor.
The implement
method looks through each of the items from the object argument and checks whether it should process that particular item before it adds it to the prototype
. The processing depends on the type of the item: properties undergo different processing from methods, and there is special processing done to items with particular keys. Since we're talking about members now, we won't go into detail about those keys here; instead we'll focus on how implement
processes methods and properties.
In the prototypal model, adding methods is done simply by adding a property to the prototype
with a function value:
var myFn = function(){ console.log('Hello'), }; var Person = function(){}; Person.prototype.log = myFn; var mark = new Person(); console.log(typeof mark.log); // 'function' console.log(mark.log == myFn); // true mark.log(); // 'Hello'
Here, we assign the function that's stored in the myFn
variable to Person.prototype.log
. Because functions are referenced, both myFn
and Person.prototype.log
now point to a single function, which we confirm by comparing myFn
and mark.log
. No surprises here. However, the behavior is different with classes:
var myFn = function(){ console.log('Hello'), }; var Person = new Class({ log: myFn }); var mark = new Person(); console.log(typeof mark.log); // 'function' console.log(mark.log == myFn); // false mark.log(); // 'Hello'
We know that the log
method is our original myFn
function, because mark.log()
outputs as expected. But when we compare mark.log
and myFn
, we're told that they're not the same function. This means that myFn
and Person.prototype.log
point to different functions. But how come mark.log()
still works as if both functions are the same?
This is one of the changes brought about by the implement
method. We learned about function decoration in Chapter 2; well, the implement
method decorates all methods in a class. All class methods you pass to implement
are changed to the following decorated function:
function(){ if (method.$protected && this.$caller == null) throw new Error('The method "' + key + '" cannot be called.'), var caller = this.caller, current = this.$caller; this.caller = current; this.$caller = wrapper; var result = method.apply(this, arguments); this.$caller = current; this.caller = caller; return result; }
Don't get scared yet—right now you can ignore most of the lines in this extremely confusing function. The most important line here is the one that starts with var result
. The identifier method
in this function points to the actual method you passed in your declaration. In the case of our previous example, method
points to the same function as myFn
. Because it's simply a decorator, calling this function also invokes the original function. The results of your original function are then stored in a variable and returned. The whole process is transparent, and you won't notice what's happening when you're working with classes.
This decorated function also stores the original method in a special property called $origin
. We can compare the methods as follows:
var myFn = function(){ console.log('Hello'), }; var Person = new Class({ log: myFn }); var mark = new Person();
console.log(typeof mark.log); // 'function' console.log(mark.log == myFn); // false console.log(mark.log.$origin == myFn); // true mark.log(); // 'Hello'
Arguably, this function seems like the most complex of all functions in MooTools, and it does take a while to understand it. This complex decoration might be overwhelming right now, but we'll learn how it works throughout this chapter and the next. While understanding how this function works internally isn't essential to working with classes, it'll help you see how MooTools implements classical features like calling overridden methods and hiding private methods.
If you look back at all the prototype-based examples we've seen so far, you'll notice that we never declared properties for most of them. Instead of augmenting them to the prototype itself, we created the properties inside the constructor. We did this so we wouldn't encounter this particular behavior:
var data = {name: '', age: 0}; var Person = function(name, age){ this.data.name = name; this.data.age = age; }; Person.prototype.data = data; Person.prototype.log = function(){ console.log(this.data.name + ', ' + this.data.age); }; var mark = new Person('Mark', 23); mark.log(); // 'Mark, 23' data.name = 'Joseph'; data.age = 22; mark.log(); // 'Joseph, 22' console.log(data == Person.prototype.data); // true
We discussed why this problem occurs in the last chapter, but the basic explanation is that both data
and Person.prototype.data
point to the same object, so any changes to one are reflected in the other.
However, this approach isn't recommended when using classes. In fact, it's advised to declare all properties for classes when possible. But unlike our previous example, the problem doesn't come up when using classes, and changing the value of the data
object doesn't affect the data
property of our mark
object:
var data = {name: '', age: 0}; var Person = new Class({
data: data, initialize: function(name, age){ this.data.name = name; this.data.age = age; }, log: function(){ console.log(this.data.name + ', ' + this.data.age); } }); var mark = new Person('Mark', 23); mark.log(); // 'Mark, 23' data.name = 'Joseph'; data.age = 22; mark.log(); // 'Mark, 23' console.log(data == Person.prototype.data); // false
This is another effect of using the implement
method. Properties declared for a class undergo a process called dereferencing, which removes any reference from the object. This is done by creating a copy of an object that's exactly like the original. For instance, both the data
and Person.prototype.data
objects above might look like the same object, but the latter is really just a copy. Instead of augmenting it directly into the prototype, a copy of data
was created and assigned to Person.prototype.data
. Any references to data
were removed, and changes to this object no longer affect the copy.
The dereferencing done by implement
is recursive: if an object contains another object, the inner object is also dereferenced. In an array of objects, for example, not only is a copy of an array created, but each object inside the array is also copied. However, dereferencing is limited to only some kinds of objects: arrays, objects created using literals or new Object()
, and objects created using user-defined constructors. Objects like functions, dates, and DOM objects aren't dereferenced since they're host objects, and primitives like strings and numbers don't need to be dereferenced because they're not reference types. For example, the date object won't be dereferenced in the following:
var date = new Date(); var Item = new Class({ date: date }); var obj = new Item(); console.log(obj.date == date); // true
Dereferencing, however, doesn't just happen on the class level: instances are also dereferenced. To understand why, we need to take a look at a variation of the original problem:
var Person = function(name, age){ this.data.name = name; this.data.age = age; };
Person.prototype.data = {name: '', age: 0}; Person.prototype.log = function(){ console.log(this.data.name + ', ' + this.data.age); }; var mark = new Person('Mark', 23); mark.log(); // 'Mark, 23' var joseph = new Person('Joseph', 22); joseph.log(); // 'Joseph, 22' mark.log(); // 'Joseph, 22' console.log(mark.data == joseph.data); // true
This is an even bigger problem: because both mark
and joseph
inherit from Person.prototype
, their data
properties both point to the same object. Therefore, any changes made to one instance also affect all other instances. This is what we'd expect from objects, and if you recall our discussion in the previous chapter, we solved this by moving the data
declaration into the constructor to give each instance its own separate data
property.
The MooTools class system, on the other hand, solves this automatically for us:
var Person = new Class({ data: {name: '', age: 0}, initialize: function(name, age){ this.data.name = name; this.data.age = age; }, log: function(){ console.log(this.data.name + ', ' + this.data.age); } }); var mark = new Person('Mark', 23); mark.log(); // 'Mark, 23' var joseph = new Person('Joseph', 22); joseph.log(); // 'Joseph, 22' mark.log(); // 'Mark, 23' console.log(mark.data == joseph.data); // false
The dereferencing here is no longer done by implement
, but by the common constructor. If you look back at the code for the common constructor in the section on Constructors and Initializers, you'll see that the first line is a function call: reset(this)
. The this
keyword in that code points to the newly created instance of the class, and the reset
function is the private dereferencing function for class instances.
When we called new Person()
, a new instance is created and then the Person
constructor is called. The first thing the constructor does is to call reset
to dereference the instance. This instance, of course, already inherits from Person.prototype
, so the properties declared in our prototype are copied and reassigned in the object to give each instance its own copy. This automated process ensures that all our instances have their own copies of the objects declared in the prototype.
Because classes are just abstractions of the native prototypal implementation, instances still inherit from the prototype of their constructors, and the prototype chain is still traversed to look for members. Setting up the prototype chain, though, is different with classes.
In the last chapter, we were able to make objects that inherit from multiple prototypes by creating deliberate chains:
var Animal = function(name){ this.name = name; }; Animal.prototype.eat = function(){ console.log('The ' + this.name + ' is eating.'), }; var Cat = function(){}; Cat.prototype = new Animal('cat'), Cat.prototype.meow = function(){ console.log('Meow!'), }; var cat = new Cat(); cat.eat(); // 'The cat is eating.' cat.meow(); // 'Meow!'
Here we created a Cat
constructor and then set its prototype to be an instance of Animal
. Because of how the prototype chain works, all instances of Cat
not only inherit from Cat.prototype
but also from Animal.prototype
. To set the deliberate chain, we had to assign an actual object as the prototype of the new constructor. This is the essence of prototypal inheritance: objects inherit from other objects.
Classical inheritance has a different view: objects—and classes themselves—inherit from classes. If we were to write the example above in a classical language, we would have to declare that the Cat
class inherits from the Animal
class. This inheritance is direct: the Cat
class won't be inheriting from an instance of the Animal
class, like in our JavaScript example, but from the Animal
class itself. In classical terms, we then say that the Cat
class is a subclass of the Animal
class, and the Animal
class is the superclass of the Cat
class.
The MooTools class system provides classical inheritance with a simple interface. When we implement the example above using classes, we get the following:
var Animal = new Class({ name: '', initialize: function(name){ this.name = name;
}, eat: function(){ console.log('The ' + this.name + ' is eating.'), } }); var Cat = new Class({ Extends: Animal, initialize: function(){ this.name = 'cat'; }, meow: function(){ console.log('Meow!'), } }); var cat = new Cat(); cat.eat(); // 'The cat is eating.' cat.meow(); // 'Meow!'
As you've probably guessed, the important line in this snippet is Extends: Animal. Extends
is one of the special keywords processed by the implement
method, and it's actually connected to a function that we'll talk about more in the next chapter. The important point to know right now is that this line declares that our Cat
class is a subclass of Animal
, and sets the prototype chain of the Cat
class to inherit from Animal
.
Extends, like other special keywords, is case-sensitive. Always remember to capitalize the first letter.
Unlike our prototypal example, we passed Animal
in our declaration instead of new Animal(). Extends
allows us to declare inheritance using classes themselves instead of instances of those classes. Aside from being easier to write, this has the added benefit of being easier to read: a quick glance at our Cat
class informs us that it's a subclass of Animal
because we can easily see the Extends
declaration.
The Extends
inheritance mechanism is another MooTools abstraction. Since we're still using prototypes beneath the class system, JavaScript expects our prototype chain to be declared using the prototypal model. Extends
does this automatically for us: when the implement
method sees the Extends
keyword, it creates a new instance of the class we declared and uses this new instance to set up the prototype chain.
In our example above, implement
goes through each of the items in our object literal to build the Cat
class. When it encounters Extends: Animal
, it creates a new instance of the class by instantiating new Animal()
. It then sets the prototype of the common constructor to the new Animal
instance and adds the other members of the class to the prototype. Finally, the Class
constructor returns the common constructor and it becomes the final Cat
class.
In our section on the common constructor, we found out that the class system distinguishes between instantiation and initialization. I also said that there are times we would want to create instances without initializing them. Extends
is one of those times: the instance of the superclass created via Extends
isn't initialized:
var Superclass = new Class({ initialize: function(name){ console.log('Initializing Superclass.'), } }); var Subclass = new Class({ Extends: Superclass, initialize: function(){ console.log('Initializing Subclass.'), } }); var obj = new Subclass(); // 'Initializing Subclass.'
In this code, we get only the 'Initializing Subclass'
log in our console. When Subclass
was created, an instance of Superclass
was created to become the value of Subclass.prototype
. But this instance of Superclass
wasn't initialized, which we confirm because there's no log in the console.
This behavior happens because of how initializers work. It's the job of the initializer to prepare an instance and to set it up in a way that's unique to the instance. Often, this is done using special arguments that are passed when creating an instance. Take a look at the following snippet:
var Postable = function(element){ this.element = element; this.element.method = 'POST'; };
When we create instances of Postable
, we are expected to pass an element
argument to the constructor. The instance is then initialized by setting the value of its element
member to the element
argument we passed, then a new property called method
is added to the member. A problem occurs, though, when we try to create a subclass:
var Postable = function(element){ this.element = element; this.element.method = 'POST'; }; var SubPostable = function(){}; SubPostable.prototype = new Postable(); // this will throw an error
To make SubPostable
a subclass of Postable
, we had to declare SubPostable.prototype
to be a new instance of Postable
. But since Postable
had a combined constructor and initializer, we get an error when we create the instance without passing an element
argument.
Of course, we can simply pass an argument when we create the new instance, but what argument should we pass? Remember that during the process of subclassing, we don't yet know which arguments are going to be passed—those items will be available when we instantiate the class. We could pass a dummy object, but doing so just to bypass an error isn't good programming.
On the other hand, we'll never run into such a problem with the MooTools class system because constructors and initializers are separate. Even though the class system creates an instance of Postable
and sets it as the prototype of Subpostable
, the initialized
method of the Postable
instance never gets called:
var Postable = new Class({ element: null, initialize: function(element){ this.element = element; this.element.method = 'POST'; } }); var SubPostable = new Class({ Extends: Postable });
To fully understand why MooTools does this, we need to think back to the prototype chain. We create deliberate chains in order to create extensions and variations of our original prototypes. An Animal prototype
, for instance, should contain members that are shared by all animals, such name, eat()
, and sleep()
. It could then be specialized to make Cat, Bird
, or Dog
prototypes, each with its own set of methods that are unique to cats, birds, or dogs. These specializations could be further divided into specific types of cats, birds, or dogs. The important thing to consider is that we're doing this in order to inherit members from all these classes without having to rewrite members again: an instance of Magpie
, for example, might inherit properties and methods from Magpie.prototype, Bird.prototype
and Animal.prototype
.
JavaScript's object implementation requires us to declare a prototype
as an instance of another prototype
in order to create a deliberate chain. Unfortunately, the use of constructor functions as initializers in the prototypal model works against us, since the processes of instantiation and initialization are merged into one function. If the only reason we set the prototype to an instance of another prototype is to inherit members, we shouldn't need to initialize the instance into a usable state, because we're not going to use it except for traversal.
This concern is handled properly by the MooTools class system: because we only need to instantiate the superclass in order to access its properties and methods for our subclass, the superclass is instantiated without being initialized. When we need to finally initialize our object, we can do so in the subclass itself without having to worry about how the superclass handles initialization.
The prototype chain is invisible: there's no direct way to check whether a prototype inherits from another object. MooTools classes, on the other hand, have visible links to their superclass, which is called parent
:
var Animal = new Class({ name: '',
initialize: function(name){ this.name = name; } }); var Cat = new Class({ Extends: Animal, initialize: function(){ this.name = 'cat'; } }); console.log(typeof Animal.parent); // 'undefined' console.log(typeof Cat.parent); // 'function' console.log(Cat.parent == Animal); // true
The parent
property of a class is a reference to its superclass. In our example, Animal.parent
is undefined
, since it doesn't have a superclass, but Cat.parent
correctly points to Animal
, since Cat
is a subclass of Animal
. While the parent
property of classes doesn't have much use in development, there's another parent
that is important. Before we talk about it, though, let's tackle a problem that arises with inheritance: how to access overridden methods.
When we look back at how the prototype chain is traversed, we remember that each object in the chain is checked for the property being accessed. If a property isn't found in the first item in the chain, the next item is checked—and so on until we find the property.
var Animal = function(){}; Animal.prototype.log = function(){ console.log('This is an Animal.'), }; Animal.prototype.eat = function(){ console.log('The animal is eating.'), }; var Cat = function(){}; Cat.prototype = new Animal(); Cat.prototype.eat = function(){ console.log('The cat is eating.'), }; var tibbs = new Cat(); tibbs.log(); // 'This is an Animal.' tibbs.eat(); // 'The cat is eating.'
In this example, tibbs.log()
refers to Animal.prototype.log
. Because we didn't declare Cat.prototype.log
, the original log
method from our Animal.prototype
is the one inherited by our Cat
instances. However, we specified our own Cat.prototype.eat
method, and because of the way the prototype chain is traversed, our Cat
instances inherit this method instead of Animal.prototype.eat
. We therefore say that Cat.prototype.eat
overrides Animal.prototype.eat
.
Overridden methods are very straightforward as long as you're not doing anything fancy. However, one problem that often comes up is how to access the overridden methods of a superclass. Because you won't be able to access the scope chain directly, you'll have to call the overridden method from the superclass directly via its prototype:
var Animal = function(){}; Animal.prototype.log = function(){ console.log('This is an Animal.'), }; Animal.prototype.eat = function(){ console.log('The animal is eating.'), }; var Cat = function(){}; Cat.prototype = new Animal(); Cat.prototype.eat = function(){ Animal.prototype.eat.apply(this); console.log('The cat is eating.'), }; var tibbs = new Cat(); tibbs.log(); // 'This is an Animal.' tibbs.eat(); // 'The animal is eating.' // 'The cat is eating.'
Here, we called the overridden eat
method of Animal.prototype
by directly invoking it using Animal.prototype.eat.apply(this)
. We need to use apply
or call
when accessing the overridden method because we want the function to be bound to the current instance.
MooTools classes use a more elegant approach. All subclasses are given a new method called parent
, which can be used to call the overridden method:
var Animal = new Class({ log: function(){ console.log('This is an Animal.'), }, eat: function(){ console.log('The animal is eating.'), } }); var Cat = new Class({ Extends: Animal, eat: function(){
this.parent(); console.log('The cat is eating.'), } }); var tibbs = new Cat(); tibbs.log(); // 'This is an Animal.' tibbs.eat(); // 'The animal is eating.' // 'The cat is eating.'
If you look at our eat
method in the Cat
class, we called the parent
method to call the overridden eat
method of the Animal
superclass. Like our original prototypal example, the call to this.parent()
inside the eat
method calls Animal.prototype.eat
. Unlike our prototypal example, though, we no longer need to use apply
or call
, since the parent
method already does this for us.
The parent
method becomes really handy when used in conjunction with overridden methods that do lots of processing, like initializers:
var Animal = new Class({ name: '', initialize: function(name){ this.name = name; }, getName: function(){ return this.name; } }); var Cat = new Class({ Extends: Animal, initialize: function(name){ this.parent(name); this.name += ', the Cat.'; } }); var tibbs = new Cat('Tobias'), console.log(tibbs.getName()); // 'Tobias, the Cat.'
In this example, we first call the initialize
method of the Animal
class using this.parent
, passing the necessary argument, before doing our own processing, which involves adding the string ', the Cat.'
to the name
property. In essence, the parent
method not only makes it possible to call the overridden method, but also provides a way to "extend" it with our own property.
The parent
method is available only to subclasses that inherit through the Extends
property. If we tried to call this.parent()
inside the Animal
class, for instance, we'd get an error about not having a parent
method.
The parent
method itself is an interesting function, and it's one of the most ingenious parts of the MooTools codebase. If you look at the source, you'll see that the parent
method looks like this:
function(){ if (!this.$caller) throw new Error('The method "parent" cannot be called.'), var name = this.$caller.$name, parent = this.$caller.$owner.parent; var previous = (parent) ? parent.prototype[name] : null; if (!previous) throw new Error('The method "' + name + '" has no parent.'), return previous.apply(this, arguments); }
There's only one parent
method in a subclass, but it changes depending on where it was called. We don't really need to learn how it works, but it's an interesting topic that will help us understand exactly how MooTools solves the problem of overridden methods. But before we can fully understand how the parent
method works, we have to retrace a few steps.
Remember that before adding methods to the prototype, implement
decorates the functions using the wrapper function we saw earlier. The wrapper function has several special properties, the most important ones being $owner
and $name
:
var Animal = new Class({ name: '', initialize: function(name){ this.name = name; }, getName: function(){ return this.name; } }); console.log(Animal.prototype.getName.$owner == Animal); // true console.log(Animal.prototype.getName.$name); // 'getName'
The $owner
property points to the class itself, while $name
is the name of the method you declared. In our example, Animal.prototype.getName.$owner
points to the Animal class, while Animal.prototype.getName.$name
has the value 'getName'
. Take note that the $name
property isn't taken from the identifier of the function itself, but from the key we used when we declared the method.
The main purpose of the wrapper function is to keep track of which method is currently called. When we call a method from an instance, the wrapped method sets the internal $caller
property of the instance to itself. The $caller
property is used to determine which current function is running. When we call tibbs.getName()
for example, the $caller
property will be set to the getName
method.
Now when we call this.parent()
inside a method, the function checks the $caller
property to determine which of the methods invoked it. If there's no $caller
property, it will throw an error, preventing parent
from being called from outside a method. If there's a $caller
, though, the parent method will store its $name
property to determine the name of the method and then access the superclass using $owner.parent
. The method then checks whether there's an overridden method from the superclass and invokes it, returning the arguments.
Let's trace how it all works with this example:
var Animal = new Class({ name: '', initialize: function(name){ this.name = name; }, getName: function(){ return this.name; } }); var Cat = new Class({ Extends: Animal, initialize: function(name){ this.parent(name); this.name += ', the Cat.'; } }); var tibbs = new Cat('Tobias'), console.log(tibbs.getName()); // 'Tobias, the Cat.'
First, we call new Cat()
, thereby calling the initialize method. The $caller
property is then set to initialize, with $caller.$owner
pointing to the Cat
class and $caller.$name
set to 'initialize'
. We then call this.parent()
inside the initialize method, and it gets the superclass by accessing $caller.$owner.parent
, which points to Animal
. Finally, this.parent()
checks for the existence of the initialize
method in the Animal
class and invokes it, using apply
to rebind it to the current instance. The result is that we were able to invoke an overridden method from our superclass.
It might be hard to wrap you head around at first, but once you understand how parent
works, you'll realize how elegantly MooTools solves complex problems to provide a nice API.
JavaScript's inheritance model allows for only single direct inheritance. An object directly inherits only from a single prototype, although it can indirectly inherit from multiple prototypes via the prototype chain.
Using the prototype chain for multiple inheritance, though, has its limitations. Take, for example, the following snippet:
var Phone = new Class({ sound: 'phone.ogg', initialize: function(number){ this.number = number; }, call: function(from){
this.ring(); new Notification('New call!'), }, ring: function(sound){ sound = sound || this.sound; new Sound(sound).play(); } }); var AlarmClock = new Class({ sound: 'alarm.ogg', initialize: function(alarmTime){ this.time = alarmTime; }, alarm: function(time){ if (time == this.time) { this.ring(); new Notification('Wake up sleepy head!'), } }, ring: function(sound){ sound = sound || this.sound; new Sound(sound).play(); } });
Here we have two classes, Phone
and AlarmClock
, and both have the same method called ring
that plays a sound. This is considered bad design because we repeat the code for the ring
method in both classes. When developing applications, we want to reuse code as much as possible.
We can, of course, simply make a new superclass that will be inherited by Phone
and AlarmClock
. But direct inheritance isn't a good design choice here: a phone and an alarm clock aren't really variations of the same object, so a RingingObject
class is out of the question.
The MooTools class system supplies a very good system for code reuse:
var Ringer = new Class({ sound: 'ring.ogg', ring: function(sound){ sound = sound || this.sound; new Sound(sound).play(); } }); var Phone = new Class({
Implements: Ringer, initialize: function(number){ this.number = number; this.sound = 'phone.ogg'; }, call: function(from){ this.ring(); new Notification('New call!'), } }); var AlarmClock = new Class({ Implements: Ringer, initialize: function(alarmTime){ this.time = alarmTime; this.sound = 'alarm.ogg'; }, alarm: function(time){ if (time == this.time) { this.ring(); new Notification('Wake up sleepy head!'), } } });
Here, we declare a new class called Ringer
, and we transfer the sound
and ring
members to this new class. But instead of declaring them to be subclasses of this new class, we use the Implements
keyword to declare that the Phone
and AlarmClock
classes should take the properties and methods of the Ringer
class and use them.
The Ringer
class is an example of a mixin. As its name implies, a mixin is a class that's "mixed in" or combined with another class. Mixins present a form of multiple inheritance, and the MooTools class system makes it easy to create and implement your own mixin.
The Implements
keyword is used to add mixins to your class. It can either take a single value, which is a name of the class you'd like to combine, or an array of class names:
var Ringer = new Class({ sound: 'ring.ogg', ring: function(sound){ sound = sound || this.sound; new Sound(sound).play(); } }); var Charger = new Class({
charge: function(){ new ElectricalSocket().connect(this); } }); var Phone = new Class({ Implements: [Ringer, Charger], initialize: function(number){ this.number = number; this.sound = 'phone.ogg'; }, call: function(from){ this.ring(); new Notification('New call!'), } }); var phone = new Phone(0000); console.log(typeof phone.ring); // 'function' console.log(typeof phone.charge); // 'function'
Mixins are actual classes, but they're usually written without an initialize
method since they're not meant to be used like regular classes. Rather, mixins are considered to be collections of properties and methods that will be added to other classes, and they provide a simple way to share and reuse code among many classes.
The Implements
keyword should give you a clue as to how mixins are added to classes. When the implement
method sees this keyword, it creates new instances of these classes that are then fed to another implement
call. This process adds the members of the instance that are inherited from the mixin class to the class itself. Without using the Implements
keyword, we could mimic this process by using implement
directly:
var Ringer = new Class({ sound: 'ring.ogg', ring: function(sound){ sound = sound || this.sound; new Sound(sound).play(); } }); var Phone = new Class({ initialize: function(number){ this.number = number; this.sound = 'phone.ogg'; },
call: function(from){ this.ring(); new Notification('New call!' + this.from); } }); Phone.implement(new Ringer);
This snippet shows exactly how mixins were done in older versions of MooTools. You shouldn't do this with the current versions, though, since it's verbose and hard to read. You should declare your mixins using Implements
in your class declaration for readability.
One cool thing about mixins, though, is that the parent
method respects their previous prototype chain rather than their new one. What this means is that when the parent
method is called from within a method in a mixin that has been added to a class, the method will call the corrresponding overriden method in the mixin's original prototype chain:
var MixinSuper = new Class({ log: function(){ console.log('MixinSuper'), } }); var Mixin = new Class({ Extends: MixinSuper log: function(){ this.parent(); } }); var Super = new Class({ log: function(){ console.log('Super'), } }); var Sub = new Class({ Extends: Super, Implements: Mixin }); new Sub().log(); // 'MixinSuper'
Unlike direct inheritance using Extends
, mixins aren't linked to the prototype chain. Instead, their members are added directly to the prototype of your class. This means that changes to a mixin after it's been implemented into your class aren't inherited:
var Ringer = new Class({ sound: 'ring.ogg', ring: function(sound){ sound = sound || this.sound; new Sound(sound).play(); } }); var Phone = new Class({ Implements: Ringer, initialize: function(number){ this.number = number; this.sound = 'phone.ogg'; }, call: function(from){ this.ring(); new Notification('New call!'), } }); var phone = new Phone(0000); console.log(typeof phone.ring); // 'function' Ringer.implement('ringLoudly', function(sound){ sound = sound || this.sound; new Sound(sound).playLoudly() }); console.log(typeof phone.ringLoudly); // 'undefined'
Moreover, you can't call overridden methods via the parent
method. The only way to call overridden mixin methods is to call them directly from your mixin's prototype, as we did in an example earlier.
In this chapter, we learned a lot about classical programming and how it differs from the prototypal model. We learned about classes, their similarities to prototypes, and the differences between the two. We also learned about the MooTools class system and how it provides a classical interface to JavaScript's object implementation. We found out how MooTools separates constructors from initializers, and how it encapsulates working with prototypes via the implement
method. We dived into the internals of inheritance with classes, the parent
method, and how to do multiple inheritance with mixins.
If you think we're done talking about classes, though, you're in for a surprise. The MooTools class system is as elegant as it is complex, and we've only touched the base of it in this chapter. In the next chapter, we'll learn more about the secrets of MooTools classes, as well as how to do complex programming using them.
So if you're ready, strap on your backpack and tie your shoelaces—it's time to trek deeper into the MooTools class system.