AJS Object User-Guide

Introduction

The AJS object is a singleton JavaScript-object that is instantiated by a self-executing function, and which supports just three methods that can be used to intercept calls to object-methods. By providing a 'prefix' and/or 'suffix' function when you apply an interception, the AJS object will ensure that the prefix executes before the intercepted method (the 'interceptee'), and that the suffix executes after.

The AJS object allows you to apply, in principle, an infinite number of such 'affixes' to a given interceptee, with full control over their lifetime and execution order; moreover, all affixes receive the arguments passed to the interceptee, while suffix functions also receive the value that the inteceptee returned.

The AJS object manages all elements of the interception mechanism, which it implements by substituting a proxy function for the reference that the object in question possesses for a given method. The diagram illustrates this mechanism, which is completely transparent from the point of view of the interceptee and its callers. This means that argument-passing, value-return, and the passage of exception objects as the call stack is un-wound all occur normally, and, aside from one very minor exception, an intercepted method appears throughout to its callers to be the original non-intercepted method.

The AJS object is ECMAScript-5 compliant, and will run on ES3 run-times also, adapting automatically to the platform on which it finds itself. Moreover, it makes no use of host or native objects, and so you can use it in any context, not just within web browsers. Do note that, if you deploy a minified codebase that includes the AJS object, you should read the Minification page, which covers the issues that pertain here.

Contents Introduction
Importing the AJS Object
Applying Prefixes
Applying Suffixes
Applying Wrappers
Symmetric Wrappers
Using Inline Affix-Function Definitions
Validating AJS Method-Calls
Interception Candidates
Intercepting Inner Functions and Call-Backs
Intercepting Constructors

 // -- AJS-Object Interface Snapshot (pseudo code) -----------------------------
 //
 //    See the API documentation for formal property-definitions.
 //

 var AJS              =
    {
    addPrefix         : function (IOwner, I, PFunc)        { ... },
    addSuffix         : function (IOwner, I, SFunc)        { ... },

    addWrapper        : function (IOwner, I, PFunc, SFunc) { ... },

    //---------------------------------------

    AJS_Validator_Tag : "AJS_Object",
    Version           : "1.2"

    };
            
Diagram showing the nature of the interception mechanism.

Importing the AJS Object

To use the AJS object, import its code into your application as Example 1 shows (and which assumes a browser context).

Should you wish also to use one of the client components (in order, say, to implement design-by-contract practices), you should also import the code for the relevant AspectJS component, along with any supporting components, and specific instructions on these steps are given in the user guides for AJS_Validator, AJS_Logger and AJS_ODL respectively.


 <!-- Example 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->

 <html>
    <head>
       <script src = "AJS.js"></script>
       <script>

       // Application code

       </script>
    </head>

    <body> ... </body>

 </html>
            

Applying Prefixes

Within the scope of AspectJS terminology, and as the overview above mentions, a function that executes when another function is invoked, and which executes before that intercepted function, is called a 'prefix'. Conversely, a function that executes after the interceptee is called a 'suffix' – collectively, these are referred to as 'affixes'.

Applying a prefix (or suffix, or wrapper – see below) requires just a single call to the AJS object, and Example 3 illustrates the essential principle. Here the code adds a prefix (called 'prefixFunc' here, although it could be any user-defined function) to a method called method_A. The code then calls method_A, the result being that prefixFunc executes first, after which method_A executes.

The meaning of the arguments that are passed to addPrefix is as follows:

  1. The first indicates the object of which the interceptee is a property (the 'interceptee owner'). In the example, method_A is a method of the MyObj object, therefore 'MyObj' is passed as the value for this argument. Note that, when setting an intercept on a global function you should pass 'window' as the value for the first argument.
  2. The second indicates the name of the method that is to be intercepted (the interceptee). This must be a string object or string-literal.
  3. The third should be a reference to the function that is to serve as the prefix to method_A.

 // -- Example 3 --------------------------------------------------------------

 var MyObj =
    {
    method_A : function ()
       {
       console.log ("method_A executed");
       }

    };

 function prefixFunc ()
    {
    console.log ("prefixFunc executed");
    }

 AJS  .addPrefix (MyObj, "method_A", prefixFunc);

 MyObj.method_A  ();

 -- Output --------------------------------------------------------------------

 prefixFunc executed
 method_A executed
            

The diagram illustrates what happens when method_A is invoked.

Note that the value of the 'this' reference when the affix executes is the same as when the interceptee is executed. This means that affixes have access to the same object-properties that the interceptee does, and note too that the call and apply methods of the Function constructor operate normally. This means that you can set the this reference to point to some object other than the method-owner, while the interception mechanism remains transparent throughout.

Inner functions cannot be intercepted directly because they are not properties of an object, but they are powerful affix-candidates. That is to say: passing an inner-function reference as the third argument to addPrefix (or addSuffix, or addWrapper – see below) creates a closure, which gives the function private access to objects that persist between calls to the interceptee.

