Invoking Javascript Methods With Both Named And Ordered Arguments

One of the features that I absolutely love about ColdFusion is its ability to invoke methods using both named and ordered arguments. And, after yesterday's deep exploration of the argumentCollection behavior in named-argument invocation in ColdFusion, I wanted to see if this kind of dual invocation nature could be ported over to Javascript. I like the way that the existing call() and apply() methods work in Javascript; so, I decided to augment the Function prototype with my own invoke() method that would accept an invocation context and a named-argument map.

I'm not crazy about the idea of altering core prototypes, but for this experiment, I added the invoke() method to the Function.prototype object. This runtime change grants all functions in the given page access to the new invoke method:

Function.prototype.invoke = function( context, namedArguments );

Here, the context argument is the "this" reference we want to bind to when invoking the given method. The namedArguments argument is a hash of name-value pairs that will be mapped to the internal, ordered-argument invocation of the given method. Any named-argument that doesn't map to an ordered-argument during invocation will be presented as null at the time of invocation.

Essentially, the invoke() method takes the namedArguments collection and maps it to an array of ordered arguments before it invokes the native apply() method. Let's take a look at the code:

<!DOCTYPE html>

<html lang="en">

<head>

<title>Invoking Javascript Methods With Named Arguments</title>

</head>

<body>

<h1>

Invoking Javascript Methods With Named Arguments

</h1>

<script type="text/javascript">

(function(){

// I take the given function [as a string] and extract

// the arguments as a named-argument map. This will

// return the map as an array of ordered names.

function extractArgumentMap( functionCode ){

// Extract the argument string.

var argumentStringMatch = functionCode.match(

new RegExp( "\\([^)]*\\)", "" )

);

// Now, extract the arguments.

var argumentMap = argumentStringMatch[ 0 ].match(

new RegExp( "[^\\s,()]+", "g" )

);

// Return the argument map.

return( argumentMap );

}

// I allow the current method (this) to be executed

// using a named-argument map rathre than ordered

// arguments. Any non-provided arguments will be null

// for method execution.

Function.prototype.invoke = function( context, namedArguments ){

// Check to see if the arguments have been mapped for

// this method yet.

if (!("argumentMap" in this)){

// Extract and store the argument map. We need to

// pass in the target method (this) as a string

// in order to extract the argument map.

this.argumentMap = extractArgumentMap(

this.toString()

);

}

// Create an array for our invocation arguments.

var orderedArguments = [];

// Now, let's loop over the argument map to move the

// named arguments over to the apply arguments.

for (var i = 0 ; i < this.argumentMap.length ; i++){

// Check to see if the named-argument was

// provided by the caller.

if (this.argumentMap[ i ] in namedArguments){

// Map the named-argument to the ordered

// argument.

orderedArguments.push(

namedArguments[ this.argumentMap[ i ] ]

);

} else {

// The named-argument was not provided. Just

// add null for invocation.

orderedArguments.push( null );

}

}

// Invoke the target argument (this) in the given

// context and return the result.

return(

this.apply( context, orderedArguments )

);

};

})();

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// -------------------------------------------------- //

// Define a Sarah object.

var sarah = {

name: "Sarah",

sayHello: function( name, compliment ){

return(

"Hi " + name + ", I'm " + this.name + ". " +

"You're so sweet to say that I'm " +

compliment + "."

);

}

};

// -------------------------------------------------- //

// -------------------------------------------------- //

// Invoke sarah's sayHello() method using named arguments.

var response = sarah.sayHello.invoke(

sarah,

{

compliment: "wicked hot",

name: "Ben"

}

);

// Output the result.

console.log( response );

</script>

</body>

</html>

In order to map the named-arguments to ordered-arguments, the actual source code of the target function has to be parsed. This is an expensive operation so we only do it once per function. After the first invoke() of a function, the argument map is cached as a property of the function. All subsequent invoke() calls then use this cached map when converting the named arguments to ordered arguments.

Because the invoke() method is a property of the target function and not of the parent object, we lose sight of the parent object during invocation. As such, we need to pass in the execution context (the parent object) when using the invoke() method. In our case, we need to pass in the "sarah" reference even though we are invoking a class method on the sarah instance. This is less than ideal; however, when we run the above code, we get the following output:

Hi Ben, I'm Sarah. You're so sweet to say that I'm wicked hot.

As you can see, the named-argument hash was properly mapped to the ordered arguments, "name" and "compliment," of sarah's sayHello() class method.

Right now, in Javascript, if you want to allow for optional arguments, you either have to perform argument checking or convert your method signature to accept an argument hash. By allowing for named-argument invocation, however, you can get a best-of-both-worlds approach: you can have the simplicity and self-documenting code afforded by ordered-arguments while allowing for the flexibility afforded by non-consecutive, named-arguments.

Reader Comments

Thanks for this. A lot of people don't know the key trick of using toString() on a function. And your extactArgumentMap and building the orderedArguments array add real, non-trivial extra value.

I really think you should send this to John Resig. jQuery routines often accept object references for named options, typically options that are NOT on the formal arguments list, so I'm not thinking he would use it for that. But maybe he'll use it to make every jQuery function capable of call-by-name.

By the way, do you know Niklaus Wirth's joke about his own name should be pronounced? Someone asked if it should be pronounced "veert" or "worth". He said, "If you call me by name, it's veert. If you call me by value, it's worth." A variant of that joke is on Wikipedia:

Thanks my man, I'm glad you found this interesting. Coming from a ColdFusion background, I thought maybe no one else would find this intriguing since not too many languages let you call things both by name as well as by value. I happen to think it's one of the super powerful features of ColdFusion, though.

Of course, it requires the naming of arguments to be meaningful and accessible. Now that I say that, I am not sure how this works with minified code. I am not sure how the toString() will represent itself. That's probably going to be the biggest hurdle for any kind of main-stream usage.

Hey Ben, pretty interesting! How about instead of invoke() that directly calls the original function, something like Function.withNamedArgs() that returns a new function that caches the argument map in a closure instead of adding a property to the original function? Something like:

function foo(arg1, arg2) { ... }

// get wrapped foo with cached argument map

var foo_that_accepts_named_args = foo.withNamedArgs();

foo_that_accepts_named_args({arg1: "thing1", arg2: "thing2"});

That would also allow calling foo_that_accepts_named_args directly, which makes for slightly nicer code, imho.

I really like that approach too, especially for stand-alone functions. But, when it comes to class methods, I think we still run into the problem of going too far down the "this-chain". Perhaps the context could be passed into the withNamedArgs() method:

foo.withNamedArgs( context )

Then, the returned function would be bound to the given context when it is eventually invoked with the named-argument hash.

That's a pretty clever idea. Sure, you can't pass a single object; but, I would think that with a method where one would want to used named-arguments, it's probably because there are more than one argument being passed. As such, even as a use-case, it's very unlikely.

Perfect. That was just what I needed. I used this to map PHP-generated JSON to javascript function calls directly! It saved me hours of work because the functiones used ordered parameters.Many thanks Ben.