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!
How do proxies work? A quick intro
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.
Proxies vs Reflect
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.
Enhancement #1: dynamic property access
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!
Enhancement #2: custom error handling for invalid property names
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.
Enhancement #3: dynamic behavior based on method names
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:
- Notice the regular expression. We’ve been using them in the previous examples as well but they were simpler. Here we need a way to catch a repetitive pattern: findBy + propName + And as many times as we need.
- With the
map
call, we’re making sure we map every prop name to the value we received. And we get the actual value using thearguments
object. Which is why the function we’re returning can’t be an arrow function (those don’t have thearguments
object available). - We’re also using the target’s
table
property, and itsconn
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. - Finally, we’re calling the
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.
Conclusion
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!
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Try it for free.
The post 3 ways to use ES6 proxies to enhance your objects appeared first on LogRocket Blog.
Top comments (0)