Args()
A class for remote script arguments
Getting the arguments out of a query string in the URI of a
<script>’s src
attribute turned out to be
surprisingly easy in Passing JavaScript arguments via the src attribute. It’s only about 20 lines
of code but we really don’t want it floating around in a dozen
different service script and we’d rather never have to see it directly
again. Just have a simple interface to use the functionality and get
it out of the way. Enter the object class.
Our work is already mostly done. We’re just going to wrap what we’ve already written into an object like we talked about in the last chapter: Object orientating and prototypes.
First, a constructor
function Args () { var caller = this.findCaller(); var qString = caller.src.replace(/^[^\?]+\??/,''); if ( qString ) { this.queryString = qString; this.params = this.parseArgs(); } }
We’ve leapt ahead a bit here. The argument object always needs
to know the query string from its <script>’s src
attribute. That’s the whole point of the object. So we want to
initialize with it. We get at it with the method
findCaller()
, which we haven’t written yet.
We could dispense with findCaller()
and do the
src
lookup inside the constructor, Args()
.
It is preferable to break code up into the smallest holistic units.
The constructor is about making an object, not about finding its
src
, so we break it out. When writing methods, functions,
and subroutines, it’s a good rule of thumb to never write more than be
taken in at a glance. It’s easier to work with and it’s
especially easier to debug. You’ll thank yourself later.
We’re not quite ready to call new Args()
. We need the
methods it uses first.
Args.findCaller()
Args.prototype.findCaller = function () { var scripts = document.getElementsByTagName('script'); return scripts[ scripts.length - 1 ]; }
We’ve also taken the liberty of installing the parsed parameters into
our object’s params
attribute. We use another method that
the object, this
, calls upon itself to load the returned
data into itself. Aren’t objects swell.
Args.parseArgs()
Args.prototype.parseArgs = function () { var Params = new Object (); var query = this.queryString; if ( ! query ) return Params; var Pairs = query.split(/;/) for ( var i = 0; i < Pairs.length; i++ ) { var KeyVal = Pairs[i].split('='); if ( ! KeyVal.length == 2 ) continue; if ( ! ( KeyVal[0] || KeyVal[1] ) ) continue; var key = unescape( KeyVal[0] ); var val = unescape( KeyVal[1] ); val = val.replace(/\+/g, ' '); val = val.replace(/&/g, '&'); val = val.replace(/>/g, '>'); val = val.replace(/</g, '<'); Params[key] = val; } return Params; }
This is our friend from Passing JavaScript arguments via the src attribute. NB: we’ve chosen the version that does not support multiple values per key. This isn’t the standard but it’s easier to deal with and it’s an in house library so we don’t have to support any standards at all if we don’t feel so inclined.
We’ve also made an important addition to avoid cross-site
scripting attacks. After unescaping the argument value we convert
<
, >
, and &
into
their HTML entities. We might consider doing this for the keys as well
but we’re never going to echo keys back to the document so it’s not in
this implementation.
Sample usage
// ALL the Args() code goes here; omitted to save space // Now we can construct objects var args = new Args(); var params = args.params; for ( var key in params ) { document.write( '<div style="margin:1ex">' ); document.write( "key: " + b(key) ); document.write( "<br />" ); document.write( "value: " + b(params[key]) ); document.write( "</div>" ); } // little extra something to get bold tags easily function b (str) { return '<b>' + str + '</b>' }
Now a call like so:
Sample client call
<script type="text/javascript" src="http://elektrum.org/js/args.js?holiday=Valentine's;flower=Roses"> </script>
The output
That’s good but...
We have a nice arguments object1 now but one part of it is irksome. This step:
var params = args.params;
An arguments object should be just that: arguments. It shouldn’t have a level of indirection to get at them. It’s inelegant. This is fixable if we’re careful.
Args(), take 2
The new concerns
Since we’ll be putting the query string keys at the top level of the
Args()
object we need a way to keep them separate from
other attributes in the object. We track things like the
queryString
through the object’s properties. We can’t
just take them out because they have nowhere to go but global. That’s
a bad solution.
There is a hacker convention for marking a variable or attribute name
private. Prefix it with an underscore. varName
becomes
_varName
and secretMethod()
becomes
_secretMethod()
.
It is works because other hackers will respect its privacy and the general user would never think to start a variable name with an underscore. It doesn’t mean the variables and methods are protected, it just means they’re a bit out of reach of casual users and marked “Don’t Touch” for the savvy.
We will prefix our private, or non query param, attribute variables with an underscore. Don’t forget that methods are also attributes of an object/class. That means we’ll need to hide the private methods away too.
Finally we’ll need a custom way to iterate through the object. If we
do something like for ( var argKey in argObj ) ...
we’ll get a nasty surprise because we’ll get more than the query
string keys back. We’ll also get the method objects and any internal
variables the class uses for housekeeping.
The new object constructor
function Args () { var caller = this._findCaller(); var qString = caller.src.replace(/^[^\?]+\??/,''); if ( qString ) { this._queryString = qString; this._parseArgs(); } }
The new concerns are reflected in the new names, like the method
_parseArgs()
and the attribute
_queryString
.
The new _findCaller()
Args.prototype._findCaller = function () { var scripts = document.getElementsByTagName('script'); return scripts[ scripts.length - 1 ]; }
Only the method name has changed for _findCaller()
.
The new _parseArgs()
Args.prototype._parseArgs = function () { if ( ! this._queryString ) return false; var Pairs = this._queryString.split(/;/); for ( var i = 0; i < Pairs.length; i++ ) { var KeyVal = Pairs[i].split('='); if ( ! KeyVal.length == 2 ) continue; if ( ! ( KeyVal[0] || KeyVal[1] ) ) continue; if ( KeyVal[0].match(/^_/) ) continue; var key = unescape( KeyVal[0] ); var val = unescape( KeyVal[1] ); val = val.replace(/\+/g, ' '); val = val.replace(/&/g, '&'); val = val.replace(/>/g, '>'); val = val.replace(/</g, '<'); this[key] = val; } }
_parseArgs()
works a bit differently. We no longer use
the intermediary object named Params()
and we return
nothing. We’re not giving back a parameters object. We are the
parameters object. Which is reflected specifically in the following
change from the first version.
Params[key] = val;this[key] = val;
For safety’s sake we also disallow parameter names with leading underscores. If you do allow it, it gives anyone with a browser an open channel to mess with the internals of your services.
if ( KeyVal[0].match(/^_/) ) continue;
We also need a way to iterate on the arguments. Since we’ve put them
at the top level, they clash with methods and private variables. We do
a simple check, /^(_|getKeys)/
, to build a list of
argument names in the object.
Arg.getKeys()
Args.prototype.getKeys = function () { var keys = new Object(); for ( var attr in this ) { if ( attr.match(/^(_|getKeys)/) ) continue; keys[attr] = undefined; } return keys; }
Note we could instead use delete
in the constructor to
manually remove every property of the object that’s not an argument
key. Then for ( key in args ) ...
would work just fine.
This precludes using the object as anything but an associate array of
arguments thereafter. It means no class data allowed either. We have
grander plans for the object when we start building libraries.
Sample usage
// ALL the new Args() code goes here; omitted to save space var args = new Args(); // skip getting the "params" out, iterate with our getKeys() method for ( var key in args.getKeys() ) { document.write( '<div style="margin:1ex">' ); document.write( "key: " + b(key) ); document.write( "<br />" ); document.write( "value: " + b(args[key]) ); document.write( "</div>" ); } // little extra something to get bold tags easily function b (str) { return '<b>' + str + '</b>' } // We also have direct access to the keys we know/expect document.write( '<div style="margin:1ex">' ); document.write( 'Direct access via <i>args.art</i>: ' + b( args.art ) ); document.write( "</div>" );
Now a call like so:
Sample client call
<script type="text/javascript" src="http://elektrum.org/js/args2.js?art=Tae+Kwon+Do;sport=satire"> </script>
“getKeys” is now, for all purposes, a reserved word. As far as parameter names go it can’t be used. Choose this sort of thing carefully.
The output
Ta! We’ll make a a couple of adjustments to the class along the way to extend where it can be used. The final version is in and the reasons for changes to it are discussed in Using JS code libraries.
Now for a slightly more complex object: Query catching for fun and profit.
arguments()
object
built into JavaScript. You have access to it inside functions and
methods and nowhere else. It contains an indexed list of the arguments
passed to the function as well as two special attributes:
length
and callee
which contains a
reference to the function itself which facilitates recursive logic.