Note also that, to apply call-interceptions to the methods of the Global object you should, in a browser context, stipulate 'window' as the interceptee owner.

Diagram showing execution path through an interceptee and a prefix function.

Applying Suffixes

Applying suffixes is the complement of applying prefixes, and the diagram illustrates the execution path when a suffixed interceptee is called.

Diagram showing execution path through an interceptee and a suffix function.

To add a suffix to a method, call AJS.addSuffix, as Example 4 demonstrates. Here a call to method_A causes that procedure to execute first, after which the suffix executes. Aside from that, the meaning of the arguments passed to addSuffix is identical to those passed to addPrefix; although do note that suffixes also receive the value that the interceptee returned. This is explored in a subsequent section of this guide.


 // -- Example 4 --------------------------------------------------------------

 var MyObj =
    {
    method_A : function ()
       {
       console.log ("method_A executed");
       }

    };

 function suffixFunc () { console.log ("suffixFunc executed"); }

 AJS  .addSuffix (MyObj, "method_A", suffixFunc);

 MyObj.method_A  ();

 -- Output --------------------------------------------------------------------

 method_A executed
 suffixFunc executed
            

Applying Wrappers

Obviously, there could be times when you wish to add both a prefix and a suffix to a given method, where doing so in two steps by means of a call to addPrefix and addSuffix would be unwieldy and inefficient. Given this, the AJS object supports a third method called addWrapper.

This method follows the same principle as addPrefix and addSuffix, where you pass a reference to the method-owner, the name of the method as a string, and a reference to the function that should serve as the prefix. The only difference is that you can also pass a fourth argument, which is a reference to the function that should serve as the suffix.

Example 5 shows the application of a wrapping interception to a function. Do note that the fourth suffixFunc argument is optional, and the the next section explains the reason for this.


 // -- Example 5 --------------------------------------------------------------

 var MyObj =
    {
    method_A : function ()
       {
       console.log ("method_A executed");
       }

    };

 function prefixFunc () { console.log ("prefixFunc executed"); }
 function suffixFunc () { console.log ("suffixFunc executed"); }

 AJS  .addWrapper (MyObj, "method_A", prefixFunc, suffixFunc);

 MyObj.method_A   ();

 -- Output --------------------------------------------------------------------

 prefixFunc executed
 method_A executed
 suffixFunc executed
            

Symmetric Wrappers

Nothing prevents a given function from acting as both a prefix and suffix to a given interceptee, and this is why addWrapper's fourth argument is optional. That is: omitting that argument causes the AJS object to use the value of the third argument as the reference to the suffix function. This means that the same function will act as prefix and suffix, and Example 6 illustrates the creation of such a 'symmetric' wrapper.


 // -- Example 6 --------------------------------------------------------------

 var MyObj =
    {
    method_A : function ()
       {
       console.log ("method_A executed");
       }

    };

 function wrapfixFunc () { console.log ("wrapfixFunc executed"); }

 AJS  .addWrapper (MyObj, "method_A", wrapfixFunc);

 MyObj.method_A   ();

 -- Output --------------------------------------------------------------------

 wrapfixFunc executed
 method_A executed
 wrapfixFunc executed
            

Using Inline Affix-Function Definitions

Note that you do not need to define affix functions separately from the call to the AJS method that applies them to a given interceptee. Syntactically, JavaScript allows an inline, anonymous-function-definition (rather than a reference to a named function that is defined elsewhere), to act as an argument to another function – one of the great strengths of the language – and it means that a prefix or suffix can be defined within a call to the relevant AJS method.

Example 7 illustrates this by calling AJS.addPrefix wherein the prefix is defined anonymously and in-line in the argument set.


 // -- Example 7 --------------------------------------------------------------

 var MyObj =
    {
    method_A : function ()
       {
       console.log ("method_A executed");
       }

    };

 AJS.addPrefix (MyObj, "method_A", function ()
    {
    console.log ("Inline-defined prefix executed");
    });

 MyObj.method_A ();

 -- Output --------------------------------------------------------------------

 Inline-defined prefix executed
 method_A executed
            

Validating AJS Method-Calls

As the Product Overview page explains, none of the methods of the AJS object (or of the objects they generate) validate the number, type or value of the arguments that they receive. You could, for example pass a string where a function-reference is required as the third argument to, say, addSuffix, which would cause an exception later on when you call the intercepted method.

However, as the Product Overview page also explains, you can use the AJS object in conjunction with AJS_Validator to attach affixes to the methods of the AJS object itself, and these will perform all the validation that you need when developing code that calls AJS object-methods directly (with, if you wish, none of the concomitant overhead when you deploy).

