Explore the Proxy proxy of ES6 in detail

  • 2021-07-04 17:59:51
  • OfStack

Preface

In ES 6, the Proxy Constructor is an accessible global object that allows you to collect information about the requested operation between the object and the behavior of the various manipulators, and return whatever you want to do. Features such as arrow function, array deconstruction and rest parameters in ES6 are widely circulated after implementation, but features like Proxy are rarely used by developers. One aspect lies in the compatibility of browsers, and the other aspect lies in the fact that developers need to deeply understand their usage scenarios in order to give full play to the advantages of these features. Personally, I like Proxy of ES6 very much, because it allows us to control external access to objects in a simple and easy-to-understand way. In the following, I will first introduce how to use Proxy, and then give specific examples to explain the use scenario of Proxy.

Proxy, known by name, is very similar to the proxy pattern in design pattern, which is commonly used in three aspects:

1. And monitoring external access to objects

2. Complexity of a function or class

3. Verify the operation or manage the required resources before the operation

In a browser environment that supports Proxy, Proxy is a global object that can be used directly. Proxy(target, handler) Is a constructor, target Is the object being proxied, handlder Is an object that declares various proxy operations, and finally returns 1 proxy object. Every time the outside world accesses through proxy objects, target Object, it passes through the property of the handler Object, from the perspective of this process, the proxy object is very similar to middleware (middleware). So what operations can Proxy intercept? The most common operations are get (read), set (modify) object properties, etc. Please click here for a complete list of interceptible operations. In addition, the Proxy object provides a revoke Method, you can log off all proxy operations at any time. Before we formally introduce Proxy, we recommend that you have a certain understanding of Reflect, which is also a new global object of ES6.

Basic


const target = { 
  name: 'Billy Bob',
  age: 15
};

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

    return Reflect.get(target, key, proxy);
  }
};

const proxy = new Proxy(target, handler);
proxy.name;
// => "GET request made for name at Thu Jul 21 2016 15:26:20 GMT+0800 (CST)"
// => "Billy Bob"

In the above code, we first define a proxy target object target Object that contains all proxy operations handler Object, then use the Proxy(target, handler) Create a proxy object proxy , and all subsequent uses proxy Right target Attribute is accessed through the handler The treatment of.

1. Pull out the calibration module

Let's start with a simple type validation, which demonstrates how to use Proxy to ensure the accuracy of data types:


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

numericDataStore = new Proxy(numericDataStore, { 
  set(target, key, value, proxy) {
    if (typeof value !== 'number') {
      throw Error("Properties in numericDataStore can only be numbers");
    }
    return Reflect.set(target, key, value, proxy);
  }
});

//  Throw an error because  "foo"  Not a numerical value 
numericDataStore.count = "foo";

//  Successful assignment 
numericDataStore.count = 333;

If you want to develop a validator directly for all the attributes of an object, it may soon make the code structure bloated. Using Proxy, you can separate the validator from the core logic and form its own body:


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 {
        throw Error(`${key} is not a valid property`)
      }
    }
  });
}

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);

//  Errors will be reported for the following operations 
bill.name = 0; 
bill.age = 'Bill'; 
bill.age = 15; 

By separating the verifier from the main logic, you can expand infinitely personValidators Validator contents without causing direct damage to related classes or functions. More complicated, we can also use Proxy to simulate type checking to check whether the function receives parameters of the correct type and number:


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 || typeof arg !== 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 warnings logged
obj.pickyMethodOne({}, "a little string", 123); 
obj.pickyMethodOne(123, {});

2. Private properties

In JavaScript or other languages, it is customary to underline variable names _ To indicate that this is a private property (not really private), but we can't guarantee that no one will really access or modify it. In the following code, we declare a private apiKey , convenient api Method calls inside this object, but you don't want to be able to access it from outside api._apiKey :


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';

Obviously, convention has no binding force. Using ES6 Proxy, we can implement real private variables. Here are two different privatization methods for different reading methods.

The first method is to use set/get to intercept read and write requests and return undefined:


let api = { 
  _apiKey: '123abc456def',
  getUsers: function(){ }, 
  getUser: function(userId){ }, 
  setUser: function(userId, config){ }
};

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);
  }
});

//  The following operations will throw errors 
console.log(api._apiKey);
api._apiKey = '987654321'; 

The second method is to use has to intercept in operations:


var api = { 
  _apiKey: '123abc456def',
  getUsers: function(){ }, 
  getUser: function(userId){ }, 
  setUser: function(userId, config){ }
};

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...")
  }
}

3. Access the log

For those attributes or interfaces that are frequently called, run slowly, or occupy more resources of the execution environment, developers will want to record their usage or performance. At this time, Proxy can be used as middleware to easily realize the logging function:


let api = { 
  _apiKey: '123abc456def',
  getUsers: function() { /* ... */ },
  getUser: function(userId) { /* ... */ },
  setUser: function(userId, config) { /* ... */ }
};

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

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);
    };
  }
});

api.getUsers();

4. Early warning and interception

Suppose you don't want other developers to delete it noDelete Property, and you also want to call the handlder0 Some developers know that this method has been discarded, or tell the developers not to modify it handlder1 Property, you can use Proxy to implement:


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

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

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();

5. Filter operations

Some operations will take up resources very much, such as transferring large files. At this time, if the files have been sent in blocks, there is no need to make corresponding (non-absolute) new requests. At this time, Proxy can be used to detect the characteristics of the requests, and filter out which ones do not need to respond and which ones need to respond according to the characteristics. The following code briefly demonstrates the way to filter features, but it is not complete code. I believe everyone will understand the beauty:


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);
    }
  }
});

6. Interrupt Agent

Proxy supports canceling the target This 1 operation is often used to completely block access to data or interfaces. In the following example, we use the Proxy.revocable Method creates a proxy object that can undo the proxy:


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

numericDataStore = new Proxy(numericDataStore, { 
  set(target, key, value, proxy) {
    if (typeof value !== 'number') {
      throw Error("Properties in numericDataStore can only be numbers");
    }
    return Reflect.set(target, key, value, proxy);
  }
});

//  Throw an error because  "foo"  Not a numerical value 
numericDataStore.count = "foo";

//  Successful assignment 
numericDataStore.count = 333;
0

Decorator

Decorator implemented in ES7 is equivalent to the decorator pattern in design pattern. If we simply distinguish the usage scenarios of Proxy and Decorator, it can be summarized as follows: the core function of Proxy is to control the external access to the inside of the agent, and the core function of Decorator is to enhance the function of the decorated person. As long as they are distinguished in their core usage scenarios, functions such as accessing logs can be implemented by Proxy in this paper, but they can also be implemented by Decorator. Developers can choose freely according to the needs of the project, the specifications of the team and their own preferences.

Summarize

The ES of ES 6 is still very practical, and its seemingly simple features are of great use. I hope it will be helpful for everyone to learn ES6.


Related articles: