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 »
10,743 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 »
10,743 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 »
10,743 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 »
10,743 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
InVision App - Prototyping Made Beautiful With Prototyping Tools Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 16, 2012 at 8:18 PM
Best Of ColdFusion 10 Contest Entry - HTML Email Utility
Just found this, looks good! I'm trying to run it on local, it's the 64bit version and I'm experiencing horrible lag. On average the generate.cfm processes the content change in 60-90 seconds. I've ... read »
May 16, 2012 at 6:40 PM
Maintaining Sessions Across Multiple ColdFusion CFHttp Requests
I am trying to integrate this CFHTTPsession into an application that will log into zeekrewards.com to post ads and I am not having any luck. The code works perfectly for logging into other websites, ... read »
May 16, 2012 at 2:44 PM
Creating A Sometimes-Fixed-Position Element With jQuery
Thank you, very useful technique! Worked like a charm. ... read »
May 16, 2012 at 1:58 PM
Movies As A Religious Experience
Acting can, in a way, ruin the movie-goer's experience. I used to be able to get so caught up in movies and their plots, and totally engaged. But lately, I haven't been able to as much with a lot o ... read »
May 16, 2012 at 1:52 PM
The Science Of Optimal Post-Exercise Nutrition
children of this age eat very less vegetables so u can opt for salads they will like it also carrot ,cucumber,onion and as far as pulses are concerned u can boil them ,give him along with mashed rice ... read »
May 16, 2012 at 1:34 PM
Strange ColdFusion JRUN Stack Overflow Error
Hey, Recently I updated my jrun4 using the latest updater 7 and now i am having memory issues :(:(:( any help is appreciated ... read »
May 16, 2012 at 9:56 AM
ColdFusion 10 Beta, Apache Tomcat, And Symbolic Links On Mac OSX
Hi, Now that ColdFusion 10 is out I have stumbled over this as well and I cannot figure out the proper solution. We're running virtual hosts via Apache2; the ColdFusion-applications store their fil ... read »
May 15, 2012 at 6:03 PM
Movies As A Religious Experience
@Ben, I don't know whether you'd consider this a religious observation, but it seems to me, in a sense, movies multiply how many lives we get to have. Each movie is like a little extra life we get ... read »