Have you ever encountered a situation where you want to be able to control values in an object or array? Maybe you want to block certain types of data, even validate the data before storing it into the object. Suppose you want to react to incoming data or even outgoing data in some way? For example, maybe you want to update the DOM or swap the class that changes style by displaying the results, because the data will change. Ever wanted to work on a simple page idea or part that only requires some functionality of frameworks like Vue or React, but don't want to start a new app?
Then JavaScript Proxy may be exactly what you need!
Let me make a statement first: I am more of a UI developer in front-end technology; like the non- JavaScript centralized aspect described, it is part of the "huge disagreement". I would love to create only beautiful projects that are consistent in the browser and have all the relevant features. So, when it comes to purer JavaScript functionality, I tend not to go deeper.
However, I still love doing research and I am always looking for something to add to my new learning list. It turns out that JavaScript proxy is an interesting topic, because just looking back at the basics opens up many possibilities about how to take advantage of this feature. Still, at first glance, the code can quickly get heavy. Of course, it all depends on your needs.
The concept of proxy objects has been around for quite some time. In my research, I can find references from a few years ago. However, it is not high on my list because it has never been supported in Internet Explorer. By comparison, it has been excellently supported in all other browsers over the years. This is one of the reasons Vue 3 is incompatible with Internet Explorer 11 because the proxy is used in the latest Vue project.
So, what exactly is the proxy object?
MDN describes a Proxy object as:
[…] Enables you to create a proxy for another object that intercepts and redefines the underlying operations of that object.
The general idea is that you can create an object with functionality that allows you to control the typical actions that occur when using the object. The two most common are getting and setting values stored in objects.
const myObj = { mykey: 'value' } console.log(myObj.mykey); // "get" the value of the key, output 'value' myObj.mykey = 'updated'; // "Set" the value of the key to make it 'updated'
So, in our proxy object, we will create "traps" to intercept these operations and perform any functions we may wish to accomplish. There are up to thirteen such traps available. I'm not necessarily covering all of these pitfalls, as not all of them are necessary for the simple examples I provide below. Again, it depends on what you need for the specific context of creating the content. Trust me, just mastering the basics can go a long way.
To extend the above example to create a proxy, we will do the following:
const myObj = { mykey: 'value' } const handler = { get: function (target, prop) { return target[prop]; }, set: function (target, prop, value) { target[prop] = value; return true; } } const proxy = new Proxy(myObj, handler); console.log(proxy.mykey); // "get" the value of the key, output 'value' proxy.mykey = 'updated'; // "Set" the value of the key to make it 'updated'
First, let's start with the standard object. We then create a handler object that holds the handler function , commonly known as a trap. These represent actions that can be performed on traditional objects, in this case, which simply pass content without any changes. After that, we create our proxy using a constructor with the target object and handler object. At that time, we can reference the proxy object to get and set values that will be the proxy for the original target object myObj.
Note the return true at the end of the set trap. This is intended to inform the proxy that the setting value should be considered successful. In some cases, if you want to prevent setting values (considering verification errors), you should return false. This will also cause a console error and output a TypeError.
Now, one thing to remember this pattern is that the original target object is still available. This means you can bypass the proxy and change the value of the object without using the proxy. While I'm reading about using Proxy objects, I've found some useful patterns that can help with this problem.
let myObj = { mykey: 'value' } const handler = { get: function (target, prop) { return target[prop]; }, set: function (target, prop, value) { target[prop] = value; return true; } } myObj = new Proxy(myObj, handler); console.log(myObj.mykey); // "get" the value of the key, output 'value' myObj.mykey = 'updated'; // "Set" the value of the key to make it 'updated'
In this mode, we use the target object as the proxy object while referencing the target object in the proxy constructor. Yes, that's it. This works, but I find it easy to confuse what is happening. So let's create the target object in the proxy constructor:
const handler = { get: function (target, prop) { return target[prop]; }, set: function (target, prop, value) { target[prop] = value; return true; } } const proxy = new Proxy({ mykey: 'value' }, handler); console.log(proxy.mykey); // "get" the value of the key, output 'value' proxy.mykey = 'updated'; // "Set" the value of the key to make it 'updated'
In fact, we can create target objects and handler objects in the constructor if we want:
const proxy = new Proxy({ mykey: 'value' }, { get: function (target, prop) { return target[prop]; }, set: function (target, prop, value) { target[prop] = value; return true; } }); console.log(proxy.mykey); // "get" the value of the key, output 'value' proxy.mykey = 'updated'; // "Set" the value of the key to make it 'updated'
In fact, this is the pattern I use most frequently in the example below. Thankfully, the way to create proxy objects is flexible. Just use any mode that suits you.
Here are some examples covering the usage of JavaScript Proxy from basic data validation to using fetch to updating form data. Remember, these examples do cover the basics of JavaScript Proxy; it can dig in very quickly if you prefer. In some cases, they simply create regular JavaScript code in the proxy object to perform regular JavaScript operations. Think of them as ways to extend some common JavaScript tasks by more control over the data.
My first example covers what I've always thought was a rather simple and weird coding interview question: inverting the string. I have never liked this question and never asked this question during interviews. As someone who likes to go against the current of this kind of thing, I tried non-traditional solutions. You know, just for fun sometimes throwing it out, one of the solutions is great front-end fun. It also provides a simple example showing how the proxy is used.
If you type in the input, you will see that the typed is printed out in reverse below. Obviously, there are many ways to invert strings that can be used. However, let's take a look at my strange inversion method.
const reverse = new Proxy( { value: '' }, { set: function (target, prop, value) { target[prop] = value; document.querySelectorAll('[data-reverse]').forEach(item => { let el = document.createElement('div'); el.innerHTML = '\u{202E}' value; item.innerText = el.innerHTML; }); return true; } } ) document.querySelector('input').addEventListener('input', e => { reverse.value = e.target.value; });
First, we create a new proxy, the target object is a single-key value that holds anything typed in the input. The get trap does not exist because we only need simple passthrough because we do not have any actual functionality to bind to it. In this case, nothing is required. We will discuss this later.
For the set trap, we do have some features to perform. There is still a simple passthrough where the value is set as the value key in the target object as usual. Then there is a querySelectorAll that looks for all elements on the page that have the data-reverse data attribute. This allows us to locate multiple elements on the page at once and update them. This gives us a framework-like binding operation that everyone loves to see. This can also be updated to positioning inputs to allow for the case of appropriate two-way binding types.
This is where my quirky way of inverting strings comes into play. A div is created in memory and then updated with the string to the element's innerHTML. The first part of the string uses a special Unicode decimal code that will actually reverse everything afterwards, making it from right to left. Then, the innerText of the actual element on the page will get the innerHTML of the in-memory div. This is run every time you enter something in the input; therefore, all elements with the data-reverse attribute are updated.
Finally, we set an event listener on the input that sets the value key in the target object by the input value (i.e. the target of the event).
Finally, a very simple example of executing side effects on the DOM of the page by setting the value to an object.
A common UI pattern is to format input values into sequences that are more precise than just letters and numeric strings. An example of this is telephone input. Sometimes, if the typed phone number looks like a phone number, it looks and feels better. The trick, though, is that when we format the input values, we may still need a non-formatted version of the data.
This is an easy task for JavaScript Proxy.
When you type numbers in the input, they will be formatted as standard U.S. phone numbers (e.g. (123) 456-7890). Note that the phone number is also displayed below the input in plain text, just like the reverse string example above. This button outputs both the formatted and non-formatted versions of the data to the console.
So, here is the code for the proxy:
const phone = new Proxy( { _clean: '', number: '', get clean() { return this._clean; } }, { get: function (target, prop) { if (!prop.startsWith('_')) { return target[prop]; } else { return 'entry not found!' } }, set: function (target, prop, value) { if (!prop.startsWith('_')) { target._clean = value.replace(/\D/g, '').substring(0, 10); const sections = { area: target._clean.substring(0, 3), prefix: target._clean.substring(3, 6), line: target._clean.substring(6, 10) } target.number = target._clean.length > 6 ? `(${sections.area}) ${sections.prefix}-${sections.line}`: target._clean.length > 3 ? `(${sections.area}) ${sections.prefix}`: target._clean.length > 0 ? `(${sections.area}` : ''; document.querySelectorAll('[data-phone_number]').forEach(item => { if (item.tagName === 'INPUT') { item.value = target.number; } else { item.innerText = target.number; } }); return true; } else { return false; } } } );
There is more code in this example, so let's break it down. The first part is the target object that we are initializing internally in the proxy. It has three aspects.
{ _clean: '', number: '', get clean() { return this._clean; } },
The first key _clean is our variable that holds the unformatted version of the data. It starts with the traditional variable naming pattern, treating it as "private". We want to make it unavailable under normal circumstances. As we go deeper, we will introduce this more.
The second key number simply saves the formatted phone number value.
The third "key" is a get function with the name clean. This will return the value of our private _clean variable. In this case we just return that value, but this provides an opportunity to do other operations on it if we want. This is like the proxy getter of the proxy getter. This may seem strange, but it provides an easy way to control the data. Depending on your specific needs, this can be a fairly simple way to deal with this situation. It works for our simple example here, but additional steps may be required.
Now is the get trap for the proxy.
get: function (target, prop) { if (!prop.startsWith('_')) { return target[prop]; } else { return 'entry not found!' } },
First, we check the incoming prop or object key to determine if it does not start with an underscore. If it doesn't start with the underscore, we just need to return it. If it starts with an underscore, then we return a string indicating that the entry was not found. This type of negative return can be handled in different ways as needed. Returns a string, returns an error, or runs code with different side effects. It depends entirely on the specific situation.
One thing to note in my example is that I don't deal with other proxy traps that might be used with what is considered a private variable in the proxy. To protect this data more fully, you have to consider other pitfalls like [defineProperty]( https://www.php.cn/link/cd69510f4a69bc0ef6ba504331b9d546 or ownKeys -- usually anything about manipulating or referencing object keys. Whether you go so far may depend on who will use the proxy. If it is for yourself, then you know how to use the proxy. But if it is someone else, you may want to consider locking as much content as possible.
Now is where most of the magic happens in this example—set trap:
set: function (target, prop, value) { if (!prop.startsWith('_')) { target._clean = value.replace(/\D/g, '').substring(0, 10); const sections = { area: target._clean.substring(0, 3), prefix: target._clean.substring(3, 6), line: target._clean.substring(6, 10) } target.number = target._clean.length > 6 ? `(${sections.area}) ${sections.prefix}-${sections.line}`: target._clean.length > 3 ? `(${sections.area}) ${sections.prefix}`: target._clean.length > 0 ? `(${sections.area}` : ''; document.querySelectorAll('[data-phone_number]').forEach(item => { if (item.tagName === 'INPUT') { item.value = target.number; } else { item.innerText = target.number; } }); return true; } else { return false; } }
First, do the same checks on private variables in the proxy. I'm not really testing other types of props, but you might want to consider doing this here. I'm assuming that only the number key in the proxy target object will be adjusted.
The value passed in (the value entered) will strip everything except the numeric characters and save it to the _clean key. This value is then used throughout the process to rebuild into a formatted value. Basically, every time you type, the entire string is rebuilt in real time to the expected format. The substring method locks the number to a ten-digit number.
Then create a sections object to save different parts of our phone number, based on a breakdown of US phone numbers. As the length of the _clean variable increases, we update number to the format pattern we want to see at that time.
A querySelectorAll is looking for any elements with the data-phone_number data attribute and running them through a forEach loop. If the element is an input to update the value, the innerText of any other element is updated. This is how the text appears below the input. If we want to place another input element with that data attribute, we will see its value updated in real time. This is a way to create one-way or two-way bindings, depending on the requirements.
Finally, return true to let the proxy know that everything is going well. Returns false if the incoming prop or key starts with an underscore.
Finally, the event listener that makes this work:
document.querySelectorAll('input[data-phone_number]').forEach(item => { item.addEventListener('input', (e) => { phone.number = e.target.value; }); }); document.querySelector('#get_data').addEventListener('click', (e) => { console.log(phone.number); // (123) 456-7890 console.log(phone.clean); // 1234567890 });
The first group looks for all inputs with our specific data attributes and adds event listeners to them. For each input event, the proxy's number key value is updated with the currently entered value. Since we format the input value every time we send it, we delete any characters that are not numbers.
The second set of buttons that search for output two sets of data (as required) and output to the console. This shows how we write code to request the required data on demand. Hopefully, it is clear that phone.clean is referencing the get proxy function in the target object, which returns the _clean variable in the object. Note that it is not called as a function like phone.clean(), because it acts as a get proxy in our proxy.
You can use arrays as target "object" in the proxy instead of objects. Since it will be an array, there are a few things to consider. The functionality of an array (such as push()) will be handled somehow in the proxy's setter trap. Also, in this case, creating a custom function in the target object concept does not actually work. However, there are some useful things to do with arrays as targets.
Of course, storing numbers in arrays is nothing new. Obviously. However, I will append some rules to this digit storage array, such as not allowing duplicate values and only allowing numbers. I will also provide some output options such as sorting, summing, averaging and clearing values. Then update a small user interface that controls all of this.
The following are proxy objects:
const numbers = new Proxy([], { get: function (target, prop) { message.classList.remove('error'); if (prop === 'sort') return [...target].sort((a, b) => a - b); if (prop === 'sum') return [...target].reduce((a, b) => ab); if (prop === 'average') return [...target].reduce((a, b) => ab) / target.length; if (prop === 'clear') { message.innerText = `${target.length} number${target.length === 1 ? '' : 's'} cleared!`; target.splice(0, target.length); collection.innerText = target; } return target[prop]; }, set: function (target, prop, value) { if (prop === 'length') return true; dataInput.value = ''; message.classList.remove('error'); if (!Number.isInteger(value)) { console.error('Data provided is not a number!'); message.innerText = 'Data provided is not a number!'; message.classList.add('error'); return false; } if (target.includes(value)) { console.error(`Number ${value} has already been submitted!`); message.innerText = `Number ${value} has already been submitted!`; message.classList.add('error'); return false; } target[prop] = value; collection.innerText = target; message.innerText = `Number ${value} added!`; return true; } });
For this example, I will start with the setter trap.
The first thing to do is check whether the length attribute is set to an array. It just returns true so that it happens the usual way. If you need to react to the set length, it can always add code in the right place.
The next two lines of code refer to two HTML elements stored on the page using querySelector. dataInput is the input element, which we want to clear every time we enter. message is the element that holds the response to array changes. Since it has the concept of an error state, we make sure it is not in that state every time it is entered.
The first if checks whether the input is actually a number. If not, then it will perform a few things. It emits a console error indicating the problem. The message element gets the same statement. Then, the message enters an error state through the CSS class. Finally, it returns false, which also causes the agent to issue its own error to the console.
The second if checks if the input already exists in the array; remember that we do not want to repeat. If there is duplication, the same message passing as in the first if occurs. Message delivery is slightly different because it is a template literal so we can see duplicate values.
The last part assumes that everything goes well and can continue. The value is set as usual, and then we update the collection list. collection refers to another element on the page that shows us the current collection of numbers in the array. Similarly, messages are updated with the added entries. Finally, we return true to let the proxy know that everything is going well.
Now, the get trap is slightly different from the previous example.
get: function (target, prop) { message.classList.remove('error'); if (prop === 'sort') return [...target].sort((a, b) => a - b); if (prop === 'sum') return [...target].reduce((a, b) => ab); if (prop === 'average') return [...target].reduce((a, b) => ab) / target.length; if (prop === 'clear') { message.innerText = `${target.length} number${target.length === 1 ? '' : 's'} cleared!`; target.splice(0, target.length); collection.innerText = target; } return target[prop]; },
What happens here is that a "prop" is not a normal array method; it is passed as a prop to get trap. For example, the first "prop" is triggered by this event listener:
dataSort.addEventListener('click', () => { message.innerText = numbers.sort; });
Therefore, when the sort button is clicked, the innerText of the message element will be updated using the content returned by numbers.sort. It acts as a proxy intercepts and returns getters for atypical array-related results.
After deleting the potential error state of the message element, we then determine whether a non-standard array fetch operation is expected to occur. Each operation returns an operation on the original array data without changing the original array. This is done by using the extension operator on the target to create a new array and then using the standard array method. Each name should imply what it does: sort, sum, average, and clear. Well, clearing isn't a standard array method, but that sounds good. Since entries can be sorted in any order, we can have it provide us with a sorted list or perform mathematical functions on the entries. Clear simply clears the array, as you might expect.
Here are other event listeners for buttons:
dataForm.addEventListener('submit', (e) => { e.preventDefault(); numbers.push(Number.parseInt(dataInput.value)); }); dataSubmit.addEventListener('click', () => { numbers.push(Number.parseInt(dataInput.value)); }); dataSort.addEventListener('click', () => { message.innerText = numbers.sort; }); dataSum.addEventListener('click', () => { message.innerText = numbers.sum; }); dataAverage.addEventListener('click', () => { message.innerText = numbers.average; }); dataClear.addEventListener('click', () => { numbers.clear; });
We can extend and add functionality to arrays in a number of ways. I've seen an example of an array that allows selection of entries with a negative index, which counts from the end. Find entries in the object array based on the attribute values within the object. Return a message instead of undefined when trying to get a value that does not exist in the array. There are many ideas to take advantage of and explore proxy on arrays.
Address forms are pretty standard stuff on web pages. Let's add some interactivity to it for fun (and non-standard) confirmation. It can also act as a data collection for form values in a single object, which can be requested on demand.
The following are proxy objects:
const model = new Proxy( { name: '', address1: '', address2: '', city: '', state: '', zip: '', getData() { return { name: this.name || 'no entry!', address1: this.address1 || 'no entry!', address2: this.address2 || 'no entry!', city: this.city || 'no entry!', state: this.state || 'no entry!', zip: this.zip || 'no entry!' }; } }, { get: function (target, prop) { return target[prop]; }, set: function (target, prop, value) { target[prop] = value; if (prop === 'zip' && value.length === 5) { fetch(`https://api.zippopotam.us/us/${value}`) .then(response => response.json()) .then(data => { model.city = data.places[0]['place name']; document.querySelector('[data-model="city"]').value = target.city; model.state = data.places[0]['state abbreviation']; document.querySelector('[data-model="state"]').value = target.state; }); } document.querySelectorAll(`[data-model="${prop}"]`).forEach(item => { if (item.tagName === 'INPUT' || item.tagName === 'SELECT') { item.value = value; } else { item.innerText = value; } }) return true; } } );
The target object is very simple; each entry entered in the form. The getData function will return the object, but if the value of the property is an empty string, it will change to "no entry!" This is optional, but the function provides a clearer object than we can get by just getting the state of the proxy object.
The getter function just passes content as usual. You may not need to do this, but I like to include it in order to make it complete.
The setter function sets the value to prop. However, if checks whether the prop that will be set happens to be the postal code. If so, then we check if the value length is 5. When evaluated to true, we will execute a fetch that uses the postal code hit address finder API. Any value returned is inserted into the object property, city input, and select state in the select element. This is a convenient shortcut example that allows the user to avoid having to type these values. These values can be changed manually if needed.
For the next section, let's look at an example of an input element:
<code></code>
The proxy has a querySelectorAll which looks for any element with matching data attributes. This is the same as the reverse string example we saw before. If it finds a match, it updates the input value or the innerText of the element. This is how the rotary card is updated in real time to show the appearance of the completed address.
One thing to note is the data-model property on the input. The value of this data attribute actually informs the agent of the key to lock during its operation. The agent looks up the involved elements based on the involved keys. The event listener performs the same operation by letting the proxy know which key is being used. This is what it looks like:
document.querySelector('main').addEventListener('input', (e) => { model[e.target.dataset.model] = e.target.value; });
Therefore, all inputs within the main element will be located and the agent will be updated when the input event is triggered. The value of the data-model property is used to determine which key in the proxy to locate. In fact, we are using a model system. Consider how this kind of thing can be used further.
As for the "Get Data" button? It is a simple console log of the getData function...
getDataBtn.addEventListener('click', () => { console.log(model.getData()); });
This is an interesting example to build and use to explore the concept. This is an example that makes me think about what I can build with JavaScript Proxy. Sometimes you only need a small widget that has some data collection/protection capabilities and can manipulate the DOM by simply interacting with the data. Yes, you can use Vue or React, but sometimes they can be too complicated for something so simple.
"Just that's it" means it depends on each of you and whether you will dig deeper into JavaScript Proxy. As I said at the beginning of this article, I only covered the basics of this feature. It can provide more features and can be bigger than the examples I provide. In some cases, it can provide the basis for a small helper program for niche solutions. It is obvious that these examples can be easily created using basic functions that perform the same function. Even most of my sample code is mixed with regular JavaScript with proxy objects.
The point, however, is to provide examples of using a proxy to show how to react to data interactions—even control how to react to those interactions to protect data, validate data, manipulate DOM, and get new data—all based on someone trying to save or get data. This can be very powerful in the long run and allows for the creation of simple applications that may not require larger libraries or frameworks.
So if you're a front-end developer who is more focused on the UI, like me, you can explore some of the basics to see if there is a small project that might benefit from JavaScript Proxy. If you are more of a JavaScript developer, you can start delving deeper into agents for larger projects. Maybe a new framework or library?
Just an idea...
The above is the detailed content of An Intro to JavaScript Proxy. For more information, please follow other related articles on the PHP Chinese website!