To use this facet of the library, read the Dog-Food Consumption section in the Product Overview page thoroughly, and then follow the pattern in Example 8. Doing this will ensure that every call to an AJS method is validated fully and automatically by AJS_Validator, which will apprise you of any bad calls.

Remember that you need to use the contents of AJS.val.js only when developing code that calls the methods of the AJS object directly. It is not necessary when your code calls only the methods of AJS_Validator, or AJS_Logger and AJS_ODL when AJS_Logger.val.js and AJS_ODL.val.js respectively are in place. This is because AJS_Validator validates all calls to its methods, as do the ValidationDefs in AJS_Logger.val.js and AJS_ODL.val.js (in respect of AJS_Logger and AJS_ODL), and this ensures that the client components do not pass incorrect arguments to the methods of the AJS object.


 <!-- Example 8 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->

 <html>
    <head>
       <script src = "ThrowException.js"></script>  <!-- Or ThrowException_Remote.js, or your own exception   -->
                                                    <!-- handler. Note that ThrowException_Remote.js requires -->
       <script src = "AJS.js"           ></script>  <!-- ThrowException.js.                                   -->
       <script src = "AJS_DefMgr.js"    ></script>
       <script src = "AJS_Validator.js" ></script>
       <script src = "AJS.val.js"       ></script>
       <script>

       "use strict";

       AJS_Validator = createAJS_Validator (AJS, createDefMgr, throwException);

       emplaceAJSValidation (AJS, AJS_Validator);

       // Application code from here on.

       </script>
    </head>

    <body> ... </body>

 </html>
            

Interception Candidates

Any method of a user-defined object is a candidate for interception, and thus you can intercept global user-defined functions too, as these are simply methods of the Global object; i.e. in a browser environment, you should pass window as the first argument to the AJS add... methods, as pointed out above.

You can also intercept the methods of prototype objects, meaning that the affix function(s) in question will execute for a given prototype-object method when the method is executed on any object that shares that prototype. Remember here too that you can also employ user-defined prototype methods as affix functions.

Affix functions can also set new interceptions themselves (perhaps even citing themselves as the affix(es) to be applied), and an interceptee can also attach new affix functions to itself. Some of these permutations may seem strange, and may be no more than a curiosity, but they are harmless from the interception mechanism's point of view, and they may well be of use to you in the context of some innovative design that you have in mind.

You can also intercept calls to recursive methods, meaning that the affixes will execute for each recursive execution. Remember, however, that while each call will see any prefix(es) execute just before the method, the situation is different for suffixes. These do not execute until after a given method returns, and so each suffix-execution for a given recursive call will not occur until the recursion stops and the execution thread returns back up the call stack. This means that they execute one after the other before control passes back to the original caller.

That is: if a recursive method (with a single prefix and suffix) calls itself, say, five times, there will be five prefix-then-method executions, and then five consecutive executions of the suffix, and the diagram illustrates this concept.

Diagram, showing the call path for suffixed recursive-methods.

The methods of native and host types are also viable interception candidates, although do note that you must apply the affixes to the protoype of any native/host object that you create. In Example 9, the code adds a prefix to the Date prototype, and then creates a Date object before calling the getYear method of that object. Duly the suffix executes and calls the getTime method.


 // -- Example 9 --------------------------------------------------------------

 function suffixFunc ()
   {
   console.log ("Time = " + this.getTime ());
   }

 AJS.addSuffix (Date.prototype, "getYear", suffixFunc);

 var MyDate = new Date ();

 console.log   ("Year = " + MyDate.getYear ());

 -- Output (depending, obviously, on when the code is run) --------------------

 Time = 1383777196459
 Year = 113
           

Similarly, Example 10 adds a prefix to the push method of the Array prototype, and then creates an array before pushing a value to that object. Duly, the prefix executes first, demonstrating that it has access to the value that was pushed. As the Interceptee Arguments and Affixes section explains, the IArgs argument (Interceptee ARGumentS) represents the arguments array that the run-time passes implicitly to each function.

This example serves to illustrate the value of AJS-object clients like AJS_Logger. You could, for example, apply a LogDef to the push and pop methods of the Array prototype, which would cause AJS_Logger to apply prefixes and suffixes to those methods, and this would allow you therefore to log (and perhaps profile) all push-pop activity in your application.


 // -- Example 10 -------------------------------------------------------------

 function prefixFunc (IArgs)
    {
    console.log ("Value pushed: " + IArgs[0]);
    }

 AJS.addPrefix (Array.prototype, "push", prefixFunc);

 var MyArray = [];

 MyArray.push (42);

 -- Output --------------------------------------------------------------------

 Value pushed: 42
           

