This article focuses on the powerful functions of Proxy. If you need to understand the basic definition and syntax of Proxy, refer to Proxy

What is a Proxy? What does it do? Before explaining, let’s look at a real example.

Each of us has many things to do in our daily life, such as reading emails, receiving deliveries and so on. Sometimes we might feel a little anxious: we have a lot of spam on our mailing list and it takes a lot of time to sift through it; The shipment may contain bombs planted by terrorists and threaten our security.

You may need a loyal housekeeper. You want your butler to do the following for you: have it go through your inbox and delete all the junk mail before you start reading; When you receive a package, have it check the package with specialized equipment to make sure there are no bombs inside.

In the example above, the butler is our Proxy. When we wanted to do something, the butler did something extra for us.

Now let’s go back to JavaScript. We know that JavaScript is an object-oriented programming language and we can’t write code without objects. But JavaScript objects are completely open to running, and you can do anything with them. This, in many cases, makes your code less secure.

Proxies are introduced in ECMAScript2015. With Proxy, we can find a loyal steward to help us enhance the original functionality of objects.

The basic syntax for using Proxy is as follows:

// This is a generic object
let obj = {a: 1.b:2}
// Use Proxy to configure objects
let objProxy = new Proxy(obj, handler)
Copy the code

This is just pseudocode. Because we haven’t written the handler yet, this code won’t work for now.

As a person, we might have operations like reading mail, picking up packages, etc., and housekeepers can help us do that. For objects, it can read properties, set properties, and so on, which can be enhanced by Proxy objects.

In handler, we can list the operations to Proxy. For example, if you wanted to print a statement in the console when you get an object property, you could write:

let obj = {a: 1.b:2}
// Use Proxy syntax to find a steward for the object
let objProxy = new Proxy(obj, {
  get: function(item, property, itemProxy){
    console.log(`You are getting the value of '${property}' property`)
 return item[property]  } }) Copy the code

In the example above, our handler is:

{
  get: function(item, property, itemProxy){
    console.log(`You are getting the value of '${property}' property`)
    return item[propery]
  }
} Copy the code

When we try to read the properties of an object, the get function performs:

The get function can take three arguments:

  • item: Object itself
  • property: The property name of the property you want to read
  • itemProxy: The butler object we just created

You may have read Proxy tutorials elsewhere, and you’ll notice that I named the parameters differently. I did this to get closer to my previous example to help you understand. I hope it works for you.

The return value of the get function is the value of the read property. Because we don’t want to change anything yet, we just return the property values of the original object.

We can also change the results if necessary. For example, we can do this:

let obj = {a: 1.b:2}
let objProxy = new Proxy(obj, {
  get: function(item, property, itemProxy){
    console.log(`You are getting the value of '${property}' property`)
    return item[property] * 2
 } }) Copy the code

Here is the result of reading its properties:

We will demonstrate this technique in action with practical examples.

In addition to intercepting reads of properties, we can also intercept changes to properties. Like this:

let obj = {a: 1.b:2}
let objProxy = new Proxy(obj, {
  set: function(item, property, value, itemProxy){
    console.log(`You are setting '${value}' to '${property}' property`)
    item[property] = value
 } }) Copy the code

The set function is fired when we try to set the value of an object property.

The set function above takes one more argument than the GET function because an extra value is passed when setting the property value.

In addition to intercepting reading and modifying properties, Proxy can intercept a total of 13 operations on objects.

