Lately, I have been experimenting with various JavaScript MVCs, and I was surprised when AngularJS threw an error, when I changed the argument name in a controller function declaration using a factory resource. The library was obviously doing some magic to parse the names of the arguments of a function and validating it against a previously defined namespace. This was interesting, so I thought I would discover how they were doing it and some possible applications.
How do it…
There are some hacky ways of doing this, which use the arguments.callee
and .caller
properties, but they are deprecated and may become unavailable in the future, and several approaches which use eval
. However it is best to avoid eval
and any deprecated feature. The last approach is to use .toString
, which is simple and has full cross-browser supported.
Here is a function to get the names of the arguments of a function instance:
function getParamNames(fn) { var funStr = fn.toString(); return funStr.slice(funStr.indexOf('(') + 1, funStr.indexOf(')')).match(/([^\s,]+)/g); }
This snippet shows the output of passing a test function in:
function fnTest(a, b, c, d) {} getParamNames(fnTest) === ['a', 'b', 'c', 'd'];
One application of getParamNames
might be to check that an argument name is used. The following checks the provided function for an arbitrary number of argument names, returning true
or false
:
function assertFunctionDefines(fn) { var aFuncParamNames = getParamNames(fn), oFuncParamNames = {}, i = aFuncParamNames.length - 1; while (i >= 0) { oFuncParamNames[aFuncParamNames[i]] = 1; i -= 1; } i = arguments.length - 1; while (i > 0) { if (!oFuncParamNames[arguments[i]]) { return false; } i -= 1; } return true; }
This shows a couple of examples using the test function again:
assertFunctionDefines(fnTest, 'a', 'b', 'c', 'd') === true; assertFunctionDefines(fnTest, 'g') === false;
Lastly, you might want to use the keys of an object to determine which argument should be assigned where when calling a function:
function function_kwargs(fn, o) { var argNames = getParamNames(fn), nameIndexMap = {}, i = argNames.length - 1, args = []; while (0 <= i) { nameIndexMap[argNames[i]] = i; i -= 1; } for (i in o) { if (o.hasOwnProperty(i)) { if (undefined !== nameIndexMap[i]) { args[nameIndexMap[i]] = o[i]; } } } return fn.apply(this, args); }
Executing the following:
function_kwargs(fnTest, {a: 'foo', c: 'bar'});
Would pass a='foo', b=undefined, c='bar', and d=undefined into the test function.
Here is a jsfiddle showing each of the above functions.
How it works…
The getParamNames
function calls the .toString
method of the function instance, which will return the function code as a string. It then slices the string between the parentheses and uses a regex to create an array of all words separated by spaces and commas. These words are the argument names of the function. This technique works on all modern browsers, although I have not checked on IE < 9.
The assertFunctionDefines
function simply creates a map of all the known arguments and then tests if the provided argument names are in the map, returning true
if they all are, or false
if one or more are not. There are nicer ways in jQuery for searching arrays, but I wanted the function to be library independent.
Lastly, the function_kwargs
maps the keys of the object to its position in the arguments of the provided function. Again, I used native looping, so that the code is library independent. I think this function may have some real practical use for handling configurations objects. Consider the following two simple examples:
Classical Configuration
function MyClass(data, conf) { this.data = data; this.configuration(conf || {}); } MyClass.prototype = { configuration: function(conf) { this.config = { option1: conf.option1 || default1, option2: conf.option2 || default2, ... }; } }
function_kwargs Configuration
function MyClass(data, conf) { this.data = data; function_kwargs.call(this, this.configuration, conf || {}); } MyClass.prototype = { configuration: function(option1, option2) { this.config = { option1: option1 || default1, option2: option2 || default2, ... }; } }
We can explicitly define the known options on the configuration function, which requires slightly less code, but is the real benefit is, IMHO, that the code is more obviously documented and easier to read.