Invoking Javascript Methods With Both Named And Ordered Arguments

Posted October 26, 2010 at 10:20 AM by Ben Nadel

Tags: Javascript / DHTML

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

Oct 26, 2010 at 12:33 PM // reply »
70 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).


Oct 26, 2010 at 9:46 PM // reply »
11,238 Comments

@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.


Oct 27, 2010 at 11:05 AM // reply »
2 Comments

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?


Oct 27, 2010 at 11:24 AM // reply »
11,238 Comments

@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.


Oct 27, 2010 at 11:38 AM // reply »
2 Comments

@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.


Oct 27, 2010 at 9:36 PM // reply »
11,238 Comments

@Brian,

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


Nov 2, 2010 at 12:17 PM // reply »
1 Comments

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.


Nov 3, 2010 at 10:40 AM // reply »
11,238 Comments

@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.


Post A Comment

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.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 21, 2013 at 11:51 AM
Ask Ben: Parsing Very Large XML Documents In ColdFusion
Looking at my first ever XML document that I have to parse and put into MS SQL 2000 with CF8. I get it to list the desired Field name, many times over, and have a long list of this field name displa ... read »
May 21, 2013 at 9:25 AM
Turning Off and On Identity Column in SQL Server
you are awesome..i am lucky to get this blog between such a garbage one....Thanks, Prashant ... read »
May 20, 2013 at 4:38 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Dana, Your confusion is well founded, since this is a very confusing features. In fact, it ONLY works if you use array notation. Meaning, that this: arrayToList( query[ "columnName" ] ) ... read »
May 20, 2013 at 4:34 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
I was thinking chicken and the egg, I wouldn't have expected it to work in the valuelist going in I guess. Maybe I just need a beer, long day :) ... read »
May 20, 2013 at 4:29 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Dana, That's if you're trying to reference a specific row. In this case, we're trying to reference the entire query column as one cohesive value. So, you are correct that if you wanted to output a ... read »
May 20, 2013 at 4:24 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
I thought when you used array notation to reference queries you always had to have the row or it would throw a similar error as well? ... read »
May 20, 2013 at 11:45 AM
Using jQuery's Animate() Step Callback Function To Create Custom Animations
This is really useful. I found out that you don't actually have to use a dummy css property (surprisingly). To animate a property in a linear-gradient for instance I did this this.css('someLinearGra ... read »
May 20, 2013 at 10:51 AM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Josh, Oh snap! You're totally right! I'm not sure I've ever tried that. I did know that you can call a number of other array-methods on ColdFusion query columns: http://www.bennadel.com/blog/167 ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools