Function Interception
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. Figure 1 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; see Listing Three.
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
Cohesion Restored
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. Figure 2 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 DLLs; see Listing Four.
function LoadLib_XHR (LibName) { ... } function MyFunc () { MyOtherFunc (); } function Proxy () { LoadLib_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 () { ... }
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 reexecute 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 preserving transparency.