Monkey Patch Methods and Properties in JavaScript

Posted in software by Christopher R. Wirz on Wed Aug 03 2016



Monkey patching is a technique to add, modify, or suppress the default behavior of a piece of code at runtime without changing its original source code. It is often seen in Ruby [on Rails] and JavaScript. With JavaScript, it can ensure compatibility - both backwards and cross-browser.

Note: For reasons of compatibility, these examples may already have implementations in your browser or loading this site!

Let's take the sample examples of adding functions to strings:


/**
 * Splits a string based on a word, forming an array
 * @param word, the word to split on
 * @returns {array} an array of strings
 */
if (!String.prototype.splitWord){
    String.prototype.splitWord = function(word){
        if (typeof(word)!="string" || word.length > this.length) {return [this];}
        var arr = [];
        try {
            var index = 1;
            var str = this;
            while (str.length > 0 && index >= 0) {
                index = str.indexOf(word);
                if (index >= 0){
                    arr.push(str.slice(0, index));
                    str = str.slice(index + word.length);
                }
            }
            if (str.length > 0){
                arr.push(str);
            }
        }
        catch (err){}
        return arr;
    };
}

/**
 * Shortens a string, adding dots at the end
 * @param length, the word to split on
 * @returns {string} a string no greater than the length
 */
if (!String.prototype.shorten){
    String.prototype.shorten = function(length){
        if (typeof(length)!='number'){
            length = 140;
        }
        var str = this;
        return str.length > length ?
            str.substring(0, length - 3).trim() + "..." :
            str;
    };
}

/**
 * Checks if a string contains a word
 * @param word, the word to check for
 * @returns {boolean} true if the string contains the word
 */
if (!String.prototype.contains){
    String.prototype.contains = function(word){
		if (typeof(word)!="string"){return false;}
        return this.indexOf(word) > -1;
    };
}

/**
 * Checks if a string starts with a word
 * @param word, the word to check for
 * @returns {boolean} true if the string starts with the word
 */
if (!String.prototype.startsWith){
    String.prototype.startsWith = function(word){
        if (typeof(word)!="string") {return false;}
        if (word.length > this.length){return false;}
        return this.indexOf(word) == 0;
    };
}


/**
 * Checks if a string ends with a word
 * @param word, the word to check for
 * @returns {boolean} true if the string ends with the word
 */
if (!String.prototype.endsWith){
    String.prototype.endsWith = function(word){
        if (typeof(word)!="string") {return false;}
        if (word.length > this.length){return false;}
		// lastIndexOf is unreliable, check manually
        var wl = word.length-1;
        var tl = this.length-1;
        for (var i = 0; i < word.length; i++){
            var wc = word[wl-i];
            var tc = this[tl-i];
            if (wc != tc){return false;}
        }
        return true;
    };
}

/**
 * Combines two strings using a word in the middle (good for joining paths)
 * @param other, the string to combine at the end
 * @param combinator, the the joining word
 * @returns {string} a new string combining the old strings
 */
if (!String.prototype.combineWith){
    String.prototype.combineWith = function(other, combinator){
        if (typeof (other)!="string"){return this;}
        if (typeof (combinator)!="string"){return this + other;}
        var str = this;
        if (str.endsWith(combinator) && other.startsWith(combinator)){
            str = str + other.substr(combinator.length);
        }
        else if (!str.endsWith(combinator) && !other.startsWith(combinator)){
            str = str + combinator + other;
        } else {
            str = str + other;
        }
        return str;
    };
}


/**
 * Replaces all of a certain string with another string
 * @param search, the string to replace
 * @param replacement, the string to replace with
 * @returns {string} a new string replacing the old string
 */
if (!String.prototype.replaceAll){
    String.prototype.replaceAll = function(search, replacement) {
        return this.splitWord(search).join(replacement);
    };
}
if (!String.prototype.replaceAll){
    String.prototype.replaceAll = function(search, replacement) {
        var target = this;
        return target.replace(new RegExp(search, 'g'), replacement);
    };
}

Strings are one thing, but how about some HTMLElements


/**
 * Adds a class to the element
 * @param name, the class to add to the element
 * @returns {void}
 */
if (!HTMLElement.prototype.addClass){
    HTMLElement.prototype.addClass = function(name){
        if (typeof(this.className)=="undefined"){
            this.className = name;
            return;
        }
        if (this.className.toString().split(" ").indexOf(name) == -1) {
            this.className += " " + name;
        }
    };
}

/**
 * Removes a style class from an element
 * @param name, the class to remove from the element
 * @returns {void}
 */
if (!HTMLElement.prototype.removeClass){
    HTMLElement.prototype.removeClass = function(name){
        if (typeof(this.className)=="undefined"){
            return;
        }
        this.className = this.className.toString().split(" ").filter(function(c){return c != name;}).join(" ");
    };
}

