How to Correctly Wrap a JavaScript Function
(Source/Credits: https://dev.to/trackjs/how-to-correctly-wrap-a-javascript-function-go)
Wrapping JavaScript functions lets you add common logic to functions you do not control, like native and external functions.
title: How to Correctly Wrap a JavaScript Function published: true tags: javascript,webdev,function, canonical_url: https://trackjs.com/blog/how-to-wrap-javascript-functions/ cover_image: https://trackjs.com/assets/images/blog/2019-08-how-to-wrap-javascript-functions/title.png description: "Wrapping JavaScript functions lets you add common logic to functions you do not control, like native and external functions."
Wrapping JavaScript functions lets you add common logic to functions you do not control, like native and external functions. Many JavaScript libraries, like the TrackJS agents, need to wrap external functions to do their work. Adding wrappers allows us to listen for Telemetry, errors, and logs in your code, without you needing to call our API explicitly.
You may want to wrap a function to add instrumentation or temporary debugging logic. You can also change the behavior of an external library without needing to modify the source.
Basic Function Wrapping
Because JavaScript is wonderfully dynamic, we can wrap a function by simply redefining the function with something new. For example, consider this wrapping of myFunction
:
javascript
var originalFunction = myFunction;
window.myFunction = function() {
console.log("myFunction is being called!");
originalFunction();
}
In this trivial example, we wrapped the original myFunction
and added a logging message. But there is a lot of things we didn’t handle:
- How do we pass function arguments through?
- How do we maintain scope (the value of
this
)? - How do we get the return value?
- What if an error happens?
To handle these things, we need to get a little more clever in our wrapping.
javascript
var originalFunction = myFunction;
window.myFunction = function(a, b, c) {
/* work before the function is called */
try {
var returnValue = originalFunction.call(this, a, b, c);
/* work after the function is called */
return returnValue;
} catch (e) {
/* work in case there is an error */
throw e;
}
}
Notice that we are not just invoking the function in this example, but call
-ing it with the value for this
and arguments a
, b
, and c
. The value of this
will be passed through from wherever you attach your wrapped function, Window
in this example.
We also surrounded the whole function in a try/catch
block so that we can invoke custom logic in the case of an error, rethrow it, or return a default value.
Advanced Function Wrapping
The basic wrapping example will work 90% of the time, but if you are building shared libraries, like the TrackJS agents, that’s not good enough! To wrap our functions like a pro, there are some edge cases that we should deal with:
- What about undeclared or unknown arguments?
- How do we match the function signature?
- How do we mirror function properties?
javascript
var originalFunction = myFunction;
window.myFunction = function myFunction(a, b, c) { /* #1 */
/* work before the function is called */
try {
var returnValue = originalFunction.apply(this, arguments); /* #2 */
/* work after the function is called */
return returnValue;
}
catch (e) {
/* work in case there is an error */
throw e;
}
}
for(var prop in originalFunction) { /* #3 */
if (originalFunction.hasOwnProperty(prop)) {
window.myFunction[prop] = originalFunction[prop];
}
}
There are 3 subtle but important changes. First (#1), we named the function. It seems redundant, but user code can check the value of function.name
, so it’s important to maintain the name when wrapping.
The second change (#2) is in how we called the wrapped function, using apply
instead of call
. This allows us to pass through an arguments
object, which is an array-like object of all the arguments passed to the function at runtime. This allows us to support functions that may have undefined or variable number of arguments.
With apply
, we don’t need the arguments a
, b
, and c
defined in the function signature. But by continuing to declare the same arguments as the original function, we maintain the function’s arity. That is, Function.length
returns the number of arguments defined in the signature, and this will mirror the original function.
The final change (#3) copies any user-specified properties from the original function onto our wrapping.
Limitations
This wrapping is thorough, but there are always limitations in JavaScript. Specifically, it’s difficult to correctly wrap a function with a non-standard prototype, such as an object constructor. This is a use case better solved by inheritance.
In general, changing the prototype of a function is possible, but it’s not a good idea. There are serious performance implications and unintended side-effects in manipulating prototypes.
Respect the Environment
Function wrapping gives you a lot of power to instrument and manipulate the JavaScript environment. You have a responsibility to wield that power wisely. If you’re building function wrappers, be sure to respect the user and the environment you’re operating in. There may be other wrappers in place, other listeners attached for events, and expectations on function APIs. Tread lightly and don’t break external code.
JavaScript breaks a lot, and in unpredictable ways. TrackJS captures client-side JavaScript errors so you can see and respond to errors. Give it a try free, and see how awesome our function wrapping is.
Comments section