Micro Templating from John Resig: a code review

John Resig (author of jQuery) wrote a little script that implements client-side templating in JavaScript. This is of particular interest to me because I had to write something similar a few weeks ago. This code has a bunch of interesting idioms and features in it. It's unreadably short (IMHO) but here's a breakdown, statement by statment (generally going from outside in):
  1. (function(){})(); This is a new idiom to me; it's just a terse way to define a function then execute it. Contrast this with the YUI idiom that is YAHOO.namespace = function(){}; which usually attaches to a browser event at some point to start things off.
  2. var cache = {}; A private data area. It's a hash because you have to index the cache on the input string. This is an example of memoization.
  3. this.tmpl = function tmpl(str, data){...}; A somewhat verbose way to declare a function, but it combines with #1 to install a function on the global object. Generally, this isn't a very good idea, as there can be namespace collisions there.
  4. return data ? fn(data):fn; This is currying, I guess. (Return the transformed data, or the transformation if there is no data).
  5. var fn = expr0 ? [expr1] || [expr2] : [expr3]; This is idomatic JavaScript. Well, the expr1 || expr2 is idiomatic - it's a common way to say "if expr1 doesn't work, try expr2", very similar usage in perl. The nesting inside a ternary operator, and with complex multi-line expressions, just adds to the fun.
  6. !/\W/.test(str) - \W matches non-word characters. This is a handy way to say "if str only has word characters". Knowing what this means requires both regex and RegEx knowledge, if you know what I mean. (I personally hadn't run across the RegExp.test() method before). [Thanks for the correction!]
  7. new Function("obj", [method body]); I didn't know this, but JavaScript allows an alternate method of doing meta programming, the Function constructor, which doesn't require eval(). The Function() constructor takes arbitrary numbers of string arguments, the first of which are interpreted as names of declared parameters, the last being the function body. In this script, you call it by assigning to a var then calling the var as a normal function. Neat.
  8. var p=[],print=function(){p.push.apply(p,arguments);}; No idea what this does. push() is an array function that returns a number, so I'm not sure how "apply" is even defined here!
  9. with(obj){p.push(' - I know what this does, but not sure why it's here. Seems like p will have only element at this point. Yes, I realize this is the "outer push". Egads!
  10. split().join() is a fast way to do static replacement (according to John).
  11. with(obj){}; Flanagan's book (JavaScript: The Definitive Guide) warns against using "the with statement". However Resig uses it here so that the template expressions are evaluated relative to the data object. (Anyone who's coded in VB knows the with statement - I personally really liked it and wish it was in Java and was usable in JavaScript)
So, there are some moving parts here that I don't completely understand, especially the use of push() inside the with() expression. I mean, why bother with that? I'm pretty sure the answer will come eventually, but for now I find myself puzzled, just like when the post-doc TAs would skip like 10 steps doing that line integral. I'm pretty sure it has something to do with that wierd print assignment.

5 comments:

Daniel Steigerwald said...

\W Matches any non-word character. Equivalent to [^A-Za-z0-9_]. For example, /\W/ or /[^A-Za-z0-9_]/ matches '%' in "50%."

var p=[],print=function(){p.push.apply(p,arguments);};

It's easy. Method push accept more then one arguments, you can write push(1,2,3..). The reason, why push is called with apply, is because apply is clever way, how to pass array as an arguments.
Imagine this: print(1,2,3) -> p will be [1,2,3].

with(obj){p.push(' - It's also easy. Try to alert generated function and you will see :)

josh said...

Thanks for the clarification on what \W means. I've updated the post!

I have to think about how push is being used here. It seems that the str is actually being modified to be a comma delimited argument to push. You are right that I should be more emprical and write some test scripts. :)

Mark said...

Thanks for your post Josh.

This comment may be a bit late. And I may be mistaken, but it appears to me that:

"print=function(){p.push.apply(p,arguments);}"

is not needed for the template to work.

The variable print is assigned the function, but the function is never called within the templating method. As a test, I removed it and the code ran successfully. (I assume this function was added for some debugging purposes).

The "obj" variable holds the data and the elements of the "p" array are designed to hold a generated html string representing a single item in the template. The "arguments" variable that is contained in the print function is never utilized by the template generator.

Mark

josh said...

It's pretty cool to reread this post after almost 2 years. So much has changed in my understanding of JavaScript. The code is still very clever, but I can understand most of it :)

For example, the function 'tmpl' is defined on 'this' which is actually a reference to the object containing the anonymous function. What is this object you ask? In a browser that would be 'window'. Easy, see?

Zork538 said...

@Mark
the print function isn't used for generating the template.
But it is a convenience method which can be used inside the <% %> tags, to print something to the output of the template.