1. What is the proxy mode

Proxy Pattern is a design Pattern in program design.

In real life, a proxy is a person authorized to represent others. For example, many states allow proxy voting, which means you can authorize someone to vote on your behalf in an election.

You’ve probably heard of proxy servers, which take all the traffic from you, send it on your behalf, and send the response back to you. Using a proxy server is useful when you don’t want the destination of your request to know the specific source of your request. All the target server sees is the request from the proxy server.

Moving closer to the topic of this article, this type of proxy is very similar to what ES6 proxies do, involving wrapping class (A) with class (B) and intercepting/controlling access to class (A).

Proxy mode is often useful when you want to do the following:

  • Intercepts or controls access to an object
  • Reduce method/class complexity by hiding transactions or auxiliary logic
  • Prevents heavily resource-dependent operations from being performed without validation/preparation

When multiple copies of a complex object must exist, the proxy pattern can be combined with the share pattern to reduce memory usage. The typical approach is to create a complex object with multiple agents, each referring to the original complex object. Operations performed on the agent are forwarded to the original object. Once all agents are gone, the complex object is removed.

The above is a general definition of the proxy pattern from Wikipedia. The specific representation of the Proxy pattern in JavaScript is the new object in ES6 -Proxy

2 Proxy mode in ES6

The Proxy constructor provided in ES6 makes it easy to use the Proxy pattern:

let proxy = new Proxy(target, handler);
Copy the code

The Proxy constructor passes in two parameters, the first parameter target representing the object to be propped up, and the second parameter Handler, which is also an object that sets the behavior of the propped object. For details about how to use Proxy, see Introduction to ECMAScript – Proxy by Yifeng Ruan.

The Proxy constructor can be queried on a global object. With it, you can effectively intercept operations on objects, gather information about access, and return any values you wish. From this perspective, proxy and middleware have a lot in common.

let dataStore = {
  name: 'Billy Bob'.age: 15
};

let handler = {
  get(target, key, proxy) {
    const today = new Date(a);console.log(`GET request made for ${key} at ${today}`);
    return Reflect.get(target, key, proxy);
  }
}

dataStore = new Proxy(dataStore, handler);

// This performs our interception logic, logging the request and assigning the value to the 'name' variable
const name = dataStore.name;
Copy the code

Specifically, proxies allow you to intercept many commonly used methods and properties on objects, the most common being GET, set, apply(for functions) and Construct (for constructors called using the new keyword). Refer to the specification for a complete list of methods that can be intercepted using proxy. Proxy can also be configured to stop accepting requests at any time, effectively canceling all access to the target object being proxied. This can be done with a REVOKE method.

3 Common scenarios of proxy mode

3.1 Stripped authentication logic

An example of Proxy validation is to verify that all properties in a data source are of the same type. The following example ensures that every time you set a property to the numericDataStore data source, its value must be a number.

let numericDataStore = {
  count: 0.amount: 1234.total: 14
};

numericDataStore = new Proxy(numericDataStore, {
  set(target, key, value, proxy) {
    if (typeofvalue ! = ='number') {
      throw Error("Properties in numericDataStore can only be numbers");
    }
    return Reflect.set(target, key, value, proxy); }});// This throws an exception
numericDataStore.count = "foo";

// This will set successfully
numericDataStore.count = 333;
Copy the code

That’s interesting, but what are the odds that you’ll create one? Certainly not…

If you want to write custom validation rules for some or all of the attributes on an object, the code can be a little more complicated, but I really like the fact that Proxy can help you separate validation code from core code. Am I the only one who hates mixing validation code with methods or classes?

// Define a validator that accepts custom validation rules and returns a proxy
function createValidator(target, validator) {
  return new Proxy(target, {
    _validator: validator,
    set(target, key, value, proxy) {
      if (target.hasOwnProperty(key)) {
        let validator = this._validator[key];
        if(!!!!! validator(value)) {return Reflect.set(target, key, value, proxy);
        } else {
          throw Error(`Cannot set ${key} to ${value}. Invalid.`); }}else {
        // Prevents the creation of a nonexistent attribute
        throw Error(`${key} is not a valid property`)}}}); }// Define validation rules for each attribute
