Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Delegation vs Inheritance in JavaScript

DZone's Guide to

Delegation vs Inheritance in JavaScript

· Web Dev Zone
Free Resource

Start coding today to experience the powerful engine that drives data application’s development, brought to you in partnership with Qlik.

When asked what he might do differently if he had to rewrite Java from scratch, James Gosling suggested that he might do away with class inheritance and write a delegation only language.

Using inheritance as a vehicle for code reuse is a bit like ordering a happy meal because you wanted the plastic toy. Sure a circle is a shape and a dog is a mammal – but once we get past those textbook examples most of our hierarchies get arbitrary and tenuous – built for manipulating behaviour even as we pretend we are representing reality. Successive descendants are saddled with an ever increasing number of unexpected or irrelevant behaviours for the sake of re-using a few.

Delegation is a technique that promotes code reuse by allowing runtime function invocation in the context of a specific instance – regardless of the hierarchical lineage of instance and function. JavaScript has excellent support for Delegation in the form of call and apply which lets us inject an object into the this value of any function. This permits unfeterred code sharing, free from the constraints of unwieldy, unnatural and overly complex hierarchies.

I’m going to demonstrate, by way of a use case, how call and apply can promote a clean, functional approach code to re-use. Then I’ll discuss how the ES 5 specification enables re-use of built-in functions by formalizing the concept of generic functions.

Custom Function Delegation

Suppose we need a Rectangle object for a drawing app. Lets create it the old fashioned way using new and constructor.

var Rectangle = function(left, top, length, width, options) {
02	    this.left = left;
03	    this.top = top;
04	    this.length = length;
05	    this.width = width;
06	    if (options) {
07	        this.color = options.color;
08	        this.border = options.border;
09	        this.opacity = options.opacity;
10	        //... etc.
11	    }
12	}
13	 
14	var myRectangle = new Rectangle(10, 10, 30, 20, {color:'#FAFAFA', opacity:0.7});



We’re also going to need to know if the rectangle overlaps with another. We’ll add this function to the prototype:

Rectangle.prototype.overlaps = function(another) {
02	    var r1x1 = this.left,
03	        r1x2 = this.left + this.width,
04	        r1y1 = this.top,
05	        r1y2 = this.top + this.height,
06	        r2x1 = another.left,
07	        r2x2 = another.left + another.width,
08	        r2y1 = another.top,
09	        r2y2 = another.top + another.height;       
10	 
11	    return (r1x2 >= r2x1) && (r1y2 >= r2y1) && (r1x1 <= r2x2) && (r1y1 <= r2y2);
12	}
13	 
14	myRectangle.overlaps(myOtherRectangle);


Now suppose elsewhere in our app we have a dashboard which renders a bunch of dashlets. We would like to know whether these dashlets overlap one another. We could use inheritance – have Dashlet’s prototype inherit from Rectangle. But dashlet instances are now encumbered by a set of irrelevant attributes: opacity, color (and other typical drawing functions like rotate, scale and skew). Think obfuscation. Think memory footprint. Moreover, if inheritance is our thing, there may be more suitable candidates to extend from, such as ContentFrame or Portlet.

Think about it…all we really want to do is see whether two dashlets overlap. Assuming a dashlet has attributes for left, top, width and height (or even if we have to derive them), delegation fulfills the same goal with a much lighter footprint:

Rectangle.prototype.overlaps.call(dashlet1, dashlet2);


We can even compare two object literals in this way. Here’s the entire script so you can test it:

var Rectangle = function(left, top, length, width, options) {
02	    //whatever...
03	}
04	 
05	Rectangle.prototype.overlaps = function(another) {
06	    var r1x1 = this.left,
07	        r1x2 = this.left + this.width,
08	        r1y1 = this.top,
09	        r1y2 = this.top + this.height,
10	        r2x1 = another.left,
11	        r2x2 = another.left + another.width,
12	        r2y1 = another.top,
13	        r2y2 = another.top + another.height;       
14	 
15	    return (r1x2 >= r2x1) && (r1y2 >= r2y1) && (r1x1 <= r2x2) && (r1y1 <= r2y2));
16	}
17	 
18	Rectangle.prototype.overlaps.call(
19	    {left: 10, top: 10, width 12, height: 6},
20	    {left: 8, top: 15, width 9, height: 16});
21	//true
22	Rectangle.prototype.overlaps.call(
23	    {left: 10, top: 10, width 12, height: 6},
24	    {left: 8, top: 25, width 9, height: 16});
25	//false;


