JavaScript Mapping for Classes

On this page:

Basic JavaScript Mapping for Classes

A Slice class is mapped to a JavaScript type with the same name. For each Slice data member, the JavaScript instance contains a corresponding property (just as for structures and exceptions). Consider the following class definition:

Slice
class TimeOfDay {
    short hour;         // 0 - 23
    short minute;       // 0 - 59
    short second;       // 0 - 59
    string format();    // Return time as hh:mm:ss
};

The Slice compiler generates the following code for this definition:

JavaScript
TimeOfDay = function(hour, minute, second) { ... }
TimeOfDay.prototype = new Ice.Object();
TimeOfDay.prototype.constructor = TimeOfDay;
// Nothing generated for format()...

There are a number of things to note about the generated code:

  1. The prototype for generated type TimeOfDay inherits from Ice.Object. This means that all Slice classes implicitly inherit from Ice.Object, which is the ultimate ancestor of all classes. Note that Ice.Object is not the same as Ice.ObjectPrx. In other words, you cannot pass a class where a proxy is expected and vice versa.
  2. The generated type provides a constructor that accepts a value for each data member.
  3. The generated type defines a property for each Slice data member.
  4. The generated type's prototype does not include any definitions for the class' operations.

There is quite a bit to discuss here, so we will look at each item in turn.

Inheritance from Ice.Object in JavaScript

As for Slice interfaces, the generated type for a Slice class implicitly inherits from a common base type, Ice.Object. However, as shown in the illustration below, the types inherit from Ice.Object instead of Ice.ObjectPrx (which is at the base of the inheritance hierarchy for proxies). As a result, you cannot pass a class where a proxy is expected (and vice versa) because the base types for classes and proxies are not compatible.


Inheritance from Ice.ObjectPrx and Ice.Object.

Ice.Object defines a number of member functions:

JavaScript
Object.prototype.ice_isA = function(s, current) { ... }
Object.prototype.ice_ping = function(current) { }
Object.prototype.ice_ids = function(current) { ... }
Object.prototype.ice_id = function(current) { ... }
Object.prototype.ice_preMarshal = function() { }
Object.prototype.ice_postMarshal = function() { }

The member functions of Ice.Object behave as follows:

  • ice_isA
    This function returns true if the object supports the given type ID, and false otherwise.
  • ice_ping
    As for interfaces, ice_ping provides a basic reachability test for the class.
  • ice_ids
    This function returns a string sequence representing all of the type IDs supported by this object, including ::Ice::Object.
  • ice_id
    This function returns the actual run-time type ID for a class. If you call ice_id through a reference to a base instance, the returned type ID is the actual (possibly more derived) type ID of the instance.
  • ice_preMarshal
    The Ice run time invokes this function prior to marshaling the object's state, providing the opportunity for a subtype to validate its declared data members.
  • ice_postUnmarshal
    The Ice run time invokes this function after unmarshaling an object's state. A subtype typically overrides this function when it needs to perform additional initialization using the values of its declared data members.

Class Constructors in JavaScript

The type generated for a Slice class provides a constructor that initializes each data member to a default value appropriate for its type:

Data Member TypeDefault Value
stringEmpty string
enumFirst enumerator in enumeration
structDefault-constructed value
NumericZero
boolFalse
sequenceNull
dictionaryNull
class/interfaceNull

If you wish to ensure that data members of primitive and enumerated types are initialized to specific values, you can declare default values in your Slice definition. The constructor initializes each of these data members to its declared value instead.

The constructor accepts one argument for each member of the class. This allows you to create and initialize an instance in a single statement, for example:

JavaScript
var tod = new TimeOfDayI(14, 45, 00); // 14:45pm

For derived classes, the constructor requires an argument for every member of the class, including inherited members. For example, consider the the definition from Class Inheritance once more:

Slice
class TimeOfDay {
    short hour;         // 0 - 23
    short minute;       // 0 - 59
    short second;       // 0 - 59
};

class DateTime extends TimeOfDay {
    short day;          // 1 - 31
    short month;        // 1 - 12
    short year;         // 1753 onwards
};

The constructors generated for these classes are similar to the following:

JavaScript
var TimeOfDay = function(hour, minute, second)
{
    this.hour = hour;
    this.minute = minute;
    this.second = second;
};

var DateTime = function(hour, minute, second, day, month, year)
{
    TimeOfDay.call(this, hour, minute, second);
    this.day = day;
    this.month = month;
    this.year = year;
};

Pass undefined as the value of any optional data member that you wish to remain unset.

Class Data Members in JavaScript

By default, data members of classes are mapped exactly as for structures and exceptions: for each data member in the Slice definition, the generated type defines a corresponding property.

Optional data members use the same mapping as required data members, but an optional data member can also be set to undefined to indicate that the member is unset. A well-behaved program must compare an optional data member to undefined before using the member's value:

