996 lines
37 KiB
Plaintext
996 lines
37 KiB
Plaintext
@c This document is part of the ease.js manual
|
|
@c Copyright (c) 2011 Mike Gerwitz
|
|
@c Permission is granted to copy, distribute and/or modify this document
|
|
@c under the terms of the GNU Free Documentation License, Version 1.3
|
|
@c or any later version published by the Free Software Foundation;
|
|
@c with no Invariant Sections, no Front-Cover Texts, and no Back-Cover
|
|
@c Texts. A copy of the license is included in the section entitled ``GNU
|
|
@c Free Documentation License''.
|
|
|
|
@node Classes
|
|
@chapter Working With Classes
|
|
In Object-Oriented programming, the most common term you are likely to encounter
|
|
is ``Class''. A @dfn{class} is like a blueprint for creating an @dfn{object},
|
|
which is an @dfn{instance} of that class. Classes contain @dfn{members}, which
|
|
include primarily properties and methods. A @dfn{property} is a value, much like
|
|
a variable, that a class ``owns''. A @dfn{method}, when comparing with
|
|
JavaScript, is a function that is ``owned'' by a class. As a consequence,
|
|
properties and methods are not part of the global scope.
|
|
|
|
JavaScript does not support classes in the manner traditionally understood by
|
|
Object-Oriented programmers. This is because JavaScript follows a different
|
|
model which instead uses prototypes. Using this model, JavaScript supports
|
|
basic instantiation and inheritance. Rather than instantiating classes,
|
|
JavaScript instantiates constructors, which are functions. The following example
|
|
illustrates how you would typically create a class-like object in JavaScript:
|
|
|
|
@float Figure, f:class-js
|
|
@verbatim
|
|
/**
|
|
* Declaring "classes" WITHOUT ease.js
|
|
*/
|
|
|
|
// our "class"
|
|
var MyClass = function()
|
|
{
|
|
this.prop = 'foobar';
|
|
}
|
|
|
|
// a class method
|
|
MyClass.prototype.getProp = function()
|
|
{
|
|
return this.prop;
|
|
};
|
|
|
|
// create a new instance of the class and execute doStuff()
|
|
var foo = new MyClass();
|
|
console.log( foo.getProp() ); // outputs "foobar"
|
|
@end verbatim
|
|
@caption{Basic ``Class'' in JavaScript @emph{without} using ease.js}
|
|
@end float
|
|
|
|
This gets the job done, but the prototypal paradigm has a number of limitations
|
|
amongst its incredible flexibility. For Object-Oriented programmers, it's both
|
|
alien and inadequate. That is not to say that it is not useful. In fact, it is
|
|
so flexible that an entire Object-Oriented framework was able to be built atop
|
|
of it.
|
|
|
|
ease.js aims to address the limitations of the prototype model and provide a
|
|
familiar environment for Object-Oriented developers. Developers should not have
|
|
to worry about @emph{how} classes are implemented in JavaScript (indeed, those
|
|
details should be encapsulated). You, as a developer, should be concerned with
|
|
only how to declare and use the classes. If you do not understand what a
|
|
prototype is, that should be perfectly fine. You shouldn't need to understand it
|
|
in order to use the library (though, it's always good to understand what a
|
|
prototype is when working with JavaScript).
|
|
|
|
In this chapter and those that follow, we will see the limitations that ease.js
|
|
addresses. We will also see how to declare the classes using both prototypes and
|
|
ease.js, until such a point where prototypes are no longer adequate.
|
|
|
|
@menu
|
|
* Declaring Classes:: Learn how to declare a class with ease.js
|
|
* Inheritance:: Extending classes from another
|
|
* Member Visibility:: Encapsulation is a core concept of Object-Oriented
|
|
programming
|
|
@end menu
|
|
|
|
|
|
@node Declaring Classes
|
|
@section Declaring Classes
|
|
We just took a look at what it's like declaring a class using prototypes
|
|
(@pxref{f:class-js,}). This method is preferred for many developers, but it is
|
|
important to recognize that there is a distinct difference between Prototypal
|
|
and Object-Oriented development models. As an Object-Oriented developer, you
|
|
shouldn't concern yourself with @emph{how} a class is declared in JavaScript. In
|
|
true OO fashion, that behavior should be encapsulated. With ease.js, it is.
|
|
|
|
Let's take a look at how to declare that exact same class using ease.js:
|
|
|
|
@float Figure, f:class-easejs
|
|
@verbatim
|
|
// if client-side, use: var Class = easejs.Class;
|
|
var Class = require( 'easejs' ).Class;
|
|
|
|
var MyClass = Class(
|
|
{
|
|
'public prop': 'foobar',
|
|
|
|
'public getProp': function()
|
|
{
|
|
return this.prop;
|
|
}
|
|
} );
|
|
|
|
// create a new instance of the class and execute doStuff()
|
|
var foo = MyClass();
|
|
console.log( foo.getProp() ); // outputs "foobar"
|
|
@end verbatim
|
|
@caption{Basic anonymous class declaration using ease.js}
|
|
@end float
|
|
|
|
That should look much more familiar to Object-Oriented developers. There are a
|
|
couple important notes before we continue evaluating this example:
|
|
|
|
@itemize
|
|
@item
|
|
The first thing you will likely notice is our use of the @code{public} keyword.
|
|
This is optional (the default visibility is public), but always recommended.
|
|
Future versions of ease.js may provide warnings when the visibility is omitted.
|
|
We will get more into visibility later on.
|
|
|
|
@item
|
|
Unlike @ref{f:class-js,}, we do not use the @code{new} keyword in order to
|
|
instantiate our class. You are more than welcome to use the @code{new} keyword
|
|
if you wish, but it is optional when using ease.js. This is mainly because
|
|
without this feature, if the keyword is omitted, the constructor is called as a
|
|
normal function, which could have highly negative consequences. This style of
|
|
instantiation also has its benefits, which will be discussed later on.
|
|
|
|
@item
|
|
ease.js's class module is imported using @code{require()} in the above example.
|
|
If using ease.js client-side (@pxref{Client-Side Include}), you can instead use
|
|
@samp{var Class = easejs.Class}. From this point on, importing the module will
|
|
not be included in examples.
|
|
@end itemize
|
|
|
|
The above example declares an anonymous class, which is stored in the
|
|
variable @var{MyClass}. By convention, we use CamelCase, with the first letter
|
|
capital, for class names (and nothing else).
|
|
|
|
@menu
|
|
* Anonymous vs. Named Classes::
|
|
* Constructors:: How to declare a constructor
|
|
* Temporary Classes:: Throwaway classes that only need to be used once
|
|
* Temporary Instances:: Throwaway instances that only need to be used once
|
|
@end menu
|
|
|
|
@node Anonymous vs. Named Classes
|
|
@subsection Anonymous vs. Named Classes
|
|
We state that @ref{f:class-easejs,} declared an @dfn{anyonmous class} because
|
|
the class was not given a name. Rather, it was simply assigned to a variable,
|
|
which itself has a name. To help keep this idea straight, consider the common
|
|
act of creating anonymous functions in JavaScript:
|
|
|
|
@float Figure, f:anon-func
|
|
@verbatim
|
|
// anonymous
|
|
var myFunc = function() {};
|
|
|
|
// named
|
|
function myNamedFunc() {};
|
|
@end verbatim
|
|
@caption{Anonymous functions in JavaScript}
|
|
@end float
|
|
|
|
If the function itself is not given a name, it is considered to be anonymous,
|
|
even though it is stored within a variable. Just as the engine has no idea what
|
|
that function is named, ease.js has no idea what the class is named because it
|
|
does not have access to the name of the variable to which it was assigned.
|
|
|
|
Names are not required for classes, but they are recommended. For example,
|
|
consider what may happen when your class is output in an error message.
|
|
|
|
@float Figure, f:anon-err
|
|
@verbatim
|
|
// call non-existent method
|
|
foo.baz();
|
|
|
|
// TypeError: Object #<anonymous> has no method 'baz'
|
|
@end verbatim
|
|
@caption{Anonymous classes do not make for useful error messages}
|
|
@end float
|
|
|
|
If you have more than a couple classes in your software, that error message is
|
|
not too much help. You are left relying on the stack trace to track down the
|
|
error. This same output applies to converting a class to a string or viewing it
|
|
in a debugger. It is simply not helpful. If anything, it is confusing. If you've
|
|
debugged large JS applications that make liberal use of anonymous functions, you
|
|
might be able to understand that frustration.
|
|
|
|
Fortunately, ease.js permits you to declare a named class. A @dfn{named class}
|
|
is simply a class that is assigned a string for its name, so that error
|
|
messages, debuggers, etc provide more useful information. @emph{There is
|
|
functionally no difference between named and anonymous classes.}
|
|
|
|
@float Figure, f:class-named
|
|
@verbatim
|
|
var MyFoo = Class( 'MyFoo', {} ),
|
|
foo = MyFoo();
|
|
|
|
// call non-existent method
|
|
foo.baz();
|
|
|
|
// TypeError: Object #<MyFoo> has no method 'baz'
|
|
@end verbatim
|
|
@caption{Declaring an empty @emph{named} class}
|
|
@end float
|
|
|
|
Much better! We now have a useful error message and immediately know which class
|
|
is causing the issue.
|
|
|
|
@node Constructors
|
|
@subsection Constructors
|
|
In JavaScript, we are used to functions themselves being a constructor because
|
|
only constructors can be instantiated. With ease.js, constructors are handled in
|
|
a manner similar to most other languages, by providing a separate method. The
|
|
implementation ease.js chose is very similar to that of PHP's
|
|
(@pxref{Constructor Implementation}).
|
|
|
|
@float Figure, f:constructor
|
|
@verbatim
|
|
var Foo = Class( 'Foo',
|
|
{
|
|
'public __construct': function( name )
|
|
{
|
|
console.log( 'Hello, ' + name + '!' );
|
|
}
|
|
} );
|
|
|
|
// instantiate the class, invoking the constructor
|
|
Foo( 'World' );
|
|
|
|
// Output:
|
|
// Hello, World!
|
|
@end verbatim
|
|
@caption{Declaring constructors using ease.js}
|
|
@end float
|
|
|
|
When the class is instantiated, the constructor is invoked, permitting you do to
|
|
any necessary initialization tasks before the class can be used. The constructor
|
|
operates exactly how you would expect a constructor to in JavaScript, with one
|
|
major difference. Returning an object in the constructor does @emph{not} return
|
|
that object instead of the new class instance, since this does not make sense in
|
|
a Class-based model.
|
|
|
|
If you wish to prevent a class from being instantiated, simply throw an
|
|
exception within the constructor. This is useful if the class is intended to
|
|
provide only static methods, or if you wish to enforce a single instance
|
|
(one means of achieving a Singleton).
|
|
|
|
@float Figure, f:constructor-prevent
|
|
@verbatim
|
|
var Foo = Class( 'Foo',
|
|
{
|
|
'public __construct': function( name )
|
|
{
|
|
throw Error( "Cannot instantiate class Foo" );
|
|
}
|
|
} );
|
|
@end verbatim
|
|
@caption{Prevent class from being instantiated}
|
|
@end float
|
|
|
|
Constructors are optional. By default, nothing is done after the class is
|
|
instantiated.
|
|
|
|
@node Temporary Classes
|
|
@subsection Temporary Classes
|
|
In @ref{f:class-easejs,}, we saw that the @code{new} keyword was unnecessary
|
|
when instantiating classes. This permits a form of shorthand that is very useful
|
|
for creating @dfn{temporary classes}, or ``throwaway`` classes which are used
|
|
only once.
|
|
|
|
Consider the following example:
|
|
|
|
@float Figure, f:class-tmp
|
|
@verbatim
|
|
// new instance of anonymous class
|
|
var foo = Class(
|
|
{
|
|
'public bar': function()
|
|
{
|
|
return 'baz';
|
|
}
|
|
} )();
|
|
|
|
foo.bar(); // returns 'baz'
|
|
@end verbatim
|
|
@caption{Declaring a temporary (throwaway) class}
|
|
@end float
|
|
|
|
In @ref{f:class-tmp,} above, rather than declaring a class, storing that in a
|
|
variable, then instantiating it separately, we are doing it in a single command.
|
|
Notice the parenthesis at the end of the statement. This invokes the
|
|
constructor. Since the @code{new} keyword is unnecessary, a new instance of the
|
|
class is stored in the variable @var{foo}.
|
|
|
|
We call this a temporary class because it is used only to create a single
|
|
instance. The class is then never referenced again. Therefore, we needn't even
|
|
store it - it's throwaway.
|
|
|
|
The downside of this feature is that it is difficult to notice unless the reader
|
|
is paying very close attention. There is no keyword to tip them off. Therefore,
|
|
it is very important to clearly document that you are storing an instance in the
|
|
variable rather than an actual class definition. If you follow the CamelCase
|
|
convention for class names, then simply do not capitalize the first letter of
|
|
the destination variable for the instance.
|
|
|
|
@node Temporary Instances
|
|
@subsection Temporary Instances
|
|
Similar to @ref{Temporary Classes,}, you may wish to use an @emph{instance}
|
|
temporarily to invoke a method or chain of methods. @dfn{Temporary instances}
|
|
are instances that are instantiated in order to invoke a method or chain of
|
|
methods, then are immediately discarded.
|
|
|
|
@float Figure, f:inst-tmp
|
|
@verbatim
|
|
// retrieve the name from an instance of Foo
|
|
var name = Foo().getName();
|
|
|
|
// method chaining
|
|
var car = VehicleFactory().createBody().addWheel( 4 ).addDoor( 2 ).build();
|
|
|
|
// temporary class with callback
|
|
HttpRequest( host, port ).get( path, function( data )
|
|
{
|
|
console.log( data );
|
|
} );
|
|
|
|
// Conventionally (without ease.js), you'd accomplish the above using the
|
|
// 'new' keyword. You may still do this with ease.js, though it is less
|
|
// clean looking.
|
|
( new Foo() ).someMethod();
|
|
@end verbatim
|
|
@caption{Declaring a temporary (throwaway) class}
|
|
@end float
|
|
|
|
Rather than storing the class instance, we are using it simply to invoke
|
|
methods. The results of those methods are stored in the variable rather than the
|
|
class instance. The instance is immediately discarded, since it is no longer
|
|
able to be referenced, and is as such a temporary instance.
|
|
|
|
In order for method chaining to work, each method must return itself.
|
|
|
|
This pattern is useful for when a class requires instantiation in order to
|
|
invoke a method. Classes that intend to be frequently used in this manner should
|
|
declare static methods so that they may be accessed without the overhead of
|
|
creating a new class instance.
|
|
|
|
|
|
@node Inheritance
|
|
@section Inheritance
|
|
Inheritance can be a touchy subject among Object-Oriented developers. It is a
|
|
powerful feature that is easily abused. @dfn{Inheritance} is the term used to
|
|
describe the process of creating a @dfn{child} class that @dfn{extends}
|
|
(inherits members from) another @dfn{parent} class. The parent class is also
|
|
referred to as the @dfn{supertype} and the child is called the @dfn{subtype}.
|
|
Let's consider the following example, where we have a dog, a lazy dog, and a dog
|
|
that walks on two legs:
|
|
|
|
@float Figure, f:inheritance-ex
|
|
@image{img/inheritance-ex}
|
|
@caption{Basic inheritance example}
|
|
@end float
|
|
|
|
In the above example, we would say that @var{LazyDog} and @var{TwoLeggedDog}
|
|
are @emph{subtypes} of @var{Dog}, and that @var{Dog} is the @emph{supertype}
|
|
of the two. We describe inheritance as an ``is a'' relationship. That is:
|
|
|
|
@itemize
|
|
@item
|
|
@var{LazyDog} is a @var{Dog}.
|
|
|
|
@item
|
|
@var{TwoLeggedDog} is also a @var{Dog}.
|
|
|
|
@item
|
|
@var{Dog} is @emph{not} a @var{LazyDog} or a @var{TwoLeggedDog}.
|
|
@end itemize
|
|
|
|
Subtypes @dfn{inherit} all public and protected members of their supertypes
|
|
(@pxref{Member Visibility}). This means that, in the case of our above example,
|
|
the @code{walk()} and @code{bark()} methods would be available to our subtypes.
|
|
If the subtype also defines a method of the same name, as was done above, it
|
|
will @dfn{override} the parent functionality. For now, we will limit our
|
|
discussion to public members. How would we represent these classes using
|
|
ease.js?
|
|
|
|
@float Figure, f:inheritance
|
|
@verbatim
|
|
// our parent class (supertype)
|
|
var Dog = Class( 'Dog',
|
|
{
|
|
'public walk': function()
|
|
{
|
|
console.log( 'Walking the dog' );
|
|
},
|
|
|
|
'public bark': function()
|
|
{
|
|
console.log( 'Woof!' );
|
|
}
|
|
} );
|
|
|
|
// subclass (child), as a named class
|
|
var LazyDog = Class( 'LazyDog' ).extend( Dog,
|
|
{
|
|
'public walk': function()
|
|
{
|
|
console.log( 'Lazy dog refuses to walk.' );
|
|
}
|
|
} );
|
|
|
|
// subclass (child), as an anonymous class
|
|
var TwoLeggedDog = Dog.extend(
|
|
{
|
|
'public walk': function()
|
|
{
|
|
console.log( 'Walking the dog on two feet' );
|
|
}
|
|
} );
|
|
@end verbatim
|
|
@caption{Inheritance in ease.js}
|
|
@end float
|
|
|
|
You should already understand how to define a class (@pxref{Declaring Classes}).
|
|
The above example introduced two means of @dfn{extending} classes -- defining a
|
|
new class that inherits from a parent:
|
|
|
|
@table @strong
|
|
@item Named Subclasses
|
|
@var{LazyDog} is defined as a @emph{named} subclass
|
|
(@pxref{Anonymous vs. Named Classes}). This syntax requires the use of
|
|
@samp{Class( 'Name' )}. The @code{extend()} method then allows you to extend
|
|
from an existing class by passing the class reference in as the first argument.
|
|
|
|
@item Anonymous Subclasses
|
|
@var{TwoLeggedDog} was declared as an @emph{anonymous} subclass. The syntax for
|
|
this declaration is a bit more concise, but you forfeit the benefits of named
|
|
classes (@pxref{Anonymous vs. Named Classes}). In this case, you can simply call
|
|
the supertype's @code{extend()} method. Alternatively, you can use the
|
|
@samp{Class.extend( Base, @{@} )} syntax, as was used with the named subclass
|
|
@var{LazyDog}.
|
|
@end table
|
|
|
|
You are @emph{always} recommended to use the named syntax when declaring classes
|
|
in order to provide more useful error messages. If you are willing to deal with
|
|
the less helpful error messages, feel free to use anonymous classes for their
|
|
conciseness.
|
|
|
|
@menu
|
|
* Understanding Member Inheritance:: How to work with inherited members
|
|
* Overriding Methods:: Overriding inherited methods
|
|
* Type Checks and Polymorphism:: Substituting similar classes for
|
|
one-another
|
|
@end menu
|
|
|
|
@node Understanding Member Inheritance
|
|
@subsection Understanding Member Inheritance
|
|
In @ref{f:inheritance}, we took a look at how to inherit from a parent class.
|
|
What does it mean when we ``inherit'' from a parent? What are we inheriting? The
|
|
answer is: the API.
|
|
|
|
There are two types of APIs that subtypes can inherit from their parents:
|
|
|
|
@table @emph
|
|
@item Public API
|
|
This is the API that is accessible to everyone using your class. It contains all
|
|
public members. We will be focusing on public members in this chapter.
|
|
|
|
@item Protected API
|
|
Protected members make up a protected API, which is an API available to
|
|
subclasses but @emph{not} the outside world. This is discussed more in the
|
|
Member Visibility section (@pxref{Member Visibility}), so we're going to leave
|
|
this untouched for now.
|
|
@end table
|
|
|
|
When a subtype inherits a member from its parent, it acts almost as if that
|
|
member was defined in the class itself@footnote{This statement is not to imply
|
|
that inheritance is a case of copy-and-paste. There are slight variations, which
|
|
are discussed in more detail in the Member Visibility section (@pxref{Member
|
|
Visibility}).}. This means that the subtype can use the inherited members as if
|
|
they were its own (keep in mind that members also include properties). This
|
|
means that we @emph{do not} have to redefine the members in order to use them
|
|
ourselves.
|
|
|
|
@var{LazyDog} and @var{TwoLeggedDog} both inherit the @code{walk()} and
|
|
@code{bark()} methods from the @var{Dog} supertype. Using @var{LazyDog} as an
|
|
example, let's see what happens when we attempt to use the @code{bark()} method
|
|
inherited from the parent.
|
|
|
|
@float Figure, f:using-inherited-members
|
|
@verbatim
|
|
var LazyDog = Class( 'LazyDog' ).extend( Dog,
|
|
{
|
|
/**
|
|
* Bark when we're poked
|
|
*/
|
|
'public poke': function()
|
|
{
|
|
this.bark();
|
|
}
|
|
} );
|
|
|
|
// poke() a new instance of LazyDog
|
|
LazyDog().poke();
|
|
|
|
// Output:
|
|
// Woof!
|
|
@end verbatim
|
|
@caption{Using inherited members}
|
|
@end float
|
|
|
|
In @ref{f:using-inherited-members} above, we added a @code{poke()} method to our
|
|
@var{LazyDog} class. This method will call the @code{bark()} method that was
|
|
inherited from @var{Dog}. If we actually run the example, you will notice that
|
|
the dog does indeed bark, showing that we are able to call our parent's method
|
|
even though we did not define it ourselves.
|
|
|
|
@node Overriding Methods
|
|
@subsection Overriding Methods
|
|
When a method is inherited, you have the option of either keeping the parent's
|
|
implementation or overriding it to provide your own. When you override a method,
|
|
you replace whatever functionality was defined by the parent. This concept was
|
|
used to make our @var{LazyDog} lazy and our @var{TwoLeggedDog} walk on two legs
|
|
in @ref{f:inheritance}.
|
|
|
|
After overriding a method, you may still want to invoke the parent's method.
|
|
This allows you to @emph{augment} the functionality rather than replacing it
|
|
entirely. ease.js provides a magic @code{__super()} method to do this. This
|
|
method is defined only for the overriding methods and calls the parent method
|
|
that was overridden.
|
|
|
|
In order to demonstrate this, let's add an additional subtype to our hierarchy.
|
|
@var{AngryDog} will be a subtype of @var{LazyDog}. Not only is this dog lazy,
|
|
but he's rather moody.
|
|
|
|
@float Figure, f:super-method
|
|
@verbatim
|
|
var AngryDog = Class( 'AngryDog' ).extend( LazyDog,
|
|
{
|
|
'public poke': function()
|
|
{
|
|
// augment the parent method
|
|
console.log( 'Grrrrrr...' );
|
|
|
|
// call the overridden method
|
|
this.__super();
|
|
}
|
|
} );
|
|
|
|
// poke a new AngryDog instance
|
|
AngryDog().poke();
|
|
|
|
// Output:
|
|
// Grrrrrr...
|
|
// Woof!
|
|
@end verbatim
|
|
@caption{Using @code{__super()} method}
|
|
@end float
|
|
|
|
If you remember from @ref{f:using-inherited-members}, we added a @code{poke()}
|
|
method to @var{LazyDog}. In @ref{f:super-method} above, we are overriding this
|
|
method so that @var{AngryDog} growls when you poke him. However, we still want
|
|
to invoke @var{LazyDog}'s default behavior when he's poked, so we also call the
|
|
@code{__super()} method. This will also make @var{AngryDog} bark like
|
|
@var{LazyDog}.
|
|
|
|
It is important to note that @code{__super()} must be invoked like any other
|
|
method. That is, if the overridden method requires arguments, you must pass them
|
|
to @code{__super()}. This allows you to modify the argument list before it is
|
|
sent to the overridden method.
|
|
|
|
@node Type Checks and Polymorphism
|
|
@subsection Type Checks and Polymorphism
|
|
The fact that the API of the parent is inherited is a very important detail. If
|
|
the API of subtypes is guaranteed to be @emph{at least} that of the parent, then
|
|
this means that a function expecting a certain type can also work with any
|
|
subtypes. This concept is referred to as @dfn{polymorphism}, and is a very
|
|
powerful aspect of Object-Oriented programming.
|
|
|
|
Let's consider a dog trainer. A dog trainer can generally train any type of dog
|
|
(technicalities aside), so it would stand to reason that we would want our dog
|
|
trainer to be able to train @var{LazyDog}, @var{AngryDog}, @var{TwoLeggedDog},
|
|
or any other type of @var{Dog} that we may throw at him/her.
|
|
|
|
@float Figure, f:polymorphism-uml
|
|
@image{img/composition-uml}
|
|
@caption{Class structure to demonstrate polymorphism}
|
|
@end float
|
|
|
|
Type checks are traditionally performed in JavaScript using the
|
|
@code{instanceOf} operator. While this can be used in most inheritance cases
|
|
with ease.js, it is not recommended. Rather, you are encouraged to use ease.js's
|
|
own methods for determining instance type@footnote{The reason for this will
|
|
become clear in future chapters. ease.js's own methods permit checking for
|
|
additional types, such as Interfaces.}. Support for the @code{instanceOf}
|
|
operator is not guaranteed.
|
|
|
|
Instead, you have two choices with ease.js:
|
|
|
|
@table @code
|
|
@item Class.isInstanceOf( type, instance );
|
|
Returns @code{true} if @var{instance} is of type @var{type}. Otherwise, returns
|
|
@code{false}.
|
|
|
|
@item Class.isA( type, instance );
|
|
Alias for @code{Class.isInstanceOf()}. Permits code that may read better
|
|
depending on circumstance and helps to convey the ``is a'' relationship that
|
|
inheritance creates.
|
|
@end table
|
|
|
|
For example:
|
|
|
|
@float Figure, f:instanceof-ex
|
|
@verbatim
|
|
var dog = Dog()
|
|
lazy = LazyDog(),
|
|
angry = AngryDog();
|
|
|
|
Class.isInstanceOf( Dog, dog ); // true
|
|
Class.isA( Dog, dog ); // true
|
|
Class.isA( LazyDog, dog ); // false
|
|
Class.isA( Dog, lazy ); // true
|
|
Class.isA( Dog, angry ); // true
|
|
|
|
// we must check an instance
|
|
Class.isA( Dog, LazyDog ); // false; instance expected, class given
|
|
@end verbatim
|
|
@caption{Using ease.js to determine instance type}
|
|
@end float
|
|
|
|
It is important to note that, as demonstrated in @ref{f:instanceof-ex} above, an
|
|
@emph{instance} must be passed as a second argument, not a class.
|
|
|
|
Using this method, we can ensure that the @var{DogTrainer} may only be used with
|
|
an instance of @var{Dog}. It doesn't matter what instance of @var{Dog} - be it a
|
|
@var{LazyDog} or otherwise. All that matters is that we are given a @var{Dog}.
|
|
|
|
@float Figure, f:polymorphism-easejs
|
|
@verbatim
|
|
var DogTrainer = Class( 'DogTrainer',
|
|
{
|
|
'public __construct': function( dog )
|
|
{
|
|
// ensure that we are given an instance of Dog
|
|
if ( Class.isA( Dog, dog ) === false )
|
|
{
|
|
throw TypeError( "Expected instance of Dog" );
|
|
}
|
|
}
|
|
} );
|
|
|
|
// these are all fine
|
|
DogTrainer( Dog() );
|
|
DogTrainer( LazyDog() );
|
|
DogTrainer( AngryDog() );
|
|
DogTrainer( TwoLeggedDog() );
|
|
|
|
// this is not fine; we're passing the class itself
|
|
DogTrainer( LazyDog );
|
|
|
|
// nor is this fine, as it is not a dog
|
|
DogTrainer( {} );
|
|
@end verbatim
|
|
@caption{Polymorphism in ease.js}
|
|
@end float
|
|
|
|
It is very important that you use @emph{only} the API of the type that you are
|
|
expecting. For example, only @var{LazyDog} and @var{AngryDog} implement a
|
|
@code{poke()} method. It is @emph{not} a part of @var{Dog}'s API. Therefore, it
|
|
should not be used in the @var{DogTrainer} class. Instead, if you wished to use
|
|
the @code{poke()} method, you should require that an instance of @var{LazyDog}
|
|
be passed in, which would also permit @var{AngryDog} (since it is a subtype of
|
|
@var{LazyDog}).
|
|
|
|
Currently, it is necessary to perform this type check yourself. In future
|
|
versions, ease.js will allow for argument type hinting/strict typing, which will
|
|
automate this check for you.
|
|
|
|
|
|
@node Member Visibility
|
|
@section Member Visibility
|
|
One of the major hurdles ease.js aimed to address (indeed, one of the core
|
|
reasons for its creation) was that of encapsulation. JavaScript's prototype
|
|
model provides limited means of encapsulating data. Since functions limit scope,
|
|
they may be used to mimic private members. These are often referred to as
|
|
@dfn{privileged members}. However, declaring classes in this manner can be
|
|
messy. ease.js aims to provide an elegant implementation that is both a pleasure
|
|
to work with and able to support protected members.
|
|
|
|
The term @dfn{visibility} refers to how a class member may be accessed. There
|
|
are three levels of visibility implemented by ease.js, which are listed here
|
|
from most visible to least:
|
|
|
|
@table @dfn
|
|
@item public
|
|
Accessible outside of the instance (e.g. @samp{foo.publicProp}). Inherited by
|
|
subtypes.
|
|
|
|
@item protected
|
|
Not accessible outside of the instance (only accessible by
|
|
@samp{this.protectedProp}). Inherited by subtypes.
|
|
|
|
@item private
|
|
Not accessible outside of the instance. Not inherited by subtypes.
|
|
@end table
|
|
|
|
By default, all members are public. This means that the members can be accessed
|
|
and modified from within an instance and from outside. Subtypes (classes that
|
|
inherit from it) will inherit public members. Public methods expose an API by
|
|
which users may use your class. Public properties, however, should be less
|
|
common in practice for a very important reason, which is explored throughout the
|
|
rest of this section.
|
|
|
|
@dfn{Encapsulation} is the act of hiding information within a class or instance.
|
|
Classes should be thought of black boxes. We want them to do their job, but we
|
|
do not care @emph{how} they do their job. Encapsulation takes the complexity out
|
|
of a situation and allows the developer to focus on accomplishing the task using
|
|
familiar concepts. For example, consider a class named @var{Dog} which has a
|
|
method @code{walk()}. To walk a dog, we simply call @code{Dog().walk()}. The
|
|
@code{walk()} method could be doing anything. By preventing the details of the
|
|
method from being exposed, we present the developer with a very simple
|
|
interface. Rather than the developer having to be concerned with moving each of
|
|
the dog's legs, all they have to do is understand that the dog is being walked.
|
|
|
|
Let's consider our @var{Dog} class in more detail:
|
|
|
|
@float Figure, f:encapsulation
|
|
@verbatim
|
|
Class( 'Dog',
|
|
{
|
|
'private _legs': {},
|
|
|
|
'private _body': {},
|
|
|
|
// ...
|
|
|
|
|
|
'public walk': function()
|
|
{
|
|
this.stand();
|
|
this._moveFrontLeg( 0 );
|
|
this._moveBackLeg( 1 );
|
|
this._moveFrontLeg( 1 );
|
|
this._moveBackLeg( 0 );
|
|
},
|
|
|
|
'protected stand': function()
|
|
{
|
|
if ( this.isSitting() )
|
|
{
|
|
// ...
|
|
}
|
|
},
|
|
|
|
'public rollOver': function()
|
|
{
|
|
this._body.roll();
|
|
},
|
|
|
|
'private _moveFrontLeg': function( leg )
|
|
{
|
|
this._legs.front[ leg ].move();
|
|
},
|
|
|
|
'private _moveBackLeg': function( leg )
|
|
{
|
|
this._legs.back[ leg ].move();
|
|
},
|
|
|
|
// ...
|
|
} );
|
|
@end verbatim
|
|
@caption{Encapsulating behavior of a class}
|
|
@end float
|
|
|
|
As you can see above, the act of making the dog move forward is a bit more
|
|
complicated than the developer may have originally expected. The dog has four
|
|
separate legs that need to be moved individually. The dog must also first stand
|
|
before it can be walked, but it can only stand if it's sitting. Detailed tasks
|
|
such as these occur all the time in classes, but they are hidden from the
|
|
developer using the public API. Why should the developer be concerned with all
|
|
of the legs?
|
|
|
|
As a general rule of thumb, you should use the @emph{lowest} level of visibility
|
|
possible unless you have a strong reason for increasing it. Start by declaring
|
|
everything as private and work from there.
|
|
|
|
@menu
|
|
* Private Members::
|
|
* Protected Members::
|
|
* Visibility Escalation:: Increasing visibility of inherited members
|
|
@end menu
|
|
|
|
@node Private Members
|
|
@subsection Private Members
|
|
Let's first explore private members. The majority of the members in the
|
|
@var{Dog} class (@pxref{f:encapsulation,}) are private. This is the lowest level
|
|
of visibility (and consequently the @emph{highest} level of encapsulation). By
|
|
convention, we prefix private members with an underscore. Private members are
|
|
available @emph{only to the class that defined it} and are not available outside
|
|
the class.
|
|
|
|
@float Figure, f:encapsulation-call-priv
|
|
@verbatim
|
|
var dog = Dog();
|
|
dog._moveFrontLeg( 1 );
|
|
|
|
// TypeError: Object #<Dog> has no method '_moveFrontLeg'
|
|
@end verbatim
|
|
@caption{Cannot access private members outside the class}
|
|
@end float
|
|
|
|
You will notice that the dog's legs are declared private as well
|
|
(@pxref{f:encapsulation,}). This is to ensure we look at the dog as a whole; we
|
|
don't care about what the dog is made up of. Legs, fur, tail, teeth, tongue, etc
|
|
- they are all irrelevant to our purpose. We just want to walk the dog.
|
|
Encapsulating those details also ensures that they will not be tampered with,
|
|
which will keep the dog in a consistent, predictable state.
|
|
|
|
Private members cannot be inherited. Let's say we want to make a class called
|
|
@var{TwoLeggedDog} to represent a dog that was trained to walk only on two feet.
|
|
We could approach this in a couple different ways. The first way would be to
|
|
prevent the front legs from moving. What happens when we explore that approach:
|
|
|
|
|
|
@float Figure, f:encapsulation-inherit-priv
|
|
@verbatim
|
|
var two_legged_dog = Class( 'TwoLeggedDog' ).extend( Dog,
|
|
{
|
|
/**
|
|
* This won't override the parent method.
|
|
*/
|
|
'private _moveFrontLeg': function( leg )
|
|
{
|
|
// don't do anything
|
|
return;
|
|
},
|
|
} )();
|
|
|
|
two_legged_dog.walk();
|
|
@end verbatim
|
|
@caption{Cannot override private members of supertype}
|
|
@end float
|
|
|
|
If you were to attempt to walk a @var{TwoLeggedDog}, you would find that
|
|
@emph{the dog's front legs still move}! This is because, as mentioned before,
|
|
private methods are not inherited. Rather than overriding the parent's
|
|
@var{_moveFrontLeg} method, you are instead @emph{defining a new method}, with
|
|
the name @var{_moveFrontLeg}. The old method will still be called. Instead, we
|
|
would have to override the public @var{walk} method to prevent our dog from
|
|
moving his front feet.
|
|
|
|
@node Protected Members
|
|
@subsection Protected Members
|
|
Protected members are often misunderstood. Many developers will declare all
|
|
of their members as either public or protected under the misconception that they
|
|
may as well allow subclasses to override whatever functionality they want. This
|
|
makes the class more flexible.
|
|
|
|
While it is true that the class becomes more flexible to work with for subtypes,
|
|
this is a dangerous practice. In fact, doing so @emph{violates encapsulation}.
|
|
Let's reconsider the levels of visibility in this manner:
|
|
|
|
@table @strong
|
|
@item public
|
|
Provides an API for @emph{users of the class}.
|
|
|
|
@item protected
|
|
Provides an API for @emph{subclasses}.
|
|
|
|
@item private
|
|
Provides an API for @emph{the class itself}.
|
|
@end table
|
|
|
|
Just as we want to hide data from the public API, we want to do the same for
|
|
subtypes. If we simply expose all members to any subclass that comes by, that
|
|
acts as a peephole in our black box. We don't want people spying into our
|
|
internals. Subtypes shouldn't care about the dog's implementation either.
|
|
|
|
Private members (@pxref{Private Members,Private}) should be used whenever
|
|
possible, unless you are looking to provide subtypes with the ability to access
|
|
or override methods. In that case, we can move up to try protected members.
|
|
Remember not to make a member public unless you wish it to be accessible to the
|
|
entire world.
|
|
|
|
@var{Dog} (@pxref{f:encapsulation,}) defined a single method as protected -
|
|
@code{stand()}. Because the method is protected, it can be inherited by
|
|
subtypes. Since it is inherited, it may also be overridden. Let's define another
|
|
subtype, @var{LazyDog}, which refuses to stand.
|
|
|
|
@float Figure, f:encapsulation-inherit-prot
|
|
@verbatim
|
|
var lazy_dog = Class( 'LazyDog' ).extend( Dog,
|
|
{
|
|
/**
|
|
* Overrides parent method
|
|
*/
|
|
'protected stand': function()
|
|
{
|
|
// nope!
|
|
this.rollOver();
|
|
return false;
|
|
},
|
|
} )();
|
|
|
|
lazy_dog.walk();
|
|
@end verbatim
|
|
@caption{Protected members are inherited by subtypes}
|
|
@end float
|
|
|
|
There are a couple important things to be noted from the above example. Firstly,
|
|
we are able to override the @code{walk()} method, because it was inherited.
|
|
Secondly, since @code{rollOver()} was also inherited from the parent, we are
|
|
able to call that method, resulting in an upside-down dog that refuses to stand
|
|
up, just moving his feet.
|
|
|
|
Another important detail to notice is that @code{Dog.rollOver()} accesses a
|
|
private property of @var{Dog} -- @var{_body}. Our subclass does not have access
|
|
to that variable. Since it is private, it was not inherited. However, since the
|
|
@code{rollOver()} method is called within the context of the @var{Dog} class,
|
|
the @emph{method} has access to the private member, allowing our dog to
|
|
successfully roll over. If, on the other hand, we were to override
|
|
@code{rollOver()}, our code would @emph{not} have access to that private object.
|
|
Calling @samp{this.__super()} from within the overridden method would, however,
|
|
call the parent method, which would again have access to its parent's private
|
|
members.
|
|
|
|
@node Visibility Escalation
|
|
@subsection Visibility Escalation
|
|
@dfn{Visibility escalation} is the act of increasing the visibility of a member.
|
|
Since private members cannot be inherited, this essentially means making a
|
|
protected member public.
|
|
|
|
If you override a protected method, you may increase its visibility to public
|
|
without any problems. If you follow the convention of prefixing private members
|
|
with an underscore, you may find that it's not recommended doing so for
|
|
protected members. This is because subtypes may decide to make the member
|
|
public.
|
|
|
|
In order to increase the visibility, you do have to override the member. For
|
|
properties, this has no discernible effect; you're just redefining it. For
|
|
methods, this means that you are overriding the entire body. Therefore, you will
|
|
either have to provide an alternate implementation, or call
|
|
@samp{this.__super()} to invoke the original method.
|
|
|
|
Note that @emph{you cannot go from public to protected}. This will throw an
|
|
error. You can only increase the level of visibility. This ensures that once a
|
|
class defines an API, subclasses cannot alter it. That API is forever for all
|
|
subtypes. This means that, if you are expecting a certain type, you can rest
|
|
assured that whatever you are given, even if it is a subtype, has the API you
|
|
are expecting.
|
|
|
|
Let's take a look at an example.
|
|
|
|
@float Figure, f:vis-esc
|
|
@verbatim
|
|
var Foo = Class(
|
|
{
|
|
'protected canEscalate': 'baz',
|
|
|
|
'protected escalateMe': function( arg )
|
|
{
|
|
console.log( 'In escalateMe' );
|
|
},
|
|
|
|
'public cannotMakeProtected': function()
|
|
{
|
|
}
|
|
} ),
|
|
|
|
SubFoo = Foo.extend(
|
|
{
|
|
/**
|
|
* Escalating a property means redefining it
|
|
*/
|
|
'public canEscalate': 'baz',
|
|
|
|
/**
|
|
* We can go protected -> public
|
|
*/
|
|
'public escalateMe': function( arg )
|
|
{
|
|
// simply call the parent method
|
|
this.__super( arg );
|
|
}
|
|
} );
|
|
@end verbatim
|
|
@caption{Visibility can be escalated}
|
|
@end float
|
|
|
|
Note that, in the above example, making the public @var{cannotMakeProtected}
|
|
method protected would throw an error.
|
|
|