Please only shim to standards

I’ve made this mistake myself so I’m desperate to ensure it doesn’t happen again. If you’re going to shim JavaScript, please only do it to standards.

Whoa, just a second… shim?

JavaScript allows us to shim (or patch) it, to fill in any missing pieces of functionality that aren’t supported by the current browser. Now that ECMAScript 5 is filtering into the releases, shims are being used more and more to allow developers to use the latest methods. Here’s an example of a shim for Array.prototype.forEach:

if (!Array.prototype.hasOwnProperty('forEach')) {
    Array.prototype.forEach = function (func) {
        var i = 0,
            n = this.length;
        while (i < n) {
            func.call(undefined, this[i], i, this);
            i += 1;
        }
    };
}

Simple, huh? Now we can use the following piece of code anywhere on the page without the fear of errors:

[1, 2, 3, 4].forEach(function (n) {/*Do something with n*/});

There's just one major problem: that shim does not handle everything it should. The standards have very clear instructions for what the new methods should do. In the case of forEach, it should allow use to bind the function's this keyword to a given object, it should ignore undefined entries, it should check this.length is numeric and small enough and it should be generic. None of these requirements are covered by the shim above.

What's happened here is the developer needed to use Array.prototype.forEach and only filled in the parts of the workaround that he needed for this project. The code he's written will work fine his project so he'll assume it's fine. Sadly, when another developer does something on that page that uses Array.prototype.forEach, it may start doing things he doesn't expect. How may of us would think to check that the shim from another project is complete?

OK, what is a complete version?

I'm glad you asked. Here's a full version of Array.prototype.forEach:

(function () {

    var undef, // = undefined;
    
        isStringArray = 'a'[0] === 'a', // Used in toObject.
        toString = Object.prototype.toString; // Used for class checking.

// The "this" given to an Array method should be converted into an object. This
// means that a String should be converted into an Array. Sadly, the native
// Object() does not do this in IE8-, so this helper function will make sure
// that it always happens.
    function toObject(obj) {
        if (toString.call(obj) === '[object String]' && !isStringArray) {
            obj = obj.split('');
        }
        return Object(obj);
    }
    
// The length of an array must be a number between -2^31 and 2^31 - 1
// inclusive. This little helper function makes sure that the number satisfies
// those conditions, returning 0 if it's not the case.
// http://es5.github.com/#x9.5
    function toUnit32(str) {
        var num = Number(str),
            ret = 0;
        if (!isNaN(num) && isFinite(num)) {
            ret = Math.abs(num % Math.pow(2, 32));
        }
        return ret;
    }

// Add Array.prototype.forEach if it doesn't already exist. Be sure to convert
// the "this" into a proper object, check that the length is small enough and
// do not execute the function for undefined entries of the array. Check that
// the object can be iterated and that the function can be called. Return
// undefined, though this happens automatically in JavaScript if nothing is
// explicitly returned.
    if (!Array.prototype.hasOwnProperty('forEach')) {
        Array.prototype.forEach = function (func, thisArg) {
            var i  = 0,
                t  = toObject(this),
                il = toUnit32(t.length);

            if (t === undef || t === null) {
                throw new ReferenceError('Unable to iterate through object.');
            }

            if (func === undef || toString.call(func) !== '[object Function]') {
                throw new TypeError('Unable to execute function.');
            }

            while (i < il) {
                if (t[i] !== undef) {
                    func.call(thisArg, t[i], i, t);
                }
                i += 1;
            }
        };
    }

}());

That a lot of code, dude. Can't I just use my smaller fix?

If it doesn't do everything that the standards say it should then do not add it globally. Seriously, there should be laws against such things. But before you start thinking it's all doom and gloom (or that you have to remember that chunk of code), JavaScript offers us another useful trick:

var forEach = Array.prototype.forEach || function (func) {
    var i = 0,
        n = this.length;
    while (i < n) {
        func.call(undefined, this[i], i, this);
        i += 1;
    }
};

To use it, you'd write code like this:

forEach.call([1, 2, 3, 4], function (n) {/*Do something with n*/});

I've heard this technique called the "shim what you need to" approach. It allows you to write a very simple work-around for Array.prototype.forEach that does everything you need it to without potentially breaking someone else's script in the future. It's also a small acknowledgement to the limitations of the workaround. Anyone who knows JavaScript well enough to use your workaround should be able to see that it doesn't cover every eventuality. Even better, if a future developer needs that functionality, they can put in a proper and complete shim later, your variable will automatically tick over (assuming, of course, that the future developer even uses it).

There is no shame in patching JavaScript with a complete fix and there's equally little shame in using a local callable variable with an incomplete fix. Just please don't put put an incomplete fix into the global scope like the first example.

Major update (31 July 2012)

Having re-read the specs and other implimentations I've come to realise that this shim is not accurate. I no longer recommend using it - please use my new shim instead.

Leave a Reply

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