Home » Guides Advanced Article

Correct OOP for Javascript

3.7/5.0 (3 votes total)
Rate:

Shelby H. Moore III
October 23, 2006


Shelby H. Moore III
Shelby H. Moore III has written 1 articles for JavaScriptSearch.
View all articles by Shelby H. Moore III...

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.


Add commentAdd comment (Comments: 1)  
Title: Is each method copied to each instance? January 24, 2007
Comment by MDeibert

I'm huge fan of this JS OOP approach. I'm using your inheritance technique now and it works very well. However, I'm no JS expert so I can't tell if every instance of an object instantiated this way has copies of each method. Does that make sense? When you put object methods in .prototype, there is only one instance of the method regardless of how many new objects you create. Is this the same here?

Mark :-)

Advertisement

Partners

Related Resources

Other Resources

arrow