JavaScript Guides Advanced
Correct OOP for Javascript
Incorrect
Among the numerous articles on the topic of inheritance and OOP (Object Oriented Programming) using Javascript, many of them share a fundamental flaw.
For example, let's analyze the the first source code example in the page authored by Gavin Kistner (http://phrogz.net/JS/Classes/OOPinJS2.html). Note, as of Jan 2006, Gavin's page is linked prominently from the main Mozilla Developer Documentation page for Javascript.
To prove this fundamental flaw, simply add the following example code to Gavin's first example:
var myPet2 = new Cat('Felix2'); alert('myPet2 is '+myPet2); // results in 'myPet is [Cat "Felix2"]' myPet2.haveABaby(); // calls a method inherited from Mammal alert(myPet2.offspring.length); // shows that the cat has two babies now alert(myPet2.offspring[1]); // results in '[Mammal "Baby Felix2"]'
Note that myPet2 then reports that it has two offspring (babies), but it should only have one. The reason is because:
Cat.prototype = new Mammal();
places a single instance of Mammal in the prototype chain of Cat. Thus, any instances of Cat will modify the same single instance offspring property in Mammal. The parent class's instance members become prototype members, which are shared by all instances.
Correct
The fix to this flaw employs the "masking effect" of the order in which Javascript searches for elements (properties and methods) of an object. When resolving object.identifier, where object.identifier is a reference to a Javascript data type (e.g. Function, Object, Number, or String), then Javascript does the equivalent of:
function resolve( identifier, object ) { for( var element in object ) { if( element == identifier ) { return object.element; } } if( object.constructor.prototype != null ) { return resolve( identifier, object.constructor.prototype ) } return "undefined"; }
Thus, if identifier exists in the child class, then it will "mask" any duplicate in the prototype chain. Thus, an obvious "bandaid" to the Gavin's example is to add an offspring property to Cat:
function Cat(name){ this.name=name; this.offspring=[]; }
But this defeats the purpose of inheritance. If we need to know the internal datastructure of Mammal (the parent class) in order to implement Cat (the child class), then we don't have data encapsulation and thus we don't have OOP (Object Oriented Programming). Also note that name argument of the constructor of Cat is not passed to Mammal's constructor. This violates the data encapsulation of constructor of Mammal, as it assumes the constructor does nothing more than assignment to the name property on construction.
Others have pointed the way to a generalized solution (fm.dept-z.com/index.asp?get=/Resources/OOP_with_ECMAScript/Inheritance), which maintains data encapsulation, which is also mentioned in Mozilla's Javascript documentation for Function.call and Function.apply:
function Cat( name ) { Mammal.call( this, name ); }
The above code is calling the Mammal constructor function and executing it in the scope of (this is) the new Cat object, which creates all elements of Mammal in Cat object.
Details
In addition to executing the parent's constructor in the scope of the object of the child's constructor, we also have to include the parent's prototype in the child's prototype hierarchy:
Cat.prototype = new Mammal();
However, Mammal expects a name argument. Depending on what the parent's constructor does, it might generate errors if the expected arguments are not provided. We could solve this either by passing dummy argument(s) and modifying the parent constructor to not accept typeof() == "undefined" argument(s), or by modifying the parent constructor to handle typeof() == "undefined" argument(s). Thus, we provide the equivalent of a default constructor:
function Mammal(name){ if( typeof( name ) == "undefined" ) { name = ""; } this.name=name; this.offspring=[]; }
By default, Javascript sets the prototype of the child constructor to an empty Object, but with the child's constructor:
Cat.prototype = new Object(); Cat.prototype.constructor = Cat;
When an object of the child's constructor is created, Javascript does the equivalent of:
var object = new Cat(); object.constructor = Cat.prototype.constructor;
Yet when assigning an object to a prototype, Javascript does the equivalent of:
var object = new Mammal(); Cat.prototype = object; Cat.prototype.constructor = object.constructor;
Thus, it is important to insure that the child constructor's prototype.constructor is set to the child constructor:
Cat.prototype = new Mammal(); Cat.prototype.constructor = Cat;
Improved
The execution of the parent's constructor in the scope of the object of the child's constructor can be encapsulated by adding a convenience method to the Object.prototype:
Object.prototype.Inherits = function( parent ) { // Apply parent's constructor to this object if( arguments.length > 1 ) { // Note: 'arguments' is an Object, not an Array parent.apply( this, Array.prototype.slice.call( arguments, 1 ) ); } else { parent.call( this ); } }
Cat.prototype = new Mammal(); Cat.prototype.constructor = Cat; function Cat( name ) { this.Inherits( Mammal, name ); }
And the prototype inheritance can be encapsulated by adding a convenience method to the Function.prototype:
Function.prototype.Inherits = function( parent ) { this.prototype = new parent(); this.prototype.constructor = this; }
Cat.Inherits( Mammal ); function Cat( name ) { this.Inherits( Mammal, name ); }
Summary
Simply declare the following methods once:
Object.prototype.Inherits = function( parent ) { if( arguments.length > 1 ) { parent.apply( this, Array.prototype.slice.call( arguments, 1 ) ); } else { parent.call( this ); } }
Function.prototype.Inherits = function( parent ) { this.prototype = new parent(); this.prototype.constructor = this; }
Then declare inheritance easily:
Cat.Inherits( Mammal ); function Cat( name ) { this.Inherits( Mammal, name ); }
ColoredCat.Inherits( Cat ); function ColoredCat( name, color ) { this.Inherits( Cat, name ); }
Lion.Inherits( ColoredCat ); function Lion( name ) { this.Inherits( ColoredCat, name, "gold" ); }
And don't forget to implement a default constructor within each constructor function, by handling input arguments whose typeof() is "undefined".
Copyright (c) 2006, Shelby H. Moore III.
—
JavaScripts Guides: Beginner, Advanced
JavaScripts Tutorials: Beginner, Advanced
[Home] [Templates] [Blog] [Forum] [Directory] JavaScript Guides Advanced -
Correct OOP for Javascript
|