const personValidators = {
  name(val) {
    return typeof val === 'string';
  },
  age(val) {
    return typeof age === 'number' && age > 18; }}class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
    return createValidator(this, personValidators); }}const bill = new Person('Bill'.25);

// All of the following operations throw exceptions
bill.name = 0;
bill.age = 'Bill';
bill.age = 15;
Copy the code

In this way, you can extend validation rules indefinitely without changing classes or methods.

One more idea about validation. Suppose you want to check the parameters passed to a method and print some useful help if the parameters passed do not match the function signature. You can do this with a Proxy without having to change the method code.

let obj = {
  pickyMethodOne: function(obj, str, num) { / *... * / },
  pickyMethodTwo: function(num, obj) { / *... * /}};const argTypes = {
  pickyMethodOne: ["object"."string"."number"].pickyMethodTwo: ["number"."object"]}; obj =new Proxy(obj, {
  get: function(target, key, proxy) {
    var value = target[key];
    return function(. args) {
      var checkArgs = argChecker(key, args, argTypes[key]);
      return Reflect.apply(value, target, args); }; }});function argChecker(name, args, checkers) {
  for (var idx = 0; idx < args.length; idx++) {
    var arg = args[idx];
    var type = checkers[idx];
    if(! arg ||typeofarg ! == type) {console.warn(`You are incorrectly implementing the signature of ${name}. Check param ${idx + 1}`);
    }
  }
}

obj.pickyMethodOne();
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 1
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 2
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 3

obj.pickyMethodTwo("wopdopadoo"{});// > You are incorrectly implementing the signature of pickyMethodTwo. Check param 1

// No warning message will be output
obj.pickyMethodOne({}, "a little string".123);
obj.pickyMethodOne(123{});Copy the code

Look at an example of form validation. The set method in the second argument to the Proxy constructor is a handy way to verify that a value is passed to an object. Take a traditional login form as an example. The form object has two attributes, account and Password. Each attribute value has a simple authentication method corresponding to its attribute name.

// Form object
const userForm = {
  account: ' '.password: ' ',}// Verify the method
const validators = {
  account(value) {
    // Account can only be in Chinese
    const re = /^[\u4e00-\u9fa5]+$/;
    return {
      valid: re.test(value),
      error: '"account" is only allowed to be Chinese'
    }
  },

  password(value) {
    // The password should be longer than 6 characters
    return {
      valid: value.length >= 6.error: '"password "should more than 6 character'}}}Copy the code

Let’s implement a generic form validator using Proxy

const getValidateProxy = (target, validators) = > {
  return new Proxy(target, {
    _validators: validators,
    set(target, prop, value) {
      if (value === ' ') {
        console.error(`"${prop}" is not allowed to be empty`);
        return target[prop] = false;
      }
      const validResult = this._validators[prop](value);
      if(validResult.valid) {
        return Reflect.set(target, prop, value);
      } else {
        console.error(`${validResult.error}`);
        return target[prop] = false; }}})}const userFormProxy = getValidateProxy(userForm, validators);
userFormProxy.account = '123'; // "account" is only allowed to be Chinese
userFormProxy.password = 'he'; // "password "should more than 6 character
Copy the code

We call the getValidateProxy method to generate a proxy object, userFormProxy, which validates values against validators’ validation rules when setting properties. We use console.error to throw error messages, but we can also add events to the DOM to implement validation prompts on the page.

3.2 True private properties

A common practice in JavaScript is to place an underscore before or after an attribute name to indicate that the attribute is for internal use only. But that doesn’t stop others from reading or modifying it.

In the example below, there is an apiKey variable that we want to access inside the API object, but we don’t want it to be accessible outside the object.

var api = {
  _apiKey: '123abc456def'./* mock methods that use this._apiKey */
  getUsers: function(){}, 
  getUser: function(userId){}, 
  setUser: function(userId, config){}};// logs '123abc456def';
console.log("An apiKey we want to keep private", api._apiKey);

// get and mutate _apiKeys as desired
var apiKey = api._apiKey;  
api._apiKey = '987654321'; 
Copy the code

Using ES6 Proxies, you can implement real, fully private properties in several ways.

