Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / ECMAScript

JavaScript Functions Named Arguments, Even Yet Another Approach

5.00/5 (3 votes)
27 Feb 2018CPOL8 min read 9.1K  
If you think named function arguments are needed in your JavaScript projects, you may consider extending built-in JavaScript Function objects to add them automatically.

Introduction

Reading current article's title and its short description, a lot of JavaScript experts will argue that ECMAScript specification does not mention named arguments and such feature has been implemented in many ways already. Anyway, neglecting such aspect, I am not trying to reinvent the wheel, I simply want to bring up a few interesting points, also useful in your daily JavaScript coding practice which are so often underutilized.

Background

As the aforementioned experts may add, changing JS built-in objects can give you some surprises and your work might accidentally be overwritten by other extensions, for example, if you add external libraries or change the version of one of them for good reasons. While these points are true and valid, I believe among driving motivations which could encourage developers to improve expertise, the willingness to embrace those intimidating features used to solve specific problems is often an underestimated good point of start for new explorative journeys. Also, in my case, what provided me more than a hint was a Sergey Alexandrovich Kryukov's article (and its follow-up) published recently here, which I would suggest to read to look at the problem from a different point of view.

Using the Code

Now I'm ready to show you how the main source code looks like, and to illustrate its usage through code samples created to explain the feature realization. Here below, you will find the source for argumentify extension, which dismisses the function wrapper approach - probably necessary to deal with Object.seal not reversible behavior - attaching named arguments straight to the function at runtime using the Object.defineProperty feature introduced with ECMAScript 5 to modify JavaScript objects.

To prevent argument name clash, the hasOwnProperty native method is used to check a property presence.

In any event, the functions are first-class citizens in JavaScript, thus they can have properties as any other object. We need these properties to represent the named formal arguments of the original target function. Writing the calling code, the developer may choose to define one or more named argument default values, or should assign values to these properties later on.

This is the complete implementation for argumentify:

JavaScript
"use strict";

Function.prototype.argumentify = function argumentify() {
    var self = this,
        argumentValues = Array.prototype.slice.call(arguments),
        argumentNames = (self + "").match(/function[^(]*\(([^)]*)\)/)[1].split(/,\s*/);

    if (!(!argumentNames || !argumentNames.length)) {
        for (var i = 0, l = self.length; i < argumentNames.length; ++i) {
            if (!self.hasOwnProperty(argumentNames[i]) || (self.$$arguments &&
				self.$$arguments.names.indexOf(argumentNames[i]) > -1)) {
                Object.defineProperty(self, argumentNames[i], {
                    get: function(index, length) {
                        return function() {
                            return argumentValues[index + length];
                        };
                    }.call(null, i, l),
                    set: function(index, length) {
                        return function(value) {
                            argumentValues[index + length] = value;
                        }
                    }.call(null, i, l),
                    configurable: true,
                    enumerable: false
                });
                self[argumentNames[i]] = argumentValues[i];
            }
        }
	}
    
    Object.defineProperty(self, "$$arguments", {
        configurable: true,
        enumerable: false,
        value: {
            names: argumentNames,
            values: argumentValues
        }
    });

    return self;
};

After the function is instantiated and augmented through argumentify, the developer can execute the function logic calling invoke method on that instance any number of times. The invoke method behavior mimic the apply native method, accepting two parameters respectively the this context object and an object containing the named function arguments. So, if correctly supplied, the invoke method does expect a second parameter typed as literal object passed through, hosting some properties representing the function parameter values, which at the moment of the call, can host all of the parameters required, or a part of them. And, in case of misspelled property name, the fault does not remain untrapped since the method fails early raising a TypeError exception.

Here is the invoke source code:

JavaScript
Function.prototype.invoke = function invoke(context) {
    var i = 0, args, invokeArgs, $$arguments = this.$$arguments;
    for (; $$arguments && i < $$arguments.names.length; i++)
        (args || (args = [])).push(this[$$arguments.names[i]]);
    invokeArgs = Array.prototype.slice.call(arguments, 1);
    if (invokeArgs.length === 1 && invokeArgs[0].constructor === Object) {
        var $args = invokeArgs[0];
        for (var prop in $args) {
            if ($args.hasOwnProperty(prop)) {
                if ((i = $$arguments.names.indexOf(prop)) === -1) {
                	throw new TypeError("\"" + prop + "\" argument name is invalid");
                } else {
                    args[i] = $args[prop];
                }
            }
        }
    }
    return this.apply(context, args || invokeArgs);
};

