In the Monkey Patching article, I introduced the term “Duck Typing.” Coined by Ruby coders, Duck Typing is based on the principle, “If it walks like a duck and it quacks like a duck, then it must be a duck.” Following that precept, rather than attempt to determine an object’s suitability for a given purpose according to its type, in duck typing, an object’s suitability is determined by the presence of certain methods and properties. Once you get familiar with the concept, you begin to notice where Duck Typing has been employed in many programming languages — including JavaScript — with great success. In today’s article, we’ll trace its evolution in JavaScript and explore some situations where we can apply Duck Typing for our own objectives.
Duck Typing Roots in JavaScript
Once upon a time, script writers employed a technique called Browser Sniffing — a.k.a user-agent sniffing — to determine whether to code in JavaScript (Netscape) or JScript (Internet Explorer). Over time, this practice was eclipsed by Feature Detection, whereby a specific feature/capability was targeted instead. For example:
//Browser Sniffing if (navigator.userAgent.indexOf("MSIE 7") > -1){ //do something } //Feature Detection if(document.all){ //do something }
Feature Detection was a notable improvement over Browser Sniffing, but unfortunately, a lot of web developers went a step further by using the document.all test as an implicit check for Internet Explorer. Assuming that the browser is in fact IE, coders would then proceed with erroneous assumption that it was also safe to use other IE-specific methods and properties like document.uniqueID property. In Duck Typing, one would not make any such assumptions based on any particular test.
Later, several shim libraries were developed to detect HTML5 and CSS3 features in various browsers in a standardized manner. The more well known of these is Modernizr. Created by a few developers, including Paul Irish, Modernizr has won several awards. It creates a global Modernizr object that contains a set of boolean properties for each feature it can detect. For example, if a browser supports the canvas API, the Modernizr.canvas property will be set to true, or false otherwise:
if (Modernizr.canvas) { // let's draw some shapes…! } else { // no native canvas support available 🙁 }
Unlike the earlier Feature Detection for browsers, shims like Modernizr restrict their feature checks to individual properties.
Emulating Interfaces with Duck Typing
Interfaces are one of the most powerful features of programming languages like Java. It’s a file that contains a collection of function signatures. When an object implements the interface, it is symbolically signing a contract whereby it agrees to perform the specific behaviors (functions) of the interface. If an object fails to live up to its side of the bargain, your program won’t even compile!
While JavaScript is not an Object-Oriented language like Java (it uses Prototypal inheritance), there are ways to emulate Interface-like features. Let’s see how.
For our demo, let’s create three types of cars:
function Infiniti(model) { this.model = model; } function Honda(model) { this.model = model; } function Hyundai(model) { this.model = model; }
We can instantiate each Car type using the new keyword
, passing in its type:
var cars = [ new Infiniti("G37"), new Honda("Civic"), new Hyundai("Accent") ];
Suppose we wanted to race our cars. We could code something like this, where the accelerating prowess of each car is described:
function race(car) { if (car instanceof Infiniti) { console.log(car.model + ": Going from 0 to 60mph really fast!"); } else if (car instanceof Honda) { console.log(car.model + ": Going from 0 to 60mph at a moderate speed."); } else if (car instanceof Hyundai) { console.log(car.model + ": Going from 0 to 60mph at a crawl."); } } //Let's race some cars! for (var i in cars) { race(cars[i]); } /* outputs: "G37: Going from 0 to 60mph really fast!" "Civic: Going from 0 to 60mph at a moderate speed." "Accent: Going from 0 to 60mph at a crawl."* */
*No disrespect to owners of Hyundai Accents. I’m merely poking fun at a buddy of mine!
The instanceof
operator tests for specific Car types and outputs text accordingly.
Although there is nothing technically wrong with the above code, there is a fairly serious design flaw, and that is that every time we create a new Car type, we have to update the race() function!
Needless to say, there is a better way to tailor the race() function for a variety of Car types.
Let’s imagine that car types must implement an accelerate() method that is specific to that particular Car type. Then, in the race() function, we can simply pass the Car instance to the race() function so that it may in turn invoke the object’s accelerate() method:
function Infiniti(model) { this.model = model; } Infiniti.prototype.accelerate = function() { console.log(this.model + ": Going from 0 to 60mph really fast!"); } function Honda(model) { this.model = model; } Honda.prototype.accelerate = function() { console.log(this.model + ": Going from 0 to 60mph at a moderate speed."); } function Hyundai(model) { this.model = model; } Hyundai.prototype.accelerate = function() { console.log(this.model + ": Going from 0 to 60mph at a crawl."); } function race(car) { //Dynamically invoke accelerate() method //of whatever car was passed in. car.accelerate(); } //Let's race some cars! var cars = [ new Infiniti("G37"), new Honda("Civic"), new Hyundai("Accent") ]; for (var i in cars) { race(cars[i]); }
Guarding Against Impostors
Since JavaScript lacks the compiler checking of a programming languages such as Java, it still wouldn’t hurt to check that the passed object can in fact accelerate and throw an error if it cannot. To illustrate, we’ll pass a couch to the race() function:
function race(car) { //Dynamically invoke accelerate() method //of whatever car was passed in. if (car.accelerate) car.accelerate(); else throw new DuckTypeError("Hey, you're not a car!"); } //try to race a sofa!? function Couch(type) { this.type = type; } race(new Couch("Sofa"));
Here is the output produced when trying to race the Couch. Note that the DuckTypeError code is not shown here, but it is in the demo:
G37: Going from 0 to 60mph really fast!
Civic: Going from 0 to 60mph at a moderate speed.
Accent: Going from 0 to 60mph at a crawl.
SCRIPT5022: DuckTypeError: Hey, you're not a car!
index.html (71,7)
Duck Typing makes sure that our object “quacks like a duck.” The advantage to this approach is that we don’t have to make any further updates the the race() function to accommodate new Car types.
Conclusion
In today’s article, we traced the evolution of Duck Typing in JavaScript and explored how to utilize Duck Typing to emulate the behavior of Java Interfaces in JS objects. There is a full demo on Codepen for you to explore.