They are:

  • get(item, propKey, itemProxy):Intercepts reads of object properties, such asobj.aobj['b']
  • set(item, propKey, value, itemProxy):Intercepting setting operations on object properties, such asobj.a = 1
  • has(item, propKey):Interceptor operationspropKey in objProxy, returns a Boolean value
  • deleteProperty(item, propKey):Interceptor operationsdelete proxy[propKey], returns a Boolean value
  • ownKeys(item):Intercepting operations, such asObject.getOwnPropertyNames(proxy).Object.getOwnPropertySymbols(proxy).Object.keys(proxy).for... in, returns an array. This method returns the property name of the target object’s own property, howeverObject.keys()The result of the operation is to contain only the enumerable properties of the target object itself
  • getOwnPropertyDescriptor(item, propKey):Interceptor operationsObject.getOwnPropertyDescriptor(proxy, propKey)Returns the descriptor for the property
  • defineProperty(item, propKey, propDesc):Intercept operation:Object. DefineProperty (proxy, propKey propDesc).Object.defineProperties(proxy, propDescs), returns a Boolean value
  • preventExtensions(item):Interceptor operationsObject.preventExtensions(proxy), returns a Boolean value
  • getPrototypeOf(item):Interceptor operationsObject.getPrototypeOf(proxy), returns an object
  • isExtensible(item):Interceptor operationsObject.isExtensible(proxy), returns a Boolean value
  • setPrototypeOf(item, proto):Interceptor operationsObject.setPrototypeOf(proxy, proto), returns a Boolean value

If the target object is a function, there are two additional operations to intercept

  • apply(item, object, args):Intercepting function call operations, such asproxy(... args).proxy.call(object, ... args).proxy.apply(...)
  • construct(item, args):Intercepts operations that Proxy instances invoke as constructors, for examplenew proxy(... args)

Some intercepts are not commonly used, so I won’t go into the details. Now let’s get into a real example and see what a Proxy can actually do for us.

1, implement array negative index

We know that other programming languages, such as Python, support access to negative indexes of arrays.

A negative index starts at the last position in the array and counts forward. Such as:

  • Arr [-1] is the last element of the array.
  • Arr [-3] is the third to last element of the array.

Many people find this a very useful feature, but unfortunately JavaScript does not currently support negative indexing syntax.

But powerful proxies in JavaScript provide metaprogramming capabilities.

We can wrap arrays as Proxy objects. When a user attempts to access a negative index, we can intercept this operation through the Proxy’s GET method. The access is completed by converting the negative index to a positive index according to the rules defined earlier.

Let’s start with a basic operation: intercepting the read of an array property.

function negativeArray(array) {
  return new Proxy(array, {
    get: function(item, propKey){
      console.log(propKey)
      return item[propKey]
 }  }) } Copy the code

The above function wraps an array. Let’s see how it is used.

As you can see, our reading of the array properties is indeed intercepted.

Note: Objects in JavaScript can only have keys of type String or Symbol. When we write arr[1], it is actually accessing arr[‘1’]. The key is the string “1”, not the number 1.

So now what we need to do is: when the user tries to access a property, the property is the index of the array, and finds that it is a negative index, and then intercepts and does the corresponding processing; If the attribute is not an index, or if it is a positive index, we do nothing.

Combined with the above requirements, we can write the following pseudo-code.

