It is axiomatic that JavaScript allows programmers to change the value of object-properties. Given that a method is simply a property of an object, this makes it possible to change the function-body to which a given method-name refers, such that it refers to a alternative function-body. Example 1 illustrates this.
It follows that, if the replacement function possesses a reference to the original method-body, it can call that original function, passing it whatever arguments that client code provided. From the point of view of the client and the original function, a call to that function will proceed as usual, even though the replacement function operates as a transparent interlocutor or middleman between them – we can say in this instance that the replacement function 'intercepts' the call to the original function, and thus acts as a proxy for that method.
Given this, it is possible for the proxy to call another function before the method-body executes, and to call a third function after the method body has executed. Furthermore, we can view these prior and subsequent functions as a 'prefix' and a 'suffix' ('affixes' collectively), and Example 2 illustrates this essential concept.
This is more than a curiosity, because we can put the prefix and suffix to work. For example, it is possible to furnish the prefix with the arguments array that is passed in the call from the client to the method, and this allows us to check the type and value of the arguments passed, and to raise an exception if these do not fall within prescribed limits.
Similarly, we can pass the value returned from the method-body to the suffix function, which, like the prefix, can validate that value against prescribed limits, and these two principles open the door to a range of design-by-contract techniques. As experience shows, this is of considerable benefit in JavaScript development.
Method-call interception can also be employed to log method calls, where the prefix records, say, the time of the call, along with the arguments that were passed, and where the suffix records the time that the method returned, along with any value that it generated.
Critically, we can employ these two techniques without perturbing the code under study. This is of profound value, as it allows us to attach design-by-contract constraints when the system is under development, and to omit the code that imposes those constraints when we deploy the system. This gives us the ability to detect and diagnose defects easily and quickly when we need to, while we enjoy maximal performance and efficiency when the application is out in the wild – a win-win situation.
But the advantages do not end there: we can also use method-call interception to trigger the loading of resources on-demand and only when they are needed, which confers great efficiency gains. Similarly, we can use it to effect the idea of argument mutation and injection, which at the least allows in-situ unit testing (and which the AJS object's user-guide discusses in detail). Moreover, enterprising developers may find the technique to be of use in imaginative designs that approach JavaScript programming in original and innovative ways.
Contents |
Manipulating Function References The Challenge of Scaling The AJS Object |
//-- Example 1 --------------------------------------------------------------- var MyObject = { method_A : function () { } }; function SomeOtherFunction () { console.log ("SomeOtherFunction executed"); } MyObject.method_A = SomeOtherFunction; // Change the reference. MyObject.method_A (); // Execute what appears to // be the original method. -- Output -------------------------------------------------------------------- SomeOtherFunction executed
//-- Example 2 --------------------------------------------------------------- var MyObject = { method_A : function () { } // The code for this method }; // remains unperturbed. //-------------------------------------- var OriginalMethod = MyObject.method_A; // Hold on to the reference // to the original method-body. function Prefix () { console.log ("Prefix executed"); } function Suffix () { console.log ("Suffix executed"); } function SomeOtherFunction () { Prefix (); // Execute the prefix. OriginalMethod (); // Execute the original method. Suffix (); // Execute the suffix. } //-------------------------------------- MyObject.method_A = SomeOtherFunction; // Change the reference. MyObject.method_A (); // Execute what appears to // be the original method. -- Output -------------------------------------------------------------------- Prefix executed SomeOtherFunction executed Suffix executed
While the benefits of this simple technique are clear, the approach also raises significant questions. What if we wish to apply more than one prefix or suffix to a method? We might, for example, wish to use method-call validation in conjunction with method-call logging. We might also wish to instrument a method in a variety of ways, which would require the ability to attach multiple affixes to that method in order to avoid redundancy and conflicts between different concerns; and what happens if we throw on-demand loading into the mix at the same time?
Equally, what happens in the case of large teams that are developing a range of interrelated libraries and applications? In this scenario, different team-members may need to employ method-call interception independently of (and possibly in ignorance of) each other, without the need to coordinate their actions. This requires the ability to intercept calls to a given method irrespective of other interceptions that our fellow developers have applied to that method. Moreover, we may need to suspend and then resume subsequently the operation of a given prefix or suffix, because, say, we wish to suppress logging activity temporarily.
There will also be times when we need to remove the interception mechanism from a given method too. In the case of on-demand loading, a call to a method may trigger the loading of a resource, but it follows that it would be utterly redundant for subsequent calls to that method to cause a re-load (if you have retrieved a resource once, there is no point in retrieving it again). This requires therefore the ability to remove a loading prefix, while leaving other affixes in place. Moreover, if all prefixes and suffixes are removed from a given method, the method in question should be returned to its normal, non-intercepted state, in order to maximise performance and efficiency.
It turns out that, in the absence of supporting technology, such scenarios cause the essential proxy-function technique demonstrated above to fail, thus placing the benefits of method-call interception beyond reach in all but the simplest scenarios.
However, it is possible to implement a formal 'call-interception manager' that addresses all of the considerations detailed above (along with others not mentioned), while preserving the transparency and power of of the technique at any scale. AspectJS, specifically, the AJS object, is just such a call-interception manager that supports the application, management and removal of multiple prefixes and suffixes to a given method, while relieving programmers from all the book-keeping headaches.
Moreover, it is inevitable that any approach to method-call validation, method-call logging and on-demand loading etc. will face the same essential challenges, and, in doing so, will use the same techniques. This suggests that a set of libraries should be possible that use method-call interception to implement method-call validation, logging and on-demand loading, while managing all the complexity those approaches incur. This is the purpose of AJS_Validator, AJS_Logger, and AJS_ODL, which are a set of client components that use the AJS object to implement the techiques detailed above.
So, to learn how to use the AJS object directly, go now to the user guide. Alternatively, if you wish to explore in more detail the raison d'être for AJS_Validator, AJS_Logger , and AJS_ODL go now to the appropriate motivaton pages for each of those adjuncts, and go thereon to their respective user-guides to learn how to employ them in your applications.