Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with:

Invoking Javascript Methods With Both Named And Ordered Arguments

By Ben Nadel on

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

@Ben,

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:

http://en.wikipedia.org/wiki/Niklaus_Wirth

(Creator of Pascal, Modula, Object Pascal, etc, computer languages.)

P.S.: Changing my name here for uniqueness (formerly Steve, which duplicated others).

Reply to this Comment

@WebManWalking,

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.

Reply to this Comment

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.

What do you think?

Reply to this Comment

@Brian,

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.

Reply to this Comment

@Ben,

Yep, good points. I think an optional context is the way to go. Or, instead of augmenting Function, withNamedArgs could simply accept both a function and a context.

Either way, this is cool meta-programming stuff, Ben.

Reply to this Comment

@Brian,

Thanks my man; just trying to keep up with the crazy awesome Javascript stuff you and John have got your hands in :)

Reply to this Comment

What about python-like decorator?

  • function NamedArgsFunction(func) {
  • return function (args) {
  • if ($.isObject(args)) {
  • return func.invoke(this, args);
  • }
  •  
  • return func.apply(this, arguments);
  • }
  • };
  •  
  • var sarah = {
  • name: "Sarah",
  • sayHello: NamedArgsFunction(function( name, compliment ){
  • return(
  • "Hi " + name + ", I'm " + this.name + ". " +
  • "You're so sweet to say that I'm " +
  • compliment + "."
  • );
  • })
  • };

Then you can invoke NamedArgsFunction both ways:

  • sarah.sayHello({compliment: "wicked hot", name: "Ben"});
  • sarah.sayHello("Ben", "wicked hot");

Obvious limitation: function cannot accept object as first argument in second form.

Reply to this Comment

@Yuriy,

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.

Reply to this Comment

Your regex is unfortunately broken by embedding a coment in the parameter list of the function. You can close the parens so more arguments are invisible, or just outright break a parameter name.

(function(/*)*/ myParamNameWithAComment, my, secret, parameters) {}).toString()

But having comments in the function's string representation is not so bad. You could use comments in arguments to document what Type each argument is supposed to be, and whether it is optional.

function hi(msg /*String*/, cb /*Function?*/) {
}

hi.strictInvoke(1); // TypeError: 1st argument of function "hi" must be of Type String

Hell, you could even add assertions!

function div(a /* Number */, b /* Number */ /* b !== 0 || throw 'up' */) {
return a / b;
}

Reply to this Comment

Post A Comment

?
You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.