11.2. Appendix B: Advanced JavaScript tutorial.

<< Click to Display Table of Contents >>

Navigation:  11. Appendix >

11.2. Appendix B: Advanced JavaScript tutorial.

 

It’s not required to read this section in order to understand how to create new powerful Script-based-Anatella Actions.

 

You can safely skip this section, if you are not interested in advanced & specific notions related to the JavaScript language.

 

For Java and C++ programmers, arguably the most difficult aspect of JavaScript is its object model. JavaScript is an object-based object-oriented language, setting it apart from C++, C#, Java, Simula, and Smalltalk, which are all class-based. Instead of a class concept, JavaScript provides us with lower-level mechanisms that let us achieve the same results.

 

The first mechanism that lets us implement classes in JavaScript is that of a constructor. A constructor is a function that can be invoked using the new operator. For example, here is a constructor for a Shape object:

 

function Shape(x, y) {

   this.x = x;

   this.y = y;

}

 

The Shape constructor has two parameters and initializes the new object's x and y properties (member variables) based on the values passed to the constructor. The this keyword refers to the object being created. In JavaScript, an object is essentially a collection of properties; properties can be added, removed, or modified at any time. A property is created the first time it is set, so when we assign to this.x and this.y in the constructor, the x and y properties are created as a result.

 

A common mistake for C++ and Java developers is to forget the this keyword when accessing object properties. In the preceding example, this would have been unlikely, because a statement such as x = x would have looked very suspicious, but in other examples this would have led to the creation of spurious additional variables.

 

To create (or “instantiate”) a new Shape object, we use the new operator as follows:

 

var shape = new Shape(10, 20);

 

If we use the typeof operator on the shape variable, we obtain Object, not Shape, as the data type. If we want to determine whether an object has been created using the Shape constructor, we can use the instanceof operator:

 

var array = new Array(100);

array instanceof Shape;            // returns false

 

var shape = new Shape(10, 20);

shape instanceof Shape;            // returns true

 

JavaScript lets any function serve as a constructor. However, if the function doesn't perform any modifications to the “this” object, it doesn't make much sense to invoke it as a constructor. Conversely, a constructor can be invoked as a plain function, but again this rarely makes sense.

 

In addition to the primitive data types, JavaScript provides built-in constructors that let us instantiate fundamental object types, notably Array, Date, and RegExp. Other constructors correspond to the primitive data types, allowing us to create objects that store primitive values. The valueOf() member function lets us retrieve the primitive value stored in the object. For example:

 

var boolObj = new Boolean(true);

typeof boolObj;                    // returns "object"

 

var boolVal = boolObj.valueOf();

typeof boolVal;                    // returns "boolean"

 

We have seen how to define a constructor in JavaScript and how to add member variables to the constructed object. Normally, we also want to define member functions. Because functions are treated as first-class citizens in JavaScript, this turns out to be surprisingly easy. Here's a new version of the Shape constructor, this time with two member functions, “manhattanPos()” and “translate()”:

 

function Shape(x, y) {

   this.x = x;

   this.y = y;

   this.manhattanPos = function() {

       return Math.abs(this.x) + Math.abs(this.y);

   };

   this.translate = function(dx, dy) {

       this.x += dx;

       this.y += dy;

   };

}

 

 

We can then invoke the member functions using the . (dot) operator:

 

var shape = new Shape(10, 20);

shape.translate(100, 100);

print(shape.x + ", " + shape.y + " (" + shape.manhattanPos() + ")");

 

With this approach, each Shape instance has its own “manhattanPos” and “translate” properties.

 

Our “shape” object possesses several properties (the “manhattanPos” and “translate” functions for example). These properties should be identical for all Shape instances, it is desirable to store them only once rather than in every instance. JavaScript lets us achieve this by using a prototype. A prototype is an object that serves as a fallback for other objects, providing an initial set of properties. One advantage of this approach is that it is possible to change the prototype object at any time and the changes are immediately reflected in all objects that were created with that prototype. Consider the following example:

 

function Shape(x, y) {

   this.x = x;

   this.y = y;

}

 

Shape.prototype.manhattanPos = function() {

   return Math.abs(this.x) + Math.abs(this.y);

};

 

Shape.prototype.translate = function(dx, dy) {

   this.x += dx;

   this.y += dy;

};

 

In this version of Shape, we create the “manhattanPos” and “translate” properties outside the constructor, as properties of the “Shape.prototype” object. When we instantiate a Shape, the newly created object keeps an internal pointer back to “Shape.prototype”. Whenever we retrieve the value of a property that doesn't exist in our Shape object, the property is looked up in the prototype as a fallback. Thus, the Shape prototype is the ideal place to put member functions, which should be shared by all Shape instances.

 

It might be tempting to put all sorts of properties that we want to share between Shape instances in the prototype, similar to C++'s static member variables or Java's class variables. This idiom works for read-only properties (including member functions) because the prototype acts as a fallback when we retrieve the value of a property. However, it doesn't work as expected when we try to assign a new value to the shared variable; instead, a fresh variable is created directly in the Shape object, shadowing any property of the same name in the prototype. This asymmetry between read and write access to a variable is a frequent source of confusion for novice JavaScript programmers.

 

In class-based languages such as C++ and Java, we can use class inheritance to create specialized object types. For example, we would define a Shape class and then derive Triangle, Square, and Circle from Shape. In JavaScript, a similar effect can be achieved using prototypes. The following example shows how to define Circle objects that are also Shape instances:

 

function Shape(x, y) {

   this.x = x;

   this.y = y;

}

 

Shape.prototype.area = function() { return 0; };

 

function Circle(x, y, radius) {

   Shape.call(this, x, y);

   this.radius = radius;

}

 

Circle.prototype = new Shape;

Circle.prototype.area = function() {

   return Math.PI * this.radius * this.radius;

};

 

We start by defining a Shape constructor and associate an “area()” function with it, which always returns 0. Then we define a Circle constructor, which calls the "base class" constructor using the “call()” function defined for all function objects (including constructors), and we add a radius property. Outside the constructor, we set the Circle's prototype to be a Shape object, and we override Shape's “area()” function with a Circle-specific implementation. This corresponds to the following C++ code:

 

class Shape

{

public:

   Shape(double x, double y) {

       this->x = x;

       this->y = y;

   }

   virtual double area() const { return 0; }

   double x,y;

};

 

class Circle : public Shape

{

public:

   Circle(double x, double y, double radius) : Shape(x, y)

   {

       this->radius = radius;

   }

   double area() const { return M_PI * radius * radius; }

   double radius;

};

 

This corresponds to the following Java code:

 

class Shape

{

   public Shape(double _x, double _y) {

       x = _x;

       y = _y;

   }

   public double area() { return 0; }

   double x,y;

};

 

class Circle extends Shape

{

   Circle(double _x, double _y, double _radius)

   {

       super(_x,_y);

       radius = _radius;

   }

 

   double area(){ return 3.1415 * radius * radius; }

 

   double radius;

};

 

The instanceof operator walks through the prototype chain to determine which constructors have been invoked. As a consequence, instances of a subclass are also considered to be instances of the base class:

 

var circle = new Circle(0, 0, 50);

circle instanceof Circle;          // returns true

circle instanceof Shape;           // returns true

circle instanceof Object;          // returns true

circle instanceof Array;           // returns false

 

David Flanagan's “JavaScript: The Definitive Guide (O'Reilly, 2006)” is recommended both as a tutorial and as a reference manual.