Delegation vs Inheritance in JavaScript
Join the DZone community and get the full member experience.
Join For FreeWhen 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).
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.
Published at DZone with permission of Angus Croll. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
Replacing Apache Hive, Elasticsearch, and PostgreSQL With Apache Doris
-
Web Development Checklist
-
Never Use Credentials in a CI/CD Pipeline Again
-
Transactional Outbox Patterns Step by Step With Spring and Kotlin
Comments