One of the aspects of programming I love the most is meta-programming, which references the ability to change the basic building blocks of a language, using that language itself to make the changes. Developers use this technique to enhance the language or even, in some cases, to create new custom languages known as Domain Specific Language (or DSL for short).
Many languages already provide deep levels of meta-programming, but JavaScript was missing some key aspects.
Yes, it’s true, JavaScript is flexible enough that it allows you to stretch the language quite a bit, considering how you can add attributes to an object during run-time, or how you can easily enhance the behavior of a function by passing it different functions as a parameter. But with all of that, there were still some limits, which the new proxies now allow us to surpass.
In this article, I want to cover three things you can do with proxies that will enhance your objects specifically. Hopefully, by the end of it, you’ll be able to expand my code and maybe apply it yourself to your own needs!
Proxies basically wrap your objects or functions around a set of traps, and once those traps are triggered, your code gets executed. Simple, right?
The traps we can play around with are:
Trap | Description |
---|---|
getPrototypeOf | Triggered when you call the method with the same name on your own object. |
setPrototypeOf | Same as before, but for this particular method. |
isExtensible | Triggered when we try to understand if an object can be extended (i.e get new properties added to it during run-time). |
preventExtensions | Same as before, but for this particular method (which BTW, it ignored any new properties you add to the object during run-time). |
getOwnPropertyDescriptor | This method normally returns a descriptor object for a property of a given object. This trap is triggered when the method is used. |
defineProperty | Executed when this method is called. |
has | Triggered when we use the in operator (like when we do if(``'``value``' in array) ). This is very interesting since you’re not restricted to adding this trap for arrays, you can extend other objects as well. |
get | Quite straightforward, triggered when you try to access a property value (i.e yourObject.prop ). |
set | Same as the one above, but triggered when you set a value on a property. |
deleteProperty | Basically, a trap triggered when you use the delete operator. |
ownKeys | Triggered when you use the getOwnPropertyNames and getOwnPropertySymbols methods on your object. |
apply | Triggered when you call a function. We’ll be paying a lot of attention to this one, you just wait. |
construct | Triggered when you instantiate a new object with the new operator. |
Those are the standard traps, you’re more than welcome to check out Mozilla’s Web Docs for more details on each and every one of them since I’ll be focusing on a subset of those for this article.
That being said, the way you create a new proxy or, in other words, the way you wrap your objects or function calls with a proxy, looks something like this:
let myString = new String("hi there!") let myProxiedVar = new Proxy(myString, { has: function(target, key) { return target.indexOf(key) != -1; } }) console.log("i" in myString) // false console.log("i" in myProxiedVar) //true
That’s the basis of a proxy, I’ll be showing more complex examples in a second, but they’re all based on the same syntax.
But before we start looking at the examples, I wanted to quickly cover this question, since it’s one that gets asked a lot. With ES6 we didn’t just get proxies, we also got the Reflect
object, which at first glance, does exactly the same thing, doesn’t it?
The main confusion comes because most documentation out there, states that Reflect
has the same methods as the proxy handlers we saw above (i.e the traps). And although that is true, there is a 1:1 relationship there, the behavior of the Reflect
object and its methods are more alike to that of the Object
global object.
For example, the following code:
const object1 = { x: 1, y: 2 }; console.log(Reflect.get(object1, 'x'));
Will return a 1, just as if you would’ve directly tried to access the property. So instead of changing the expected behavior, you can just execute it with a different (and in some cases, more dynamic) syntax.
Let’s now look at some examples. To start things off, I want to show you how you can provide extra functionality to the action of retrieving a property’s value.
What I mean by that is, assuming you have an object such as:
class User { constructor(fname, lname) { this.firstname = fname this.lastname = lname } }
You can easily get the first name, or the last name, but you can’t simply request the full name all at once. Or if you wanted to get the name in all caps, you’d have to chain method calls. This is by no means, a problem, that’s how you’d do it in JavaScript:
let u = new User("fernando", "doglio") console.log(u.firstname + " " + u.lastname) //would yield: fernando doglio console.log(u.firstname.toUpperCase()) //would yield: FERNANDO
But with proxies, there is a way to make your code more declarative. Think about it, what if you could have your objects support statements such as:
let u = new User("fernando", "doglio") console.log(u.firstnameAndlastname) //would yield: fernando doglio console.log(u.firstnameInUpperCase) //would yield: FERNANDO
Of course, the idea would be to add this generic behavior to any type of object, avoiding manually creating the extra properties and polluting the namespace of your objects.
This is where proxies come into play, if we wrap our objects and set a trap for the action of getting the value of a property, we can intercept the name of the property and interpret it to get the wanted behavior.
Here is the code that can let us do just that:
function EnhanceGet(obj) { return new Proxy(obj, { get(target, prop, receiver) { if(target.hasOwnProperty(prop)) { return target[prop] } let regExp = /([a-z0-9]+)InUpperCase/gi let propMatched = regExp.exec(prop) if(propMatched) { return target[propMatched[1]].toUpperCase() } let ANDRegExp = /([a-z0-9]+)And([a-z0-9]+)/gi let propsMatched = ANDRegExp.exec(prop) if(propsMatched) { return [target[propsMatched[1]], target[propsMatched[2]]].join(" ") } return "not found" } }); }
We’re basically setting up a proxy for the get
trap, and using regular expressions to parse the property names. Although we’re first checking if the name actually meets a real property and if that’s the case, we just return it. Then, we check for the matches on the regular expressions, capturing, of course, the actual name in order to get that value from the object to then further process it.
Now you can use that proxy with any object of your own, and the property getter will be enhanced!
Next, we have another small but interesting enhancement. Whenever you try to access a property that doesn’t exist on an object, you don’t really get an error, JavaScript is permissive like that. All you get is undefined
returned instead of its value.
What if, instead of getting that behavior, we wanted to customize the returned value, or even throw an exception since the developer is trying to access a non-existing property.
We could very well use proxies for this, here is how:
function CustomErrorMsg(obj) { return new Proxy(obj, { get(target, prop, receiver) { if(target.hasOwnProperty(prop)) { return target[prop] } return new Error("Sorry bub, I don't know what a '" + prop + "' is...") } }); }
Now, that code will cause the following behavior:
> pa = CustomErrorMsg(a) > console.log(pa.prop) Error: Sorry bub, I don't know what a 'prop' is... at Object.get (repl:7:14) at repl:1:16 at Script.runInThisContext (vm.js:91:20) at REPLServer.defaultEval (repl.js:317:29) at bound (domain.js:396:14) at REPLServer.runBound [as eval] (domain.js:409:12) at REPLServer.onLine (repl.js:615:10) at REPLServer.emit (events.js:187:15) at REPLServer.EventEmitter.emit (domain.js:442:20) at REPLServer.Interface._onLine (readline.js:290:10)
We could be more extreme like I mentioned, and do something like:
function HardErrorMsg(obj) { return new Proxy(obj, { get(target, prop, receiver) { if(target.hasOwnProperty(prop)) { return target[prop] } throw new Error("Sorry bub, I don't know what a '" + prop + "' is...") } }); }
And now we’re forcing developers to be more mindful when using your objects:
> a = {} > pa2 = HardErrorMsg(a) > try { ... console.log(pa2.property) } catch(e) { ... console.log("ERROR Accessing property: ", e) } ERROR Accessing property: Error: Sorry bub, I don't know what a 'property' is... at Object.get (repl:7:13) at repl:2:17 at Script.runInThisContext (vm.js:91:20) at REPLServer.defaultEval (repl.js:317:29) at bound (domain.js:396:14) at REPLServer.runBound [as eval] (domain.js:409:12) at REPLServer.onLine (repl.js:615:10) at REPLServer.emit (events.js:187:15) at REPLServer.EventEmitter.emit (domain.js:442:20) at REPLServer.Interface._onLine (readline.js:290:10)
Heck, using proxies you could very well add validations to your sets, making sure you’re assigning the right data type to your properties.
There is a lot you can do, using the basic behavior shown above in order to mold JavaScript to your particular desire.
The last example I want to cover is similar to the first one. Whether before we were able to add extra functionality by using the property name to chain extra behavior (like with the “InUpperCase” ending), now I want to do the same for method calls. This would allow us to not only extend the behavior of basic methods just by adding extra bits to its name, but also receive parameters associated with those extra bits.
Let me give you an example of what I mean:
myDbModel.findById(2, (err, model) => { //.... })
That code should be familiar to you if you’ve used a database ORM in the past (such as Sequelize or Mongoose, for example). The framework is capable of guessing what your ID field called, based on the way you set up your models. But what if you wanted to extend that into something like:
myDbModel.findByIdAndYear(2, 2019, (err, model) => { //... })
And take it a step further:
myModel.findByNameAndCityAndCountryId("Fernando", "La Paz", "UY", (err, model) => { //... })
We can use proxies to enhance our objects into allowing for such behavior, allowing us to provide extended functionality without having to manually add these methods. Besides, if your DB models are complex enough, all the possible combinations become too much to add, even programmatically, our objects would end up with too many methods that we’re just not using. This way we’re making sure we only have one, catch-all method that takes care of all combinations.
In the example, I’m going to be creating a fake MySQL model, simply using a custom class, to keep things simple:
var mysql = require('mysql'); var connection = mysql.createConnection({ host : 'localhost', user : 'user', password : 'pwd', database : 'test' }); connection.connect(); class UserModel { constructor(c) { this.table = "users" this.conn = c } }
The properties on the constructor are only for internal use, the table could have all the columns you’d like, it makes no difference.
let Enhacer = { get : function(target, prop, receiver) { let regExp = /findBy((?:And)?[a-zA-Z_0-9]+)/g return function() { // let condition = regExp.exec(prop) if(condition) { let props = condition[1].split("And") let query = "SELECT * FROM " + target.table + " where " + props.map( (p, idx) => { let r = p + " = '" + arguments[idx] + "'" return r }).join(" AND ") return target.conn.query(query, arguments[arguments.length - 1]) } } } }
Now that’s just the handler, I’ll show you how to use it in a second, but first a couple of points:
map
call, we’re making sure we map every prop name to the value we received. And we get the actual value using the arguments
object. Which is why the function we’re returning can’t be an arrow function (those don’t have the arguments
object available).table
property, and its conn
property. The target is our object, as you’d expect, and that is why we defined those back in the constructor. In order to keep this code generic, those props need to come from outside.query
method with two parameters, and we’re assuming the last argument our fake method received, is the actual callback. That way we just grab it and pass it along.That’s it, the TL;DR of the above would be: we’re transforming the method’s name into a SQL query and executing it using the actual query
method.
Here is how you’d use the above code:
let eModel = new Proxy(new UserModel(connection), Enhacer) //create the proxy here eModel.findById("1", function(err, results) { //simple method call with a single parameter console.log(err) console.log(results) }) eModel.findByNameAndId('Fernando Doglio', 1, function(err, results) { //extra parameter added console.log(err) console.log(results) console.log(results[0].name) })
That is it, after that the results are used like you would, nothing extra is required.
That would be the end of this article, hopefully, it helped clear out a bit of the confusion behind proxies and what you can do with them. Now let your imagination run wild and use them to create your own version of JavaScript!
See you on the next one!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowMaking carousels can be time-consuming, but it doesn’t have to be. Learn how to use React Snap Carousel to simplify the process.
Consider using a React form library to mitigate the challenges of building and managing forms and surveys.
In this article, you’ll learn how to set up Hoppscotch and which APIs to test it with. Then we’ll discuss alternatives: OpenAPI DevTools and Postman.
Learn to migrate from react-native-camera to VisionCamera, manage permissions, optimize performance, and implement advanced features.
2 Replies to "3 ways to use ES6 proxies to enhance your objects"
You said you were going to pay a lot more attention to the “apply” trap. That was the last time in the article you mentioned it.
@Alastair Haigh: Like every article about Proxies in the internet right? 😉