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.
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.