Cover image credits go to nearsay.com.
Let's say you have a class that represents a motorcycle. It has one property. A brand. But you don't want to write by hand the getters for that class. You could use a Proxy to do the job.
"use strict";
class Motorcycle {
constructor(constructionYear) {
this.constructionYear = constructionYear;
}
}
const withGetters = {
get(object, property) {
// "getConstructionYear"
if (property.startsWith("get")) {
// "ConstructionYear"
const getter = property.slice(3);
// "c"
const firstLetter = getter[0].toLowerCase();
// "onstructionYear"
const rest = getter.slice(1);
// "constructionYear"
const fullProperty = firstLetter + rest;
// motorcycle.getConstructionYear()
return () => object[fullProperty];
}
// motorcycle.constructionYear
return object[property];
}
};
const motorcycle = new Proxy(new Motorcycle(2020), withGetters);
console.log(motorcycle.constructionYear); // 2020
console.log(motorcycle.getConstructionYear()); // 2020
We want to access a property
Let's explain the code step by step.
First we have our class. We defined a constructor method in which we choose to receive one property. We then attach the property. Plain and simple.
Next, we have our Proxy handler. It will receive all properties and methods accessed, just like a Web Proxy receiving the request before processing it (like Service Workers). If we try to access a method that starts with get
, it means we want to access a property using its getter. But we do not have one. So we try to convert this method name into its property name. Once we know what property the user is trying to access, we can fake the method call by returning a function that will just return the property from that object.
And if the property does not start with a get
, it means that our job is done and we just return the accessed property.
Now we just have to instanciate our class by wrapping it up with a proxy. Next time we try to access a property, we can use both the getter and the property syntax. This also means that it will be automated for all properties that we decide to attach to our instance.
"use strict";
class Motorcycle {
constructor(brand, model, constructionYear) {
this.brand = brand;
this.model = model;
this.constructionYear = constructionYear;
}
}
const withGetters = {
get(object, property) {
// "getConstructionYear"
if (property.startsWith("get")) {
// "ConstructionYear"
const getter = property.slice(3);
// "c"
const firstLetter = getter[0].toLowerCase();
// "onstructionYear"
const rest = getter.slice(1);
// "constructionYear"
const fullProperty = firstLetter + rest;
// motorcycle.getConstructionYear()
return () => object[fullProperty];
}
// motorcycle.constructionYear
return object[property];
}
};
const motorcycle = new Proxy(new Motorcycle("Triumph", "Street Triple", 2020), withGetters);
console.log(motorcycle.brand); // "Triumph"
console.log(motorcycle.model); // "Street Triple"
console.log(motorcycle.constructionYear); // 2020
console.log(motorcycle.getBrand()); // "Triumph"
console.log(motorcycle.getModel()); // "Street Triple"
console.log(motorcycle.getConstructionYear()); // 2020
Getters & setters
Of course we could do the same for setters as well.
"use strict";
class Motorcycle {
constructor(brand, model, constructionYear) {
this.brand = brand;
this.model = model;
this.constructionYear = constructionYear;
}
}
function getPropertyFromGetterSetter(property) {
const sliced = property.slice(3);
const firstLetter = sliced[0].toLowerCase();
const rest = sliced.slice(1);
return firstLetter + rest;
}
const withGettersSetters = {
get(object, property) {
// "getConstructionYear"
if (property.startsWith("get")) {
// motorcycle.getConstructionYear()
return () => object[getPropertyFromGetterSetter(property)];
}
if (property.startsWith("set")) {
// motorcycle.setConstructionYear(2021)
return (newValue) => {
object[getPropertyFromGetterSetter(property)] = newValue;
};
}
// motorcycle.constructionYear
return object[property];
}
};
const motorcycle = new Proxy(
new Motorcycle("Triumph", "Street Triple", 2020),
withGettersSetters
);
console.log(motorcycle.getConstructionYear()); // 2020
motorcycle.setConstructionYear(2021);
console.log(motorcycle.getConstructionYear()); // 2021
You could even use the proxyfication inside your class in the constructor to ease the syntax.
"use strict";
function getPropertyFromGetterSetter(property) {
const sliced = property.slice(3);
const firstLetter = sliced[0].toLowerCase();
const rest = sliced.slice(1);
return firstLetter + rest;
}
const withGettersSetters = {
get(object, property) {
// "getConstructionYear"
if (property.startsWith("get")) {
// motorcycle.getConstructionYear()
return () => object[getPropertyFromGetterSetter(property)];
}
if (property.startsWith("set")) {
// motorcycle.setConstructionYear(2021)
return (newValue) => {
object[getPropertyFromGetterSetter(property)] = newValue;
};
}
// motorcycle.constructionYear
return object[property];
}
};
class Motorcycle {
constructor(brand, model, constructionYear) {
this.brand = brand;
this.model = model;
this.constructionYear = constructionYear;
return new Proxy(this, withGettersSetters);
}
}
const motorcycle = new Motorcycle("Triumph", "Street Triple", 2020);
console.log(motorcycle.getConstructionYear()); // 2020
motorcycle.setConstructionYear(2021);
console.log(motorcycle.getConstructionYear()); // 2021
And you could even go further (if you do not extend from any other classes) by creating a class for an easier integration with child classes.
"use strict";
function getPropertyFromGetterSetter(property) {
const sliced = property.slice(3);
const firstLetter = sliced[0].toLowerCase();
const rest = sliced.slice(1);
return firstLetter + rest;
}
const withGettersSetters = {
get(object, property) {
// "getConstructionYear"
if (property.startsWith("get")) {
// motorcycle.getConstructionYear()
return () => object[getPropertyFromGetterSetter(property)];
}
if (property.startsWith("set")) {
// motorcycle.setConstructionYear(2021)
return (newValue) => {
object[getPropertyFromGetterSetter(property)] = newValue;
};
}
// motorcycle.constructionYear
return object[property];
}
};
class GettersSetters {
constructor() {
return new Proxy(this, withGettersSetters);
}
}
class Motorcycle extends GettersSetters {
constructor(brand, model, constructionYear) {
super();
this.brand = brand;
this.model = model;
this.constructionYear = constructionYear;
}
}
const motorcycle = new Motorcycle("Triumph", "Street Triple", 2020);
console.log(motorcycle.getConstructionYear()); // 2020
motorcycle.setConstructionYear(2021);
console.log(motorcycle.getConstructionYear()); // 2021
Advantages
A huge advantage to using Proxy to automate your getters & setters is that it is now easier to write trivial classes that do not have much logic in the setters & getters.
Also, people that have no IDEs like me (I code using VIM on my terminal) and that do not have access to getters/setters generator can now enjoy writing classes with getters and setters as quickly as you would in an IDE.
And another great advantage is that you don't have to think about removing you getters/setters for unused properties that you will want to remove since it is computed by the proxy at runtime.
There are also drawbacks from using this technique but I'll let you experiment and go as far as you can to see it in action and make yourself an idea of what Proxies can bring to your project.
To sum up
There are, in my opinion, unlimited possibilities for using Proxies in your project. This getter/setter example was just a pretext to show you how awesome Proxies are in JavaScript. But you could go so much further and do something similar to what Symfony does with their annotations by creating a Proxy that will be responsible to turn a class into an entity connected to your API. This is left as an exercise for the reader and you could begin by creating a Proxy that will communicate with an API like JSONPlaceholder, maybe a class called Fetchable
to make it sounds and look cool!
So that's it for today. I hope you see now that Proxies are really powerful. Let me know if you have some other useful usage for Proxies and let's discuss about that in the comment section below. Thanks for reading me!
Top comments (5)
thank u so much but i did change it a quiet little bit to Fits my needs like this
"use strict";
the main proplem it here objectKeys.forEach((el, index) => {
that.el =param[index]});
i think all i need ti do here's i want to loop throw object propeties and use it and as u see from my params i send the values and loop throw it ans evently i need to
to make equality between them like here
that.el = param[index]
but i all got undefined
console.log(GetSetGen.getId()); // undefined
Hi Amin, can you expand on why you chose to use the same get trap for the setter? Is there an advantage to not putting this in the set trap?
Hi Stephen and thanks for your question which is interesting!
The
set
trap will be triggered when you set a property for the object that is being proxied.But when using the setter method
motorcycle.setBrand("Triumph");
you are really just triggering theget
trap, whether it is a property or in this case a method. Using aset
trap wouldn't have helped us much.Hope this answers your question.
can you give some example in react js
Hi Nahid and thanks for your comment!
Unfortunately, I haven't experienced enough with React to find something that could be useful. By creating this article, I was thinking more about use-cases related to the inner logic of your application, rather than something bound to a framework in specific.
But since React can be used with classes, you could make your own component class, for instance
FetchableComponent
to reuse api calls for instance (yeah I know, I'm giving the answer, it's too tempting).In this case, you could see
FetchableComponent
like some sort of Trait like in PHP. Except you cannot use multiple trait like in PHP in this case since it is an extend. But I think you get the idea.I haven't done React for a while so pardon my mistakes if there are any. What is important is the idea behind Proxies and React. I hope that answer your question!