And of course, all the arguments (properties) can be set to undefined calling cleanUpNamedArguments:

JavaScript
Function.prototype.cleanUpNamedArguments = function cleanUpNamedArguments(undefined) {
    var $$arguments = this.$$arguments;
    for (var i = 0; $$arguments && i < $$arguments.names.length; i++)
        this[$$arguments.names[i]] = undefined;
};

Actually, to add ancillary features, such as those apt to observe the changes of one or more arguments, watchNamedArguments and unWatchNamedArguments has been implemented as mechanism allowing to trigger watch-blocks for the specified object instance/property.

The watch/unwatch methods and dependencies look like this:

JavaScript
if (!Function.prototype.watch) {
    Object.defineProperty(Function.prototype, "watch", {
        enumerable: false,
        configurable: true,
        writable: false,
        value: function(prop, handler) {
            var oldval = this[prop],
                newval = oldval,
                getter = function() {
                    return newval;
                },
                setter = function(val) {
                    oldval = newval;
                    return newval = handler.call(this, prop, oldval, val);
                };

            if (delete this[prop]) {
                Object.defineProperty(this, prop, {
                    get: getter,
                    set: setter,
                    enumerable: true,
                    configurable: true
                });
            }
        }
    });
}

if (!Function.prototype.unwatch) {
    Object.defineProperty(Function.prototype, "unwatch", {
        enumerable: false,
        configurable: true,
        writable: false,
        value: function(prop) {
            var val = this[prop];
            delete this[prop];
            this[prop] = val;
        }
    });
}

Function.prototype.watchNamedArguments = function watchNamedArguments(callback) {
    var $$arguments = this.$$arguments;
    for (var i = 0; $$arguments && i < $$arguments.names.length; i++)
        this.watch([$$arguments.names[i]], function (id, oldval, newval) {
            callback.call(null, id, oldval, newval);
            return newval;
        });
};

Function.prototype.unwatchNamedArguments = function watchNamedArguments() {
    var $$arguments = this.$$arguments;
    for (var i = 0; $$arguments && i < $$arguments.names.length; i++)
        this.unwatch([$$arguments.names[i]]);
};

As said, the argumentify method has been conceived as a more JavaScript function friendly approach to named argument, and this does mean it is always ready to be called as many times as a developer needs an enhanced function instance without any sort of extra shield.

To see a complete example, let's consider the "ordinary" function using 3 arguments shown into Sergey's article:

JavaScript
var fn, fn0;

function f(first, medium, last) {
    return first + medium + last;
}

try {
    fn = f.argumentify(5, 10);
    
    console.log("fn: " + (fn.last = 1, fn.invoke(null)));

    console.log(fn.first);
    console.log(fn.medium);
    
    fn0 = f.argumentify(15, 20);

    console.log("fn0: " + fn0.invoke(null, {last: 1}));
    
    console.log(fn.first);
    console.log(fn.medium);
} catch (ex) {
    console.log(ex);
} finally {
    fn.cleanUpNamedArguments();
}

Well, as you see nothing is explicitly checked up by the calling code, and although the argument names can be easily misspelled in both invoke examples, it is worth noting the latter invoke call may raise a TypeError exception to notify a coding error. In the examples, the order of arguments can be changed also thanks to the fact that you can define default argument values for the omitted ones within argumentify method call. On every function invocation, we may use invoke method with different argument values, passing in the foremost argument as this context or nullifying its value. This add a bit of negligible extra labor to function call coding, but, despite this, the JavaScript programmer may choose to stick with comma operator syntax or the more common, and validated against formal arguments list for mispelling, literal object syntax. In this case, both examples preferred to leave last argument set to undefined as default value. Anyway, the real problem we are caring about here is readability and, of course, better maintenance of the codebase, especially when you have many duties to take care of and getting back to the same codebase after prolonged period of time becomes problematic.

