⬆️ ⬇️

JavaScript proxy: beautiful and useful

In JavaScript, relatively recently, a new feature has emerged that is not widely used yet. We are talking about proxy objects . Proxies allow you to create wrappers for other objects by organizing the interception of access operations to their properties and operations for invoking their methods. Moreover, it works even for non-existent properties and methods of proxied objects.



image


We present to your attention the translation of the article by programmer Alberto Himeno, in which he shares various ideas of using Proxy objects in JavaScript.



First acquaintance with proxy objects



Let's start with the basics. Here is the simplest example of working with a proxy object:



 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) 


This code prints the following:

')

 Reading property "message" hello world 


In this example, we perform some action before we give the calling mechanism access to the property or method of the proxied object, and then return this property or this method. A similar approach is applicable to intercepting property change operations by implementing a set handler.



Proxy objects can be useful for checking attributes or performing similar actions, but I think the possible scope of their application is much wider. For example, I believe that there should be new frameworks that use proxies in their basic functionality. In thinking about all this, I had some ideas that I want to share.



API SDK in 20 lines of code



As I said, Proxy objects allow you to intercept calls to methods that do not exist in proxied objects. When calling the method of a proxied object, the get handler is called, after which a dynamically generated function can be returned. However, this object, if it is not necessary, does not need to be changed.



Armed with this idea, you can analyze the name and arguments of the called method and dynamically, during the execution of the program, implement its functionality.



For example, there may be a proxy object that, when you call api.getUsers() , can create a GET/users path in the API. This approach can be developed further. For example, a command like api.postItems({ name: 'Item name' }) can invoke POST /items using the first parameter of the method as a request body.



Let's look at the programmatic expression of these arguments:



 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(/([az])([AZ])/g, '$1/$2')         .replace(/\$/g, '/$/')         .toLowerCase()     return (...args) => {       const finalPath = path.replace(/\$/g, () => args.shift())       const queryOrBody = args.shift() || {}       //    fetch       // 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    api.postItems({ name: 'Item name' }) // api.foobar    api.foobar() 


Here we create a wrapper for an empty object - {} , while all methods are implemented dynamically. Proxy does not need to be used with objects containing the necessary functionality or its parts. The $ icon is used as a wildcard for parameters.



Here I would like to note that the above example is quite permissible to implement otherwise. For example, it can be optimized. Let's say that dynamically generated functions can be cached, which saves us from having to constantly return new functions. All this is not directly related to proxy objects, so I, in order not to overload the examples, cite them in this form.



Querying data structures using convenient and understandable methods



Suppose there is an array of information about certain people and you need to work with it something like this:



 arr.findWhereNameEquals('Lily') arr.findWhereSkillsIncludes('javascript') arr.findWhereSkillsIsEmpty() arr.findWhereAgeIsGreaterThan(40) 


This can be done using a proxy. Namely, the array can be wrapped with a proxy object, which analyzes the calls to the methods and searches for the requested data in the array.



Here is what it might look like:



 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')) //  Lily console.log(arr.findWhereSkillsIncludes('javascript')) //  Iris 


Very similar to what is shown here, it might look like writing using proxy library objects to work with statements like expect .



And here is another idea of ​​using proxy objects. It consists in creating a library for building queries to the database with the following API:



 const id = await db.insertUserReturningId(userInfo) //   INSERT INTO user ... RETURNING id 


Monitoring Asynchronous Functions







Proxy objects allow you to intercept method calls. If the method call returns a promise, the proxy capabilities let you know when the promise is resolved. This feature can be used to monitor asynchronous methods of an object and generate statistical information on these methods, which, for example, can be output to the terminal.



Consider an example. There is a service object that implements some asynchronous functionality, and the monitor function, which accepts objects, wraps them into proxy objects and organizes monitoring of asynchronous methods of proxied objects. Here is how, at a high level, the work of these mechanisms looks like:



 const service = { callService() {   return new Promise(resolve =>     setTimeout(resolve, Math.random() * 50 + 50)) } } const monitoredService = monitor(service) monitoredService.callService() //  ,     


Here is a complete 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) 


Results



Proxy objects in JavaScript are a very powerful tool. In addition, the ability to dynamically implement methods based on their names adds code clarity and readability. However, like any other additional level of abstraction, proxies create some load on the system. I have not yet analyzed their performance, but if you plan to use them in production, check first how this will affect the speed of your project. But, be that as it may, nothing prevents the use of proxies during development, realizing with their help, for example, monitoring asynchronous functions for debugging purposes.



Dear readers! Here is a discount especially for you :)



Source: https://habr.com/ru/post/347754/



All Articles