Preface
In ES6, the Proxy constructor is an accessible global object, using it you can collect various information about requested operations between the object and the behavior of various operation objects and return whatever you want to do. The arrow functions, array deconstruction, rest parameters and other features in ES6 have been widely circulated once they have been implemented, but features like Proxy are rarely seen by developers. On the one hand, it is because of browser compatibility, and on the other hand, it is because of the advantages of these features that developers need to deeply understand their usage scenarios. Personally, I really like ES6's Proxy because it allows us to control external access to objects in a concise and easy-to-understand way. In the following, I will first introduce how Proxy is used, and then list specific examples to explain the usage scenarios of Proxy.
Proxy , see the name and meaning, has a very similar function to the proxy mode in the design mode, which is often used in three aspects:
1. Monitor external access to objects
2. Function or class complexity
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 prosecuted, and handlder is an object that declares various proxy operations, and finally returns a proxy object. Every time the outside world accesses the properties of target object through the proxy object, it passes through the handler object. From 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 interceptable operations. In addition, the Proxy object also provides a revoke method that can log out 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 added to 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 target object to be proxyed, and then declare a handler object containing all proxy operations. Next, we use Proxy(target, handler) to create the proxy object proxy . After that, all accesses to target attribute using proxy will be processed by handler .
1. Exit the verification module
Let's start with a simple type verification, 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); }});// An error was thrown because "foo" is not a numericDataStore.count = "foo";// Assigned successfully numericDataStore.count = 333;If you want to develop a verifier directly for all the properties of an object, it may quickly make the code structure bloated, using Proxy, you can separate the verifier from the core logic and come into one:
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);// The following operations will report an error bill.name = 0; bill.age = 'Bill'; bill.age = 15; Through the separation of the checker and main logic, you can infinitely extend the content of personValidators verifier without causing direct damage to the relevant classes or functions. To be more complicated, we can also use Proxy to simulate type checking to check whether the function receives parameters with correct type and quantity:
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 3obj.pickyMethodTwo("wopdapodoo", {}); // > You are incorrectly implementing the signature of pickyMethodOne. Check param 3obj.pickyMethodTwo("wopdapodoo", {}); // > You are incorrectly implementing the signature of pickyMethodOne. Check param 3obj.pickyMethodTwo("wopdapodoo", {}); // > You are incorrectly implementing the signature of pickyMethodTwo. Check param 1// No warnings loggedobj.pickyMethodOne({}, "a little string", 123); obj.pickyMethodOne(123, {});2. Private attributes
In JavaScript or other languages, it is customary to add an _ before the variable name to indicate that this is a private property (not really private), but we cannot guarantee that no one will access or modify it. In the following code, we declare a private apiKey to facilitate the method calls inside the api object, but we do not want to be able to access api._apiKey from the outside:
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 apiKey = api._apiKey; api._apiKey = '987654321';Obviously, the convention is unrestrained. Using ES6 Proxy, we can implement real private variables. The following demonstrates two different private 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 an error console.log(api._apiKey);api._apiKey = '987654321';The second method is to use has intercept in operation:
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 _apiKeyconsole.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 log
For attributes or interfaces that frequently call, run slowly, or take up more resources in the execution environment, developers will want to record their usage or performance. At this time, they can use Proxy to act as the middleware and easily implement 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 the noDelete attribute, and want the developers who call oldMethod to understand that this method has been abandoned, or tell the developers not to modify doNotChange attribute, then you can use Proxy to implement it:
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, respectivelydataStore.doNotChange = "foo"; delete dataStore.noDelete; dataStore.oldMethod();5. Filter operation
Some operations will take up resources very much, such as transferring large files. At this time, if the file is already sent in chunks, there is no need to make corresponding (non-absolute) to the new request. At this time, you can use Proxy to detect the feature of the request and filter out which ones do not need to respond and which ones need to respond according to the features. The following code briefly demonstrates how to filter features, not the complete code. I believe everyone will understand the wonderful things:
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 unproxy to target at any time, which is often used to completely enclose access to data or interfaces. In the following example, we used the Proxy.revocable method to create a proxy object that revocably proxies:
let sensitiveData = { username: 'devbryce' };const {sensitiveData, revokeAccess} = Proxy.revocable(sensitiveData, handler);function handleSuspectedHack(){ revokeAccess();}// logs 'devbryce'console.log(sensitiveData.username); handleSuspectedHack();// TypeError: Revokedconsole.log(sensitiveData.username);Decorator
The Decorator implemented in ES7 is equivalent to the decorator mode in the design mode. If you simply distinguish the usage scenarios of Proxy and Decorator , it can be summarized as: The core function of Proxy is to control the outside world's access to the agent, and the core function of Decorator is to enhance the functions of the decorator. As long as they make a good difference in their core usage scenarios, functions like access logs, although this article uses Proxy implementation, they can also be implemented using Decorator. Developers can choose freely based on project needs, team specifications, and their own preferences.
Summarize
ES6's Proxy is still very practical. Its seemingly simple features are of great use. I hope it will be helpful to everyone to learn ES6.