Introduction
I recently came across a question on StackOverflow. It's a few years old, but struck me as bizarre enough that I decided to write about it. The gist of the question in question (pun intended) is empty functions in JavaScript, specifically: if it's possible to detect them.
The reason why I find it bizarre is because the reason(s) why you'd want to do this (E.G. having a no-op in a ternary operator) can be easily negated using simple solutions (E.G. conditional statements).
Now, say you wanted to check if a function is empty to avoid running it. The question is why? If it's to improve performance, you'd be better off letting it fail safe or checking against certain conditions (E.G. if the function returned nothing). In fact you'd be using more resources to check if the function is empty than you would letting an empty function do nothing.
Simply put, why waste resources on checking if something that might do nothing does nothing? The correct way to handle this situation is to determine what to do if it does something and how to let it fail safe if it does nothing. This is best achieved by adhering to best practices and by utilising basic tools like if conditions, typeof/instanceof checks and equality operators in a non-repetitive manner.
That said, I'm not here to talk you out of this so lets get started.
String manipulation
In JavaScript you can turn pretty much anything into a string: Booleans, Numbers, Arrays, Objects, Errors, Dates, URLs, URL Search Parameters, JSON, a DOM and XML. You can even convert a string into a string using String.prototype.toString.
Most built-in types, objects and classes implement the toString()
method, including the Function object: Function.prototype.toString.
This is great because it allows us to use string manipulation to determine if there are any statements, expressions or declarations in a function and because all javascript function bodies follow the same syntax - an opening curly brace that has a matching closing curly brace - we can easily isolate the function's body using the indexOf()
and lastIndexOf()
methods:
- Use the
toString()
method to return the function as a string - Remove new lines and trim off leading and trailing whitespace
- Find the index of the first instance of an opening curly brace
- Find the index of the last instance of a closing curly brace
- Remove the characters before/after those curly braces (inclusive)
- Trim the resulting string
- If it's length is greater than
0
returnfalse
, otherwise returntrue
To demonstrate, we'll need a few sample functions with varying indentation:
const f1 = function()
{ }
;
const f2 = function wow()
{
};
const f3 = function amazing (a) {
};
function f4(a, b) {
const i = 1;
return i + 1;
};
And the function, implemented as such:
function is_fn_empty(fn) {
// convert the function into a string
// remove the new lines
// trim leading and trailing whitespace
const string_fn = fn.toString().replace(/\n+/g, "").trim();
// get the first index of an opening curly brace
const first_index = string_fn.indexOf("{");
// remove that curly brace and everything before it from the body of the function
let body = string_fn.substring(first_index + 1);
// get the last index of a closing curly brace
const last_index = body.lastIndexOf("}");
// remove that curly brace and everything after it from the body of the function
body = body.substring(0, last_index);
// remove leading and trailing whitespace from the function body
body = body.trim();
// return true if the string is empty, false if not
return !!(body.length < 1);
}
Which, when called:
console.log(is_fn_empty(f1));
console.log(is_fn_empty(f2));
console.log(is_fn_empty(f3));
console.log(is_fn_empty(f4));
Produces the following output:
true
true
true
false
We could simplify the is_fn_empty
function and have it work exactly the same.
function is_fn_empty(fn) {
// convert the function into a string
// remove new lines from the string
// trim leading and trailing whitespace from the string
const string_fn = fn.toString().replace(/\n+/g, "").trim();
// get the first and last index of an opening and a closing brace
const [first_index, last_index] = [string_fn.indexOf("{"), string_fn.lastIndexOf("}")];
// remove the data before/after those braces (inclusive)
const body = string_fn.substring(0, last_index).substring(first_index + 1).trim();
// return true if empty, false if not
return !!(body.length < 1);
}
Regular expressions
If you're comfortable with using regular expressions, you could use quantifiers to do the same thing using regex.
For a greedy non-specific search, use /\{(.*)\}/s
to isolate everything in between the first opening and last closing curly brace:
function is_fn_empty(fn) {
// the regular expression to check against
const re_check = /\{(.*)\}/s;
// convert the function into a string
// trim leading and trailing whitespace from the string
// remove all new lines from the string
const string_fn = fn.toString().trim().replace(/\n+/g, "");
// try to match the regular expression against the function string
const match = string_fn.match(re_check);
// if a match exists
if (match !== null) {
// get the trimmed function body
const body = match[1].trim();
// return true if empty and false if not
return !!(body.length < 1);
}
// otherwise fail safe and return null
return null;
}
Otherwise, for a more specific and thorough search, use /^function(.*?\()(.*?\)|\)).*?\{(.*?)\}($|\;$)/
to isolate the function body:
function is_fn_empty(fn) {
// the regular expression to check against
const re_check = /^function(.*?\()(.*?\)|\)).*?\{(.*?)\}($|\;$)/;
// convert the function into a string
// trim leading and trailing whitespace from the string
// remove all new lines from the string
const string_fn = fn.toString().trim().replace(/\n+/g, "");
// try to match the regular expression against the function string
const match = string_fn.match(re_check);
// if a match exists
if (match !== null) {
// get the trimmed function body
const body = match[3].trim();
// return true if empty and false if not
return !!(body.length < 1);
}
// otherwise fail safe and return null
return null;
}
To demonstrate and test this method effectively, we'll need a test function with multiple nested block statements to make sure our regular expressions aren't caught out by multiple curly braces in the function body:
function f4(a, b) {
{
console.log("Hello");
{
console.log("World");
}
}
const i = 1;
return i + 1;
};
Both functions should output the following:
console.log(is_fn_empty(f1)); // true
console.log(is_fn_empty(f2)); // true
console.log(is_fn_empty(f3)); // true
console.log(is_fn_empty(f4)); // false
Closing thoughts
Although I said I wouldn't talk you out of this, please remember that this article was only written because I wanted to try and solve a bizarre problem. I wouldn't recommend you use any of these functions in production and I reiterate that there are better ways of dealing with void functions.
Thank you for reading.