Cross-browser code is harder than you think

If you’re following JavaScript best practices or good patterns you know already how bad it is to extend native objects’ prototype, in this article we’ll see why this practice should be avoided and also why polyfills might hurt your feelings.

Software is hard, and having to write code meant to run in different browsers versions and vendors does not make our life easier.
The temptation to provide methods already available in modern browsers is strong, enough to explain why savvy guys created libraries like underscore.js or jQuery and more and more people use it.
But when you feel the urge of creating your polyfill, try to resist. Let’s learn by examples.

Unsafe code

When you want to do an action for each and every item inside an array, most likely you’ll have code like this

var array = [1, 2, 3];

for (var i = 0, len = array.length; i < len; i += 1) {
   doSomething(array[i]);
}

Isn’t it more reable like this?

array.forEach(doSomething);

Unfortunately forEach is an addition to ECMA-262 standard and it’s not available in Internet Explorer 8 or less.

Now you have two choices, keep your for loop or add forEach to Array’s prototype

Array.prototype.forEach = function () {
   //
}

This also means that you’ll override the native forEach implementation also for browsers that respect the standard.

if (!Array.prototype.forEach) {
  Array.prototype.forEach = function () {
     //
  }
}

This solution is less intrusive but still bad. Different browsers will execute different code, are you sure that your code behaves exactly the same?
Of course things are easy for Array.forEach, you can find a good implementation on MDN, but in the following examples we’ll see how easily we can make mistakes.

Both solutions have a major drawback. They are dangerous!
Methods added on an prototype are available on all object instances linked to that prototype.

If you’ve mastered other programming languages before learning JavaScript, you might be tempted to use arrays as object (as in PHP)

* Disclaimer: the code below is bad, don’t use it and don’t learn from it!

var contactInformation = [];   // seriously, an array is not the correct data type
contactInformation["name"] = "Fabio Crisci";
contactInformation["url"] = "https://github.com/piuccio";

// ... later on in your code ...
for (var key in contactInformation) {
   console.log(key, ":", contactInformation[key]);

   // it'll log also   forEach : function () {...}
}

Iterating with a for .. in loop on an array includes also the method we added on the Array prototype.

Code like the one before is the reason why jslint/jshint remind you the importance of hasOwnProperty

* Disclaimer: the code below is much better, but please don’t use arrays when keys are strings, use objects!

for (var key in contactInformation) {
   if (contactInformation.hasOwnProperty(key)) {
      console.log(key, ":", contactInformation[key]);

      // this code runs fine
   }
}

Wrong code

Another useful method that unfortunately is not available on all browsers is Function.bind

bind returns a new function that, when called, itself calls the original function with the specified context and arguments.

var exp = function (exponent, base) {
   return Math.pow(base, exponent);
};

exp(2, 3)   // 9

var square = exp.bind(null, 2);
square(5)  // 25

square is bound to the function exp, when square is called it calls exp with a null context (this inside the method) and arguments obtained concatenating the ones specified in bind and the ones passed to square.

A much simplified implementation of bind can be obtained with a closure

* Disclaimer: Never use this code! Please

Function.prototype.bind = function () {
   // Original function
   var boundTo = this;
   // The first argument is the scope
   var scope = arguments[0];
   // Then the arguments
   var args = [].slice.call(arguments, 1);

   return function () {
      var newArgs = [].slice.call(arguments, 0).concat(args);

      // or is it
      //var newArgs = args.concat([].slice.call(arguments));
      // ? can't remember!  >.<

      return boundTo.apply(scope, newArgs);
   };
}

var exp = function (exponent, base) {
   return Math.pow(base, exponent);
};

var square = exp.bind(null, 2);
square(5)  // 25 ?

As it turns out our implementation is wrong, according to the standard the arguments passed to bind should come before the ones passed to square. We did it wrong!

The error described so far seems trivial to find, but it might not be the case if you don’t test correctly your code, for instance relying only on scope binding and not on arguments usage. The next example show a more subtle error.

Incomplete code

An interesting issue was spotted recently in one of Aria Templates widgets, AutoComplete. The widget works perfectly fine until getting to a page containing a Microsoft map. To make things worse, it happens only in FireFox.

The code that was breaking is the following

// ... executed on every key press, to get the caret position in the input field ...
if (document.selection) {
   // document.selection is only available in Internet Explorer
   var sel = document.selection.createRange();
   var initialLength = sel.text.length;
   // ... more
} else if (ctrl.selectionStart || ctrl.selectionStart == '0') {
   // Firefox and others
   pos.start = ctrl.selectionStart;
   pos.end = ctrl.selectionEnd;
}

There’s nothing wrong in it, except the fact that we try to use a non standard behavior before checking that the browser supports the standards.

Loading Microsoft maps the following script gets called

// This must be the first script file (except for the optional debug library)
// Dependencies: none
// Note: This file must only be downloaded by Mozilla
//   If you can't exclude this file via server processing, it should be included using
//   a conditional comment such as:
//<script type="text/javascript" src="AtlasCompat.js"></script>

Web.Browser.AttachMozillaCompatibility = function (w)
{
  // Mozilla->IE Compatibility Library

  // ... some code before ...

  // TODO - implement selection
  w.Document.prototype.selection = new Object();
  w.Document.prototype.selection.clear = function() {}; // Need to find Mozilla equivalent
  w.Document.prototype.selection.createRange = function() {return window.getSelection().getRangeAt(0);}

  // ... some other code after ...
}

The script polyfills FireFox with methods only available in Internet Explorer. Oddly enough it’s not loaded in Chrome, Safari and other browsers.

As the TODO comment suggests, this is only a partial implementation of document.selection, probably enough to meet the needs of Microsoft map.
Aria Templates however tries to use selection.text which is left undefined by this partial implementation.

Tracking this issue was not trivial, nor fun.
Lesson learnt? Check for standards first.

Conclusions (aka tl;dr)

Writing cross-browser code is hard, using polyfills or external libraries makes things easier, but third party code comes with third party bugs!

My suggestions

  • Never extend native objects’ prototype, it might work with your code but you might break someone else code, and you don’t want that, right?
  • When dealing with quirks, test for standards first. There’s a reason if they are standard!

In Aria Templates we try to respect these principles as much as possible, they are part of our code review process and they are the reason why we created classes like

  • aria.utils.Array
  • aria.utils.Date
  • aria.utils.Function
  • aria.utils.Object
  • aria.utils.String

and others inside package aria.utils
These classes have code that you’d love having on the corresponding object’s prototype, have a look at their API documentation.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>