Skip to main content
deletethis.net

Watch JavaScript via Proxy

JavaScript has a type Proxy that lets you intercept all interactions with an object - all property reads, writes, method calls, and so on. One fun thing you can do with this is watch how JavaScript itself interacts with your objects.

If we put a Proxy in another Proxy we can see how JavaScript uses our objects:

function trace(obj, name) {
return new Proxy(obj, new Proxy({}, {
get: (t, property, r) =>
logCall.bind(null, name + ' ' + property, Reflect[property], Reflect)
}));
}

function logCall(description, fn, fnThis, ...fnArgs) {
try {
const r = fn.call(fnThis, ...fnArgs);
console.log(description + '\n\t', ...fnArgs, "\n -->", r);
return r;
} catch (e) {
console.log(description + '\n\t', ...fnArgs, "\n -fail->", e);
throw e;
}
}

How this works #

trace returns a Proxy around your object that uses Reflect to perform the normal Proxy operations but then console.log the call.

The constructor for Proxy takes two parameters: first the object you're wrapping, and second an object with functions for all the different kind of object interactions that you want to intercept. Because we want to just log the interaction and not do anything specific we can use a Proxy around Reflect to define this second parameter.

Our inner Proxy implements the handler of the outer Proxy. The handler is supposed to have methods called get, set, apply and others for each operation to intercept. Instead we define our inner Proxys get to return a function that will perform the operation and log it using logCall. The function we pass in to logCall to run is one of the functions off of Reflect. Reflect defines all the Proxy handler functions get, set, apply and so on but where each just performs those operations. Reflect is like a convenient default for all Proxy handler functions. We want to perform the default operations but additionally log the result so using Reflect is exactly what we want.

What you can do with it #

With that you can call trace with an object and get out a Proxy that acts just like your object but also console.logs every get, set, has, getOwnPropertyDescriptor, and so on that JavaScript asks of your object.

You can watch how Array.sort sorts an array.

Or how Array.toString calls Array.join.

Or how Array.concat checks for Symbol.isConcatSpreadable.

Check out the corresponding ECMAScript standard and you can see how the logged Proxy calls match the standard defined steps for things like Array.sort.

Because I pass objects to console.log DevTools will show you an interactive object in the console. If you try it out the log is easier to understand than as text in the gist:

array get 
(3) [300, 200, 100] sort Proxy {0: 300, 1: 200, 2: 100} )
--> ƒ sort() { [native code] }

The above is a Proxy handler's get call. The first param is the array (3) [300, 200, 100] on which we will 'get'. The second param is the string 'sort' that is the property to get. The third is our proxy.

I haven't tried wrapping the return values in trace Proxy although maybe that would give an even better picture of what's happening.

Various Array functions can operate on a this that is a not an Array. If you're trying to make a non-Array object work with Array functions, then using a Proxy to watch what JavaScript is doing can help debug.

Watch a for loop #

But Proxy watching works for other parts of JavaScript and not just standard functions. For example watch a for loop enumerate over your array:

let arr = trace([300, 200, 100], 'array'); 
for (let name in arr) { }

And the log:

array ownKeys
(3) [300, 200, 100]
--> (4) ['0', '1', '2', 'length']
array getPrototypeOf
(3) [300, 200, 100]
--> [constructor: ƒ, at: ƒ, concat: ƒ, copyWithin: ƒ, fill: ƒ,]
array getOwnPropertyDescriptor
(3) [300, 200, 100] 0
--> {value: 300, writable: true, enumerable: true, configurable: true}
array getOwnPropertyDescriptor
(3) [300, 200, 100] 1
--> {value: 200, writable: true, enumerable: true, configurable: true}
array getOwnPropertyDescriptor
(3) [300, 200, 100] 2
--> {value: 100, writable: true, enumerable: true, configurable: true}
array getOwnPropertyDescriptor
(3) [300, 200, 100] length
--> {value: 3, writable: true, enumerable: false, configurable: false}

The for loop asks your object for the names of its properties and then the value of each.

Watch instanceof #

Or instanceof with your proxy on the left hand side

arr instanceof String

And watch JavaScript get the prototype of array

array getPrototypeOf
(3) [300, 200, 100]
--> [constructor: ƒ, at: ƒ, concat: ƒ, copyWithin: ƒ, fill: ƒ,]

Or instanceof with your proxy on the right hand side

String instanceof arr

And watch JavaScript ask your array for its Symbol.hasInstance property.

array get
(3) [300, 200, 100] Symbol(Symbol.hasInstance) Proxy(Array) {0: 300, 1: 200, 2: 100}
--> undefined

(Here's a related gist.)