Finally in this section: ES5 introduced the capablity to define an object's properties using the defineProperty or defineProperties methods of the Object constructor, where you provide functions for the get and set keys in the corresponding accessor-descriptor. This causes the run-time to implement access-to and mutation-of such properties as implicit calls to those functions, however it is not possible to intercept calls to those functions (because such calls occur 'behind the scenes'). Nor is it possible to apply an interception in the following fashion:

   AJS.addPrefix (MyObj.P, "set", function () { });

...As this implies that MyObj.P has a property called 'set', which is not the case.

The truly-determined programmer might think it possible to define a property using get and set, where the property is a function reference, and to then intercept invocation of that property. This proves to be impossible, because properties defined by means of the get and set descriptor-keys are rendered non-writable automatically (otherwise there would be no point in having get and set in the first place), and so this renders the AJS object unable to replace the function-reference in question with a reference to an instance of its proxy function.

Example 11 demonstrates this point.


 // -- Example 11 (pseudocode) ------------------------------------------------

 var MyObj    =
    {
    method_A  : function () { ... },
    };

 var method_B = function () { ... };

 Object.defineProperty (MyObj, "method_B",
    {
    get : function ()        { return method_B;    },
    set : function (NewFunc) { method_B = NewFunc; }
    });

 AJS.addPrefix  (MyObj, "method_B", function () { ... });  // Will not work, MyObj.func is
                                                           // defined implicitly as non-writable.
           

Intercepting Inner Functions and Call-Backs

It may seem impossible to intercept calls to inner functions and call-backs, as these are 'free standing' functions that are not members of a given object. In fact, this is possible with just a little sleight of hand. In the case of inner functions, we need only make the function in question a method of an object that is local to the 'outer function', after which we proceed normally by calling one of the AJS object's add... methods.

In the case of call-backs, you should do the same thing, by making the relevant function-reference a member of a temporary object, and then passing that object's reference to that method (not the value of the original argument) on to whatever other functions require the call-back.

As the Manipulating Method References section of this guide explains, doing this will cause a reference to the proxy function that the AJS.add... method in question emplaces to be passed to a given callee, meaning that subsequent invocation of the call-back will cause any prefix it possesses to execute first and so on.

In Example 12, function A receives a reference to a call-back from its caller, and makes that a member of Tmp, which it then passes to the AJS object's addPrefix method. When that method returns, the code simply passes the function reference that Tmp possesses to function B. From B's point of view, it has received an ordinary call-back function-reference (which it calls in blind faith), where, in fact, it has received a reference to the proxy function that AJS.addPrefix generated. Given this, B's invocation of the call-back see the prefix execute first.

This may seem a little circuitous, but that is irrelevant given that it adds only one more line of code, yet yields the potential to validate, log etc. calls to call-backs (using AJS_Validator, AJS_Logger etc.). If you do intend to use AJS_Validator in this way, you should see the sections in the AJS_Validator user guide on Transitive ValidationDefs and Allowing Method-Owner Changes.


 // -- Example 12 -------------------------------------------------------------

 function CallBack   () { console.log ("CallBack executed");   }
 function prefixFunc () { console.log ("prefixFunc executed"); }

 function A          (CallBack)
    {
    var Tmp = { CallBack : CallBack };                     // Assign the argument value to a property of Tmp.

    AJS.addPrefix (Tmp, "CallBack", prefixFunc);           // Add a prefix to that method...

    // ...

    console.log ("A executed");
    B           (Tmp.CallBack);                            // ...And pass the method-reference, not the original
                                                           // argument.
    }

 function B (CallBack)                                     // B now has a reference to the proxy, not the original
    {                                                      // reference passed to A, although it can treat that
    console.log ("B executed");                            // reference exactly as it would treat the original.
    CallBack    ();

    }

 A (CallBack);

 -- Output --------------------------------------------------------------------

 A executed
 B executed
 prefixFunc executed
 CallBack executed
            

Intercepting Constructors

Sadly, while it is possible to intercept calls to user-defined constructors, along with calls to the constructors of native and host types, things do not operate here as one would expect. That is to say: you can add a prefix and suffix to a constructor, and those affixes will execute normally before/after the constructor executes, but on all the major JS run-times, the 'this' reference does not point to the newly-created object, instead it refers to the proxy function itself.

Moreover, when the thread of execution returns to the point at which the constructor was invoked, the object returned is not the newly-created object but is also a reference to the proxy function. These two points mean that using the AJS object in conjunction with constructor functions will cause working code to fail.

In the case of native and host constructors, the only option here is to wrap calls to those functions in a method, calls to which you intercept instead. Although this does defeat the object of method-call interception, which is to attach extra processing to existing method-calls arbitrarily and transparently.

In the case of user-defined constructors, however, the 'wrong this' problem may not be a problem at all if you are a developer who eschews the use of new, and opts instead for creating objects by the use of factory functions. This approach allows you to intercept object-creation calls just as you would intercept calls to any other function, as long as that function is a method of some object at the time that you try to emplace the interception mechanism.