JavaScript's flexibility allows you to replace an object's reference to one of its methods with a reference to another function, which will cause that other function to execute when the method is invoked. This is the very principle that the AJS object exploits, and it has the effect of changing the 'behaviour' of a given method, even though its name within the object in question remains the same.
Given, however, that an intercepted method embodies a reference to a proxy function, replacing that reference with one that points to a different function-body will prevent the execution of the affixes that are associated with the original function-body (because only the proxy function knows about those affixes). The top half of the diagram illustrates this point.
It follows too that, if a non-intercepted method of a given object is replaced with a reference to an intercepted function-body, that method will operate thereon in an intercepted manner, and the lower half of the diagram illustrates this. Given this, it follows that restoring an intercepted function to its original owner (the reverse of the scenario depicted in the top half of the diagram) will preserve the operation of any affixes it has, which will execute as normal subsequently on invocation of the interceptee.
Contents |
Manipulating Method References Freezing Method-Owners Freezing Methods Freezing AffixCtrl Objects Function Properties |
It follows too that, if you keep a copy of the reference to the replaced method, it is still possible to invoke the interceptee by applying parentheses to the object holding the reference-copy (because the parentheses are an 'execute this' operator), and doing so will cause its affixes to execute as well.
Given these points, and if, say, you wish to nominate a given method as an event handler, where you want to intercept calls to that method, you should apply the interception first, and then attach it to the event in question.
For example, if you pass a reference to a method to setTimeout or setInterval, where you want one or more affixes to execute before and/or after that method, you should apply the interception to the method before you pass it to the function in question. Doing it the other way round will cause the timer function to execute the method body as if it were non-intercepted.
Example 42 illustrates this point in code, and do note that the AJS_Validator user guide considers this issue in some depth.
// -- Example 42 ------------------------------------------------------------- function prefixFunc () { console.log ("prefixFunc executed"); } var MyObj = { method_A : function () { console.log ("method_A executed"); } }; AJS.addPrefix (MyObj, "method_A", prefixFunc); MyObj.method_A (); var MethodRef = MyObj.method_A; MyObj.method_A = function () { console.log ("Replacement function executed"); } MyObj.method_A (); MethodRef (); MyObj.method_A = MethodRef; MyObj.method_A (); -- Output -------------------------------------------------------------------- prefixFunc executed method_A executed Replacement function executed prefixFunc executed method_A executed prefixFunc executed method_A executed
When intercepting method calls, either directly through the use of the AJS object, or indirectly by using one of its client components such as AJS_Validator, you should bear in mind that you cannot apply an interception to a method of an object that has been frozen by means of the freeze method of the Object constructor (note: this does not mean freezing the method itself – see below).
This is because freezing an object prevents all changes to its property set, thus making it impossible for the AJS object to emplace the proxy function (although the use of the seal and preventExtensions methods poses no problem in this respect).
Given this, if your run-time emits an exception notification that reports an inability to re-define or modify a non-configurable or non-writable property, and where it indicates a location within the code in AJS.js, then this does not indicate a defect in the AJS object. Instead, it indicates that you should follow the instructions in the Validating AJS Method-Calls section, which will enable you to locate the call in your code that nominated a frozen object as an interceptee owner.
Note that this applies only if you are working with the AJS object directly. As stated elsewhere, you do not need AJS.val.js if you are using one of the client components (AJS_Validator, AJS_Logger, AJS_ODL) because the AJS_DefMgr objects they employ check for frozen method-owners.
However, if you freeze an object after you have applied an interception to one or more of its methods, you will still be able to add and remove affixes with impunity, as long as at least one remains. This is because adding and removing affixes to/from an existing interceptee imposes no change on the corresponding method-owner's reference to the proxy function.
Nevertheless, freezing the owner of an intercepted method will render you unable to remove the interception subsequently (when you attempt to remove the final affix), because freezing the method-owner precludes future modification, which makes it impossible for the AJS object to replace the proxy function with the original method-body. Note that this is of no consequence in the context of AJS_Validator and AJS_Logger because they do not remove affixes from method owners.
Note that you can freeze, seal or render-inextensible a method before you apply an interception to it, as this does not affect emplacement of the proxy function, nor does doing the same to a method during its time as an interceptee (which means that you are, in reality, freezing/sealing/rendering-inextensible the proxy function). Moreover, a method that is frozen/sealed/rendered-inextensible before or during its time as an interceptee will retain the state in question should it be returned to normal at any point.
Similarly, you can define a method using the defineProperty or defineProperties methods of the Object constructor, with any permutation of the various attributes that you can apply when using those methods (i.e. the enumerable, configurable, writable etc. properties of the corresponding descriptor-object), and this will have no effect on the application or removal of affixes to/from the method in question except where writable and configurable are both false.
You can be forgiven for finding all the various freezing, configuration, writability etc. options a little confusing when considered in conjunction with the AJS object (it gave this developer a fair-old headache), and so the table shows the permissibility of each factor in conjunction with the application and removal of affixes.
Method-Owner | Method Interceptable | |
Inextensible | ||
Sealed | ||
Frozen | ||
Method-Owner | Interception Removable | |
Inextensible | ||
Sealed | ||
Frozen | ||
Method Writable | Method Configurable | Method Interceptable |
True | True | |
True | False | |
False | True | |
False | False | |
Method Writable | Method Configurable | Interception Removable |
True | True | |
True | False | |
False | True | |
False | False |
Note, however, that, while a method is in an intercepted state, you must never freeze the AffixCtrl object that was returned from the call to addPrefix/addSuffix/addBefore etc. that emplaced the interception mechanism for that method. Nor should you freeze the AffixCtrl objects to which a given WrapperCtrl-object refers while the corresponding affix functions are in place.
This is because, upon removal of the corresponding affix function(s), the AJS object overwrites the references to which the methods of an AffixCtrl object correspond with references to 'safe' functions that do nothing, or simply return zero or false, and so it follows that freezing a 'live' AffixCtrl object will frustrate the AJS object in that respect.
However, this does not apply if you apply an interception, and never attempt to remove the interception subsequently. Moreover, you can do what you want with a given AffixCtrl object after its corresponding affix-function has been removed from the method in question, because such objects cannot be used with the AJS object thereafter. However, do note here that, if your application retain references to such objects, their storage cannot be recovered by the garbage collector (as pointed out elsewhere in this user guide).
Obviously, none of the considerations in this and the sections above apply if you are running on an ES3-compliant (or previous) run-time, because that standard supports neither freezing, sealing nor in-extensibility, nor all the options that the defineProperty etc. methods of the Object constructor offer.
Given that functions are first-class objects in JavaScript, you can add and delete properties to/from a given method, just like any other object (other than the literal types).
Given this, and in order to preserve the transparency of the interception mechanism, the AJS object will copy any properties that a given method possesses to the proxy function when an interception is applied to that method. This is to ensure that the proxy always appears to be the method-body itself, and it means that, when you add/remove any properties to/from the method during its time as an interceptee, you are, in reality, adding/removing properties to/from the proxy.
Accordingly, should you return the method to its non-intercepted state (by removing its final affix), the AJS object will remove any properties from the method-body that were removed from the proxy (while the method was in an intercepted state), and will add any new properties to the method body that the proxy received during that time, and the diagram illustrates this principle.
Furthermore, if you are running on an ES5-compliant run-time, and you create a given method-property by means of Object.defineProperties, the AJS object will, upon application of the interception mechanism to that method, re-define that property on the proxy function with exactly the same attribute-values (i.e. the same values for the writable, configurable etc. attributes).
The same holds for when you remove the interception from the method – the AJS object copies any properties that a method acquired while it was in an intercepted state back to the original method body. Again, all of this ensures that the method appears to its clients to be the 'real thing' all along.
Note that the AJS object does not copy the value of the __proto__ property from method-body to proxy-function upon interception, and back again upon final-affix removal (if, indeed, the run-time in question supports the __proto__ property), because this causes a run-time exception if the method is sealed (which, by rights, should not happen because sealing an object should still allow you to update the values of its properties).
If this seemingly-strange conflict disappears in future JS run-times, and if the __proto__ property is standardised, then the AJS object may be be modified such that its value is preserved across the intercepted/non-intercepted threshold.