function negativeArray(array) {
  return new Proxy(array, {
    get: function(target, propKey){
      if(/** attribute is negative index */) {        // Convert a negative index to a positive index
 }  return target[propKey]  }) } Copy the code

So how do we identify negative indexes? It’s easy to make mistakes here, so I’m going to elaborate.

First, the Proxy’s GET method intercepts access to all of the array’s properties, including access to the array index and other array properties. Access to an element in an array is performed only if the attribute name can be converted to an integer. We actually need to intercept this operation to access the elements in the array.

We can determine if an array is an index by checking whether its properties can be converted to integers.

Number(propKey) ! =NaN && Number.isInteger(Number(propKey))
Copy the code

So, the complete code could be written like this:

function negativeArray(array) {
  return new Proxy(array, {
    get: function(target, propKey){
      if (Number(propKey) ! =NaN && Number.isInteger(Number(propKey)) && Number(propKey) < 0) {
        propKey = String(target.length + Number(propKey));
 }  return target[propKey]  }  }) } Copy the code

Here’s an example:

2. Data verification

Javascript is known to be a weakly typed language. Usually, when an object is created, it runs completely open. Anyone can modify it.

But in most cases, an object’s attribute values need to satisfy certain conditions. For example, the age attribute of an object that records user information would normally be an integer greater than 0 and less than 150.

let person1 = {
  name: 'Jon'.  age: 23
}
Copy the code

By default, however, JavaScript provides no security mechanism, and you can change this value at will.

person1.age = 9999
person1.age = 'hello world'
Copy the code

To make our code more secure, we can use proxies to wrap objects. We can intercept the set operation of an object and verify that the new value of the age field conforms to the rule.

let ageValidate = {
  set (item, property, value) {
    if (property === 'age') {
      if (!Number.isInteger(value) || value < 0 || value > 150) {
        throw new TypeError('age should be an integer between 0 and 150');
 }  }  item[property] = value  } } Copy the code

Now that we try to modify the value of this property, we can see that the protection mechanism we set up is working.

3. Associated properties

Many times, the attributes of an object are related to each other. For example, for an object that stores user information, its zip code and location are two highly related attributes. When a user’s zip code is determined, his location is also determined.

For the convenience of readers from different countries, I use a virtual example here. Assume that location and zip code have the following relationship:

JavaScript Street  --  232200
Python Street -- 234422
Golang Street -- 231142
Copy the code

Here is the code that represents their correspondence:

const location2postcode = {
  'JavaScript Street': 232200.  'Python Street': 234422.  'Golang Street': 231142
}
const postcode2location = {  '232200': 'JavaScript Street'. '234422': 'Python Street'. '231142': 'Golang Street' } Copy the code

Then look at the following example:

let person = {
  name: 'Jon'
}
person.postcode = 232200
Copy the code

We want person.location=’JavaScript Street’ automatically triggered when we set Person. postcode=232200.

The solutions are as follows:

let postcodeValidate = {
  set(item, property, value) {
    if(property === 'location') {
      item.postcode = location2postcode[value]
    }
 if(property === 'postcode') { item.location = postcode2location[value]  }  } } Copy the code

So we tied the zip code to the location.

4. Private properties

We know that JavaScript has historically not supported private genera, which makes it impossible to properly manage access when writing code.

To solve this problem, the convention in the JavaScript community is to treat attributes that begin with an _ character as private.

var obj = {
  a: 1.  _value: 22
}
Copy the code

The _value attribute above is considered private. However, it must be pointed out that this is only a convention and there are no such rules at the linguistic level.

Now that we have a Proxy, we can simulate private properties.

Private attributes have the following characteristics compared to general attributes:

  • Property value cannot be read
  • When the user wants to see all the property names of the object, this property is not visible

We can then examine the 13 interception operations of the Proxy mentioned earlier and see that three operations need to be intercepted.

function setPrivateField(obj, prefix = "_"){
  return new Proxy(obj, {
    // Intercept operation 'propKey in objProxy'
    has: (obj, prop) = > {},
    // Intercept operations, such as' object.keys (proxy) '
 ownKeys: obj= > {},  // Intercepts read object property operations  get: (obj, prop, rec) = > {})  }); } Copy the code

We then add the appropriate code to the pseudocode above: if we find a user trying to access a field starting with _, deny access.

function setPrivateField(obj, prefix = "_"){
  return new Proxy(obj, {
    has: (obj, prop) = > {
      if(typeof prop === "string" && prop.startsWith(prefix)){
        return false
 }  return prop in obj  },  ownKeys: obj= > {  return Reflect.ownKeys(obj).filter(  prop= > typeofprop ! = ="string"| |! prop.startsWith(prefix) )  },  get: (obj, prop) = > {  if(typeof prop === "string" && prop.startsWith(prefix)){  return undefined  }  return obj[prop]  }  }); } Copy the code

Here’s an example:

This article is formatted using MDNICE