Generic Functions

This is all great, but wouldn’t it be nice to inject instances into built in functions too? Unfortunately many built in functions are designed to throw a TypeError if the this value is not of the specified type:

Date.prototype.getMilliseconds.apply({year:2010});
2	//TypeError: Date.prototype.getMilliseconds called on incompatible Object

Fortunately the EcmaScript 5 specification formalizes the concept of generic functions. These are functions that, by design, allow the this value to be of any type. For example we can invoke String’s search method in the context of an Array.

var hasNumbers = "".search.call(['a','b','c'],/[0-9]/) > -1;


I’ve catalogued the entire list of built-in generic functions at the end of the article. First lets go through some examples by type:

Generic methods of Array.prototype
toString, toLocaleString, concat, join, pop, push, reverse, shift, slice, sort, splice, unshift, indexOf, lastIndexOf, every, some, forEach, map, filter, reduce, reduceRight

Most of these functions will convert this to an Object before invoking, so if we are using a String as the context, those functions that directly manipulate the argument (e.g. push and shift) will surprise the user by returning an Object. However some of Array’s other generic functions work well with Strings:

[].forEach.apply("javascript",[function(char) {console.log("give me a " + char.toUpperCase())}]);
02	//give me a J
03	//give me a A
04	//etc...
05	 
06	var increment = function(char) {return String.fromCharCode(char.charCodeAt(0)+1)};
07	var hiddenMammal = [].map.call('rhinocerous',increment).join(''); // "sijopdfspvt"
08	 
09	var myObj = {'0':'nil', '1':'one', length:2};
10	[].push.call(myObj,'two');
11	myObj; //{'0':'nil', '1':'one', '2':'two' length:3}


Generic methods of String.prototype
charAt, charCodeAt, concat, indexOf, lastIndexOf, localeCompare, match, replace, search, splice, split, substring, toLowerCase, toLocaleLowerCase, toUpperCase, to LocaleLowerCase, trim, substr

Most of these functions will convert the this object to a String before invoking. Thus if we are injecting an Array as context we will need to convert the result back to an Array at the end using split.

"".trim.apply([" a","b "]).split(",");
02	//["a","b"]
03	 
04	"".toLowerCase.apply(["DIV","H1","SPAN"]).split(",");
05	//["div","h1","span"]
06	 
07	"".match.call(["a16","b44","b bar"],/[a-z][0-9]+/g);
08	//["a16", "b44"]
09	 
10	"".replace.call(
11	    ['argentina','brazil','chile'],
12	    /\b./g, function(a){ return a.toUpperCase(); }
13	).split(',');
14	//['Argentina',"Brazil","Chile"]


Generic methods of Date.prototype
toJSON

This method requires the this value to have a toISOString method.

Object.prototype.toString
OK not strictly a generic function (since every first-class object is an Object – a type error can never be thrown on call or apply – unless using ES 5 strict mode), nevertheless this is a great candidate for demonstrating the power of delegation.

Since the early days of JavaScript, developers have struggled over the best way to determine if an object is an Array. The water-tight solution has only recently seen mainstream adoption and it leverages the ability of an Array to get inside Object’s toString method:

function isArray(obj) {
2	    return Object.prototype.toString.call(obj) == "[object Array]";
3	}


Meta Delegation (sort of)
As of ES 5 the apply function itself has been “generecized”. The second argument need no longer be an array. Any object which has a length and index properties can be used (for example arguments or presumably a string).

ES 5, 15.3.4.3: In Edition 3, a TypeError is thrown if the second argument passed to Function.prototype.apply is neither an array object nor an arguments object. In Edition 5, the second argument may be any kind of generic array-like object that has a valid length property.

 
Sadly browsers have not been quick to adopt this one.