The original function referenced by fn and fn0 variables is the same as well as the property definitions attached to the function object. This is the main reason because within finally block, cleanUpNamedArguments is called only once.

At the same time, the misspell proof mechanism offered, paired with watching/unwatching mechanisms give the developer a barely complete foundation to work upon. In fact, the programming style shown in this article becomes more interesting when enriched with the following watching callback.

JavaScript
fn.watchNamedArguments(function(id, oldval, newval) {
    console.log('o.' + id + ' changed from ' + oldval + ' to ' + newval);
});

Here, I used the common idea of taking a callback function to log out the named arguments activities. Please beware that only the first watching callback is triggered for the current proposed implementation.

ECMAScript 6 to the Rescue

ECMAScript 5 is the most adopted ECMAScript standard at the moment in all major browsers, anyway ECMAScript 6 introduced this possibility to define default parameter values: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters. Even if this syntax sugar addition may sound irrelevant, it could be a nice to have ECMAScript language improvement if you are among those developers begging for named arguments in JavaScript.

Points of Interest

Probably, there are two likely perplexing things about the presented approach. One of the two is the style of argument initialization, since I suggested the use of comma operator, while the other one regards the fact that the actual arguments apparently have nothing to do with the parameters passed in a regular function call statement.

Let's start to say that comma operator is not specific to JavaScript, and is available in other languages like C and C++ too. Being a binary operator inherited from C language, it is useful when the left-hand operand, which is generally an expression, has desired side effect required by the second operand. Anyway, to be exhaustive, the comma has a special meaning in var statements. In fact, a variable declaration such as var a, b refers to the two or more variables you are going to initialize and use in your code, which is not the case for an expression where the result is given by the last variable or instruction separated by comma operator. One obfuscated example of this could be like this:

JavaScript
var b = (1,2) > (4,8) ? 'bar' : (4,2) > (1,0) ? '' : 'baz'; // b == ''

The code smelling example leads to the result of a variabile intialization to '' using an at first sight convulted sequence of steps which starts the resolution (1,2) expression to 2 and (4,8) expression to 8, followed by the resolution of (4,2) expression to 2 and (1,0) expression to 1. Then these transient values are used respectively as left-hand and right-hand for the greater-than evaluation feeding the two ?: ternary operators leading to the mentioned '' final result. A partially deobfuscation effect could be exemplified like this:

JavaScript
var b = 2 > 8 ? 'bar' : 2 > 0 ? '' : 'baz';

So, moving to the other potential perplexing point, it may seem that ignoring the arguments from the arguments list passed in the round brackets of the call statement is something that would look strange if you come from a different language. Considering that the developer writing a function call needs to have a concrete idea on its outcome, an important thing to think about, using the approach described in this article, is that the set of arguments may be passed as a set of property assignments, leaving the argument list to our disposal for others potential uses.

Let's see a couple of equivalent function call statements:

JavaScript
console.log(fn(1, -2, 3)); // => 2
console.log((fn.first = 1, fn.medium = -2, fn.last = 3, fn.invoke(null))); // => 2

These two statements are equivalent as final result expected, and, despite anything to the contrary about calling invoke method with zero parameters writing fn.invoke(), it is also important to bear in mind that f.first is still a legit way to reference the first argument value because of the function name declared in our main example. This is a justified approach if you want to reuse function objects in order to avoid greedy GC operations, and you code in a way which is to create a single object once and update its named argument property values during argumentify call.

A different concern could be about Object.defineProperty parameterization; so, I hope ECMAScript specification could help to clear up things especially about configurable set to true in order to allow named argument default values further changes.

- 8.6.1 excerpt: "[[Configurable]]: If false, attemps to delete the property, change the property to be an accessor property, or change its attributes (other than [[Value]]) will fail."

In the same area of code design decision, as a matter of personal preference, I have decided to set enumerable to false; this way a for-in loop going through all enumerable properties of an object, even those in the prototype chain, will skip named argument properties.

Conclusions

I have spent some time to come to the conclusion, matured through a few cycles of rewriting, that the simple technique I've described in this article can be even in part useful to other developer projects. Not only does it provide a way to govern control flow about passing function's arguments, it also could become a basis for thinking about a certain way to design JavaScript solutions in a more maintainable way.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)