First, you can use a proxy to intercept requests for a property and restrict them or return undefined.

var api = {  
  _apiKey: '123abc456def'./* mock methods that use this._apiKey */
  getUsers: function(){},getUser: function(userId){},setUser: function(userId, config){}};// Add other restricted properties to this array
const RESTRICTED = ['_apiKey'];

api = new Proxy(api, {  
    get(target, key, proxy) {
        if(RESTRICTED.indexOf(key) > - 1) {
            throw Error(`${key} is restricted. Please see api documentation for further info.`);
        }
        return Reflect.get(target, key, proxy);
    },
    set(target, key, value, proxy) {
        if(RESTRICTED.indexOf(key) > - 1) {
            throw Error(`${key} is restricted. Please see api documentation for further info.`);
        }
        return Reflect.get(target, key, value, proxy); }});// throws an error
console.log(api._apiKey);

// throws an error
api._apiKey = '987654321'; 
Copy the code

You can also use hastrap to mask the existence of this property.

var api = {  
  _apiKey: '123abc456def'./* mock methods that use this._apiKey */
  getUsers: function(){},getUser: function(userId){},setUser: function(userId, config){}};// Add other restricted properties to this array
const RESTRICTED = ['_apiKey'];

api = new Proxy(api, {  
  has(target, key) {
    return (RESTRICTED.indexOf(key) > - 1)?false :
      Reflect.has(target, key); }});// these log false, and `for in` iterators will ignore _apiKey

console.log("_apiKey" in api);

for (var key in api) {  
  if (api.hasOwnProperty(key) && key === "_apiKey") {
    console.log("This will never be logged because the proxy obscures _apiKey...")}}Copy the code

3.3 Record object access silently

You might like to count the usage or performance of methods or interfaces that rely heavily on resources, perform slowly, or are frequently used. Proxies can easily do this quietly in the background.

Note: You can’t just use applyTrap to block methods. When you want to execute a method, you first need to get the method. Therefore, if you want to intercept a method call, you need to intercept the GET operation on the method first, and then the apply operation.

let api = {  
  _apiKey: '123abc456def'.getUsers: function() { / *... * / },
  getUser: function(userId) { / *... * / },
  setUser: function(userId, config) { / *... * /}}; api =new Proxy(api, {  
  get: function(target, key, proxy) {
    var value = target[key];
    return function(. arguments) {
      logMethodAsync(new Date(), key);
      return Reflect.apply(value, target, arguments); }; }});// executes apply trap in the background
api.getUsers();

function logMethodAsync(timestamp, method) {  
  setTimeout(function() {
    console.log(`${timestamp} - Logging ${method} request asynchronously.`);
  }, 0)}Copy the code

This is cool because you can record all kinds of information without having to modify your application code or block code execution. And you only need to modify this code slightly to record the performance of the property function.

3.4 Giving prompts or blocking specific operations

Suppose you want to prevent others from deleting the noDelete property, let the person calling the oldMethod method know that the method is deprecated, or prevent others from modifying the doNotChange property. Here’s a quick way to do it.

let dataStore = {
  noDelete: 1235.oldMethod: function() {/ *... * / },
  doNotChange: "tried and true"
};

const NODELETE = ['noDelete'];
const DEPRECATED = ['oldMethod'];
const NOCHANGE = ['doNotChange'];

dataStore = new Proxy(dataStore, {
  set(target, key, value, proxy) {
    if (NOCHANGE.includes(key)) {
      throw Error(`Error! ${key} is immutable.`);
    }
    return Reflect.set(target, key, value, proxy);
  },
  deleteProperty(target, key) {
    if (NODELETE.includes(key)) {
      throw Error(`Error! ${key} cannot be deleted.`);
    }
    return Reflect.deleteProperty(target, key);

  },
  get(target, key, proxy) {
    if (DEPRECATED.includes(key)) {
      console.warn(`Warning! ${key} is deprecated.`);
    }
    var val = target[key];

    return typeof val === 'function' ?
      function(. args) {
        Reflect.apply(target[key], target, args); } : val; }});// these will throw errors or log warnings, respectively
dataStore.doNotChange = "foo";  
delete dataStore.noDelete;  
dataStore.oldMethod(); 
Copy the code

