In this blog post, I explain how you can trace method calls via ECMAScript Proxies. The techniques I show are relevant whenever you want to intercept and forward method calls via Proxies.

Required knowledge: You should be loosely familiar with Proxies. If not, please consult chapter “Metaprogramming with proxies” in “Exploring ES6”.

The object to be traced #

For our examples, we’ll use the following class.

class Point { constructor (x, y) { this .x = x; this .y = y; } dist(other) { return Math .sqrt( (other.x- this .x)** 2 + (other.y- this .y)** 2 ); } me() { return this ; } }

Tracing “get” operations #

Let’s start by tracing whenever someone reads properties. The following tool function lets us do that.

function traceGets ( obj ) { const handler = { get (target, propKey, receiver) { console .log( 'GET' , propKey); return Reflect .get(target, propKey, receiver); } }; return new Proxy (obj, handler); }

We can try it out on an instance of Point :

const pt = traceGets( new Point( 3 , 2 )); console .log(pt.x);

Why use Reflect.get() ? #

Why are we using the first one of the following two expressions and not the second one?

Reflect .get(target, propKey, receiver) target[propKey]

Answer: The difference matters with getters. Then you want to invoke the getter that is stored in target , but you want its this to be set to receiver . That allows you to continue with tracing, because this still points to the proxy. More on that at the end of this blog post.

Tracing method calls #

Tracing method calls is more complicated, because Proxies don’t have traps for method calls, but instead translate them to a “get” and a function call. In principle, obj.prop and obj.method(x, y) are two different kinds of dot operators. The second one is an abbreviation for:

obj.method.call(obj, x, y)

As a consequence, you must trace method calls by returning appropriate values from “get” traps:

function traceMethodCalls ( obj ) { const handler = { get (target, propKey, receiver) { const targetValue = Reflect .get(target, propKey, receiver); if ( typeof targetValue === 'function' ) { return function ( ...args ) { console .log( 'CALL' , propKey, args); return targetValue.apply( this , args); } } else { return targetValue; } } }; return new Proxy (obj, handler); }

If the property value is a function, we return a function that traces and forwards both this and the arguments (line A). Otherwise, we simply return the target’s property value.

The following code traces a call to .dist() .

const pt = traceMethodCalls( new Point( 3 , 2 )); console .log(pt.dist( new Point( 5 , 4 )));

this remains the Proxy #

The way we have set up things ensures that this continues to point at the Proxy. Even if a traced method calls other traced methods. And even if a traced method returns this . You can see that in the following example – both method calls .me() and .dist() are being traced.