How to use JavaScript Proxies for Fun and Profit
There’s a very recent new feature of the JavaScript language that is still not being widely used: JavaScript proxies.
With JavaScript proxies you can wrap an existing object and intercept any access to its attributes or methods. Even if they do not exist! 💥
You can intercept calls to methods that do not exist
👋 Hello World Proxy
Let’s start with the basics. A ‘hello world’ example could be:
const wrap = obj => {
return new Proxy(obj, {
get(target, propKey) {
console.log(`Reading property "${propKey}"`)
return target[propKey]
}
})
}
const object = { message: 'hello world' }
const wrapped = wrap(object)
console.log(wrapped.message)
Which outputs:
Reading property "message"
hello world
In this example we just do something before accessing the property / method. But then we return the original property or method.
You can also intercept changes to properties by implementing a set
handler.
This could be useful to validate attributes or things like that. But I think this feature is a lot more promising. I hope new frameworks will arise that will use proxies for its core functionality. I’ve been thinking about it and these are some ideas:
🚀 An SDK for an API with 20 lines of code
As I said, you can intercept method calls for methods that… don’t even exist. When somebody calls a method on a proxied object the get
handler will be called and then you can return a dynamically generated function. You don’t have to touch the proxied object if you don’t need to.
With that idea in mind, you can parse the method being invoked and dynamically implement its functionality in runtime! For example we could have a proxy that when invoked with api.getUsers()
it could make a GET /users
in an API. With this convention we can go further and api.postItems({ name: ‘Item name' })
would call POST /items
with the first parameter as request body.
Let’s see a full implementation:
const { METHODS } = require('http')
const api = new Proxy({},
{
get(target, propKey) {
const method = METHODS.find(method =>
propKey.startsWith(method.toLowerCase()))
if (!method) return
const path =
'/' +
propKey
.substring(method.length)
.replace(/([a-z])([A-Z])/g, '$1/$2')
.replace(/\$/g, '/$/')
.toLowerCase()
return (...args) => {
const finalPath = path.replace(/\$/g, () => args.shift())
const queryOrBody = args.shift() || {}
// You could use fetch here
// return fetch(finalPath, { method, body: queryOrBody })
console.log(method, finalPath, queryOrBody)
}
}
}
)
// GET /
api.get()
// GET /users
api.getUsers()
// GET /users/1234/likes
api.getUsers$Likes('1234')
// GET /users/1234/likes?page=2
api.getUsers$Likes('1234', { page: 2 })
// POST /items with body
api.postItems({ name: 'Item name' })
// api.foobar is not a function
api.foobar()
Here the proxied object is just {}
because all methods are dynamically implemented. We don’t actually need to wrap a functional object.
You’ll see that some methods have an $
This is a placeholder for inlined parameters.
If you don’t like that, it could be implemented in a different way. 🙂
Side note : These examples can be optimized. You could cache the dynamically generated function in a hash object instead of returning a new function every time. But for clarity I left it like that for the examples.
📦 Querying data structures with more readable methods
What if you had an array of people and you could do:
arr.findWhereNameEquals('Lily')
arr.findWhereSkillsIncludes('javascript')
arr.findWhereSkillsIsEmpty()
arr.findWhereAgeIsGreaterThan(40)
Sure you can with proxies! We can implement a proxy that wraps an array, parses method calls and does queries like that one.
I’ve implemented a few possibilities here:
const camelcase = require('camelcase')
const prefix = 'findWhere'
const assertions = {
Equals: (object, value) => object === value,
IsNull: (object, value) => object === null,
IsUndefined: (object, value) => object === undefined,
IsEmpty: (object, value) => object.length === 0,
Includes: (object, value) => object.includes(value),
IsLowerThan: (object, value) => object === value,
IsGreaterThan: (object, value) => object === value
}
const assertionNames = Object.keys(assertions)
const wrap = arr => {
return new Proxy(arr, {
get(target, propKey) {
if (propKey in target) return target[propKey]
const assertionName = assertionNames.find(assertion =>
propKey.endsWith(assertion))
if (propKey.startsWith(prefix)) {
const field = camelcase(
propKey.substring(prefix.length,
propKey.length - assertionName.length)
)
const assertion = assertions[assertionName]
return value => {
return target.find(item => assertion(item[field], value))
}
}
}
})
}
const arr = wrap([
{ name: 'John', age: 23, skills: ['mongodb'] },
{ name: 'Lily', age: 21, skills: ['redis'] },
{ name: 'Iris', age: 43, skills: ['python', 'javascript'] }
])
console.log(arr.findWhereNameEquals('Lily')) // finds Lily
console.log(arr.findWhereSkillsIncludes('javascript')) // finds Iris
It would be super similar to write an assertion library like expect using proxies.
Another idea would be to create a library to query databases with an API like this:
const id = await db.insertUserReturningId(userInfo)
// Runs an INSERT INTO user ... RETURNING id
📊 Monitoring async functions
Since you can intercept method calls, if a method call returns a promise you can also track when the promised is fulfilled. With that idea I made a quick example of monitoring the async methods of an object and printing some statistics in the command line.
You have a service like the following one and with one method call you can wrap it:
const service = {
callService() {
return new Promise(resolve =>
setTimeout(resolve, Math.random() * 50 + 50))
}
}
const monitoredService = monitor(service)
monitoredService.callService() // we want to monitor this
This is a full example:
const logUpdate = require('log-update')
const asciichart = require('asciichart')
const chalk = require('chalk')
const Measured = require('measured')
const timer = new Measured.Timer()
const history = new Array(120)
history.fill(0)
const monitor = obj => {
return new Proxy(obj, {
get(target, propKey) {
const origMethod = target[propKey]
if (!origMethod) return
return (...args) => {
const stopwatch = timer.start()
const result = origMethod.apply(this, args)
return result.then(out => {
const n = stopwatch.end()
history.shift()
history.push(n)
return out
})
}
}
})
}
const service = {
callService() {
return new Promise(resolve =>
setTimeout(resolve, Math.random() * 50 + 50))
}
}
const monitoredService = monitor(service)
setInterval(() => {
monitoredService.callService()
.then(() => {
const fields = ['min', 'max', 'sum', 'variance',
'mean', 'count', 'median']
const histogram = timer.toJSON().histogram
const lines = [
'',
...fields.map(field =>
chalk.cyan(field) + ': ' +
(histogram[field] || 0).toFixed(2))
]
logUpdate(asciichart.plot(history, { height: 10 })
+ lines.join('\n'))
})
.catch(err => console.error(err))
}, 100)
JavaScript proxies are super powerful. ✨
They add a little overhead but on the flip side having the ability to dynamically implement methods at runtime by their name makes the code super elegant and readable. I haven’t done any benchmarks yet, but if you plan to use them in production I would do some performance testing first.
However, there’s no problem of using them on development, like the monitoring of async functions for debugging!