JavaScript inheritance in details
One of the most powerful features offered by object oriented programming (OOP) is inheritance (or ability to create subclasses of particular class). On the one hand, JavaScript does not involve full-fledged support of classes such as one offered by C++ or Java. On the other hand, it supports constructors which are able to create objects with the use of executing code which allocates storage for the objects and initializes these objects (or part of them) by assigning initial values to their properties. There are several possible ways to implement inheritance in JavaScript which are widely known and used. All of them have their own advantages and disadvantages. However, there is an additional approach for implementing inheritance in JavaScript, which is supported by corresponding JavaScript library. That library is named JSINER (originated from JavaScript INheritance supportER). Below we consider implementation of JavaScript inheritance proposed by JSINER as well as several other common ways of inheritance implementation in JavaScript. "Lazy inheritance" is a design pattern used in JavaScript computer programming. It designates a "postponed linking" of an object with it's prototype (class) until it is needed. If used properly, such approach may increase efficiency, simplicity and flexibility of OOP based code written using JavaScript. Since lazy inheritance is called only for instance creation, it seems logical to combine process of class prototype creation with resolving necessary dependencies of that class. The instances of objects in lazy inheritance are created in "mixed" mode — on first invocation, a factory is used to modify class prototype which is later used to create subsequent object instances. The main features of "Lazy inheritance" approach are:
Here we start investigating patterns implemented by JSINER. But before digging into details, let's consider existing approaches used to implement inheritance. JavaScript supports prototype-based inheritance. Each object constructor has a prototype property which is used to implement prototype-based inheritance and shared properties. Every object created by that constructor has an implicit reference to the prototype associated with its constructor. Such approach allows writing object oriented code in JavaScript. The following fragment of code illustrates the common way to do this. Let's assume that the following code is defined in person_cl.js JavaScript file and declares Person class // Person's constructor. function Person(aName) { this.fName = aName; } // Here we define function which returns // name of person and assign that function to // Person class prototype. Person.prototype.getName = function() { return this.fName; }; // Here we declare function toString() // that returns string representation of // Person and assign that function // to Person class prototype Person.prototype.toString = function() { return "Person: " + this.getName(); }; Let's declare another class, Employee which is inherited from Person one. Let's assume that declaration of that class is placed to employee_cl.js JavaScript file. // Employee constructor function Employee(aName, aUID) { this.fName = aName; this.fUID = aUID; } // The inheritance definition. // Here we create instance of Person and define // that is inherited from Person, // specify constructor for Employee and // add superClass property to Employee class Employee.prototype = new Person(); Employee.prototype.constructor = Employee; Employee.superClass = Person.prototype; // Here we declara function which returns UID // of employee and assing it to Employee class // prototype Employee.prototype.getUID = function() { return this.fUID; }; // Here we redeclare definition of // toString() function which should be used by // Employee class. Our declaration calls function // from Person class and adds own Employee // related information Employee.prototype.toString = function() { var person = Employee.superClass.toString.call(this); return this.getUID() + ":" + person; }; And this is sample HTML page which uses declarations of JavaScript object above: <html> <head> <title>Classical JavaScript inheritance test.</title> // Order of references to scripts // is important for proper code // execution. <script src="script/person_cl.js"></script> <script src="script/employee_cl.js"></script> </head> <body> <script language="JavaScript1.2"> var employee = new Employee('John Doe', 1212); alert(employee); </script> </body> </html> Everything is seems to be quite straightforward in these examples so far. However, such a "classical" implementation of inheritance has several disadvantages:
There is another way of creating several objects with the same structure in JavaScript offered by Douglas Crockford. This approach assumes replacing prototype based inheritance for prototypal appoach using factory method instead of constructor. Such a factory method is responsible for creation of new object instance. Generally speaking, such approach does not represent pure inheritance, since it is oriented to creation JavaScript instances with the same properties, rather than classes. However, it is intended to solve tasks which are close to ones solved by inheritance, and that's why we consider it there. The following code snippets illustrate prototypal approach in more details. We declare the same example classes - Person and Employee. First, define Person using prototypal approach. Let's assume that declaration of that class is placed to person_pp.js JavaScript file. // First, we define function which represents // a factory for creation of Person objects function Person(aName) { var self = PROTOTYPAL.object(); self.fName = aName; self.toString = Person.toString; self.getName = Person.getName; return self; } // Here we declare function which // will return name of Person and // adding it to Person object Person.getName = function() { return this.fName; }; // Here we declare function which returns // representation of the Person object and // assign it to Person object Person.toString = function() { return "Person: " + this.getName(); }; Let's declare another class, Employee which is inherited from Person one. Let's assume that declaration of that class is placed to employee_pp.js JavaScript file. // Here we define factory which will be used to // create Employee that extend Person function Employee(aName, aUID) { // here we call method from library (prototypal.js) // which supports prototypal inheritance var self = PROTOTYPAL.object( Person(aName) ); self.fUID = aUID; self.getUID = Employee.getUID; self.toString = Employee.toString; return self; } // Here we declare function which returns // representation of the Employee object and // assign it to Employee object Employee.toString = function() { return this.getUID() + ":" + this.getName(); }; // Here we declare function which // will return UID of Employee and // adding it to Employee object Employee.getUID = function() { return this.fUID; }; And this is sample HTML page which uses declarations of JavaScript object above: <html> <head> <title>Prototypal JavaScript inheritance test.</title> // We also need to include base prototypal // related code <script src="script/prototypal.js"></script> // Order of references to scripts // is important for proper code // execution. <script src="script/person_pp.js"></script> <script src="script/employee_pp.js"></script> </head> <body> <script language="JavaScript1.2"> var employee = new Employee('John Doe', 1212); alert(employee); </script> </body> </html> Comparing to "classical" inheritance, the prototypal based approach has significant advantages, but also several significant drawbacks:
JSINER' offers approach which simplifies writing object oriented code with support of prototype based inheritance. Unlike of "classical" approach, it represents a "Lazy inheritance" because reference to parent class is invoked only at the moment of object instance creation. Basically, it's possible to say that instances of objects in such approach are created in "mixed" mode — on first invocation, an appropriate factory is used to modify object constructor prototype which is later used for subsequent object instances creation. Moreover, since "Lazy inheritance" is called only once at the moment of first object instance creation, it becomes possible to combine process of class prototype creation with resolving necessary dependencies of that class. In other words, the process of prototype construction also allows loading scripts (if ones were not loaded before) which particular class depends on. Such approach to inheritance support has the following benefits:
The following code snippets illustrate JSINER approach in more details. Again, we declare the same example classes - Person and Employee. First, we define Person. Let's assume that declaration of that class is placed to person.js JavaScript file. // Constructor for Person class function Person(aName) { this.fName = aName; } // Function which returns name of Person's. // That function is assigned to prototype of // Person class similarly to "classical" // inheritance. Person.prototype.getName = function() { return this.fName; }; // Here we define function which returns // string representation of Person // assign it to prototype of Person // class (again, similar to classical // approach) Person.prototype.toString = function() { return "Person: " + this.getName(); }; No we illustrate how Person may be extended using couple of examples. Example 1 - "inheritance only". Here we assume that the code below is placed to employee1.js file. // Here we define constructor for Employee // class and declare that Employee class // inherits Person one function Employee(aName, aUID) { // lazy inheritance calling var self = JSINER.extend(this, "Person"); self.fName = aName; self.fUID = aUID; return self; } // Here we define function which return // UID of employee and assign it to // prototype of Employee class. // This is exact approach as one used by // classical inheritance Employee.prototype.getUID = function() { return this.fUID; }; // Here we define function which // returns string representation of // Employee object and assign it // to Employee class prototype // (as if classical approach is used). // Our implementation combines // string representation of Person object // and own data from Employee class Employee.prototype.toString = function() { var person = Employee.superClass.toString.call(this); return this.getUID() + ":" + person; }; And this is sample HTML page which uses declarations of JavaScript object above: <html> <head> <title>JSINER lazy JavaScript inheritance test.</title> // We need to include reference to // JSNIER code <script src="script/jsiner.js"></script> // Now it's not necessary to // maintain order of scripts loading // manually. <script src="script/employee1.js"></script> <script src="script/person.js"></script> </head> <body> <script language="JavaScript1.2"> var employee = new Employee('John Doe', 1212); alert(employee); </script> </body> </html> During execution of example of code listed above, lazy inheritance is called only once for first creation of Employee instance. It is not necessary to keep proper order of script files declaration in HTML document. Example 2 - "inheritance and resolving references" Here we assume that the code below is placed to employee2.js file. // Here we inform JSINER that Employee class depends // on person script. Actually, the "person" // string does not represent a script name, but // rather some key which is associated with // actual script. JSINER allows specifying // mapping functionality which is responsible // to determine actual name of script using // given key. // However, for simplicity of examples, // here it's assumed that resolver considers // key of script as name of script file. JSINER.addDependency( {Employee:"person"} ); // Exactly as in example 1, we define constructor // for Employee class. In general, all code // below is the same as one used in // Example 1 function Employee(aName, aUID) { var self = JSINER.extend(this, "Person"); self.fName = aName; self.fUID = aUID; return self; } Employee.prototype.getUID = function() { return this.fUID; }; Employee.prototype.toString = function() { var person = Employee.superClass.toString.call(this); return this.getUID() + ":" + person; }; This is sample HTML page which uses declarations of JavaScript object for Example 2: <html> <head> <title>JSONER lazy JavaScript inheritance test.</title> // We need to include reference to // JSNIER code <script src="script/jsiner.js"></script> // Note that we include reference // to script that contains Employee class only <script src="script/employee2.js"></script> // And no it's not required // to reference person.js script explicitely - // JSINER will resolve and load it automatically <!--<script src="script/person.js"></script>--> </head> <body> <script language="JavaScript1.2"> var employee = new Employee('John Doe', 1212); alert(employee); </script> </body> </html> During execution of example code listed above, linkage of Employee and Person as well as loading of necessary script "person.js" is performed automatically with the first creation of Employee instance. As it is illustrated by examples above, proposed "Lazy inheritance" pattern is similar to Prototypal approach, but:
In general, the proposed scheme considers "lazy" loading as primary mode for loading particular scripts. However, this is not strict requirement — it is also possible to force loading of all scripts which form particular application before starting objects creation if this way of loading is required by particular application architecture. Since "lazy" inheritance approach is non-intrusive, it could be used in combination with other inheritance pattern. However, it's necessary to remember that simultaneous use of several types of inheritance within the same project or application can be very confusing and non-convenient for development, support and maintenance. Here hightlight some details of inheritance and dependencies resolving in JSINER library. "Lazy inheritance" is invoked within the Object' constructor code and is called only once for first object instance creation. After invocation, the following steps are performed by JSINER library:
There steps are illustrated by diagram below. Please click on the image to enlarge. The derived instance of the Object may be modified in the constructor according to particular logic. For example, some properties of the object may be defined. The resulting object is the same JavaScript object as it would be created with classical prototype-based inheritance. As the matter of thumb, there is no "silver bullet" solution and proposed approach does not pretend to be universal way of implementing inheritance in JavaScript. Of course, choice of inheritance type declaration is dictated by particular needs and specifics of particular application. However, the approach described there has several significant advantages that allow simplifying of writing object oriented code in JavaScript. |
|