The 'on-demand' approach to deploying JavaScript applications incrementally confers great benefits, but non-trivial use can pollute functions with expressions that are unrelated to their essential nature. Aspect-Oriented Programming can resolve this problem completely; yet AOP in JavaScript is a hitherto unexplored area. This article therefore shows the need for such techniques in web development, and explains the principles of function interception in JavaScript. It thereby demonstrates an elegant technique for loading JavaScript libraries transparently at the point of need, and shows that generalising the underlying approach allows AOP in web applications on an industrial scale.
This article appeared originally in the November 2007 edition of Dr Dobbs Journal of Programming. |
JavaScript programs are associated with HTML documents by means of script elements where, in the simplest case, opening and closing <script> tags delimit the JavaScript syntax embedded within a page. However, many pages may require the same functionality, and this mandates placing the relevant code in discrete files that bear a .JS extension. Such libraries can then be pulled into the execution environment by specifying the file name as the value for the 'src' attribute in an empty <script> element, and listing one illustrates this.
JavaScript's interpreted nature aside, this is identical to the #include mechanism supported by C and C++, but the Pareto principle[1] also indicates the seeds of trouble. Generally, 90% of a program's run-time is spent executing only 10% of its code, implying that functionality should be loaded into the execution environment only when needed lest resources be consumed redundantly. Compiled applications accommodate this by using dynamic-link libraries, but without similar intervention a client-side JavaScript application can download large tracts of code that are never actually executed.
Such inefficiency should be avoided assiduously in web deployment; it consumes bandwidth unnecessarily, increases download times, and stresses both server and client needlessly. The poor performance that results can render an otherwise competent application unpopular if not unusable, and this concern can only grow as web developers become ever more adventurous in their use of JavaScript.
<!-- Listing 1 - DTD etc omitted for brevity --> <html> <head> <script type = "text/javascript" src = "MyLib.js"></script> </head> <body> ... </body> </html>
The only solution to this is to adopt a 'lazy' approach to loading, otherwise known as 'on-demand JavaScript', where code is retrieved from the server as the need arises. This can circumvent the redundancy problem, as only a fraction of an application need be downloaded initially, with additional functionality being acquired as necessary. This avoids the redundant consumption of bandwidth, expedites delivery, and reduces the stress on both server and client; and there are two principal routes to implementing the technique.
The first entails connecting to the server explicitly using an XMLHTTPRequest object, where the code that is returned is evaluated into the execution environment. The second approach to loading on demand, the 'script-tag hack', turns upon the fact that script elements form part of the DOM-object hierarchy for a given page. This means that an application can create a script element, and then set the value of its src attribute to the name of a JavaScript library, before inserting the element into the DOM hierarchy for the page. This causes the interpreter to download the library, thus introducing its functionality into the execution environment. Usefully, this principle also applies to CSS — setting the href of a <link> element dynamically causes the client to download and evaluate the corresponding style-sheet.
Note that these techniques yield the same net result, but that choosing one over the other presents certain tradeoffs. On the one hand, the script-tag technique is asynchronous, whereas XHR allows synchronous as well as asynchronous requests. In contrast, the script-tag technique allows retrieval of code from anywhere, whereas XHR allows requests only to the originating domain.
Obviously, the mechanics of these techniques should be implemented within discrete functions, thus reducing the act of loading a resource to a simple call. However, the optimum designs for complex and powerful applications demand a degree of granularity to match — large programs are often split into many small modules, and in this case loading on demand can cause code to become littered with extraneous calls. Listing two demonstrates this, where loadJS_STH loads a library using the script-tag approach and loadJS_XHR uses the XHR-based technique (loadCSS is self-explanatory).
In this example, using on-demand techniques necessitates populating DisplayMenu with calls to load code that will be required subsequently; yet those calls are extrinsic to the business of displaying menus. The loading syntax is therefore disjunctive, and this degrades the functional cohesion[2] of the code, thus harming readability and comprehension. In other terms: the various concerns (or 'aspects') of the system cut across each other and are therefore 'entangled'.
Moreover, a great many functions like DisplayMenu will bloat the application, thereby decreasing efficiency. Furthermore, the relevant resources need be retrieved only once, which means that repeated calls to DisplayMenu will cause redundant calls to the loading functions — those calls serve their purpose just once before becoming dead weight that impinges on performance. All of these points run contrary to the very aim of loading on-demand, which is to pay only for what is used.
// -- Listing 2 ----------------------- function DisplayMenu (...) { if (...) { loadJS_XHR ("..."); } else { loadJS_XHR ("..."); } loadCSS ("..."); loadJS_STH ("..."); // // Code for displaying a menu // } function loadJS_XHR (LibName) { ... } function loadJS_STH (LibName) { ... } function loadCSS (LibName) { ... }
Happily, these problems can be resolved decisively. All objects in JavaScript are implemented as associative arrays that hold name-value pairs; meaning that the interpreter accesses a given object-member by using the member's name as a key with which to look up the associated value. Where the member-name corresponds to a method, the associated value is a reference to the body of that method; and it follows that calling the method causes the interpreter to retrieve the relevant reference, whence it executes the function body. The first diagram illustrates this.
The value that is associated with a given member name can be changed using surprisingly simple syntax, and this means that the reference to a given object-method can be replaced with a reference to a function that acts as a proxy. This will cause calls to the original method to be 'intercepted' by the proxy, which can do whatever it likes thereafter, and listing three illustrates this idea.
// -- Listing 3 ----------------------- var MyObj = // Define the object literally { MyMethod : function () // Define a method { ... } }; function Proxy () { ... } // Define our interloper MyObj["MyMethod"] = Proxy; // Replace the reference ... MyObj.MyMethod (); // Unbeknownst to the caller, // Proxy will now execute
This in hand, the cohesion problems of loading on demand evaporate. If the proxy calls a library-loader, before invoking the original method-body, then calls to the intercepted method will trigger a download automatically and transparently, followed by execution of the method itself. The second diagram depicts the essential concept.
Critically, this technique permits complete separation of the code that creates an interception from the 'interceptee', thus removing adventitious syntax from the points at which library loading is employed, and allowing all interception-setting code to be located in one place. This disentangles concerns and restores cohesion, while conserving the benefits of loading on demand, thus yielding the JavaScript equivalent of dynamic link-libraries. Listing four demonstrates this.
Note that, after calling the library-loader, the proxy restores the reference in the owner-object to its original value. This precludes needless performance-degradation subsequently, thus ensuring that we pay only for what we use. Note also that the proxy completes this step before invoking the interceptee, otherwise recursive calls would cause it to re-execute redundantly. Furthermore, note that JavaScript functions are objects in the 'class' sense, and support a method called 'call'. Using this, the proxy can pass its arguments array to the original function, thus passing on any arguments supplied by the interceptee's caller, and thus preserving transparency.
// -- Listing 4 ----------------------- function loadJS_XHR (LibName) { ... } function MyFunc () { MyOtherFunc (); } function Proxy () { loadJS_XHR ("MyLib.js"); // load synchronously // using XHR this["MyFunc"] = OriginalRef; // Replace the ref here in case // MyFunc is recursive return OriginalRef.call (this, arguments); } var OriginalRef = this["MyFunc"]; this["MyFunc"] = Proxy; // Intercept is set at a distance // from clients of MyFunc ... MyFunc (); // Call routed through Proxy, // library is loaded transparently ... MyFunc (); // Call is now entirely conventional // -- Contents of MyLib.js ------------ function MyOtherFunc () { ... }
This technique can be generalised to allow the arbitrary interception and loading of methods and libraries respectively; and employing a function that contains an inner function is the optimum approach here. In this scheme, the outer function takes arguments denoting the interceptee, its owner-object, and the name of the library to be loaded; and on execution, it replaces the interceptee-reference in the owner-object with a reference to the inner function.
This forms a closure (and what a superb use for those exotic creatures) that makes the outer function's parameters available to the inner function when it executes. It follows that calling the interceptee invokes the inner function, which calls the library-loader before replacing the reference to itself with the original reference to the interceptee. Listing five demonstrates this.
// -- Listing 5 ----------------------- function loadJS_XHR (LibName) { ... } function SetIntercept (OwnerObject, MethodName, LibName) { var OriginalRef = OwnerObject[MethodName]; OwnerObject[MethodName] = function () // Closure created here { loadJS_XHR (LibName); OwnerObject[MethodName] = OriginalRef; return OriginalRef.call (this, arguments); } } function MyFunc () { ... } SetIntercept (this, "MyFunc", "MyLib.js"); ... MyFunc (); // Call routed through inner function ... MyFunc (); // Call is now entirely conventional
The reality is that function interception in JavaScript constitutes a form of meta-programming in an interpreted language, and has many aspect-oriented applications beyond loading on demand. Given that wider context, we can view a call to a library-loader as a 'prefix' to the interceptee, and this introduces the notion of functions that act as 'suffixes', where a method is called after, rather than before, the invocation of the interceptee.
This leads naturally to the notion of 'wrapping' an interceptee with such affixes, which leads from there to the question of affix lifetime. In the on-demand example, the prefix executes just once before it is detached, but there may be instances where an affix should execute for a certain number of times, or even infinitely; the ability to remove an affix explicitly is also desirable. Moreover, multiple affixes may be necessary — in listing two there are three loading calls, which suggests applying three discrete prefixes to DisplayMenu, and this introduces the notion of changing their execution order if needed. Then there is the question of exception handling, error checking and diagnostics, as well as performance, all of which raise significant design questions.
A clear case exists therefore for a general-purpose function-interception library that addresses these points, thereby obviating re-invention of the wheel; and AspectJS[3] is one such resource that is available on the web. Encapsulating the mechanics of function interception, it allows (among other things) unlimited numbers of affixes for a given function, with fine control over their type, number, longevity and execution order. In deference to a venerable tradition, therefore, listing six demonstrates loading a JavaScript library transparently on demand using AspectJS.
This uses the addPrefix method of a singleton object called AJS to set up the interception. The first two parameters signify the OnMouseClick method of the global object as the interceptee, and the third specifies loadJS_XHR as the prefix. The fourth is an optional parameter that is passed to the prefix — in this case the name of the library to be loaded — and the final argument specifies that the prefix should be removed after just one execution. The net result is that clicking on the client area of the browser's window causes Hello.js to be loaded, thus allowing a customary greeting to be displayed.
<!-- Listing 6 --> <html> <head> <script type = "text/javascript" src = "AspectJS.js"></script> <script type = "text/javascript"> function loadJS_XHR (LibName) { ... } // Use synchronous loader AJS.addPrefix (this, "OnMouseClick", loadJS_XHR, "Hello.js", 1); function OnMouseClick () { SayIt (); } </script> </head> <body onclick = "OnMouseClick ()"> ... </body> </html> // -- Contents of Hello.js ------------ function SayIt () { alert ("Hello World"); }
Resolving the downside to on-demand JavaScript allows web developers to exploit its potential fully; and generalising the solution opens the door to a wealth of aspect-oriented techniques. Consolidating the principles of function interception in a reusable library allows developers to affix methods arbitrarily to virtually any function call.
This permits the serious application of AOP techniques to professional web-development, thus allowing remote tracing, debugging and error reporting, along with client-side performance measurement, user-interaction modelling, and others that, as yet, are only a twinkle in the developer's eye.
[1] https://en.wikipedia.org/wiki/Pareto_principle
[2] https://en.wikipedia.org/wiki/Cohesion_%28computer_science%29
[3] https://aspectjs.com
Aspect Oriented Software Development
Filman, Elrad, Clarke and Aksit
Addison Wesley
ISBN 0-321-21976-7