Now that you've had a moment to ponder some of the various
inheritance possibilities, it's time to introduce the toolkit's
fundamental construct for declaring classes and simulating rich
inheritance hierarchies. Dojo keeps it simple by tucking away all of
the implementation details involved with class declarations and
inheritance behind an elegant little function in Base called dojo.declare
. This function is easy to
remember because you're loosely declaring a class
with it. Table 10-1 shows the brief
API.
Table 10-1. dojo.declare API
Name | Comment |
---|---|
| Provides a compact way
of declaring a constructor function. The |
Tip
As you might suspect, declare
builds upon the patterns provided
by functions like extend
,
mixin
, and delegate
to provide an even richer
abstraction than any one of those patterns could offer
individually.
Example 10-5
illustrates how you could use dojo.declare
to accomplish an inheritance
hierarchy between a shape and circle. For now, consider this example
as just an isolated bit of motivation. We'll discuss the finer points
momentarily.
Example 10-5. Simulating class-based inheritance with dojo.declare
// "Declare" a Shape dojo.declare( "Shape", //The class name null, //No ancestors, so null placeholds { centerX : 0, // Attributes centerY : 0, color : "", // The constructor function that gets called via "new Shape" constructor: (centerX, centerY, color) { this.centerX = centerX; this.centerY = centerY; this.color = color; } } ); // At this point, you could create an object instance through: // var s = new Shape(10, 20, "blue"); // "Declare" a Circle dojo.declare( "Circle", //The class name Shape, // The ancestor { radius : 0, // The constructor function that gets called via "new Circle" constructor: (centerX, centerY, color, radius) { // Shape's constructor is called automatically // with these same params. Note that it simply ignores // the radius param since it only used the first 3 named args this.radius = radius; //assign the Circle-specific argument } } ); // Params to the JavaScript constructor function get passed through // to dojo.declare's constructor c = new Circle(10,20,"blue",2);
Hopefully you find dojo.declare
to be readable, maintainable,
and self-explanatory. Depending on how you lay out the whitespace and
linebreaks, it even resembles "familiar" class-based programming
languages. The only thing that may have caught you off guard is that
Shape
's constructor
is called with the same
parameters that are passed into Circle
's constructor
. Still, this poses no problem
because Shape
's constructor
accepts only three named
parameters, silently ignoring any additional ones. (We'll come back to
this in a moment.)
Tip
Talking about JavaScript constructor functions that are used
with the new
operator to create
JavaScript objects as well as the special constructor
function that appears in
dojo.declare
's third parameter
can be confusing. To keep these two concepts straight, the parameter
that appears in dojo.declare
's
third parameter constructor will always be typeset with the code
font as constructor
, while
JavaScript constructor functions will appear in the normal
font.
The dojo.declare
function
provides a basic pattern for handling classes that is important to
understand because Dijit expands upon it to deliver a flexible
creation pattern that effectively automates the various tasks
entailed in creating a widget. Chapter 12 focuses on this topic
almost exclusively.
Although this chapter focuses on the constructor
function because it is by far
the most commonly used method, the following pattern shows that
there are two other functions that dojo.declare
provides: preamble
, which is kicked off before
constructor
, and postscript
, which is kicked off after
it:
preamble(/*Object*/ params, /*DOMNode*/node) //precursor to constructor constructor(/*Object*/ params, /*DOMNode*/node) // fire any superclass constructors // fire off any mixin constrctors // fire off the local class constructor, if provided postscript(/*Object*/ params, /*DOMNode*/node) // predominant use is to kick off the creation of a widget
To verify for yourself, you might run the code in Example 10-6.
Example 10-6. Basic dojo.declare creation pattern
dojo.addOnLoad(function( ) { dojo.declare("Foo", null, { preamble: function( ) { console.log("preamble", arguments); }, constructor : function( ) { console.log("constructor", arguments); }, postscript : function( ) { console.log("postscript", arguments); } }); var foo = new Foo(100); //calls through to preamble, constructor, and postscript });
The constructor
is where
most of the action happens for most class-based models, but preamble
and postscript
have their uses as well.
preamble
is primarily used to
manipulate arguments for superclasses. While the arguments that you
pass into the JavaScript constructor
function—new Foo(100)
in this case—get passed into
Foo
's preamble
, constructor
, and postscript
, this need not necessarily be
the case when you have an inheritance hierarchy. We'll revisit this
topic again in the "Advanced Argument Mangling" sidebar later in
this chapter, after inheritance gets formally introduced in the next
section. postscript
is primarily
used to kick off the creation of a widget. Chapter 12 is devoted almost entirely
to the widget lifecycle.
Let's dig a bit deeper with more in-depth examples that show
some of dojo.declare
's power.
This first example is heavily commented and kicks things off with a
slightly more advanced inheritance example highlighting an important
nuance of using dojo.declare
's
internal constructor
method:
<html> <head> <title>Fun with Inheritance!</title> <script type="text/javascript" src="http://o.aolcdn.com/dojo/1.1/dojo/dojo.xd.js"> </script> <script type="text/javascript"> dojo.addOnLoad(function() { //Plain old JavaScript Function object defined here. function Point(x,y) {} dojo.extend(Point, { x : 0, y : 0, toString : function( ) {return "x=",this.x," y=",this.y;} }); dojo.declare( "Shape", null, { //Clearly define members first thing, but initialize them all in //the Dojo constructor. Never initialize a Function object here //in this associative array unless you want it to be shared by //*all* instances of the class, which is generally not the case. //A common convention is to use a leading underscore to denote //"private" members _color: "", _owners: null, //Dojo provides a specific constructor for classes. This is it. //Note that this constructor will be executed with the very same //arguments that are passed into Circle's constructor //function -- even though we make no direct call to this //superclass constructor. constructor: function(color) { this._color = color; this._owners = [0]; //See comment below about initializing //objects console.log("Created a shape with color", this._color, "owned by", this._owners); }, getColor : function( ) {return this._color;}, addOwner : function(oid) {this._owners.push(oid);}, getOwners : function( ) {return this._owners;} //Don't leave trailing commas after the last element. Not all //browsers are forgiving (or provide meaningful error messages). //Tattoo this comment on the back of your hand. } ); //Important Convention: //For single inheritance chains, list the superclass's args first in the //subclass's constructor, followed by any subclass specific arguments. //The subclass's constructor gets called with the full argument chain, so //it gets set up properly there, and assuming you purposefully do not //manipulate the superclass's arguments in the subclass's constructor, //everything works fine. //Remember that the first argument to dojo.declare is a string and the //second is a Function object. dojo.declare( "Circle", Shape, { _radius: 0, _area: 0, _point: null, constructor : function(color,x,y,radius) { this._radius = radius; this._point = new Point(x,y); this._area = Math.PI*radius*radius; //Note that the inherited member _color is already defined //and ready to use here! console.log("Circle's inherited color is " + this._color); }, getArea: function( ) {return this._area;}, getCenter : function( ) {return this._point;} } ); console.log(Circle.prototype); console.log("Circle 1, coming up..."); c1 = new Circle("red", 1,1,100); console.log(c1.getCenter( )); console.log(c1.getArea( )); console.log(c1.getOwners( )); c1.addOwner(23); console.log(c1.getOwners( )); console.log("Circle 2, coming up..."); c2 = new Circle("yellow", 10,10,20); console.log(c2.getCenter( )); console.log(c2.getArea( )); console.log(c2.getOwners( )); }); </script> </head> <body> </body> </html>
Warning
Trailing commas will most likely hose you outside of Firefox, so take extra-special care not to accidentally leave them hanging around. Some programming languages like Python allow trailing commas; if you frequently program in one of those languages, take added caution.
You should notice the output shown in Figure 10-1 in the Firebug console when you run this example.
An important takeaway is that a Function object exists in
memory as soon as the dojo.declare
statement has finished
executing, an honest-to-goodness Function object exists behind the
scenes, and its prototype contains everything that was specified in
the third parameter of the dojo.declare
function. This object serves
as the prototypical object for all objects created in the future.
This subtlety can be tricky business if you're not fully cognizant
of it, and that's the topic of the next section.
As you know, a Point
has
absolutely nothing to do with Dojo. It's a plain old JavaScript
Function object. As such, however, you must not initialize it
inline with other properties inside of Shape
's associative array. If you do
initialize it inline, it will behave somewhat like a static member
that is shared amongst all future Shape
objects that are created—and
this can lead to truly bizarre behavior if you're not
looking out for it.
The issue arises because behind the scenes declare
mixes all of the properties into
the Object
's prototype
and prototype
properties are shared amongst
all instances. For immutable types like numbers or strings,
changing the property results in a local change. For mutable types
like Object
and Array
, however, changing the property in
one location promulgates it. The issue can be reduced as
illustrated in the snippet of code in Example 10-7.
Example 10-7. Prototype properties are shared amongst all instances
function Foo( ) {} Foo.prototype.bar = [100]; //create two Foo instances foo1 = new Foo; foo2 = new Foo; console.log(foo1.bar); // [100] console.log(foo2.bar); // [100] // This statement modifies the prototype, which is shared by all object instances... foo1.bar.push(200); //...so both instances reflect the change. console.log(foo1.bar); // [100,200] console.log(foo2.bar); // [100,200]
To guard against ever even thinking
about making the mistake of inadvertently initializing a
nonprimitive data type inline, perform all of your
initialization—even initialization for primitive types—inside of
the standard Dojo constructor
,
and maintain a consistent style. To keep your class as readable as
possible, it's still a great idea to list all of the class
properties inline and provide additional comments where it
enhances understanding.
To illustrate the potentially disastrous effect on the
working example, make the following changes indicated in bold to
your Shape
class and take a
look at the console output in Firebug:
//...snip... dojo.declare("Shape", null, { _color: null, //_owners: null, _owners: [0], //this change makes the _owners member //behave much like a static! constructor : function(color) { this._color = color; //this._owners = [0]; console.log("Created a shape with color ",this._colora " owned by ", this._owners); }, getColor : function( ) {return this._color;}, addOwner : function(oid) {this._owners.push(oid);}, getOwners : function( ) {return this._owners;} } ); //...snip...
After you make this change and refresh the page in Firefox, you'll see the output shown in Figure 10-2 in the Firebug Console.
In class-based object-oriented programming, a common pattern
is to override a superclass method in a subclass and then call the
inherited superclass method before performing any custom
implementation in the subclass. Though not always the case, it's
common that the superclass's baseline implementation still needs
to run and that the subclass is offering existing implementation
on top of that baseline. Any class created via dojo.declare
has access to a special
inherited method that, when called, invokes the corresponding
superclass method to override. (Note that the constructor
chain is called
automatically without needing to use inherited
.)
Example 10-8 illustrates this pattern.
Example 10-8. Calling overridden superclass methods from a subclass
dojo.addOnLoad(function( ) { dojo.declare("Foo", null, { constructor : function( ) { console.log("Foo constructor", arguments); }, custom : function( ) { console.log("Foo custom", arguments); } }); dojo.declare("Bar", Foo, { constructor : function( ) { console.log("Bar constructor", arguments); }, custom : function( ) { //automatically call Foo's 'custom' method and pass in the same arguments, //though you could juggle them if need be this.inherited(arguments); //without this call, Foo.custom would never get called console.log("Bar custom", arguments); } }); var bar = new Bar(100); bar.custom(4,8,15,16,23,42); });
And here's the corresponding Firebug output on the console:
Foo constructor [100] Bar constructor [100] Foo custom [4, 8, 15, 16, 23, 42] Bar custom [4, 8, 15, 16, 23, 42]
Get Dojo: The Definitive Guide now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.