Delegation via “Static” Functions (Mozilla only)
Dmitry Soshnikov points out that the SpiderMonkey engine supports a very simple form of delegation by simply passing arguments to the standalone function definition. Nice!

Array.map('abc', String.toUpperCase); //["A", "B", "C"]
2	String.toUpperCase(['a']); //"A"

 

Wrap Up

Implementation inheritance is a nice concept – I lived and breathed it for the 12 years I programmed in Smalltalk and Java – but we should be open to leaner, more versatile alternatives where they exist. Function delegation using call and apply allows JavaScript utilities to cherry-pick necessary functionality without the baggage of an unintuitive, bloated and overly complex hierarchy.

Appendix: Generic Function Reference

(See ECMA-262 5th Edition)
15.4.4.2 Array.prototype.toString ( )
15.4.4.3 Array.prototype.toLocaleString ( )
15.4.4.4 Array.prototype.concat ( [ item1 [ , item2 [ , … ] ] ] )
15.4.4.5 Array.prototype.join (separator)
15.4.4.6 Array.prototype.pop ( )
15.4.4.7 Array.prototype.push ( [ item1 [ , item2 [ , … ] ] ] )
15.4.4.8 Array.prototype.reverse ( )
15.4.4.9 Array.prototype.shift ( )
15.4.4.10 Array.prototype.slice (start, end)
15.4.4.11 Array.prototype.sort (comparefn)
15.4.4.12 Array.prototype.splice (start, deleteCount [ , item1 [ , item2 [ , … ] ] ] )
15.4.4.13 Array.prototype.unshift ( [ item1 [ , item2 [ , … ] ] ] )
15.4.4.14 Array.prototype.indexOf ( searchElement [ , fromIndex ] )
15.4.4.15 Array.prototype.lastIndexOf ( searchElement [ , fromIndex ] )
15.4.4.16 Array.prototype.every ( callbackfn [ , thisArg ] )
15.4.4.17 Array.prototype.some ( callbackfn [ , thisArg ] )
15.4.4.18 Array.prototype.forEach ( callbackfn [ , thisArg ] )
15.4.4.19 Array.prototype.map ( callbackfn [ , thisArg ] )
15.4.4.20 Array.prototype.filter ( callbackfn [ , thisArg ] )
15.4.4.21 Array.prototype.reduce ( callbackfn [ , initialValue ] )
15.4.4.22 Array.prototype.reduceRight ( callbackfn [ , initialValue ] )
15.5.4.4 String.prototype.charAt (pos)
15.5.4.5 String.prototype.charCodeAt (pos)
15.5.4.6 String.prototype.concat ( [ string1 [ , string2 [ , … ] ] ] )
15.5.4.7 String.prototype.indexOf (searchString, position)
15.5.4.8 String.prototype.lastIndexOf (searchString, position)
15.5.4.9 String.prototype.localeCompare (that)
15.5.4.10 String.prototype.match (regexp)
15.5.4.11 String.prototype.replace (searchValue, replaceValue)
15.5.4.12 String.prototype.search (regexp)
15.5.4.13 String.prototype.slice (start, end)
15.5.4.14 String.prototype.split (separator, limit)
15.5.4.15 String.prototype.substring (start, end)
15.5.4.16 String.prototype.toLowerCase ( )
15.5.4.17 String.prototype.toLocaleLowerCase ( )
15.5.4.18 String.prototype.toUpperCase ( )
15.5.4.19 String.prototype.toLocaleUpperCase ( )
15.5.4.20 String.prototype.trim ( )
15.9.5.44 Date.prototype.toJSON ( key )
B.2.3 String.prototype.substr (start, length)

Further Reading

Allen Holub in JavaWorldWhy Extends is Evil
Bill Venners: A Conversation with Java’s Creator, James Gosling
Nick Fitzgerald: OOP The Good Parts: Message Passing, Duck Typing, Object Composition, and not Inheritance – An excellent post in which Nick dumps on inheritance some more and outlines three additional alternatives.

Create data driven applications in Qlik’s free and easy to use coding environment, brought to you in partnership with Qlik.

Topics:

Published at DZone with permission of Angus Croll. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}