Simulating Classes with Dojo

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

dojo.declare (/*String*/ className,

/*Function|Function[]*/ superclass,

/*Object*/ props)

Provides a compact way of declaring a constructor function. The className provides the name of the constructor function that is created, superclass is either a single Function object ancestor or an Array of Function object ancestors that are mixed in, and props provides an object whose properties are copied into the constructor function's prototype.

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 Basic Class Creation Pattern

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.

A Single Inheritance Example

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.

Firebug output from

Figure 10-1. Firebug output from Example 10-6

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.

A common gotcha with prototype-based inheritance

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.

Firebug output

Figure 10-2. Firebug output

Calling an inherited method

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.