The class system we've been studying is one half of a pair of systems that MooTools implements. The other half, the MooTools type system, is very similar to the class system, but it focuses more on data as values and the various forms that values take.
Unlike the native object implementation and classes we discussed in separate chapters, the native JavaScript type system and the MooTools type system will both be tackled in this chapter. The two type systems are deeply intertwined, and discussing them separately wouldn't really aid our understanding of their similarities and differences.
With that in mind, let's talk about types.
At the heart of programming is the manipulation of data. Whether we're writing a spreadsheet program to process numbers, a complex algorithm to animate images on a web page, or a backend system to keep track of user information, we're still working with data and its representations.
In our programs, we express our data in terms of values. The following, for example, are all valid JavaScript values:
42 true 'hello' function square(x){ return x * x; }; {name: 'mark'}
The first value is a number, the second is the boolean value true, the third, a string spelling the word "hello", the fourth, a function named square, and, finally, a simple object. While all of these values represent data, they don't represent the same kind of data. Each value can be differentiated from the others by the kind of data it represents.
We distinguish the values in terms of their types. We can use the JavaScript typeof
operator to determine the type of a value:
console.log(typeof 42); // 'number' console.log(typeof true); // 'boolean' console.log(typeof 'hello'), // 'string' console.log(typeof function(){}); // 'function' console.log(typeof {name: 'mark'}); // 'object'
The type of a value determines not only what kind of data it represents, but also what operations can be performed on it. Two Number values, for instance, can be added together to obtain their sum. However, you can't directly add a Number value to an Object value. In the same sense, you can't invoke a String or get the substring of a Function.
We're going to adopt the ECMAScript specification's standard of using capitalized type names. Thus, when you see "Object," we are referring to the Object type, in contrast to "object," which could mean any object. In the same way, we have Function versus function, Number versus number, and String versus string.
The types that values can take, as well as the operations that can be performed on particular types, are determined by the language's type system. The type system also provides ways to determine the type of a value and ways to convert a value from one type to another.
JavaScript uses a type-by-specification system: the type of a particular value is defined directly by the language specification. The result of the application of the typeof
operator to a value is governed by specific rules defined by the ECMAScript specification, and the interpreter uses these rules to determine the type of any value.
JavaScript's type system is, unfortunately, quirky. It might appear fine from the example above, but it's skewed at best when used in practical situations. To illustrate, let's use the typeof
operator to determine the types of some other values:
console.log(typeof {name: 'mark'}); // 'object' console.log(typeof [1, 2, 3]); // 'object' console.log(typeof new Date()); // 'object'
Here we have three values: a basic object, an array, and a date object. These three values have different structures and uses, so we'd expect them to be different types. However, this is not the case; all these values are considered to be of the Object type in JavaScript.
JavaScript, at its core, is an object-based language. Most values in JavaScript are objects—and even the values that are not objects—like strings and numbers—are turned into objects when needed (more on this later). Weirdly enough, JavaScript's type system thinks that since all values are objects, they should all be grouped under the same type: Object. So an array or a date object, for example, is of the Object type in JavaScript, even if it's a special kind of object.
This is technically "logical," of course: since arrays and dates are objects, their type would be Object. But while this is okay from a semantic point of view, it becomes totally useless—and, often, counterproductive—for practical purposes. For us to be able to properly manipulate values, we need to be able to differentiate between the values we're working with. It's okay to think of everything as an object, but we also need a way to determine what kind of object it is exactly.
JavaScript's type system doesn't help in this area. All objects in JavaScript are of the Object type. The only exception is functions, which JavaScript distinguishes as the Function type, but that's just because these objects have the unique ability to be invoked. Any other object—regardless of its structure, use, or mode of creation—is of the Object type.
There is, however, an alternative. In the previous chapter we saw how objects are created using classes. We learned that a class is a special language construct that serves as a blueprint for creating objects by defining the structure of these objects in terms of properties and methods. When we instantiate a class, we create a new object that's structured according to the definition we put in our class.
Classes can actually be used as an alternative type system: a type-by-class system. Classes define the structure of objects and what operations can be done on these objects, effectively differentiating objects in terms of structure and use. An instance of the Car
class, for example, is a different kind of object when compared to an instance of the Animal
class. This is analogous to how types are determined: a Car
value (or object) is different from an Animal
value and it's their types (or classes) that determine what kind of value they are and what operations can be performed on them.
In fact, this type-by-class system is used as the native type system in many classical languages, like Ruby. As with JavaScript, every value in Ruby is an object instance of a class. The class and the type of an object are therefore the same thing in Ruby, and classes themselves are treated as types.
Interestingly, JavaScript also has an internal facility for a type-by-class system. Though there is no native class construct in JavaScript, the term "class" is used internally by the language to signify the type of an object. All JavaScript objects have an internal class
property, which is a string containing the name of the object's type. Unfortunately, this property is only used internally by the interpreter for its type-based operations, and is not actually used in the language-accessible type system, which makes it unusable for our current purpose. To really implement a proper type system, we must do it ourselves by extending the language.
Let's recall what we know about JavaScript objects. We know that all objects are created using a constructor function and these inherit from the prototype
properties of these constructors. Like classes, constructor functions and their prototypes define the structure of objects through properties and methods. Since every possible JavaScript value (with the exception of null
and undefined
) has its own constructor, constructors can be used to implement a type-by-class system in JavaScript. Native constructors like Object, Array
, or Date
would become "classes" in this system, and the instances of these constructors would be typed properly according to the particular constructor.
MooTools classes seem like good candidates to use in a type-by-class system, since they're just abstractions on the native constructor-prototype pair. We could simply turn native constructors like Object
into classes and use the same type-by-class system implemented in languages like Ruby. Unfortunately, this is impossible to do because of how the MooTools class system is implemented.
To illustrate, let's say we want to create a new Animal
class. To do this, we pass an object that defines the structure of the class as an argument to the Class
constructor. The Class
constructor then returns a new Animal
class. This Animal
class is actually an abstracted constructor function created internally and returned by the Class
constructor. It is the job of the Class
constructor to create the constructor function that will become the class object. Therefore, a particular constructor function can only be considered a class object if it was created using Class
.
The problem is that constructors like Object
or Array
are built-in objects; they're implemented directly by the JavaScript engine using another language, usually C or C++, and their implementation details aren't available via the JavaScript interface. Because only the Class
constructor can create the abstracted constructor function that would become our class object, we'll have to replace the native implementation of these built-in constructors with our own if we want to turn them into classes. In other words, to turn built-in constructors into classes, we have to destroy them first.
Because of this limitation, MooTools doesn't use classes to implement its type system. Instead, it introduces a new class-like construct called a type object, which is created using the Type
constructor.
Like a class, a type object is an abstracted constructor function that has additional properties and methods that make it easier to manipulate the underlying prototype
object. But unlike a class, the actual constructor function that becomes the type object isn't created inside the Type
constructor. Rather, type objects are simply existing constructor functions that are transformed by the Type
constructor into type objects. Any existing constructor can therefore be turned into a type object—which makes it possible to use built-in constructors without replacing them.
The native Array
constructor, for example, is turned into a type object simply by passing it to the Type
constructor. The Type
constructor doesn't create a new constructor function to replace the native Array
constructor. Instead, it turns the original Array
constructor function into a type object by augmenting it with new properties and methods. Thus, the built-in Array
constructor and the MooTools Array
type object are one and the same. Nothing was replaced or destroyed. In fact, most built-in constructor functions are turned into type objects automatically when MooTools loads.
Type objects perform two main functions. First, a type object is a representation of the actual kind of value in terms of what makes it different from all other values. For example, the Array
type object represents all values that are ordered lists of data, while the Function
type object represents all values that are separate units of code that can be invoked.
Second, a type object also functions as a "class" (in the object-oriented sense of the word) for the particular type, which means that the structure and behavior of the values of that type are controlled by the type object. Because the type object is a constructor function, all instances of this type inherit from its prototype
property. The type object can therefore be used to modify or augment the structure of all values under that type.
The MooTools type system abstracts part of the native type system in the same way that the class system abstracts the native constructor-prototype model. But unlike the class system, the MooTools type system is meant to be a direct replacement—not just a simple alternative. The easiest way to see the type system in action is by using the typeOf
function:
console.log( typeOf(42) ); // 'number' console.log( typeOf(true) ); // 'boolean' console.log( typeOf('hello') ); // 'string' console.log( typeOf(function (){}) ); // 'function' console.log( typeOf({name: 'mark'}) ); // 'object' console.log( typeOf([1, 2, 3]) ); // 'array' console.log( typeOf(new Date()) ); // 'date' console.log( typeOf(new Class()) ); // 'class'
The typeOf
function mimics the native typeof
operator in the MooTools type system. Like its native typeof
counterpart, the typeOf
function gives us the type of a value as a string. Unlike the typeof
operator, though, the typeOf
function knows how to properly differentiate among objects. You'll see that arrays, dates, and classes don't return 'object'
as their type, but rather their types as defined by their type objects.
typeof
is a JavaScript operator, like the addition operator +
or the instanceof
operator, so you don't need parentheses when using it. The typeOf
function, however, is a true function, so you have to invoke it using the invocation operator (aka, parentheses). Also, take note of the "camelCase" name: typeOf()
, not typeof()
.
An important point to note is that since MooTools introduces a separate construct for its type system, classes are not used to determine the type of an object. The separation of classes and types means that objects created using classes will have both a class and a type:
// a class var Person = new Class(); // an instance var mark = new Person(); console.log(mark instanceof Person); // true console.log(typeOf(mark)); // 'object'
In this example, mark
isn't of the Person
type but rather of the Object
type, even if its class is Person
. In the MooTools system, objects created from MooTools classes always have Object
as their type and only objects created from the actual MooTools type constructors are given a different type.
While this is indeed a limitation, it's not such a big problem compared to the limitations of the native type system. Take, for instance, arrays and regular expressions: both of these are vastly different kinds of objects, but the native type system marks both as Object
types—and in the case of regular expressions, they could even be typed as Functions
in some implementations! We might not be able to distinguish among the class-defined object types with the MooTools type system, but at least we're able to tell whether an object is really a regular expression or a function.
Besides, the MooTools type system is able to distinguish among all native object types, which means that the only objects that will return 'object'
when passed to the typeOf
function are plain objects and objects created using classes. So it becomes much easier to guess what kind of object we're dealing with. Combined with our knowledge of the actual classes we use in our programs, the limitation of the class system becomes almost nonexistent.
Before we actually discuss the internals of the MooTools type system, we need to familiarize ourselves with the native types and the values they take. If you're one of those people who have already memorized the ECMAScript language specification, feel free to skip this part. But if you only know the basics of JavaScript types, keep on reading because much of what follows discusses the internals of the types themselves, rather than their basic use.
The ECMAScript specification defines six main types: Undefined, Null, Boolean, String, Number, and Object. We could further classify these types into three groups:
Existence Types are special types whose values represent the condition of not having values. Null and Undefined are both existence types.
Primitive Types (or "primitives" for short) represent the simplest immutable (or unchangeable) values. Strings, numbers, and booleans are all primitives.
Composite Types represent compound values whose structures and behavior can be changed. All objects (including functions) are composite types in JavaScript.
All JavaScript values are typed as one of these native types, and we'll examine these native types and values as well as their corresponding MooTools type objects in the following sections. We'll also learn the value a particular type takes when it's converted to another value through the process of type casting.
The Null and Undefined types are the only existence types in JavaScript. These types and their values are special because they don't represent data, but instead stand for the absence of data.
The ECMAScript language specification categorizes the Null and Undefined types as primitive types. However, I put them in their own group since they behave differently from other primitive values.
The Null type has only one value, which is represented by the null
keyword. The null
value denotes the absence of a value: assigning null
to a variable means that the variable does not have an actual value. Thus, the Null value is like a placeholder value in cases where there's no value to use.
Like the Null type, the Undefined type has only one value, which is represented by the undefined
variable. The undefined
value is used to represent the value of any variable that has been declared but not defined:
// declare a variable var thing; console.log(typeof thing); // 'undefined' console.log(thing === undefined); // true // define the value thing = 1; console.log(typeof thing); // 'number' console.log(thing === undefined); // false
Remember that in Chapter 2 we learned that variable declaration and variable definition are executed by JavaScript as two distinct steps. When a variable is declared but not given a value, its value is automatically set to undefined
. The undefined
value is also used as the value of object members that don't exist, as well as the value of a function's formal parameters with no corresponding argument passed during invocation.
One important thing to consider with the Undefined type is that its only representation—the undefined
variable—is a variable, not a keyword. This means that the actual value of the undefined
variable can be changed:
// declare a variable var thing; // change the value of undefined undefined = 'nooo!'; console.log(thing === undefined); // false
The ECMAScript committee's decision to make undefined
a variable was an unfortunate choice that's been resolved in ECMAScript 5 by making the variable read-only. However, you should still be careful not to change the value of the undefined
variable since ECMAScript 3 is still widely used, and doing so would create havoc in your programs.
The null
and undefined
values can be transformed into primitives through type casting—and we'll see how this is done in a later section. For now, let's just remember that the null
value is cast into the number value 0
, the string value 'null'
and the boolean value false
. Meanwhile, the undefined
value is cast into the number value NaN
, the string value 'undefined'
and the boolean value false
.
Null and Undefined also have the distinction of being the only native types with no constructor functions. Therefore, they have no corresponding MooTools type object. Any variable that has null
or undefined
for its value will have no properties or methods, and trying to access a member of a null
or undefined
value will result in an error.
One important thing to note here is that MooTools treats the Undefined value the same as the Null value, which means that typeOf(undefined)
will return 'null'
instead of 'undefined'
. This is a simplification on the part of MooTools, since both values represent the same state of not having an actual value. Therefore, in order to check for undefined
in MooTools, you'll have to use strict comparison, such as value === undefined
.
The three primitive types—Number, String, and Boolean—represent the simplest of all possible values in JavaScript. They're the most basic types of data in terms of structure, containing only actual values without any properties or methods.
That last statement might be a bit surprising. In JavaScript, only objects have properties and methods, while primitives like strings and numbers don't—although they sometimes appear as if they do. We'll get around to that in a bit.
Primitive values are immutable, which means that they can't be modified. When a primitive value is stored in a variable or passed as an argument to a function, a new copy of the value is created—giving them their other name, value types.
The simplest primitive type is the Boolean type, which has only two possible values: true and false; these are represented in code by the true
and false
literals. Booleans represent the truth value of an expression. For instance, 4 == 4
yields true
because the number 4 is indeed equal to itself, while (2 + 2) == 5
yields false
since the correct answer should be 4. When cast to number values, true
and false
yield 1
and 0
, respectively, and when cast to strings, they become 'true'
and 'false'
.
The Number type, on the other hand, represents numeric values. A number value is represented in code using number literals, which are simply numeric characters written directly in the JavaScript program. Also within the Number type are two special values represented by the variables Infinity
and NaN
.
The Infinity
variable represents the value of numeric infinity—in other words, a really, really large number (mindblowing, in fact). The NaN
, meanwhile, represents the special not-a-number value, which is the result of an impossible mathematical operation—like squaring a negative number. NaN
also represents any non-numeric value cast into the Number type. You can check whether a number value is NaN
using the built-in isNaN
function, and whether a value is not Infinity
using the isFinite
function.
Like undefined, Infinity
and NaN
are variables, so you'll also have to be careful not to overwrite their default values. These variables have also been declared as read-only in ECMAScript 5.
All number values are cast to the boolean true
except for 0
and NaN
, which are cast to the boolean false
. When cast to strings, number literals are simply turned into their string literal representation—like 42
to '42'
—while NaN
and Infinity
are cast to 'NaN'
and 'Infinity'
.
Finally, the String type represents values that are sequences of characters—or, to put it simply, string values are representations of textual data. Strings are created using string literals, which are pairs of single or double quotes (i.e., "or") surrounding one or more characters. Each character in a string is given a numeric index that starts with 0, and strings with no characters are called empty strings.
Empty strings are cast to the boolean value false
, non-empty strings to true
. Casting strings to number values is a little more complex. An empty string is always cast to the number value 0
, while non-empty strings are checked first before being cast. If the characters in the string represent a valid number value, then the string is cast to the actual number value of the characters, otherwise the string is cast to the number value NaN
. So the string '24.50'
is cast to the number value 24.50
, but the string 'hello'
will be cast to NaN
.
A unique thing about JavaScript's primitive types is that they can behave like objects. To give you an example, let's take a look at how the substring of a string value can be retrieved:
console.log('Hello World!'.substring(0, 4)); // 'Hello'
Here, we have a string value "Hello World!"
created through a literal. We then invoked the substring
method, which takes two arguments—from
and to
—that represent the numeric indices for the beginning and the end of the substring. We passed 0
and 4
, telling the substring
method to take the first up to the fifth character in the string and return it. The result is a new string, 'Hello'
.
Remember the object method called hasOwnProperty
? We said that all objects inherit a hasOwnProperty
method from Object.prototype
, and strangely enough, even primitives have them:
console.log( typeOf('M!'.hasOwnProperty) ); // 'function' console.log( typeOf((42).hasOwnProperty) ); // 'function' console.log( typeOf(true.hasOwnProperty) ); // 'function'
So strings, numbers, and booleans all inherit the hasOwnProperty
method. But inheriting properties and method from Object.prototype
is a characteristic of objects, not primitives. Does this mean that strings, numbers, and booleans aren't really primitive values but are objects?
That's not the case, of course. Earlier in the chapter, we saw that the typeof
operator returns the proper types for primitive values. If these values were objects, typeof
would have returned 'object'
and not 'string', 'number'
, and 'boolean'
. This was clearly not the case.
But if they're truly primitive values, why do they behave like objects? The answer lies in JavaScript's use of wrapper objects. For each primitive type, JavaScript has a corresponding "object version" of that type, which is simply an object representation of a primitive value. Thus, we have three constructors that represent primitive types: String, Number
, and Boolean
.
The instances of these three constructors are objects, not primitive values, and we can confirm this using the native typeof
operator:
console.log( typeof 'hello' ); // 'string' console.log( typeof new String('hello') ); // 'object' console.log( typeof 42 ); // 'number' console.log( typeof new Number(42) ); // 'object' console.log( typeof false ); // 'boolean' console.log( typeof new Boolean(false) ); // 'object'
The name "wrapper object" comes from the use of these constructors. When the JavaScript interpreter sees a primitive value in an expression that requires an object, it creates a wrapper object that is used as an object representation of the primitive value. The following is what actually happens to the string example above:
console.log(new String('Hello World').substring(0, 4)); // 'Hello'
The string literal "Hello World"
is first turned into a string object by passing the original value to the String
constructor. The substring
method of String.prototype
is then invoked, and a new string primitive is returned as a result of the method call.
The process of turning primitives into objects is done automatically by the JavaScript interpreter, and the process is repeated for every operation. When the operation is done, the wrapper object is discarded and the original primitive value is restored. Since the wrapper object is created and destroyed for each property access operation, any property added to the wrapper object isn't persisted, and this preserves the immutability of the primitive value:
var str = 'Hello World'; str.type = 'greeting'; console.log(str.type); // undefined
Here we stored the string value "Hello World"
to our variable str
. When the second line is executed, a new string object wrapper is created to wrap the value of str
, before adding a new property called type
to the object. Because this wrapper object is destroyed right after the operation, accessing str.type
on the last line produces undefined
. Thus, the original string value remains unchanged.
You can, however, persist changes by explicitly converting a primitive into an object:
var str = new String('Hello World'), str.type = 'greeting'; console.log(str.type); // 'greeting'
You don't have to worry about the performance implications of this wrapping process. Even if wrapper objects are created and destroyed for each property access, all modern JavaScript implementations handle the process efficiently.
Because of this automated wrapping process, primitive types can be used just like normal objects. The existence of constructor functions for primitive types also makes it possible to add new properties and methods that will be "inherited" by primitive values. Primitive types are represented in the MooTools type system by the type objects Boolean, Number
, and String
.
As a final note before we move on, be reminded that numeric literals need to be enclosed in a pair of parentheses when they are used in property-access operations, like (42).hasOwnProperty()
. Forgetting to do so—like 42.hasOwnProperty()
—results in a syntax error because the parser will think you're declaring a floating point literal.
JavaScript has only one composite type: the Object type. However, it can be divided into subtypes that define the special kinds of objects, such as arrays or dates. Unlike the native type system, the MooTools type system differentiates between objects according to these subtypes.
We already talked about objects in detail before, so we won't go into the internals of JavaScript's object implementation here. We know that objects are aggregate values composed of an unordered collection of key-value pairs. We also know that all objects are associated with a constructor function and they inherit from the prototype
property of their respective constructor.
Unlike primitives, objects are mutable: their structure and their behavior can be changed. When stored in variables or passed as arguments, new copies of the object aren't created. Instead, a reference to the original object is passed, effectively "sharing" the same object in all operations. Because of this behavior, objects are also called reference types.
The ECMAScript specification divides objects into two kinds:
Native objects (also called built-in objects) are objects provided by the language and defined directly by the ECMAScript specification. All complying ECMAScript implementations provide the same set of native objects. Built-in constructor functions and their prototypes, the Math
object, and native functions like eval
are all examples of native objects.
Host objects are objects provided by the host environment (i.e., the interpreter) for the purpose of correctly executing an ECMAScript program. They are independently defined by a particular JavaScript implementation and are therefore "non-standard" when viewed against the ECMAScript specification. The DOM objects of a browser, the Module functions of a CommonJS engine, and the additional objects created by a JavaScript implementation are examples of host objects. By virtue of their being a language extension rather than a simple framework or library, we'll consider the objects provided by MooTools as host objects.
As an aside, the language we know as "JavaScript" is technically the ECMAScript language plus a standard set of browser-specific host objects. Thus, all JavaScript implementations are ECMAScript implementations—but not all ECMAScript implementations are JavaScript implementations.
We have already seen some of these native and host objects, like functions, objects, and classes, in the previous chapters. We'll discuss most of the browser-specific host objects in the second part of this book, and the CommonJS host objects in part three.
Casting objects to primitive types follows different rules depending on the kind of object being converted.
The easiest rule is object-to-boolean value casting: all objects are cast to the boolean value true
. You should take note of this since even a boolean object created using new Boolean(false)
is cast to true
. Its internal value might be false
, but it's still cast to true
since all objects are truthy.
When casting objects to strings, the toString
method of the object is called. All objects inherit a basic toString
method from Object.prototype
, which returns a string in the form '[object <Class>]'. <Class>
in this form refers to the internal class property of the object, so a basic object will return '[object Object]'
while an array will return '[object Array]'
.
However, most native objects override the toString
method with their own implementation that usually returns a more appropriate string value. The primitive wrapper objects, for instance, have toString
methods that return the string representations of their primitive values as described above.
To cast objects to numbers, JavaScript uses another object method called valueOf
. If the return value of this method is a number, or if it can be cast into any number that is not NaN
, then this value is used as the numeric value of the object. Otherwise, the toString
method of the object is called and the return value of that method is again cast into a number. If the return value of toString
is still not a number value, the object is cast to NaN
.
The default valueOf
method inherited from Object.prototype
returns the object itself, which means that all objects are cast to NaN
by default. Primitive wrapper objects, however, return their primitive values with the valueOf
method, and return their string-cast values for toString
. The primitive values inside object wrappers are then cast according to the rules we saw in the previous section.
Not all built-in objects provide their own valueOf
method, so it's safe to assume that most objects will be cast to NaN
. We'll take note of how a particular native object type implements the valueOf
and the toString
methods as we discuss them.
At the top of the object hierarchy is the basic object, created using an object literal or via the Object
constructor. The Object
constructor represents the base object, and all objects inherit from Object.prototype
.
The Object
constructor, unlike other built-in constructors, is not turned into a type object by MooTools in order to prevent extension of its prototype. This is because Object.prototype
is considered off-limits: no new properties or methods should be added to it. To understand why, let's examine a basic JavaScript program:
// a basic object var obj = new Object(); console.log(typeOf(obj.constructor)); // 'function' console.log(typeOf(obj.hasOwnProperty)); // 'function' for (var key in obj) console.log(key);
Using the typeOf
function, we confirm that a base object inherits properties and methods like constructor
and hasOwnProperty
from Object.prototype
. We then use the for-in
statement, which loops through all the members of an object, to print out the names of our objects members. If we execute this program using a JavaScript interpreter, though, the console.log(key)
in the for-in
statement will never be executed—and it will appear as though our object has no members.
The reason for this behavior is that JavaScript differentiates between enumerable and non-enumerable properties. An enumerable property is any property that will be listed by a for-in loop, while a non-enumerable property is the opposite. All non-enumerable properties are thus considered to be "invisible" in a for-in
loop.
All default properties and methods of Object.prototype
are non-enumerable to ensure that new objects appear "blank." This is a way to avoid confusion among new developers: unless you know the internals of the prototypal system, you might be surprised to see new objects already having properties and methods. Therefore, the properties and methods that are defined by the language itself are marked as non-enumerable by default to prevent novices from thinking that these methods were added directly to the object.
On the other hand, all properties and method that we define in code are enumerable by default. If we augment a native object or replace its existing member with our own value, the new value we add will appear in for-in
statements. In the case of Object.prototype
, this makes things go awry because all objects inherit from this prototype. New objects no longer appear "blank" and you no longer have a way of knowing whether a member was implemented natively or if it was added directly to the object without having to call the hasOwnProperty
method.
Because it is currently impossible to implement non-enumerable properties in most JavaScript interpreters, the JavaScript community has decided that Object.prototype
should never be augmented. As one JavaScript saying goes: "Object.prototype
is verboten." The MooTools developers agree with this statement, and it was decided that the Object
constructor itself should not be turned into a type object to prevent misuse.
I stated that it's currently impossible to implement non-enumerable properties in most interpreters because, at the time of writing, most of these implementations still adhere to ECMAScript 3. ECMAScript 5, on the other hand, allows setting properties that are non-enumerables through descriptors.
Functions represent distinct, standalone chunks of executable code. They are the only native object type that has the distinction of being given a different "type" by the typeof
operator. This is because functions are the only objects that have associated executable code that is run when the function is invoked.
A function object's executable code is stored in a special internal property called call
(not to be confused with the call
method). When either the invocation operator, ()
, or the apply
and call
methods of a function object are invoked, the executable code of the internal call
property is executed by the interpreter. This internal call
property is also the distinguishing property by which the typeof
operator differentiates a function from any other object: the ECMAScript specification explicitly defines that only functions have this internal call
property.
We've already discussed the internal details of functions in Chapter 2, so we won't go too deep here. The Function
constructor, which can be used to create function objects, is the main constructor for functions, and all functions inherit from Function.prototype
. The MooTools type system automatically turns Function
into a type object.
Function.prototype
doesn't implement its own valueOf
method, which means calling fn.valueOf()
will return the function itself. The default toString
method, on the other hand, is overridden, and it returns the source code of the function. Take note, however, that the output of the toString
method is implementation-dependent: not all JavaScript engines will output the same string source.
One other distinctive feature of functions is that they are the only other object type—along with regular expressions—that can't be implemented using a basic object. You can implement a JavaScript version of any other native object type using a basic object literal, but you can't make a function by simply using an object literal. Because of this, functions can't be subclassed using normal inheritance patterns, and this presents a very unique problem for both classes and type objects that we'll see later on.
An array is an object representing an arbitrary-length, ordered collection of values. JavaScript arrays can store any valid value, including objects, functions, and other arrays. And unlike its counterparts in some programming languages, a JavaScript array doesn't expect all values to be of the same type, which means you can store values with different types in one array. All arrays inherit from the prototype
property of the Array
constructor.
Each value in an array is called a member and each member is associated with a numeric index that signifies its position in the collection. Arrays have a special dynamic property called length
, which represents the number of members in an array. Arrays are usually created using the array literal, which is a pair of square brackets enclosing a set of values separated by commas, like [1, 2, 3]
.
Arrays inherit the default valueOf
method from Object.prototype
, and calling this method simply returns the array itself. Meanwhile, the toString
method is overridden in Array.prototype
: it returns the string representation of each member of the array separated by a comma, like '1,2,3'
. Notably, this string value is the same as the result of calling the join
method of arrays:
[1, 2, 3].join(','), // '1,2,3'
JavaScript arrays are implemented differently from the "real" arrays of other programming languages. A JavaScript array is nothing more than an object with numeric indices as keys and with a dynamic property length
—plus additional methods inherited from Array.prototype
. In fact, the array above could have very well been written as:
{ '0': 1, '1': 2, '2': 3, length: 3 }
This is the basic structure of a JavaScript array. The numeric indices for this example—as well as for regular array—are strings, not numbers, since object keys need to be valid JavaScript identifiers. When numeric values are used to access the members of an array or an object using the bracket notation like in our example, the interpreter automatically converts these numbers to strings—something we can verify by using our two examples in a similar way:
// real array var arr = [1, 2, 3]; for (var i = 0, l = arr.length; i < l; i++){ console.log(arr[i]); } /* output: 1 2 3 */ // "object" array var objArr = { '0': 1, '1': 2, '2': 3, length: 3 }; for (var i = 0, l = objArr.length; i < l; i++){ console.log(objArr[i]); } /* output: 1 2 3 */
Both the real array and the dummy array created using a basic object behaved the same in this snippet because the implementation of JavaScript arrays is based on regular objects with a few minor differences. First, real arrays inherit from Array.prototype
, which enables us to use nice array methods like forEach
, and second, the length
properties of real arrays are dynamically updated. The length
property also behaves differently from other properties because changing its value by assignment actually changes the number of members in an array.
Our dummy array actually represents another set of objects in JavaScript called array-like objects. As its name implies, an array-like object is an object that looks similar to an array: it has numeric indices for keys and it has a length
property that reflects the number of members it contains. The arguments
object of functions, DOM collections, and nodelists are examples of array-like objects.
Because these objects aren't true arrays, they don't inherit from Array.prototype
and you can't call array methods through them. However, you can invoke the methods of Array.prototype
and bind the this
keyword to these array-like objects to perform array operations using them. For example, we could turn our dummy array-like object into a true array using the Array.prototype.slice
technique we used in the chapter on functions:
var objArr = { '0': 1, '1': 2, '2': 3, length: 3 }; console.log(typeOf(objArr)); // 'object' // turn it into a true array objArr = Array.prototype.slice.call(objArr); console.log(typeOf(objArr)); // 'array'
All array methods can be used for array-like objects with this technique, not just slice
. This is possible because the methods of Array.prototype are implemented as generics, which means they can be used on any object.
Arrays, for example, have the methods unshift
and push
, which add members to the beginning and the end of the array, respectively. We can use these two methods to augment our array-like object using the same approach as with slice
:
var objArr = { '0': 1, '1': 2, '2': 3, length: 3 }; console.log(objArr.length); // 3 // the first member of the array console.log(objArr[0]); // 1 // add a member to the front Array.prototype.unshift.call(objArr, 0); console.log(objArr[0]); // 0 console.log(objArr.length); // 4 // add a member to the back
Array.prototype.push.call(objArr, 4); console.log(objArr[4]); // 4 console.log(objArr.length); // 5 // check that it's still an object console.log(typeOf(objArr)); // 'object'
The array methods not only modified our object as if it were a real array, they also automatically adjusted the length
property of the object. This proves that JavaScript arrays are truly implemented using simple objects, and it also gives us a very useful technique we can use for building our own array-like objects.
Regular expression objects or regexps are used for the string pattern-matching feature of JavaScript. Regular expressions are created using the RegExp
constructor or using a regular expression literal, and they inherit from RegExp.prototype
. Like arrays, JavaScript doesn't consider regexp
objects as separate object types, but rather as special versions of the basic object type.
The toString
method of RegExp.prototype
will return the string version of a regular expression literal. This string representation is implementation-specific, and is sometimes parsable by the RegExp
constructor. Meanwhile, the valueOf
method of regexp
s is inherited from Object.prototype
, so it returns the object itself.
In ECMAScript 5, the toString()
method of a regexp
object is required to return a string that's parsable by the RegExp
constructor.
Curiously, some JavaScript implementations will output 'function'
when the typeof
operator is applied to a regexp object. While it is true that both functions and regular expressions share the same trait of not being implementable using a basic object, this behavior is actually a byproduct of a buggy feature addition that was wrongly copied by other implementations.
Mozilla implemented a special feature for its SpiderMonkey JavaScript engine that allowed direct calls to regexp objects as though they were functions. For example, this feature allowed the expression /string/.exec('string')
to be shortened to /string/('string')
. Mozilla added the ability to use the invocation operator on regular expressions—just like functions—and doing so would automatically call the exec
method of the regexp object.
To implement this feature, Mozilla added an internal call
property to the regexp object—a property that should be available only to functions, according to ECMAScript standards. This internal call
property of the regexp object exploits the same technique of binding executable code to an object as in the case of functions, but here the executable code is actually the regexp's exec
method. And this is where everything went haywire.
The typeof
operator only checks a single property to determine whether an object is a function: the internal call
property. However, because of this new feature added by Mozilla, the typeof
operator gets tricked when it encounters regular expression objects, and it mistakenly outputs 'function'
rather than 'object'
.
This might not have been such a big deal if not for the fact that this feature—along with its bug—was copied by other implementations. JavaScriptCore, the JavaScript engine of the Webkit project and the Safari browser, implemented this same feature. Google, in developing its Chrome browser, also used Webkit, but replaced JavaScriptCore with its own JavaScript implementation called v8. Unfortunately, this new engine was implemented to remain compatible with the JavaScriptCore engine it replaced, so the same feature and the same bug persisted.
Because of this bug, it's impossible to actually differentiate between real functions and regexp objects through the typeof
operator alone in some browsers. Mozilla has fixed this bug in the version of SpiderMonkey that shipped with Firefox 3, but it still persists in the current versions (at the time of writing) of Chrome and Safari. Fortunately for us, the MooTools typeOf
function is immune from this bug.
Regexps are a powerful feature of any programming languages, and they allow us to retrieve parts of a string using patterns. We won't be discussing them in detail in this book, but we'll see much of their use in the chapters that follow.
Date objects represent calendrical values. They are created using the Date
constructor and they inherit from Date.prototype
. Unlike other built-in types, dates don't have a corresponding literal form: all date objects need to be created using the Date
constructor function.
Essentially, a date object is a collection of numeric values that represent a specific date value. Stored inside a date object are values for the year, month, day, hour, minute, second, and millisecond that represent a specific calendrical value. However, date objects are fully encapsulated: these values are accessible only through getter and setter methods defined by Date.prototype
.
The toString
method of a date object returns a string representation of the date value in the form <weekday> <month> <day> <year> <hours>:<minutes>:<seconds> <timezone>
, like 'Wed Jun 03 1987 19:30:00 GMT+0800 (PHT)'
. This string representation is parsable by the Date
constructor, so you can use it to build a similar date object.
As with the regexp object, the return value of the date toString
method is implementation-specific. However, all major JavaScript engines follow the format presented above.
Similarly, the valueOf
method from Date.prototype
also returns the value of the date object, but as a numeric timestamp. This timestamp is a representation of the number of milliseconds since January 1, 1970 00:00:00 GMT (often called the Epoch). Calling the valueOf
method for the date object above therefore yields 549718200000
. This numeric value is also the same value returned by the getTime
date method.
The epoch timestamp in most programming languages is a 10-digit number, plus additional decimal places to denote offsets. JavaScript, in contrast, uses a 12-digit timestamp.
Date objects are the only native objects that can be cast to their proper strings and number values.
ECMAScript designates a group of objects called error objects that are used for error-handling operations. When the JavaScript interpreter encounters problematic code, it "throws" an error object that can then be handled by the program through the try-catch
statement.
The main error constructor, Error
, represents the most basic type of error object. It has a name property, the default value of which is "Error"
, and a message
property, which contains the specific human-readable error message. Some JavaScript implementations also add other non-standard properties, like the line number of the error or the stack trace for the execution context where the error occurred.
JavaScript itself doesn't use the base Error
constructor, though. Instead, it uses special error subclasses:
RangeError
—for errors when numeric values exceed the defined bounds of possible numeric values.
ReferenceError
—for errors that happen when performing operations on invalid references, such as accessing the properties of an undefined variable.
SyntaxError
—for errors that arise from being unable to parse improperly written code.
TypeError
—for errors involving passing incorrect value types to operations.
URIError
—for errors involving the URI encoding and decoding functions.
The name
properties of these error subclasses are the same as the identifiers of their constructor functions.
The valueOf
method of error objects is inherited from Object.prototype
. Thus, they return the objects themselves. Meanwhile, the toString
method of error objects is implementation-specific, and weirdly enough, the return value of this method isn't required by the specification to be the actual error message—which means that implementations can simply output anything they want. Thankfully, most JavaScript implementations do return the value of the message
property of the error object when the toString
method is invoked.
In ECMAScript 5, the toString
method of error objects is required to return strings in the form '<ErrorType>: <message>'
.
The MooTools framework has a unique policy of handling errors gracefully and silently, so errors are rarely used. Thus, Error and the other built-in error subtypes are not turned into type objects.
We've seen the rules on how values are cast from one type to the other, but we didn't actually find out how to convert objects from one type to another. Type casting can be tricky in some cases, so we need to discuss how values of one type can actually be converted to another type.
Like other dynamic languages, JavaScript actually performs automatic, or implicit, type casting. Implicit type casting is when values are cast from one type to another by the interpreter in order to properly execute an operation. These transformations are usually silent and aren't noticeable unless they're carefully observed.
The best example of implicit type casting is the automatic wrapping of primitives with object wrappers when the primitive values are used in operations that require objects. The interpreter silently wraps the primitive value with an object wrapper, and we don't really need to do anything to make this happen.
Similarly, the values null
and undefined
, as well as all object values, are turned into primitives when they are used in operations that require primitives. For instance, using an object in a division or multiplication operation automatically casts the object into a number value. Similarly, statements that require boolean values, like the if
statement, as well as the property access operator []
, also do implicit boolean and string casting. And wrapper objects for primitives are automatically cast to their primitive values if needed.
JavaScript has numerous implicit casting rules, and we can't cover all of them in this section. Instead, I advise you to read one of the recommended books on JavaScript noted in the Resources section at the end of this book.
In contrast to implicit casting, explicit type casting is the process of directly transforming one value into another using special operations. There are several ways to explicitly cast a value from one type to another, and we'll explore each one in turn.
The first and easiest way to explicitly cast a value to another type is to use the built-in constructors. Most native JavaScript constructors can actually be invoked as regular functions, and they perform type conversions when used this way.
The constructor objects for primitive wrappers, for instance, can be used to convert values to primitive types:
// boolean console.log(Boolean(0)); // false console.log(Boolean('')); // false console.log(Boolean('24')); // true console.log(Boolean({})); // true // number console.log(Number(false)); // 0 console.log(Number('')); // 0 console.log(Number('24')); // 24 console.log(Number({})); // NaN // string console.log(String(false)); // 'false' console.log(String(24)); // '24' console.log(String({})); // '[object Object]' console.log(String([1, 2, 3])); // '1,2,3'
The casting operations here follow the same rules we described earlier: primitives are converted using the rules for converting one primitive type to another, while objects are converted using their valueOf
and toString
methods.
An important thing to remember is that the return values of these primitive conversions are actual primitive values and not objects—only when used in conjunction with the new
operator do they return objects. Thus, String(1)
will return the primitive string value '1'
, while new String(1)
will return a new string object.
Native object constructors, on the other hand, behave differently when used as regular functions. The Function, Array, RegExp
, and Error
constructors, when invoked as regular functions, operate the same as if they were used with the new
keyword. Thus, they don't actually perform type casting, but rather, they perform object instantiation. It is therefore advisable not to invoke them as regular functions.
The Date
constructor's behavior, in contrast, is unique when it's invoked as a function: it doesn't perform type casting nor does it create a new date object. Instead, it returns the current date as a string.
Finally, the Object
constructor performs type casting according to the actual type of value passed. If you pass in a primitive value, it will return a wrapped object version of that value—so you'll receive either an instance of String, Number
, or Boolean
. But if you pass in an object value, the constructor will simply return the same object, with no modification. And if you pass in null or undefined, it will return a new plain object—just like doing new Object()
.
There are two special functions defined in JavaScript that handle string-to-number conversions: parseFloat
and parseInt
. Unlike the Number
constructor, these functions are more lenient when used for parsing strings, as they allow non-numeric trailing characters in the strings.
The parseFloat
function converts both integer and floating point numbers, while parseInt
can only convert to integers:
console.log(Number('42 is the answer.')); // NaN console.log(parseInt('42 is the answer.')); // 42 console.log(parseFloat('42 is the answer.')); // 42 console.log(parseInt('3.14')); // 3 console.log(parseFloat('3.14')); // 3.14 console.log(parseInt('024')); // 20 console.log(parseInt('024', 10)); // 24
The last two lines feature a quirk with the parseInt
implementation. The parseInt
function has a special "feature" that treats strings that begin with 0
as octal values. Thus, parseInt('024')
returns the value 20
instead of 24
. This creates a problem when parsing non-octal strings that start with 0, although you can solve it by passing a second argument, radix
, which tells the function what base to use for the conversion. This "feature" has been removed in ECMAScript 5.
One idiom that's used to convert any value to a number is JavaScript's implicit type conversion for mathematical operations. By using numeric identity operations (such as subtracting 0 from a value or dividing or multiplying a value by 1), you can convert a value to a number:
console.log('42' / 1); // 42 console.log('42' - 0); // 42 console.log('42' * 1); // 42 console.log(true - 0); // 1 console.log(false * 1); // 0 console.log({} - 0); // NaN
But don't use the addition identity operation (x + 0)
because the +
operator is both the addition and concatenation operator in JavaScript. You can, however, use +
as a unary operator for the purpose of numeric casting:
console.log(+'42'), // 42 console.log(+true); // 1 console.log(+{}); // NaN
To convert any value (except for null
and undefined
) to a string, we can simply use the toString
method. This works with primitives as well because of the automatic object wrapping mechanisms:
console.log(true.toString()); // 'true' console.log((42).toString()); // '42' console.log([1,2,3].toString()); // '1,2,3'
Number.prototype.toString()
is a special case, because it allows you to specify an argument, radix
, that will be used as the base for conversion. By default, the radix value is 10.
Another way to convert values to strings is to exploit the concatenation operator, +
. This operator is used for both addition of numbers and concatenation of strings, but it gives special priority to string values: if one of the operands in the expression is a string, it also casts the other value into a string. We can therefore turn any value into a string by concatenating it with an empty string:
console.log(true + ''), // 'true' console.log(42 + ''), // '42' console.log([1,2,3] + ''), // '1,2,3'
Finally, casting values to booleans is rarely done since JavaScript allows any value to be used in place of an actual boolean value when needed. If you do want to convert a particular value into its boolean representation quickly, though, you can use the double-negation trick:
console.log(!!''), // false console.log(!!'M'), // true console.log(!!0); // false console.log(!!42); // true console.log(!!{}); // true console.log(!![]); // true
The negation operator, !
, automatically casts a value into a boolean and it reverses a boolean true
value to false
and vice versa. Using the negation operator twice yields the same boolean value that was negated at the start of the expression. For example, the number value 0
is cast to the boolean value false
. When negating this value, we get !false == true
. The true
value is then negated again, and the final value goes back to false
. Thus, we get a proper conversion from 0
to false
.
Now that we've familiarized ourselves with the native type system and its components, let's turn our attention to its MooTools counterpart. The most important component of the MooTools type system is the Type
constructor, and it is this simple constructor function that enables us to streamline the process of working with native types.
The Type
constructor accepts two arguments: a required name
argument, which should be a string representation of the capitalized type name, and an optional object
argument, which is the constructor function to be transformed. If an object
argument is passed, the Type
constructor returns the same object after it adds additional properties and methods to it. Otherwise, it returns null
.
As I mentioned earlier, the Type
constructor doesn't create new constructor functions but rather transforms already existing constructors into type objects by augmenting new properties and methods to it. To illustrate, here's how the native Array
constructor is turned into a type object:
new Type('Array', Array);
And that's all it takes to turn a native constructor into a type object. We simply instantiated a Type
object using the new
keyword and passed the name of the type, 'Array'
, and the native Array
constructor function. You'll notice that we didn't even need to store the results in a variable. Because the Array
constructor is transformed directly, there was no need to store the result of the expression in a new identifier. The process is both simple and elegant—and certainly says a lot about the MooTools type system implementation
One question that continually pops up when developers see that example is whether the new
keyword is actually needed. If we're not really creating a new object but simply transforming an existing constructor function into a type object, why not just make Type
a simple function? Does it really have to be a constructor?
The question is even more valid once you consider the fact that instances of Type
are not really "instances" of Type
:
new Type('Array', Array); // make sure that Array is a type object console.log(typeOf(Array)); // 'type' // is Array an instance of Type? console.log(Array instanceof Type); // false
We do know that Type
transforms the constructor directly, so it really doesn't create a new object. The type object returned by the Type
constructor isn't really an instance of Type
since it's a function that already existed before we passed it to the constructor. So does this mean we could really do away with using new
?
To answer this question, we must first examine the same question but with regard to classes, which are similar to type objects. Unlike the Type
constructor, the Class
constructor actually creates a new constructor function for the class, so using the new
operator with Class
seems like a logical thing to do. What's surprising is that the same behavior can be observed in classes as well:
var Person = new Class(); // make sure that Person is a class
console.log(typeOf(Person)); // 'class' // is Person an instance of Class? console.log(Person instanceof Class); // false
This seems a little counter-intuitive: the Class
constructor creates the new constructor for the class, so we'd expect that the result of instantiating the Class
constructor would be a class instance. But when the instanceof
operator is used to check, classes exhibit the same behavior of not being instances of the Class
constructor—just like type objects with Type
.
Recall something I mentioned earlier in this chapter about functions: they are one of the two types of objects in JavaScript that can't be created using a basic object (the other being regular expressions)—which means they can't be subclassed. The reason for this is simple: functions depend on an internal call
property that references the executable code that's called when the function is used in conjunction with the invocation operator. Since we have no way of setting this internal property in regular objects, we can't create subclasses of the Function
type because we'll end up referencing the same executable code.
The Type
and Class
constructors both deal with constructor-prototype pairs, with emphasis on constructors. In essence, type objects and classes are "subclassed" functions: they're constructor functions that have special properties and methods. But because JavaScript places no distinction between regular functions and constructors (except for their behavior when used with new
), both Type
and Class
need to circumvent the limitation of function subclassing using direct augmentation of functions, rather than simple prototypal inheritance.
Here's how it works. For the type definition new Type('Array', Array)
, a new object inheriting from Type.prototype
is created by the new
operator and then used as the this
value inside the Type
constructor. Type
then takes all the properties and methods of this new instance and adds them to the Array
constructor function, thereby making sure that the resulting Array
type object will "inherit" the members in Type.prototype
. The operation involves direct augmentation of members to the type object, and not inheritance via the prototype chain. The new
keyword is essential because it creates the template object that will be used for augmentation. The same thing is applicable to the Class
constructor, with the minor difference of the resulting class object being created inside the Class
constructor itself.
Of course, direct augmentation has its limitations, which we already saw in the Chapter 3. The main issue would be dynamic modification: since type objects and classes are augmented directly, any changes to Type.prototype
and Class.prototype
aren't propagated to existing type objects and classes. This isn't a big issue in practice, though, since the number of type objects and predefined classes in the MooTools-Core library are small enough to modify directly.
The snippets above don't just show why we need something like the new
operator when we're creating new type objects, they also shows us some limitations encountered when trying to implement a new type system on top of the existing one. In order for the replacement MooTools type system to be fully usable, it needs to cover not only the creation and management of types but also things like instance checking and type detection.
Interestingly, the core mechanisms for adding these two features to the MooTools type system are implemented by the Type
constructor itself, as we'll see in the next two sections.
Native JavaScript instance checking is done using the instanceof
operator. We've already seen it in action several times in this and earlier chapters, so it hardly needs any introduction.
The instanceof
operator works by comparing the constructor of the object on the left-hand side of the expression to the constructor function on the right-hand side. In the expression myCar instanceof Car
, for example, the constructor function that created the myCar
object is compared to the Car
constructor to see if they're the same function. If they are, the expression evaluates to true
.
However, if the constructor function for the myCar
object and the Car
constructor aren't the same, the expression doesn't immediately evaluate to false
. Instead, the prototype chain of the myCar
object is traversed, and the constructor function of each object in the prototype chain is compared with the Car
constructor. Only when the last constructor function has failed comparison will the expression evaluate to false
.
This "deep" comparison is why the instanceof
operator evaluates to true not only for the immediate constructor of the object, but also the ancestral constructors of the object. All objects are therefore seen as instances of the Object
constructor, since all objects inherit from Object
at one point in their prototype chain.
As you've probably guessed by now, the constructor
property of objects plays a central part in native instance checking. All objects are linked to their corresponding constructor functions via their constructor
property, and it's the value of this property that gets compared to the constructor function on the right-hand side of the expression.
In our examples in the previous section, both type objects and classes fail the instanceof
test because they don't have links to the Type
and Class
constructors directly. Both type objects and classes are abstracted constructor functions, so their prototype chain consists only of instances of Object
and Function
. Because the Type
and Class
constructors augment these constructor functions directly rather than through prototypal inheritance, their constructor functions never get "linked" to the object. Thus, type objects and classes are not seen as true instances of their respective constructors by the instanceof
operator.
To circumvent this limitation, a link has to be made by the MooTools type system between these objects and their constructors. This is done by adding a new property to these objects called $constructor
. Like its native constructor counterpart, the $constructor
property is a reference to the object's original constructor function. Type objects therefore have their $constructor
properties pointing to Type
, while classes have theirs pointing to Class
.
Of course, just because MooTools added a new property that resembles the native constructor
property doesn't mean that instanceof
will respect it. The MooTools type system works on a separate level from the native one, so we can't expect a simple solution like that to take care of the issue altogether. Instead, MooTools adds the second part of the equation by creating a new function to replace the instanceof
operator: the instanceOf
function.
The instanceOf
function takes in two arguments: item
, which is the object being checked, and object
, which should be a constructor function to check against. Unlike the instanceof
operator it replaces, though, the instanceOf
function properly handles cases like type objects and classes:
// Type Object new Type('Array', Array); // make sure that Array is a type object console.log(typeOf(Array)); // 'type' // is Array a native instance of Type? console.log(Array instanceof Type); // false // is Array an instance of Type in MooTools? console.log( instanceOf(Array, Type) ); // true // Class var Personn = new Class(); // make sure that Person is a class console.log(typeOf(Person)); // 'class' // is Person a native instance of Class?
console.log(Person instanceof Class); // false // is Person an instance of Class in MooTools? console.log( instanceOf(Person, Class) ); // true
Revamping our examples from the previous section, you'll notice that instanceOf(Array, Type)
and instanceOf(Person, Class)
now both return true. This tells us that Array
and Person
are instances of Type
and Class
respectively—which is exactly what we would want to know.
Like typeOf
, the instanceOf
function isn't an operator like its native instanceof
counterpart but a real function. Therefore, the use of the invocation operator is essential, as well as proper casing (it's instanceOf()
, not instanceof()
).
Like its native counterpart, the instanceOf
function works by comparing the constructor of the item
argument with the constructor function that's the value of the object
argument. Unlike the native instanceof
operator, though, instanceOf
is aware of the $constructor
property and uses this property instead of the regular constructor
property as much as possible. The comparison process involves a direct equality test, which means that instanceOf(Array, Type)
is somewhat equivalent to Array.$constructor == Type
.
I said "somewhat equivalent" because the comparison isn't a one-off process. Like the native instanceof
operator, the instanceOf
function also does "deep" comparison, so checking whether an object is an instance of some ancestral constructor also works:
new Type('Array', Array); console.log(instanceOf(Array, Type)); // true console.log(instanceOf(Array, Function)); // true console.log(instanceOf(Array, Object)); // true
The creation of the $constructor
property is done by the Type
constructor (and by the Class
constructor for classes). When the constructor function argument is processed by Type
, it adds a $constructor
property to the constructor
function that's used for the instanceOf
function. The $constructor
property of the constructor function passed to Type
is always set to the Type
function itself, so that instanceOf
will be able to determine that the type object is an instance of the Type
constructor.
However, the Type
constructor doesn't just add a $constructor
property to the constructor argument, but also to the prototype of the constructor argument. In the snippet above, for instance, Type
doesn't just set Array.$constructor
to Type
, but also adds the property Array.prototype.$constructor
. The $constructor
property of the prototype
is then set to the constructor
itself, so in our example, Array.prototype.$constructor == Array
. This additional prototype property creates one big difference between the instanceof
operator and the instanceOf
function: the ability to check primitive values.
The instanceof
operator, by design, only works if the left-hand side of the expression is an object, not a primitive. If you use a primitive as the value of the left-hand side, it won't work: 'hello' instanceof String
evaluates to false
. The instanceof
operator does not perform type-casting, so primitive values are treated as true primitives and not objects.
However, the instanceOf
function doesn't have the same limitation. Because it depends on the $constructor
property rather than the true native type of the value, the instanceOf
function is able to determine types even for primitive values. In the process of accessing the $constructor
property of the value passed, primitives are automatically turned into objects within the instanceOf
function, and therefore inherit the $constructor
property from their respective type objects. Thus, instanceOf('hello', String)
evaluates to true
.
Before we move on, though, it's important to note that the instanceOf
function also uses instanceof
internally as a fallback mechanism in case regular $constructor
checking doesn't work. This is an important thing to remember because of a special group of types that don't have type objects, which we'll encounter in a bit.
JavaScript gives us the typeof
operator for detecting the types of our values. But as we saw earlier in this chapter, the native typeof
operator leaves a lot to be desired. Its replacement from the MooTools type system, the typeOf
function, does a better job at detecting the types of values as well as differentiating among the actual types of objects.
Unlike the instanceOf
function, though, which only depends on the constructor
and $constructor
properties of the value passed, the typeOf
operator uses a couple of different ways to properly detect the type of a value. At the top of the list is the use of type objects to pass special functions to their instances, which are called family methods.
A family method is a special private method that's added by the Type
constructor to the prototype of the type object that's used to return the type of the object. It is a very simple function that simply returns the lowercase name of the type as a string. When creating new Type('Array', Array)
, for example, the Type
constructor adds a new method called $family
to Array.prototype
, and this new method simply returns the lowercase equivalent of the name passed to the Type
constructor. Thus, Array.prototype.$family
returns 'array' when invoked.
This family method is then invoked by the typeOf
function to return the type of the value. Because the family method is added to the prototype of the type object directly, all instances of the type will therefore inherit the same family method. So detecting the type of an object is as simple as calling its $family
method. In the case of arrays, for instance, doing [].$family()
or typeOf([])
returns 'array' for both cases—which is the actual type of the object. The typeOf
operator simplifies this process of calling the $family
function of the object by doing it automatically.
You might be asking then, "What's the use of calling typeOf
then, when I could just invoke the $family
method directly?" Well, the first answer is because typeOf
is part of the private API—and like many things in the private MooTools API, it's bound to change eventually. Making direct calls to this method makes your code prone to breakage when new versions of the library come out—especially ones that change the internal API dramatically (and it happens!).
But even if we're absolutely sure that it will never break—and that's highly unlikely—there's an even bigger reason why you should not call the $family
method directly:
// regular object var obj = {}; // typeOf console.log(typeOf(obj)); // 'object' // obj.$family console.log(obj.$family()); // this will throw an error
In this snippet, passing the obj
variable to typeOf
works as expected, and we get the proper type—'object'—as the return value. However, if we try to do obj.$family()
, we'll get an error—because obj
has no method named $family
.
Remember that the $family
method is inherited by objects from their type objects, and it is added directly to the prototype of the type object via the Type
constructor. However, some objects don't have a $family
method because they don't inherit the method from their type objects. In fact, it's just not the case of not inheriting the method; it's a case of not having actual type objects in the first place!
Some native constructors, Object being the most notable, are not turned into type objects by the MooTools type system. A few of them aren't turned into type objects because they're not used in common programming tasks, while some aren't converted to impose restrictions. The Object constructor is the chief example of that second reason: it isn't turned into a type object to prevent extension of Object.prototype
.
One other important set of types are those with no real constructors. For instance, argument objects, which are created for use inside functions, don't have a corresponding constructor—there's no Argument constructor in JavaScript. Other notable examples are DOM objects like textnodes and whitespaces, which also don't have constructor functions.
The MooTools type system recognizes these types even though they don't have corresponding type objects. However, it presents a little challenge to using $family
directly. Because not all values inherit a $family
method, we can't depend on this function alone to detect the type of a value. So, as in the case of our example above, we can't call the $family
method directly because there's a risk that the object might not have the method to begin with.
This is the prime reason why the $family
method should not be used as a replacement for typeOf
. The typeOf
function is smart enough to know whether the value in question has a $family
method that can be called and does so if that's the case. If there's no $family
method present, though, it uses another technique to detect the type.
So what's this other technique? It's called duck typing, and its name comes from the common saying: "If it walks like a duck and talks like a duck, then it must be a duck."
Duck typing bases its assumption of an object's type according to the structure of the object. As long as the object's properties and methods match the declared properties and methods for a type (walks like a duck and talks like a duck), it's considered to be of that type (it must be a duck). Duck typing is commonly used in dynamically typed languages to support substitutability. Because of the loose requirements of duck typing, you can design your code in such a way that it accepts objects of any type as long as they have a particular method or property.
However, very loose duck typing can't be applied to a type system directly, since we'd risk having false positives. For example, you can check for the existence of a length
property to find out whether an object is an array, and it'll work for the most part, but it'll also think that string objects, argument objects, and element collections are arrays, since all of those appear to be array-like.
For duck typing to be really useful, the criteria for considering an object to be a type need to be very strict. The typeOf
function uses several strict criteria to determine the type for values that have no type objects. Argument objects, for example, are tested not only for the presence of the length
property, but for the existence of a callee
property as well.
This, of course, isn't perfect and is easily bypassed by cunning manipulation of code. The code typeOf({length: 1, callee: 1})
, for example, will return "arguments" even if what you have is not really an argument object. Unfortunately, there's nothing we can do about this, since duck typing is the only other solution we can use for values with no corresponding type objects. Thankfully, though, we can use the idea of duck typing to craft code that doesn't rely entirely on the specificity of type to do its job.
Take note that family methods take precedence in the typing process for typeOf
. Duck typing is a fallback technique used only for objects with no corresponding type objects. And if these two techniques don't work, the typeof
operator is used by the typeOf
function as a last resort.
Up to this point we've been talking about type objects in the context of type detection. But as I said earlier in this chapter, a type object also acts as the representative for its instances. Like classes, type objects are abstracted constructor-prototype pairs that can be used to change the structure and behavior of the items of that type. In fact, both classes and type object share very similar methods to accomplish this, as we'll see later on. And just like classes, type objects also have a special set of methods available only to them that can be used to streamline the process of working with types.
When working with native JavaScript, adding new properties or methods to existing types is done by augmenting the prototype object of their respective constructor. If we want to add a new repeat
method for strings, for instance, we have to do something like this:
var str = 'hello'; console.log(typeOf(str.repeat)); // 'null' String.prototype.repeat = function(times){ var arr = []; while (times--) arr.push(this); return arr.join(''), }; console.log(typeOf(str.repeat)); // 'function' console.log(str.repeat(3)); // 'hellohellohello'
At the start of the code, the repeat
method doesn't exist yet, so str.repeat
is typed as 'null'
. We then declare this new method by assigning a function to String.prototype.repeat
. Afterwards, we check whether the repeat
method is now available for strings, and we use it to repeat the string 'hello' three times.
We looked at this technique of augmenting the prototype property of a class in a previous chapter, and we saw how it's not the best way to add new members to a class. Adding new members directly to the prototype limits the additional features MooTools can offer, and the verbosity of the code could mean having to manage really complex declarations in the future. Instead of doing it that way, we learned that it's better to use the implement
class method, which replaces the need for direct augmentation and streamlines the process of implementing new members.
Like classes, type objects also have an implement
method, and we can use this method to rewrite the previous snippet:
var str = 'hello'; console.log(typeOf(str.repeat)); // 'null' String.implement({ repeat: function(times){ var arr = []; while (times--) arr.push(this); return arr.join(''), } }); console.log(typeOf(str.repeat)); // 'function' console.log(str.repeat(3)); // 'hellohellohello'
The implement
methods of classes and type objects are used similarly: implement
accepts as an argument an object literal that describes the properties and methods you want to add and it loops through each of these members and adds them to the prototype.
While they appear to be the same function from the outside, the implement
method of objects is not the exact same method as the one for classes. In the chapter on classes, we discovered the various internal features of the class implement
method, including dereferencing, code wrapping, and mutator management. The type implement
method also has some powerful internal features, although overall, it is much simpler than its class counterpart.
One of the more noticeable differences is that the type implement
method does not do wrapping:
var myFn = function(){ console.log('fn'), }; // class var Person = new Class(); Person.implement({ method: myFn }); var shiela = new Person(); console.log(shiela.method == myFn); // false // type String.implement({ method: myFn }); var str = 'hello world'; console.log(str.method == myFn); // true
In the first part of this snippet, we compare the method
method of the Person
class with the original myFn
function using the line shiela.method == myFn
. The result is false, which is expected because the implement
method of classes wraps the original function in order to add additional class features. In the case of types, however, the methods added through implement
are not wrapped but are added directly to the type's prototype
object. Thus, when we do str.method == myFn
, we get true—and this tells us that the methods are indeed the same function.
Newly added methods are not wrapped by the type implement
method because the wrapper is no longer needed. Classes need method wrappers in order to implement protected methods and this.parent
, but type objects don't support (or need) either of these features. Moreover, type objects are created using existing constructors that often already have existing members in their prototypes, and wrapping these existing methods might break them.
In the previous chapter, we learned about the protect
method, which is used to implement protected methods in classes. The protect
method has another use, in fact, and that is to protect a native method from being overridden. Consider the following example:
// original method String.implement('method', function(){ console.log('Original'), }.protect()); 'hello'.method(); // 'Original' // override original String.implement('method', function(){ console.log('Override'), });
'hello'.method(); // 'Original'
Here we first implemented a new String
method called method
which simply logs 'Original'
in our console. However, we also called the protect
method of the function value we're passing to implement
, making our new method protected. When we tried to override this original method by calling implement
again with a new method definition, our original function wasn't overridden, which is why we still get the same string in our logs.
The protect
method is therefore very handy for making sure that native methods that browsers already implement aren't overridden by the user. By default, all the native methods for native objects defined by the ECMAScript specification are protected by MooTools, and therefore cannot be overridden by the user.
Type objects support a special feature called aliasing, which is simply reimplementing an existing method using another name. It is done using the alias
method, which takes two string arguments: the name for the new method and the name of the old method to be aliased.
var str = 'howdy'; // implement the original String.implement({ repeat: function(times){ var arr = []; while (times--) arr.push(this); return arr.join(''), } }); console.log(str.repeat(2)); // 'howdyhowdy' // alias repeat String.alias('again', 'repeat'), console.log(str.again == str.repeat); // true console.log(str.again(2)); // 'howdyhowdy'
In this example, we aliased the original repeat
method of the String
type simply by calling String.alias('again', 'repeat')
. The alias
method then takes the original function value of String.prototype.repeat
and copies it to String.prototype.again
. The result is that the two methods now point to the same function, making it possible to substitute calls to one for the other.
Aliasing is a very useful feature for creating shortcuts for commonly used methods with very long names. The Array
method forEach
, for example, is automatically aliased by MooTools into the each
method, which is shorter and easier to type. You can do the same for other methods in any type objects to shorten your code and make it cleaner.
An important thing to note, though, is that aliasing uses implement
internally, and all aliases methods are true references. Therefore, if you override the original method, your alias will not be updated:
var str = 'hi'; // implement original String.implement({ original: function(){ console.log('hello'), } }); String.alias('copy', 'original'), console.log(str.copy == str.original); // true str.original(); // 'hello' str.copy(); // 'hello' // override original String.implement({ original: function(){ console.log('woot!'), } }); console.log(str.copy == str.original); // false str.original(); // 'woot!' str.copy(); // 'hello'
In the first part of the snippet, both the original method and its aliased copy
method point to the same function, and therefore act as the same method. However, when we reimplement the original method, the aliased copy method doesn't get updated.
Another useful feature that's related to aliasing is called mirroring. While aliasing allows you to reimplement a method on a type, mirroring allows you to copy any implemented method from one type to another. To perform mirroring, we use the mirror
method of a type, which accepts a single argument that is a type object. Take a look at the following snippet:
var arr = [], str = 'hello'; console.log(arr.log); // undefined console.log(str.log); // undefined // make String "mirror" Array Array.mirror(String); // implement log on array Array.implement({ log: function(){ console.log('log called.'), }
}); console.log(arr.log == str.log); // true arr.log(); // method called. str.log(); // method called.
The important line in this snippet is Array.mirror(String)
. This tells the type system that String
is a "mirror" of Array
, and any methods implemented on Array
from then on should also be implemented on String
. Because of this mirroring, the log
method we implemented on the Array
type is also implemented on String
.
Mirroring makes it possible to automatically implement methods across multiple types. A single type can have multiple mirrors, which makes implementing the same method among many types easier and cleaner. The Array
type, for instance, could have mirrors for other array-like objects, so that any method that's implemented for arrays would also be available to these objects.
It's the job of the implement
method to manage mirrors. When the implement
method of a type is called, it not only adds the properties and methods to the type object's prototype, but also checks whether the type has mirrors. The implement
method then adds the properties and methods to these mirrors, making sure that the members are available to these types as well.
Because mirroring is managed by implement
, only those members that are added after the mirror was declared would be automatically added. Existing properties and methods aren't copied, so we have to do that manually if we want to copy them. For instance, even though String
was added as a mirror of Array
, only the methods that were added via implement
after the declaration would be copied—like the log
method. Existing array methods, like splice
or push
, aren't copied automatically.
Another thing to consider is that mirrors aren't reciprocal: implemented methods on a type will be implemented on all its mirrors, but implemented methods for the mirrors aren't added for the type. In the example above, any method implemented on Array
after the mirror
declaration would be copied to String
, but any method implemented on String
won't be copied to Array
. If you want reciprocal mirrors, you'll have to declare mirrors for both types.
The mirror
method of type objects isn't actually limited just to type object arguments. We can also pass a callback function that will be invoked for every item that's implemented:
var callback = function(name, method){ console.log(name); }; Array.mirror(callback); Array.implement({ logA: function(){}, logB: function(){} });
In this example, we declared the callback
function to be a mirror for Array
. For every item processed by implement, the callback
function will be invoked and passed two arguments. The first argument, name
, is the key of the item being implemented and the second argument, method
, is the actual function. For this code, the callback
function will be invoked first with the arguments 'logA' and a function, and then with 'logB' and another function.
Function mirrors are especially useful in cases when you want to process the method first before implementing it on another type. For example, the MooTools Elements
type uses a function mirror to transform methods from Element
into methods that work on an array-like object. We'll learn more about this particular trick in Chapter 8 on the Element
type.
We first saw the use of the extend
method in the chapter on class extras. This method is inherited from Function.prototype
, and allows us to augment function objects directly:
var fn = function(){}; console.log(fn.prop); // undefined console.log(fn.item); // undefined fn.extend({ prop: 'property', item: 'item' }); console.log(fn.prop); // 'property' console.log(fn.item); // 'item'
Instead of declaring the properties prop
and item
directly using an assignment statement, we passed an object to the extend
method of fn
containing these new properties. The result of using extend
is the same as a normal assignment, but it's cleaner and more organized.
Classes, being abstracted constructor functions, also inherit the extend
method from Function.prototype
. Type objects, on the other hand, also have an extend
method, but it's an overriding method that's inherited from Type.prototype
. The original extend
method from Function.prototype
and the one from Type.prototype
have the same external API—they both accept an object argument and they both perform the same extension operation using direct augmentation. Type.prototype.extend
has an additional feature that's not present in its Function.prototype
counterpart—but we'll talk about this in the next section.
One important use of the extend
method that we saw before was to add static methods to classes. We can also do this with type objects:
Array.extend({ identity: function(){ console.log(this === Array); }, log: function(){ console.log('Hello'), } }); Array.identity(); // true Array.log(); // 'Hello'
Here we added two static methods to Array, identity
, and log
. The identity
method is used to show that the this
keyword in the static method is bound to the type object, while the log
method is a simple method that logs the string 'Hello'. Though these two are very trivial examples, they tell us that static methods work the same for type objects as they do for classes: they're actually methods of the type object itself rather than its instances.
There's a special group of static methods, though, that are particularly important in MooTools and they're called generics. A generic is a static version of an instance method that can be applied to any object. Generics are handy for using methods from one type on another type without having to directly call the method from the prototype.
The best way to explain generics is with an example. In the section on array objects in this chapter, I mentioned that array methods are intentionally generic in the sense that they can be used on any object. For example, we can use the push
method of arrays to add a new item to an array-like object:
var obj = {0: 'pizza', length: 1}; console.log(obj.length); // 1 // invoke the push method of arrays Array.prototype.push.call(obj, 'soda'), console.log(obj.length); // 2
In order to access the push
method, we had to access the method via Array.prototype
. We then used the function
method call, passing in the arguments obj
and 'soda'
. The first argument becomes the value of the this
keyword inside the push
method, while the second is the actual item that will be pushed into the object. The result is that the string 'soda'
is pushed into our array-like object, and its length
property is automatically incremented.
Now imagine that you need to use the push
method on that particular object several times through your code. Accessing it via Array.prototype
is unnecessarily verbose, and doing it multiple times leads to unnecessary repetition—not to mention having to use the call
method, too. To really make these methods useful, we need an easier way to access them.
This is where generic methods come in. Take a look at this version of the previous example:
var obj = {0: 'pizza', length: 1}; console.log(obj.length); // 1 // invoke the push method of arrays Array.push(obj, 'soda'), console.log(obj.length); // 2
We replaced the original call to Array.prototype.push.call(obj, 'soda')
to Array.push(obj, 'soda'). Array.push
is a generic method. Using this method is the same as calling Array.prototype.push.call
, and it expects the same arguments. The first argument is always the this
keyword value for the method, and the arguments after that are the actual arguments for the method. The result is the same, but the actual invocation pattern is shorter than our original example.
MooTools automatically creates the generic versions of the methods of all native types. The string instance method split
, for example, is available via the String.split
generic, while the regexp method test
is accessible via RegExp.test
. All generic methods have the same invocation signature: the first argument is always the this
keyword value, and the arguments after that will be the actual arguments passed to the method.
The implement
method also creates generic methods automatically. Any method you add using the implement
method would have a corresponding generic method:
console.log(typeOf(Array.log)); // 'null'
Array.implement({ log: function(){ console.log('Hello'), } }); console.log(typeOf(Array.log)); // 'function' Array.log(); // 'Hello'
Generic methods are used heavily in the MooTools framework. In fact, MooTools forgoes extending Object.prototype
by implementing all additional Object
methods as generics. We've already encountered some generics used inside MooTools in the previous chapters, like Array.slice
and Object.merge
, and we'll encounter more of them as we go along.
Another set of important static methods are the from
methods, and there are four of them:
String.from
takes any argument and returns the string value of the argument. The call String.from([1, 2, 3])
, for example, returns '1,2,3'.
Number.from
takes any argument and uses parseFloat
to change the argument to a number value. It then returns that value as a number, or null if it can't be turned into a proper number.
Array.from
takes any argument and returns an array. If the argument passed is already an array, it returns the argument without modification. If the argument passed is an array-like object, array.from turns it into a true array before returning it. If the argument is neither an array nor an array-like object, it returns a new array containing the argument. For example, Array.from('hello')
will return ['hello'].
Function.from
takes any argument and returns a function. If the argument passed is already a function, function.from
returns it without modification. Otherwise, it returns a new function that returns the passed argument: Function.from('hello')
will return function(){ return 'hello' }
.
We already saw Array.from
in Chapter 5, and we'll see it—and the other from
methods—again throughout this book.
One of the best things about the MooTools type system is its extensibility and openness. Not only are developers allowed to use the type system to implement new methods for native types, but we're also encouraged to make use of the publicly available Type
constructor to implement our own set of custom types.
This was not always the case, of course. Prior to version 1.3 of MooTools, the inner workings of the MooTools type system were considered off-limits to developers. In older versions of the framework, the type system was implemented through the private Native
constructor, which was a more complex forerunner of the Type
constructor. It was one of those APIs that the experts knew about and used, but was not discussed in the documentation nor divulged to regular developers because of its private status.
However, times have changed and the Native
constructor has been replaced with the simpler Type
API—which is considered part of the public API. It's great that we can take full advantage of the type system now, and we'll do just that by implementing our own custom type.
As an example, we'll create a new type called a table. A table is a wrapper for the basic JavaScript object that supports getters and setters, as well as other utility methods. Before we actually implement it, let's see an example of its usage:
var table = new Table(); // setting values table.set('item', 'pencil'), table.set({'fruit': 'banana', 'person': 'shiela'}); // accessing objects table.get('item'), // returns 'pencil' table.get('item', 'fruit'), // returns {item: 'pencil', fruit: 'banana'} table.get('event'), // returns undefined // removal table.set('event', 'birthday'), table.get('event'), // returns 'birthday' table.remove('event'), table.get('event'), // returns undefined // membership table.hasKey('item'), // returns true table.hasValue('banana'), // returns true table.keyOf('pencil'), // returns 'item' // keys and values table.keys(); // returns ['item', 'fruit', 'person'] table.values(); // returns ['pencil', 'banana', 'shiela'] table.length(); // returns 3 // traversal table.each(function(item, key){ console.log(key + ': ' + item); }); /* item: pencil fruit: banana person: shiela */
Unlike a basic object, the items in a table aren't accessible from the table object directly via dot notation, but are accessible only using the get
and set
methods. We'll also have a remove
method for removing items from the table, and three membership methods, hasKey, hasValue
, and keyOf
, to check whether specific keys or values are present in the table. Finally, we have the utility methods keys, values
, and length
to give us more information regarding the table itself, and an each
method for table traversal.
This sounds like a very complex type, but it's actually pretty simple to implement as we'll see through this section. All the utility functions we need to implement this are already available through native JavaScript functions and MooTools language additions, and creating this new type will be quite easy.
The first thing we need to consider, though, is the constructor. Unlike classes, type objects make no distinction between constructors and initializers. In fact, there are no initializers for type objects: the constructor function acts both as object constructor and initializer.
There are two main reasons why this design was implemented for the type system. First is that native type constructors don't separate constructors and initializers, so it was deemed necessary to follow this standard in the MooTools type system. The second reason is more important: it is because type constructors were created for the purpose of initializing basic data types, which should be much simpler and more lightweight than classes. If you want a full-featured object system, use a class; otherwise, use a type. (We'll talk more about these distinctions in the last part of this section).
Since the special constructor features like automatic dereferencing available to classes aren't implemented for type objects, we need to make sure we're mindful of these issues. Dereferencing is the biggest of these issues, and we have to make sure that we create any object-based property inside the constructor itself, rather than declare them in our prototype.
In our table type, we'll need one of these object properties called $storage
. Since it's an object property, we need to set it inside our Table
constructor:
function Table(){ this.$storage = {}; };
This is the foundation of our table type: the Table
constructor. We used a function definition rather than a declaration since we want our constructor to have a proper name
property—and also since this is recommended MooTools style. Inside the constructor, we have a line that creates a $storage
property for the instance, which will then be used to store the items for our table. Because $storage
is an object, we set it inside the constructor rather than in Table.prototype
so that each table instance will have a unique storage object.
In order to make our Table
constructor similar to native types, we need to give it the ability to be called as a regular function. If you recall from earlier in this chapter, we found out that native constructors could be called as regular functions, and they either perform type casting or create new instances. We'll follow the latter: if our Table
constructor is called in conjunction with the new
keyword, it'll act as a regular object constructor; otherwise, it'll return a new Table
instance.
function Table(){ if (instanceOf(this, Table)){ this.$storage = {}; } else { return new Table(); } };
We modified our constructor by adding an instanceOf
check to see if the new
keyword was used. If our Table
constructor was called with the new
keyword, the value of this inside the function will be an instance of the Table
type. On the other hand, calling Table
as a regular function sets the this
keyword differently, so we'll need to instantiate and return a true instance of the Table
type.
Now that the constructor is ready, all we need to do is turn it into a type object, and that's as simple as passing it to the Type
constructor:
function Table(){ if (instanceOf(this, Table)){ this.$storage = {}; } else { return new Table(); } };
new Type('Table', Table); var table = new Table(); console.log(typeOf(table)); // 'table'
With the constructor done, it's now time to implement our getter and setter methods. Instead of adding these methods directly to Table.prototype
, we'll use the implement
method so that our code is more organized.
The methods themselves are very simple:
Table.implement({ set: function(key, value){ this.$storage[key] = value; return this; }, get: function(key){ return this.$storage[key]; } });
The first method is set
, and it takes two arguments: a string key, which is the identifier for the item, and value, which can be any value associated with the key. Storing the value in the $storage
itself is as simple as assigning the particular key to the value. The get
method, on the other hand, requires only one argument, key
, which is the key of the item being accessed, and returning the value from the storage is also done using the simple access expression.
However, if you look back at the example use, you'll notice that the set
and get
methods have two forms. In the first form, they're used like the simple declaration above: set
requires two arguments, while get
requires a single one. In the second form, they're used to perform multiple operations. The set
method in this form requires only one argument, an object literal, and it adds all the items from this object to the storage. Meanwhile, the get
method in the second form accepts multiple key arguments, and returns an object containing the results.
To implement these new forms, we can modify our methods so they'll accept different argument types. The set
method would have to be modified to check the first argument: if the argument is a string, it's used as a key and the second argument is used as the value, and if it's an object, the method should loop through the object and add each item to the internal storage. The get
method, on the other hand, will have to check for the number of arguments and return either a single value or an object containing multiple key-value pairs depending on the number of key arguments passed.
It sounds like a very complicated task to add these additional features. Fortunately, MooTools already has it covered with two function decorators we can use to transform our set
and get
methods. The first is the overloadSetter
decorator, which takes a function with the signature fn(key, value)
and gives it the ability to accept object literal arguments, such as fn({key: value})
. The second decorator, overloadGetter
, takes a function with the signature fn(key) -> value
, and turns it into a function that can accept the signature fn(key1, key2, ...) -> {key1: value1, key2: value2, ...}
.
These two decorators are implemented as function methods, and using them for our functions is as simple as invoking them in our declaration:
Table.implement({ set: function(key, value){ this.$storage[key] = value; return this; }.overloadSetter(), get: function(key){ return this.$storage[key]; }.overloadGetter() });
Just by adding these invocations, we've transformed our set
and get
methods into methods that take different argument types. With this declaration, our methods are now usable in the two forms we saw in the initial example.
Finally, we add the remove
method, which deletes items from the storage:
Table.implement({ remove: function(){ var storage = this.$storage; Array.from(arguments).each(function(key){ delete storage[key]; }); } });
Like our decorated get
method, the remove
method can take multiple key arguments. Unlike get
, though, we didn't decorate remove
but instead used very simple code to implement the method. First, we created a local variable storage
so we could access the internal storage object from inside our callback
function. We then used the Array.from
generic to transform the arguments
object into a true array, and finally we looped through each of the arguments using the each
method and deleted them from the storage. The result is a cleanly implemented remove
method that accepts multiple arguments.
Next on our list of methods are the three membership methods: keyOf, hasValue
, and hasKey
. The keyOf
method takes a single argument, value
, and returns the key associated with that particular value, while the hasValue
and hasKey
methods check whether a particular value or key exists in the table.
Let's implement the easiest one first: hasKey
. If you recall our discussion on objects, we learned that if we try to access a nonexistent property of an object, we'll get the undefined
value. We can therefore implement hasKey
simply by checking whether a particular key in our storage object is equal to the undefined
value:
Table.implement({ hasKey: function(key){ return this.$storage[key] !== undefined; } });
The next two methods, keyOf
and hasValue
, are trickier. Since they operate on values rather than keys, we have no way to directly access them through our storage object. What we need to do is to use a traversal loop to go through each of the items in our storage and compare their value with the argument.
The hasValue
method is easier to implement, since we'll simply have to check if the particular value exists in our storage object, regardless of which key is associated with it:
Table.implement({ hasValue: function(value){ var storage = this.$storage; for (var key in storage){ if (storage[key] === value) return true; } return false; } });
Inside our hasValue
method, we loop through each item in the storage object using a for-in
loop. For every item we go through, we compare the value of the item with the value argument passed to our method. If we get a match, we immediately halt execution of the function by returning true. If the loop finishes without finding any matches, we fall back to a return value of false.
The keyOf
method is similar to the hasValue
method, but instead of returning a boolean, we return the actual key associated with the object, or null if there's no such key:
Table.implement({ keyOf: function(value){ var storage = this.$storage; for (var key in storage){ if (storage[key] === value) return key; } return null; } });
We're almost done with our Table
type, but we need to implement a couple more methods. Thankfully, they're all similar to our previous item-traversing functions, so it will be very easy to implement them.
Two of the methods we still need to implement are keys
and values
, which deal with the keys and the values as groups. The keys
method returns all the keys of the items in a table, while the values
method returns the values of these items:
Table.implement({ keys: function(){ var storage = this.$storage; var results = []; for (var key in storage) results.push(key); return results; },
values: function(){ var storage = this.$storage; var results = []; for (var key in storage) results.push(storage[key]); return results; } });
We introduce a new variable in these methods called results
, which is an array the keys and values are pushed into. Like hasValue
or keyOf
, both methods require a traversal loop to iterate through the items in the storage object. For each pass of the traversal loop, the methods push the current key or value into the results
array. At the end of the methods, we return the results
array, which then contains the needed values.
For the length
method, we need to create a method that returns the number of items in the table. One way to do this is through another traversal loop, with an accumulator variable that is incremented with each pass through the loop. But a much simpler way is to reuse the keys
(or values
) method:
Table.implement({ length: function(){ return this.keys().length; } });
Instead of writing another traversal loop for length
, we simply invoked the keys
method to retrieve an array of keys and then return the length of the array. This shortcut is possible since the number of keys in the internal storage would be equal to the number of items it contains.
Finally, we need to implement the last method: each
. Like the array method of the same name, the each
method of our Table
type accepts a callback function that will be invoked for every item in the table as well as an optional bind
argument that will be the this
keyword value for the callback function. The callback function will receive three arguments when invoked: the current value, the current key, and the table itself.
Table.implement({ each: function(fn, bind){ var storage = this.$storage; for (var key in storage) fn.call(bind, storage[key], key, this); return this; } }); Table.alias('forEach', 'each'),
As with our previous methods, we use a for-in
loop to traverse our storage object. For each item in our storage, we invoke the callback function fn
using the call
method, binding the object value of our bind
argument and passing in the current value, the current key, and the table itself. Since we don't need to return any special value for this method, we simply return the table itself at the end of the function. We also add an alias, for the each
method called forEach
in order to conform to the native name of the method.
Now that we've implemented all the necessary methods for our Table
type, let's combine all our snippets into our final code:
function Table(){ if (instanceOf(this, Table)){ this.$storage = {}; } else { return new Table(); } }; new Type('Table', Table); Table.implement({ set: function(key, value){ this.$storage[key] = value; return this; }.overloadSetter(), get: function(key){ return this.$storage[key]; }.overloadGetter(), remove: function(){ var storage = this.$storage; Array.from(arguments).each(function(key){ delete storage[key]; }); }, hasKey: function(key){ return this.$storage[key] !== undefined; }, hasValue: function(value){ var storage = this.$storage; for (var key in storage){ if (storage[key] === value) return true; } return false; }, keyOf: function(value){ var storage = this.$storage; for (var key in storage){ if (storage[key] === value) return key; } return null; }, keys: function(){
var storage = this.$storage; var results = []; for (var key in storage) results.push(key); return results; }, values: function(){ var storage = this.$storage; var results = []; for (var key in storage) results.push(storage[key]); return results; }, length: function(){ return this.keys().length; }, each: function(fn, bind){ var storage = this.$storage; for (var key in storage) fn.call(bind, storage[key], key, this); return this; } }); Table.alias('forEach', 'each'),
It looks good, right? Using what we've learned about the Type
constructor and type objects, we were able to implement a totally new object type. The simplicity of the MooTools type system is apparent in this sample code: our code is clean and organized, the API itself is easy to follow and understand, and the results are awesome enough to warrant real-life usage.
There is, of course, room for improvement. Our Table
type is fine as it is, but still has some issues that need fixing. A particular improvement that I want you to figure out is how to limit items to only those that are directly defined through set
. For example, calling table.get('constructor')
will always return a value, even if we never set a 'constructor' item. This is because our internal storage object inherits several properties and methods from Object.prototype
, and these members are also accessible to our methods. The easiest way to solve this would be to add checks using hasOwnProperty
in our methods—and I'll leave it to you to fix the code accordingly.
In this chapter, we learned about the native JavaScript type system and its MooTools counterpart. We learned what a type is exactly, as well as how JavaScript handles native types. We went through the various native types that JavaScript implements, and the rules that govern their transformation from one type to the other.
We also learned about the components of the MooTools type system and type objects in particular, which are class-like objects that are used to manage and enhance native types. We discovered how we can easily extend native types, and we topped it off by implementing our own custom type
This chapter certainly discusses quite a handful of topics, and I hope it is a fitting finale for part one of this book. We've gone through a lot of stuff in the past six chapters, from functions and objects to classes and types, and I expect that, at this point, you've already acquired the knowledge you'll need to understand much more complicated JavaScript code.
If your understanding is still a bit shaky, though, I advise you to reread this and the previous chapters. We're going to start exploring more complicated terrain in the next chapters, and unless you're comfortable with what we've already studied, you might have a hard time understanding parts two and three of this book.
But if you think that you're ready to trek onward, better put on your hiking boots because we're about to explore exotic lands crafted outside the ECMAScript specification.
Ready? Then turn the page so we can begin our exploration of JavaScript in the browser.