JavaScript
var v = ...
if(v.optionalMember === undefined)
    console.log("optionalMember is unset")
else
    console.log("optionalMember =", v.optionalMember)

Class Operations in JavaScript

Operations of classes are mapped to methods in the generated type. This means that, if a class contains operations (such as the format operation of our TimeOfDay class), you must provide an implementation of those operations in a type that is derived from the generated type. For example:

JavaScript
var TimeOfDayI = function(hour, minute, second)
{
    TimeOfDay.call(this, hour, minute, second);
};
TimeOfDayI.prototype = new TimeOfDay();
TimeOfDayI.prototype.constructor = TimeOfDayI;

TimeOfDayI.prototype.format = function(current)
{
    var result = "";
    result += (this.hour < 10) ? "0" + this.hour : this.hour;
    result += ":";
    result += (this.minute < 10) ? "0" + this.minute : this.minute;
    result += ":";
    result += (this.second < 10) ? "0" + this.second : this.second;
    return result;
};

Note that Ice provides an equivalent but much more compact way of defining class implementations:

JavaScript
var TimeOfDayI = Ice.Class(TimeOfDay, {
    format: function(current) { ... }
});

The Ice.Class constructor function creates a new type, establishes the prototype relationship (if a base type is specified), and adds each defined method to the new type's prototype.

Class Factories in JavaScript

Having created a type such as TimeOfDayI, we have an implementation and we can instantiate TimeOfDayI objects, but we cannot receive it as the return value or as an out-parameter from an operation invocation. To see why, consider the following simple interface:

Slice
interface Time {
    TimeOfDay get();
};

When a client invokes the get operation, the Ice run time must instantiate and return an instance of TimeOfDay. However, TimeOfDay is conceptually an abstract type because it declares operations and therefore it cannot be instantiated. Unless we tell it, the Ice run time cannot magically know that we have created a TimeOfDayI type that implements the abstract format operation of the TimeOfDay abstract type. In other words, we must provide the Ice run time with a factory that knows that the TimeOfDay abstract type has a TimeOfDayI concrete implementation. The Ice::Communicator interface provides us with the necessary operations:

Slice
module Ice {
    local interface ObjectFactory {
        Object create(string type);
        void destroy();
    };

    local interface Communicator {
        void addObjectFactory(ObjectFactory factory, string id);
        ObjectFactory findObjectFactory(string id);
        // ...
    };
};

To supply the Ice run time with a factory for our TimeOfDayI type, we must define an object that implements the create and destroy methods:

JavaScript
var factory = {
    create: function(type)
    {
        if(type === TimeOfDay.ice_staticId())
        {
            return new TimeOfDayI();
        }
        return null;
    },
    destroy: function()
    {
        // Nothing to do
    }
};

The factory's create function is called by the Ice run time when it needs to create a TimeOfDay instance. The factory's destroy function is called by the Ice run time when its communicator is destroyed.

The create function is passed the type ID of the class to instantiate. For our TimeOfDay class, the type ID is "::M::TimeOfDay". Our implementation of create checks the type ID: if it matches, the function instantiates and returns a TimeOfDayI object. For other type IDs, the function returns null because it does not know how to instantiate other types of objects.

Note that we used the ice_staticId function to obtain the type ID rather than embedding a literal string. Using a literal type ID string in your code is discouraged because it can lead to errors that are only detected at run time. For example, if a Slice class or one of its enclosing modules is renamed and the literal string is not changed accordingly, a receiver will fail to unmarshal the object and the Ice run time will raise NoObjectFactoryException. By using ice_staticId instead, we avoid any risk of a misspelled or obsolete type ID, and we can more easily discover whether a Slice class or module has been renamed.

Given a factory implementation, such as the one above, we must inform the Ice run time of the existence of the factory:

JavaScript
var communicator = ...;
communicator.addObjectFactory(factory, TimeOfDay.ice_staticId());

Now, whenever the Ice run time needs to instantiate an object with the type ID "::M::TimeOfDay", it calls the create function of the registered factory.

The destroy operation of the object factory is invoked by the Ice run time when the communicator is destroyed. This gives you a chance to clean up any resources that may be used by your factory. Do not call destroy on the factory while it is registered with the communicator — if you do, the Ice run time has no idea that this has happened and, depending on what your destroy implementation is doing, may cause undefined behavior when the Ice run time tries to next use the factory.

The run time guarantees that destroy will be the last call made on the factory, that is, create will not be called once destroy has been called.

Note that you cannot register a factory for the same type ID twice: if you call addObjectFactory with a type ID for which a factory is registered, the Ice run time throws an AlreadyRegisteredException.

Finally, keep in mind that if a Slice class has only data members, but no operations, you need not create and register an object factory to transmit instances of such a class. Only if a class has operations do you have to define and register an object factory.

See Also