3.5 Prevent unnecessary resource consuming operations – caching proxy

Suppose you have a server interface that returns a huge file. A request is currently being processed, the file is being downloaded, or you do not want the interface to be requested again after the file has been downloaded. The proxy in this case can buffer access to the server and read from the cache when possible, rather than frequently requesting the server as requested by the user. The caching proxy can cache the results of some expensive methods, and when the function is called again, if the parameters are the same, it can directly return the results in the cache, without re-computing. For example, when using a back-end paged table, the back-end data needs to be requested again each time the page number changes. We can cache the page number and corresponding results, so that when requesting the same page, we don’t need to make an Ajax request and return the cached data directly. I’ll skip most of the code here, but the examples below are enough to show you how it works.

let obj = {  
  getGiantFile: function(fileId) {/ *... * /}}; obj =new Proxy(obj, {  
  get(target, key, proxy) {
    return function(. args) {
      const id = args[0];
      let isEnroute = checkEnroute(id);
      let isDownloading = checkStatus(id);      
      let cached = getCached(id);

      if (isEnroute || isDownloading) {
        return false;
      }
      if (cached) {
        return cached;
      }
      return Reflect.apply(target[key], target, args); }}});Copy the code

Let me give you another example that’s easier to understand

Let’s assume that a function that computes the Fibonacci sequence without any optimization is a very expensive method. This recursive call is significantly delayed in computes the Fibonacci term above 40.

const getFib = (number) = > {
  if (number <= 2) {
    return 1;
  } else {
    return getFib(number - 1) + getFib(number - 2); }}Copy the code

Now let’s write a factory function that creates a caching proxy:

const getCacheProxy = (fn, cache = new Map()) = > {
  return new Proxy(fn, {
    apply(target, context, args) {
      const argsString = args.join(' ');
      if (cache.has(argsString)) {
        // If there is a cache, return the cached data directly
        console.log(` output${args}Cache result:${cache.get(argsString)}`);
        return cache.get(argsString);
      }
      constresult = fn(... args); cache.set(argsString, result);returnresult; }})}const getFibProxy = getCacheProxy(getFib);
getFibProxy(40); / / 102334155
getFibProxy(40); // Output 40 cache result: 102334155
Copy the code

When we call getFibProxy(40) for the second time, the getFib function is not called, but directly returns the previously cached calculation results from the cache. By adding a caching Proxy, getFib only needs to focus on its Fibonacci calculation, which is implemented by Proxy objects. This implements the single responsibility principle we mentioned earlier.

3.6. Immediately revoke access to sensitive data

Proxy allows you to revoke access to the target object at any time. This can be useful when you want to completely block access to certain data or apis (for security, authentication, performance, etc.). Here is a simple example using the Revocable method. Note that when you use it, you don’t need to use the new keyword for the Proxy method.

let sensitiveData = {  
  username: 'devbryce'
};

const {sensitiveData, revokeAccess} = Proxy.revocable(sensitiveData, handler);

function handleSuspectedHack(){  
  // Don't panic
  // Breathe
  revokeAccess();
}

// logs 'devbryce'
console.log(sensitiveData.username);

handleSuspectedHack();

// TypeError: Revoked
console.log(sensitiveData.username); 
Copy the code

Well, that’s all I have to say. I would love to hear how you use Proxy in your work.

4 summarizes

In object-oriented programming, the proper use of the proxy pattern can embody the following two principles:

  • Principle of single responsibility: In object-oriented design, different responsibilities are encouraged to be distributed into fine-grained objects. Proxy derives functions on the basis of the original object without affecting the original object, which conforms to the design concept of loose coupling and high cohesion.

  • Open-closed principle: Proxies can be removed from the program at any time without changing other parts of the code. In a real world scenario, proxies may no longer be needed for a variety of reasons as the version iterations, so proxy objects can be easily replaced with calls to the original object

The role of Proxy in the Proxy mode is mainly reflected in the following three aspects:

Intercept and monitor external access to objects

2. Reduce the complexity of functions or classes

3. Verify operations or manage required resources before complex operations

Reference:

  • Explain the Proxy mode in ES6: Proxy