Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

I've written a small utility for monkey-patching native JavaScript constructor functions. For example, you can use it to modify input arguments before returning an instance (this can be useful for unit tests).

There is much more detail about the utility on GitHub, so it may be useful to read through the readme on there.

I would be interested to know if I've hugely over-engineered this. There may be simpler techniques that I could take advantage of. Any comments are appreciated.

Here is the code:

var patch = (function () {
    /*jshint evil: true */

    "use strict";

    var global = new Function("return this;")(), // Get a reference to the global object
        fnProps = Object.getOwnPropertyNames(Function); // Get the own ("static") properties of the Function constructor

    return function (original, originalRef, patches) {

        var ref = global[originalRef] = original, // Maintain a reference to the original constructor as a new property on the global object
            args = [],
            newRef, // This will be the new patched constructor
            i;

        patches.called = patches.called || originalRef; // If we are not patching static calls just pass them through to the original function

        for (i = 0; i < original.length; i++) { // Match the arity of the original constructor
            args[i] = "a" + i; // Give the arguments a name (native constructors don't care, but user-defined ones will break otherwise)
        }

        if (patches.constructed) { // This string is evaluated to create the patched constructor body in the case that we are patching newed calls
            args.push("'use strict'; return (!!this ? " + patches.constructed + " : " + patches.called + ").apply(null, arguments);"); 
        } else { // This string is evaluated to create the patched constructor body in the case that we are only patching static calls
            args.push("'use strict'; return (!!this ? new (Function.prototype.bind.apply(" + originalRef + ", [{}].concat([].slice.call(arguments))))() : " + patches.called + ".apply(null, arguments));");
        }

        newRef = new (Function.prototype.bind.apply(Function, [{}].concat(args)))(); // Create a new function to wrap the patched constructor
        newRef.prototype = original.prototype; // Keep a reference to the original prototype to ensure instances of the patch appear as instances of the original
        newRef.prototype.constructor = newRef; // Ensure the constructor of patched instances is the patched constructor

        Object.getOwnPropertyNames(ref).forEach(function (property) { // Add any "static" properties of the original constructor to the patched one
            if (fnProps.indexOf(property) < 0) { // Don't include static properties of Function since the patched constructor will already have them
                newRef[property] = ref[property];
            }
        });

        return newRef; // Return the patched constructor
    };

}());

And here's an example usage (this is a real-world use case for working around a bug in date parsing in PhantomJS):

Date = patch(Date, "DateOriginal", {
    constructed: function () {
        var args = [].slice.call(arguments);
        if (typeof args[0] === "string" && /^\d{4}\/\d{2}$/.test(args[0])) {
            args[0] = args[0] + "/02"; // Make sure the argument has a 'day'
        }
        return new (Function.prototype.bind.apply(DateOriginal, [{}].concat(args)));
    }
});

There are some more examples, and details of the arguments to patch in the GitHub readme.

share|improve this question
 
Why not use Object.create()? Also what JS engine or environment will your scripts be running on? If it's web, then it shouldn't work in IE 6-8 because they don't support Function.prototype.bind(). –  Larry Battle Jan 10 '13 at 23:52
 
@LarryBattle - This will only run in environments that do support Function#bind (and any other ES5 methods in there). Polyfills for bind won't work. –  James Allardice Jan 11 '13 at 8:26
add comment

Know someone who can answer? Share a link to this question via email, Google+, Twitter, or Facebook.

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Browse other questions tagged or ask your own question.