/**
 * Checks if an element has a given style class
 * @param name, the class to check for on the element
 * @returns {boolean} true if the element has a class
 */
if (!HTMLElement.prototype.hasClass){
    HTMLElement.prototype.hasClass = function(name){
        if (typeof(this.className)=="undefined"){
            return;
        }
        if (typeof(name)!="string"){return false;}
        return this.className.toString().split(" ").indexOf(name) >= 0;
    };
}

/**
 * Removes an element's parent from the DOM
 * @returns {void}
 */
if (!HTMLElement.prototype.removeParent){
    HTMLElement.prototype.removeParent = function(){
        if (typeof(this.parentNode)  == "undefined") {return;}
        if (typeof(this.parentNode.parentNode)  == "undefined") {return;}
        this.parentNode.parentNode.removeChild(this.parentNode);
    };
}

How about the HTML Document itself?



/**
 * Adds a script to the HTML
 * @param url, the URL of the script to add
 * @param callback, the function to run when the script loads
 * @returns {void}
 */
if (!HTMLDocument.prototype.addScript){
    HTMLDocument.prototype.addScript = function (url, callback) {
        if (typeof(url)!="string"){return;}
        if (typeof(document.scripts)!="undefined"){
            for (var i = 0; i < document.scripts.length; i++){
                if (document.scripts[i].outerHTML.contains(url)){
                    if (typeof(callback) ==='function'){
                        callback();
                    }
                    return;
                }
            }
        }
        var script  = this.createElement('script');
        script.setAttribute("type","text/javascript");
        script.setAttribute("src", url);
        var head = this.getElementsByTagName('head')[0];
        script.onload = script.onreadystatechange = function(){
            if (!script.readyState || script.readyState === 'loaded' || script.readyState === 'complete'){
                if (typeof(callback) ==='function'){
                    callback();
                }
                script.onload = script.onreadystatechange = null;
            }
            else {
                console.log(url + " could not be loaded...");
            }
        };
        head.appendChild(script);
    };
}

/**
 * Adds a stylesheet to the HTML
 * @param url, the URL of the sheet to add
 * @param callback, the function to run when the sheet loads
 * @returns {void}
 */
if (!HTMLDocument.prototype.addStyleSheet) {
    HTMLDocument.prototype.addStyleSheet = function (url, callback) {
        if (typeof(url)!="string"){return;}
        if (typeof(document.styleSheets)!="undefined"){
            for (var i = 0; i < document.styleSheets.length; i++){
                if (document.styleSheets[i].href.contains(url)){
                    if (typeof(callback) ==='function'){
                        callback();
                    }
                    return;
                }
            }
        }
        var sheet = document.createElement('link');
        sheet.setAttribute("rel", "stylesheet");
        // sheet.setAttribute("type", "text/css");
        sheet.setAttribute("href", url);
        var head = document.getElementsByTagName('head')[0];
        sheet.onload = sheet.onreadystatechange = function () {
            if (!sheet.readyState || sheet.readyState === 'loaded' || sheet.readyState === 'complete') {
                if (typeof (callback) === 'function') {
                    callback();
                }
                sheet.onload = sheet.onreadystatechange = null;
            } else {
                console.log(url + " could not be loaded...");
            }
        };
        head.appendChild(sheet);
    };
}

Okay, clearly methods can be added (monkey-patched) to various types. How about properties, how do we add those? Here is the interesting part: we can define a property on a prototype, not just an object.


/**
 * Sets the background URL of a HTML Element
 */
if (!HTMLElement.prototype.backgroundUrl){
    Object.defineProperties(HTMLElement.prototype, {
        "backgroundUrl" : {
            "get": function() {
                if (this == null){return null;}
                if (typeof(this.style)=="undefined"){
                    return null;
                }
                if (typeof(this.style.backgroundImage)=="undefined"){
                    return null;
                }
                var img = this.style.backgroundImage.toString();
                if (img.indexOf("url(")==0){
                    img = img.substr(5);
                    img = img.substr(0, img.length-2);
                }
                return img;
            },
            "set": function(value) {
                if (typeof(value)!="string"){return;}
                if (typeof(this.style)=="undefined"){
                    return;
                }
                if (value === null || value.trim() === ""){
                    this.style.backgroundImage = null;
                }
                if (value.toLowerCase().indexOf("url(")===0){
                    this.style.backgroundImage = value;
                    return;
                }
                this.style.backgroundImage = "url('" + value + "')";
            }
        }
    });
}

Okay, we have defined a background image on a HTML element...
Shall we try it out?


document.body.backgroundUrl = "https://www.chriswirz.com/software/monkey-patch-methods-and-properties-in-js/monkey-patch-methods-and-properties-in-js.jpg";

When you click the button, it should set the background image.