In front of the word
ES5 and ES6 aim to provide developers with functionality that JS already has but cannot call. For example, before ES5, objects in JS environments contained many non-enumerable and writable properties, but developers could not define their own non-enumerable or writable properties, so ES5 introduced the object.defineProperty () method to enable developers to do things that JS engines had long been able to do. ES6 adds some built-in objects to give developers more access to the JS engine. A Proxy is a wrapper that intercepts and changes the operations of the underlying JS engine, exposing the inner workings of objects in a new language, allowing developers to create built-in objects. This article covers Proxy and Reflection in more detail
The introduction of
[Array problem]
Prior to ES6, developers could not emulate the behavior of JS array objects with their own defined objects. When assigning a value to a particular element of an array, it affects the array’s Length property, which can also be used to modify array elements
let colors = ["red", "green", "blue"];
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"Copy the code
The colors array starts with three elements. Assigning colors[3] to “black” automatically increases the length attribute to 4, and setting the length attribute to 2 removes the last two elements of the array and keeps only the first two. Prior to ES5, developers could not implement these behaviors themselves, but now they can implement them through proxies
Proxy and reflection
A call to new Proxy() creates a Proxy instead of another target object, which virtualizes the target so they appear to function the same
The proxy intercepts low-level object operations on the target inside the JS engine, triggering trap functions that respond to specific operations
The reflection API takes the form of a Reflect object, where the default properties of the methods are the same as the underlying operations, which can be overridden by proxies, with each proxy trap corresponding to a Reflect method with the same name and parameters. The following table summarizes the characteristics of proxy traps
Each trap overrides some of the built-in features of a JS object, and you can use them to intercept and modify those features. If you still need built-in features, you can use the appropriate reflection API methods
Create a simple proxy
Creating a Proxy with the Proxy constructor requires passing in two parameters: target and handler. An object used by a handler to define one or more traps. In a proxy, the default properties are used for all operations except those defined specifically for the operation. A handler that does not use any traps is equivalent to a simple forward agent
let target = {};
let proxy = new Proxy(target, {});
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
target.name = "target";
console.log(proxy.name); // "target"
console.log(target.name); // "target"Copy the code
The proxy in this example forwards all operations directly to the target. The name is created on the target when “proxy” is assigned to the proxy.name property. The proxy simply forwards the operation to the target, and it does not store this property. Since proxy.name and target.name refer to target.name, they have the same value. Therefore, when a new value is set for target.name, proxy.name also changes
Trap agent
[Use set trap to validate properties]
If you create an object whose attribute value is a number, each new attribute in the object must be validated and an error must be thrown if it is not a number. To do this, you can define a set trap to override the default properties of the set values
The set trap accepts four parameters
TrapTaqget The object used to receive the property (the target of the proxy) Key The property key (string or Symbol type) to be written Value The value of the property being written Receiver The object (usually the proxy) on which the operation occursCopy the code
Reflect.set() is the corresponding reflection method and default feature of a set trap, which takes the same four parameters as the set proxy trap for easy use in traps. Trap should return true if the property is set, false if it is not. (The reflect.set () method returns the appropriate value based on whether the operation succeeded.)
You can validate property values by using the set trap and checking the incoming value
let target = { name: "target" }; Let proxy = new proxy (target, {set(trapTarget, key, value, receiver) {// Ignore existing attributes, avoid affect them if (! trapTarget.hasOwnProperty(key)) { if (isNaN(value)) { throw new TypeError("Property must be a number."); Return reflect. set(trapTarget, key, value, receiver); }}); // Add a new attribute proxy.count = 1; console.log(proxy.count); // 1 console.log(target.count); // 1 // You can assign a non-numeric value to name because the property already exists proxy.name = "proxy"; console.log(proxy.name); // "proxy" console.log(target.name); // "proxy" // throw error proxy.anotherName = "proxy";Copy the code
This code defines a proxy to validate the new property added to target. When proxy.count=1, the set trap is called, where trapTarget is equal to target, key is equal to “count”, value is equal to 1, and receiver is equal to proxy
Since there is no count attribute on target, the agent continues passing the value to isNaN(). If the result isNaN, it proves that the attribute value passed in is not a number and also throws an error. In this code, count is set to 1, so the proxy calls the reflect.set () method and passes in the four arguments accepted by the trap to add the new property
Proxy. You can achieve the name assigned to a string, this is because the target has a name attribute, but by calling trapTarget. Hasownproperty () method were ruled out after validation checks, so the goal of the existing digital attributes can be operation.
However, assigning proxy.anotherName to a string throws an error. There is no anotherName attribute on the target, so its value needs to be validated, and since “Proxy” is not a numeric value, an error is thrown
The SET proxy trap intercepts writing properties, and the GET proxy trap intercepts reading properties
Verify Object shapes with get traps.
JS has a special behavior that is sometimes confusing. It does not throw an error when reading a nonexistent property, but replaces the value of the property being read with undefined
let target = {};
console.log(target.name); // undefinedCopy the code
In most other languages, trying to read target.name throws an error if target does not have a name attribute. JS uses undefined instead of the value of the target.name attribute. This feature can cause major problems, especially when property names are incorrectly entered, and the agent can get around this problem by examining the object structure
Object structures are the set of all available properties and methods in an object. JS engines optimize code by using object structures, often creating classes to represent objects. If it is safe to assume that an object will always have the same properties and methods, then programs will throw errors when trying to access properties that do not exist. Proxies make object structure validation easy
Because properties are checked only when they are read, the presence or absence of a property in an object can be detected by a GET trap, which takes three arguments
TrapTarget The source object from which the property is read (the target of the agent) Key The property key to read (string or Symbol) The object (usually the agent) on which the receiver operation occursCopy the code
Since the get trap does not write values, it copies the other three arguments in the set trap except value, and reflect.get () takes the same three arguments and returns the default values for the property
If the property does not exist on the target, an error is thrown when using the get trap and reflect.get ()
let proxy = new Proxy({}, { get(trapTarget, key, receiver) { if (! (key in receiver)) { throw new TypeError("Property " + key + " doesn't exist."); } return Reflect.get(trapTarget, key, receiver); }}); // Add attribute function is normal proxy.name = "proxy"; console.log(proxy.name); // "proxy" // Error console.log(proxy.nme); // Throw an errorCopy the code
The get trap in this example intercepts the property read operation and determines whether there is a property read on the receiver using the IN operator. The reason why the IN operator is used to check the receiver instead of the trapTarget is to prevent the Receiver agent from having a HAS trap. Checking the trapTarget in this case might ignore the HAS trap and get the wrong result. Property throws an error if it does not exist, otherwise the default behavior is used
This code shows how to add a new attribute name to proxy and write and read values without errors. The last line contains a typo :proxy.nme could be proxy.namer, throwing an error because nme is a nonexistent attribute
[Has trap to hide existing attributes]
The IN operator can be used to detect whether a given object contains a property, and returns true if the property or stereotype property matches the name or Symbol
let target = {
value: 42;
}
console.log("value" in target); // true
console.log("toString" in target); // trueCopy the code
Value is a property of its own, and toString is a prototype property inherited from Object. Both exist on the Object, so checking for them with the IN operator returns true. Using has traps in the proxy intercepts these IN operations and returns a different value
The HAS trap is called every time the IN operator is used, passing in two arguments
TrapTaqget Read property object (proxy target) key Property key to check (string or Symbol)Copy the code
The reflect.has () method also takes these parameters and returns the default response from the IN operator. Using the HAS trap and reflect.has (), you can change the behavior of some properties detected by IN and restore the default behavior of others. For example, you can hide the value attribute from the previous example like this
let target = { name: "target", value: 42 }; let proxy = new Proxy(target, { has(trapTarget, key) { if (key === "value") { return false; } else { return Reflect.has(trapTarget, key); }}}); console.log("value" in proxy); // false console.log("name" in proxy); // true console.log("toString" in proxy); // trueCopy the code
The has trap in the proxy checks if the key is “value”, returns false if it is, and calls the reflect.has () method to return the default behavior if it is not. The result is that checking with the IN operator returns false even if the value attribute is actually present on target, and true correctly for name and toString
Use deleteProperty traps to prevent properties from being deleted
The delete operator removes an attribute from an object, returning true on success and false on failure. In the strict mode, an error is thrown if you attempt to remove a nonconfigurable property. Without the strict mode, the program simply returns false
let target = { name: "target", value: 42 }; Object.defineProperty(target, "name", { configurable: false }); console.log("value" in target); // true let result1 = delete target.value; console.log(result1); // true console.log("value" in target); // false // Note: The next line of code will throw an error in strict mode let result2 = delete target.name; console.log(result2); // false console.log("name" in target); // trueCopy the code
After deleting the value attribute with the delete operator, the in operation in the third console.log() call eventually returns false. The non-configurable property name cannot be deleted, so the delete operation returns false(it would throw an error if the code was running in strict mode). In the proxy, you can change this behavior through the deleteProperty trap
The deleteProperty trap is called whenever an object property is deleted through the DELETE operator, which takes two arguments
TrapTarget The object to delete the property (the target of the proxy) Key The property key to delete (string or Symbol)Copy the code
The reflect.deleteProperty () method provides a default implementation for the deleteProperty trap and takes the same two arguments. Combining the two can change the behavior of a DELETE, for example, to ensure that the value attribute is not deleted
let target = { name: "target", value: 42 }; let proxy = new Proxy(target, { deleteProperty(trapTarget, key) { if (key === "value") { return false; } else { return Reflect.deleteProperty(trapTarget, key); }}}); // Try deleting proxy.value console.log("value" in proxy); // true let result1 = delete proxy.value; console.log(result1); // false console.log("value" in proxy); // true // Try to delete proxy.name console.log("name" in proxy); // true let result2 = delete proxy.name; console.log(result2); // true console.log("name" in proxy); // falseCopy the code
This code is very similar to the has trap example, the deleteProperty trap checks if the key is “value” and returns false if so, otherwise the reflect.deleteProperty () method is called to use the default behavior. The value attribute could not be deleted because it was captured through the agent’s action, but the Name attribute was deleted as expected. This method is useful if you want to protect properties from being deleted and not throw errors in strict mode
[Prototype Proxy Trap]
The object.setPrototypeof () method is used to supplement the Object.getPrototypeof () method in ES5. The execution of these two methods can be intercepted by setPrototypeOf and getPrototypeOf traps in the proxy, in which case a method on Object calls a trap of the same name in the proxy to change the method’s behavior
Both traps are related to agents, but the specific methods are only related to the type of each trap. SetPrototypeOf traps accept the following parameters
TrapTarget The object that receives the stereotype set (the target of the proxy) Proto the object that the stereotype usesCopy the code
Both object.setPrototypeof () and reflect.setprototypeof () are passed as arguments. On the other hand, The object.getProtoTypeof () and reflect.getProtoTypeof () methods in the getPrototypeOf trap only accept the trapTarget argument
The operation mechanism of the prototype proxy trap
The prototype proxy trap has some limitations. First, the getPrototypeOf trap must return an Object or NULL, otherwise it will cause a runtime error. Return value checking ensures that Object.getProtoTypeof () always returns the expected value; Second, in setPrototypeOf traps, false must be returned if the operation fails, in which case Object.setPrototypeof () will throw an error. If setPrototypeOf returns any value that is not false, Object.setprototypeof () then assumes success
The following example hides the stereotype of the agent by always returning NULL and not allowing the stereotype to change
let target = {}; let proxy = new Proxy(target, { getPrototypeOf(trapTarget) { return null; }, setPrototypeOf(trapTarget, proto) { return false; }}); let targetProto = Object.getPrototypeOf(target); let proxyProto = Object.getPrototypeOf(proxy); console.log(targetProto === Object.prototype); // true console.log(proxyProto === Object.prototype); // false console.log(proxyProto); // null // success Object.setProtoTypeof (target, {}); Object.setprototypeof (proxy, {});Copy the code
This code highlights the difference in behavior between Target and proxy. Object.getprototypeof () returns a value to target, but null to proxy because getPrototypeOf trap is called; Similarly, object.setProtoTypeof () successfully sets a prototype for target, but when it sets a prototype for proxy, it ends up throwing an error because the setPrototypeOf trap is called
If you use the default behavior for both traps, you can use the corresponding method on Reflect. For example, the following code implements the default behavior of getPrototypeOf and setPrototypeOf traps
let target = {}; let proxy = new Proxy(target, { getPrototypeOf(trapTarget) { return Reflect.getPrototypeOf(trapTarget); }, setPrototypeOf(trapTarget, proto) { return Reflect.setPrototypeOf(trapTarget, proto); }}); let targetProto = Object.getPrototypeOf(target); let proxyProto = Object.getPrototypeOf(proxy); console.log(targetProto === Object.prototype); // true console.log(proxyProto === Object.prototype); // true // success Object.setProtoTypeof (target, {}); Object.setprototypeof (proxy, {});Copy the code
Because the getPrototypeOf and setPrototypeOf traps in this example use only the default behavior, target and paro×y can be used interchangeably and get the same result. Because the reflect.getPrototypeof () and reflect.setPrototypeof () methods have some important differences from methods of the same name on an Object, it is important to use them
Why are there two sets of methods
Confusingly, the reflect.getPrototypeof () and reflect.setprototypeof () methods look suspiciously like object.getPrototypeof () and object.setprototypeof () methods, Although the two sets of methods perform similar operations, there are some differences between them
Object.getprototypeof () and object.setPrototypeof () are advanced operations for developers; The reflect.getProtoTypeof () and reflect.setPrototypeof () methods are low-level operations that give developers access to [[getPrototypeOf]] and [[setprototypeOf]] permissions that were previously operated only internally
The reflect.getProtoTypeof () method is a wrapper around the inner [[getPrototypeOf]] operation, and the reflect.setPrototypeof () method has the same relationship with [[setPrototypeOf]]. The corresponding methods on Object also call [[GetPrototypeOf]] and [[Setprototypeof]], but perform some extra steps before doing so and determine the next action by checking the return value
If the argument passed is not an Object, the reflect.getProtoTypeof () method throws an error, while object.getProtoTypeof () methods cast the argument to an Object before the operation is executed. Passing a number to the two methods gives different results
let result1 = Object.getPrototypeOf(1); console.log(result1 === Number.prototype); // true // Throws an error reflect.getProtoTypeof (1);Copy the code
The object.getProtoTypeof () method forces the Number 1 to be a Number Object, so you can retrieve its prototype and return the value number.prototype; The reflect.getProtoTypeof () method does not enforce the type of the converted value, and 1 is not an object, so an error is thrown
The reflect.setPrototypeof () method is also different from the object.setprototypeof () method. Specifically, the reflect.setPrototypeof () method returns a Boolean indicating whether the operation succeeded, true on success and false on failure; The Object.setPrototypeof () method throws an error if it fails
Object.setprototypeof () will throw an error when setPrototypeOf proxy traps return false. The object.setPrototypeof () method returns the first parameter as its value and is therefore not suitable for implementing the default behavior of setPrototypeOf proxy traps
let target1 = {};
let result1 = Object.setPrototypeOf(target1, {});
console.log(result1 === target1); // true
let target2 = {};
let result2 = Reflect.setPrototypeOf(target2, {});
console.log(result2 === target2); // false
console.log(result2); // trueCopy the code
In this example, object.setPrototypeof () returns target1, but reflect.setPrototypeof () returns true. This subtle difference is important. There are many more seemingly duplicate methods on Object and Reflect, but be sure to use the Reflect method in all proxy traps
Object extensibility Trap
ES5 has fixed Object extensibility with the Object.preventExtensions() and Object.isextensible () methods, ES6 can intercept both methods and invoke the underlying objects via preventExtensions and isExtensible traps in the proxy. Both traps take a unique parameter, the trapTarget object, and call the method on it. The isExtensible trap must return a Boolean value indicating whether the object isExtensible; The preventExtensions trap must also return a Boolean value indicating whether the operation was successful
Reflect the preventExtensions () method and Reflect IsExtensible () method to realize the corresponding trap default behavior, both return Boolean value
Two basic examples
The following code is a practical application of the object extensibility trap, implementing the default behavior of the isExtensible and preventExtensions traps
let target = {}; let proxy = new Proxy(target, { isExtensible(trapTarget) { return Reflect.isExtensible(trapTarget); }, preventExtensions(trapTarget) { return Reflect.preventExtensions(trapTarget); }}); console.log(Object.isExtensible(target)); // true console.log(Object.isExtensible(proxy)); // true Object.preventExtensions(proxy); console.log(Object.isExtensible(target)); // false console.log(Object.isExtensible(proxy)); // falseCopy the code
This example shows how the Object.preventExtensions() and Object.isextensible () methods are passed directly from proxy to target. Of course, you can change the default behavior, for example, If you want to disable object.preventExtensions () for proxy, return false in the preventExtensions trap
let target = {};
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget) {
return false
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // trueCopy the code
The object.preventExtensions (proxy) call here is actually ignored because the preventExtensions trap returns false so the operation is not forwarded to the underlying target, object.isextensible () eventually returns true
[Repeated extensibility approach]
The Object.isextensible () method is very similar to the reflect.isextensible () method, only returning false when non-object values are passed in, Reflect.isextensible () throws an error
let result1 = Object.isExtensible(2); console.log(result1); // false // Throw an error let result2 = Reflect. IsExtensible (2);Copy the code
This restriction is similar to the difference between the object.getPrototypeof () method and reflect.getPrototypeof () method, because the underlying method has more stringent error checking than the higher-function method
Object. PreventExtensions () method and Reflect preventExtensions () method is also very similar. The object.preventExtensions () method always returns the parameter whether or not it is an Object; And if Reflect preventExtensions () method of parameter is not object will throw an error; If the parameter is an object, the operation success Reflect. PreventExtensions () will return true, otherwise it returns false
let result1 = Object.preventExtensions(2); console.log(result1); // 2 let target = {}; let result2 = Reflect.preventExtensions(target); console.log(result2); / / true / / throw an error let result3 = Reflect. PreventExtensions (2);Copy the code
Here, even if the value 2 is not an Object, the Object. The preventExtensions () method will also be the passthrough as a return value, which Reflect the preventExtensions () method can throw an error, only when the incoming Object it returns true
Attribute descriptor trap
One of the most important features of ES5 is the ability to defineProperty attributes using the object.defineproperty () method. Accessor properties could not be defined in earlier versions of JS, and properties could not be set to read-only or unconfigurable. Until the Object. DefineProperty () method after appear to support these features, and can be through the Object. The getOwnPropertyDescriptor () method to obtain these attributes
Can respectively in the agent with defineProperty trap and getOwnPropertyDescriptor trap interception Object. DefineProperty () method and Object getOwnPropertyDescriptor () method call. The definePropepty trap accepts the following parameters
TrapTarget Object of the property to be defined (the target of the proxy) Key property descriptor object of the key (string or Symbol) property descriptorCopy the code
The defineProperty trap needs to return true on success or false otherwise. The getOwnPropertyDescriptor trap takes only the trapTarget and key parameters, and eventually returns the descriptor. Reflect the defineProperty () method and Reflect getOwnPropertyDescriptor () method is the same as the corresponding trap accept parameters. This example implements the default behavior for each trap
let proxy = new Proxy({}, { defineProperty(trapTarget, key, descriptor) { return Reflect.defineProperty(trapTarget, key, descriptor); }, getOwnPropertyDescriptor(trapTarget, key) { return Reflect.getOwnPropertyDescriptor(trapTarget, key); }}); Object.defineProperty(proxy, "name", { value: "proxy" }); console.log(proxy.name); // "proxy" let descriptor = Object.getOwnPropertyDescriptor(proxy, "name"); console.log(descriptor.value); // "proxy"Copy the code
This code by the Object. DefineProperty () method on the proxy defines the attribute “name”, the property descriptor can be through the Object. The getOwnPropertyDescriptor () method to get
Add a limit to Object.defineProperty()
The defineProperty trap returns a Boolean value to indicate whether the operation was successful. The Object.defineProperty() method executes successfully when true is returned; The object.defineProperty () method throws an error when false is returned. This feature can be used to restrict the types of properties that the Object.defineProperty() method can define, for example, if you want to block properties of type Symbol, you can return false when the property key is Symbol
The defineProperty proxy trap returns false if the key is of type Symbol, otherwise the default behavior is performed. Call Object.defineProperty() and pass in “name”, so the key type is string so the method executes successfully; Call the object.defineProperty () method and pass in nameSymbol. The defineProperty trap returns false so an error is thrown
[Note] If you let the trap return true and don’t call reflect.defineProperty (), you can silently invalidate the object.defineProperty () method, which eliminates the error without actually defining the property
Descriptor object restrictions
In order to ensure the Object. DefineProperty () method and the Object. GetOwnPropertyDescriptor () method of the behavior is consistent, the incoming defineProperty trap descriptor Object has been standardized. Objects returned from the getOwnPropertyDescriptor trap are validated for the same reason
Whatever Object is passed to the object.defineProperty () method as the third argument, All the properties Enumerable, 64x, Value, Writable, GET and set will appear in the descriptor object passed to the defineProperty trap
let proxy = new Proxy({}, { defineProperty(trapTarget, key, descriptor) { console.log(descriptor.value); // "proxy" console.log(descriptor.name); // undefined return Reflect.defineProperty(trapTarget, key, descriptor); }}); Object.defineProperty(proxy, "name", { value: "proxy", name: "custom" });Copy the code
In this code, object.defineProperty () is called with an Object containing the non-standard name attribute passed in as the third argument. When the defineProperty trap is called, descriptor objects have value attributes but no name attributes, because descriptor is not a reference to the third argument actually passed to object.defineProperty (), Instead, a new object contains only those properties that are allowed to be used. The reflect.defineProperty () method also ignores all nonstandard attributes on the descriptor
The getOwnPropertyDescriptor trap is slightly different in that its return value must be NULL, undefined, or an object. If an object is returned, its own properties can only be enumepable, 64x, value, writable, get, and set. Using an invalid property in the returned object will raise an error
let proxy = new Proxy({}, { getOwnPropertyDescriptor(trapTarget, key) { return { name: "proxy" }; }}); / / throw an error descriptor = Object. GetOwnPropertyDescriptor (proxy, "name");Copy the code
Property descriptor has a name attribute is not allowed when the Object is called the getOwnPropertyDescriptor (), getOwnPropertyDescriptor return values will trigger an error. This restriction ensures that no matter what agents were used in the method, the Object, getOwnPropertyDescriptor () the structure of the return value is always reliable
Repeated descriptor methods
Again in ES6 we see these confusingly similar methods: Look Object. DefineProperty () method and the Object. GetOwnPropertyDescriptor () method, respectively, and Reflect. DefineProperty () method and Reflect getOwnPropertyDesc The riptor() method does the same thing. There are some subtle but important differences between these four methods
The object.defineProperty () method differs from the reflect.defineProperty () method only in its return value: The object.defineProperty () method returns the first argument, while reflect.defineProperty () returns an operation-dependent value, true on success and false on failure
let target = {};
let result1 = Object.defineProperty(target, "name", { value: "target "});
console.log(target === result1); // true
let result2 = Reflect.defineProperty(target, "name", { value: "reflect" });
console.log(result2); // trueCopy the code
Object.defineproperty () is called with target passed in and returns target; Target is passed in when reflect.defineProperty () is called and returns true, indicating success. Because the defineProperty proxy trap needs to return a Boolean value, it is best to implement the default behavior with reflect.defineProperty () if necessary
Call Object. GetOwnPropertyDescriptor () method is introduced to the original value as the first parameter, internal transfer this value is mandatory for an Object; . On the other hand, if the call Reflect getOwnPropertyDescriptor () method is introduced to the original value as the first parameter, it throws an error
let descriptor1 = Object.getOwnPropertyDescriptor(2, "name"); console.log(descriptor1); / / undefined / / throw an error let descriptor2 = Reflect. GetOwnPropertyDescriptor (2, "name");Copy the code
Due to the Object. GetOwnPropertyDescriptor numerical 2 () method will cast to an Object does not contain the name attribute, so it returns undefined, it is when an Object is not specified in the name attribute standards of behavior. However when calling Reflect. GetOwnPropertyDescriptor () immediately throws an error, because the method does not accept the original value as the first parameter
【ownKeys trap 】
The ownKeys proxy trap intercepts the internal method [[OwnPropertyKeys]], whose behavior we can override by returning the value of an array. The array is used for the Object. The keys (), Object, getOwnPropertyNames (), Object. GetOwnPropertySymbols () and the Object. The assign () four methods, The object.assign () method uses arrays to determine which properties to copy
OwnKeys trap implements the default behavior with reflect.ownkeys (), which returns an array containing all of its own key names, including strings and symbols. Object. GetOwnPropertyNames () method and the Object. The keys () method returns the result of exclude Symbol type of attribute names, Object. GetOwnPropertySymbols () method returns the result of the properties of the string type name out. The object.assign () method supports both string and Symbol types
The only argument the ownKeys trap takes is the target of the operation. The return value must be an array or array-like object, otherwise an error is thrown. When the Object is called. The keys (), Object. GetOwnPropertyNames (), the Object. The getOwnPropertySymbols () or the Object. The assign () method, You can use the ownKeys trap to filter out unwanted property keys. Assuming you don’t want to introduce any property names that start with an underscore character (the underscore symbol in JS indicates that the field is private), you can use the ownKeys trap to filter out those keys
let proxy = new Proxy({}, { ownKeys(trapTarget) { return Reflect.ownKeys(trapTarget).filter(key => { return typeof key ! == "string" || key[0] ! = = "_"; }); }}); let nameSymbol = Symbol("name"); proxy.name = "proxy"; proxy._name = "private"; proxy[nameSymbol] = "symbol"; let names = Object.getOwnPropertyNames(proxy), keys = Object.keys(proxy); symbols = Object.getOwnPropertySymbols(proxy); console.log(names.length); // 1 console.log(names[0]); // "name" console.log(keys.length); // 1 console.log(keys[0]); // "name" console.log(symbols.length); // 1 console.log(symbols[0]); // "Symbol(name)"Copy the code
This example uses an ownKeys trap, which first calls reflect.ownkeys () to get the target’s default key list; Next, use filter() to filter out the string starting with the underscore character. Then, add three attributes to the Proxy object: name, _NAME, and nameSymbol. Call Object. GetOwnPropertyNames () and the Object. The Keys () is introduced to the proxy, return only name attribute; Also, call Object. GetOwnPropertySymbols () is introduced to the proxy, return only nameSymbol. Because the _name attribute is filtered out, it does not appear in either result
Although the ownKeys proxy trap can modify keys returned by a small number of operations, it does not affect more common operations, such as for-of loops and the object.keys () method, which cannot be changed using the proxy. OwnKeys traps also affect for-in loops, which are called when the key used inside the loop is determined
Apply and Construct traps in function proxies
Of all the proxy traps, only apply and Construct’s proxy target is a function. The function has two inner methods [[Call]] and [[Construct]]. The Apply trap and Construct trap can override these inner methods. If a function is called using the new operator, the [[Construct]] method is executed. If not, the [[Construct] method is executed, at which point the Apply trap is executed and both reflect.apply () and reflect.apply () accept the following parameters
ThisArg function is called when the internal this value argumentsList is passed to the array of arguments to the functionCopy the code
The Construct trap called when a function is called with new takes the following arguments
TrapTarget The array of arguments passed to the function by the executed function (the agent's target) argumentsListCopy the code
The reflect.construct () method also accepts these two parameters, along with an optional third parameter, newTarget. Given this parameter, it is used to specify the value of new.target within the function. With apply and Construct traps, you can completely control the behavior of any proxy target function
let target = function() { return 42 }, proxy = new Proxy(target, { apply: function(trapTarget, thisArg, argumentList) { return Reflect.apply(trapTarget, thisArg, argumentList); }, construct: function(trapTarget, argumentList) { return Reflect.construct(trapTarget, argumentList); }}); Console. log(typeof proxy); // "function" console.log(proxy()); // 42 var instance = new proxy(); console.log(instance instanceof proxy); // true console.log(instance instanceof target); // trueCopy the code
Here, there is a function that returns the number 42, and the proxy for that function uses the Apply trap and the Construct trap to delegate those behaviors to the reflect.apply () and reflect.construct () methods, respectively. The end result is that the proxy function is exactly the same as the target function, including identifying itself as a function when using Typeof. When the proxy is called without new, 42 is returned. When the proxy is called with new, an instance object is created, which is an instanceof both the proxy and the target, because Instanceof determines this information through the stereotype chain, and the stereotype lookup is not affected by the proxy, which is why the proxy and target seem to have the same stereotype
Validate function parameters
The Apply and Construct traps increase the possibility of changing the way functions are executed. For example, if you validate that all parameters are of a specific type, you can check parameters in the Apply trap
Function sum(... values) { return values.reduce((previous, current) => previous + current, 0); } let sumProxy = new Proxy(sum, { apply: function(trapTarget, thisArg, argumentList) { argumentList.forEach((arg) => { if (typeof arg ! == "number") { throw new TypeError("All arguments must be numbers."); }}); return Reflect.apply(trapTarget, thisArg, argumentList); }, construct: function(trapTarget, argumentList) { throw new TypeError("This function can't be called with new."); }}); console.log(sumProxy(1, 2, 3, 4)); // 10 // Throws error console.log(sumProxy(1, "2", 3, 4)); Let result = new sumProxy();Copy the code
This example uses the Apply trap to ensure that all arguments are numbers, and the sum() function adds up all the arguments passed in. If a non-numeric value is passed, the function will still try to operate, possibly causing unexpected results. By encapsulating sum() in the sumProxy() proxy, this code intercepts the function call and ensures that each argument must be a number before it is called. To be safe, the code also uses the Construct trap to ensure that the function will not be called by new
You can also do the opposite, ensuring that the function must be called with new and verifying that its arguments are numbers
function Numbers(... values) { this.values = values; } let NumbersProxy = new Proxy(Numbers, { apply: function(trapTarget, thisArg, argumentList) { throw new TypeError("This function must be called with new."); }, construct: function(trapTarget, argumentList) { argumentList.forEach((arg) => { if (typeof arg ! == "number") { throw new TypeError("All arguments must be numbers."); }}); return Reflect.construct(trapTarget, argumentList); }}); let instance = new NumbersProxy(1, 2, 3, 4); console.log(instance.values); // [1,2,3,4] // Throw error NumbersProxy(1, 2,3,4);Copy the code
In this example, the Apply trap throws an error, while the Construct trap uses the reflect.construct () method to validate the input and return a new instance. Of course, you can do the same thing with new.target without an agent
The constructor is not called with new
The new.target meta-attribute is a reference to the function when called with new, so you can check the value of new.target to determine if the function was called from new
function Numbers(... values) { if (typeof new.target === "undefined") { throw new TypeError("This function must be called with new."); } this.values = values; } let instance = new Numbers(1, 2, 3, 4); console.log(instance.values); // [1,2,3,4] // Throw error Numbers(1, 2,3,4);Copy the code
In this code, calling Numbers() without new throws an error. If the goal is to prevent functions from being called with new, writing code this way is much simpler than using a proxy. But there are times when you can’t control the function whose behavior you want to modify, and in those cases it makes sense to use a proxy
Let’s say Numbers() is defined in code that can’t be modified, you know your code depends on new.target, and you want the function to avoid checking but still want to call the function. In this case, the behavior when calling with new is already set, so you can only use the Apply trap
function Numbers(... values) { if (typeof new.target === "undefined") { throw new TypeError("This function must be called with new."); } this.values = values; } let NumbersProxy = new Proxy(Numbers, { apply: function(trapTarget, thisArg, argumentsList) { return Reflect.construct(trapTarget, argumentsList); }}); let instance = NumbersProxy(1, 2, 3, 4); console.log(instance.values); / / [1, 2, 3, 4]Copy the code
The apply trap calls reflect.construct () with an argument passed in to make Numbersproxy() behave like Numbers() without using new. New. Target inside Numbers() is equal to Numbers(), so no errors are thrown. Although this example of modifying new.target is very simple, doing so is more straightforward
Overrides the abstract base class constructor
To further modify new.target, you can specify the third parameter reflect.construct () as the specific value assigned to new.target. This technique is useful when a function checks new.target against known values, such as creating an abstract base-class constructor. In an abstract base-class constructor, new.target should be different from the class constructor, as in this example
class AbstractNumbers { constructor(... values) { if (new.target === AbstractNumbers) { throw new TypeError("This function must be inherited from."); } this.values = values; } } class Numbers extends AbstractNumbers {} let instance = new Numbers(1, 2, 3, 4); console.log(instance.values); New AbstractNumbers(1, 2,3,4);Copy the code
When new AbstractNumbers() is called, new.target equals AbstractNumbers and an error is thrown. Calling new Numbers() still works, because new.target is equal to Numbers. The constructor restriction can be bypassed by manually assigning a new. Target to the agent
class AbstractNumbers { constructor(... values) { if (new.target === AbstractNumbers) { throw new TypeError("This function must be inherited from."); } this.values = values; } } let AbstractNumbersProxy = new Proxy(AbstractNumbers, { construct: function(trapTarget, argumentList) { return Reflect.construct(trapTarget, argumentList, function() {}); }}); let instance = new AbstractNumbersProxy(1, 2, 3, 4); console.log(instance.values); / / [1, 2, 3, 4]Copy the code
AbstractNumbersProxy uses the Construct trap to intercept calls to the new AbstractNumbersProxy() method. The trap argument is then passed to the reflect.construct () method and an empty function is added as the third argument. This empty function is used as the value of new.target inside the constructor. Because new. Target is not equal to AbstractNumbers, no errors are thrown and the constructor executes completely
Callable class constructor
The class constructor must be called with new because the inner method [[Call]] of the class constructor is specified to throw an error. But the proxy can intercept calls to the [[Call]] method, which means that you can effectively create callable class constructors by using the proxy. For example, if you want the class constructor to run without new, you can use the Apply trap to create a new instance
class Person {
constructor(name) {
this.name = name;
}
}
let PersonProxy = new Proxy(Person, {
apply: function(trapTarget, thisArg, argumentList) {
return new trapTarget(...argumentList);
}
});
let me = PersonProxy("huochai");
console.log(me.name); // "huochai"
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // trueCopy the code
PersonProxy objects are proxies for the Constructors of the Person class, and class constructors are functions, so they act like functions when used for proxies. The Apply trap overrides the default behavior and returns a new instance of trapTarget, which is equivalent to Pepson. Pass each argument individually by passing the argumentList to the trapTarget using the expansion operator. Calling PersonProxy() without new returns an instance of Person, and if you try calling Person () without new, the constructor throws an error. Creating a callable class constructor can only be done through a proxy
Revocable agency
Typically, an agent cannot detach from its target after it is created. However, there may be a situation where you wish to revoke the agent, and then the agent becomes ineffective. Revoking a proxy is useful whether it is to provide an object through the API for security purposes or to cut off access at any point in time
Revocable proxies can be created using the proxy.revocable() method, which takes the same parameters as the proxy constructor: the target object and the proxy handler, and returns an object with the following properties
Revokeable proxy object revoke Revokes the function to be called by the proxyCopy the code
When the REVOKE () function is called, no further action can be performed through the proxy. Any attempt to interact with a proxy object triggers a proxy trap to throw an error
let target = { name: "target" }; let { proxy, revoke } = Proxy.revocable(target, {}); console.log(proxy.name); // "target" revoke(); // Throw an error console.log(proxy.name);Copy the code
This example creates a revocable proxy that uses destruction to assign proxy and REVOKE variables to properties of the same name on objects returned by the proxy.revocable () method. Proxy objects can then be used like irrevocable proxy objects. So proxy.name returns “target” because it passes through the value of target.name directly. However, once the REVOKE () function is called, the proxy is no longer a function, and attempting to access proxy.name throws an error, just as any other action that triggers a trap on the proxy
Imitate the array
Before ES6, developers could not completely mimic the behavior of arrays in JS. The proxy and reflection apis in ES6 can be used to create an object that behaves the same as the built-in array type when adding and removing properties
let colors = ["red", "green", "blue"];
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"Copy the code
There are two particularly important behaviors in this example
1. When colors[3] is assigned, the length property is increased to 4
2. When length is set to 2, the last two elements of the array are removed
To completely recreate the built-in array, you simply simulate both of these behaviors. How do you create an object that correctly mimics these behaviors
Check array index
Assigning values to integer property keys is a special case of arrays because they are handled differently than non-integer keys. To determine whether an attribute is an array index, refer to the following instructions provided by the ES6 specification
The string attribute name P is an array index if and only if ToString(ToUint32(P)) is equal to P and ToUint32(P) is not equal to 232-1
This can be done in JS, as shown below
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}Copy the code
The toUint32() function converts the given value to an unsigned 32-bit integer using the algorithm described in the specification; The isArrayIndex() function converts the key to a uint32 structure and then performs a comparison to determine whether the key is an array index. With these two utility functions, you can start implementing an object that emulates a built-in array
[Increment length when adding new elements]
The array behavior described earlier relies on attribute assignment, and the two behaviors mentioned earlier can be implemented using only the set proxy trap. See the example below where the length attribute is incremented when the array index of the operation is greater than Length-1, which implements the first of the two features
function toUint32(value) { return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32); } function isArrayIndex(key) { let numericKey = toUint32(key); return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1); } function createMyArray(length=0) { return new Proxy({ length }, { set(trapTarget, key, value) { let currentLength = Reflect.get(trapTarget, "length"); If (isArrayIndex(key)) {let numericKey = Number(key); if (numericKey >= currentLength) { Reflect.set(trapTarget, "length", numericKey + 1); Return reflect. set(trapTarget, key, value); }}); } let colors = createMyArray(3); console.log(colors.length); // 3 colors[0] = "red"; colors[1] = "green"; colors[2] = "blue"; console.log(colors.length); // 3 colors[3] = "black"; console.log(colors.length); // 4 console.log(colors[3]); // "black"Copy the code
This code uses the set proxy trap to intercept the array index setting process. If the key is an array index, it is converted to a number because the key is always passed as a string. Next, if the value is greater than or equal to the current length attribute, the length attribute is updated to 1 more than the number key (setting position 3 means length must be 4). We then call reflect.set () to set the property by default, since we want the property to receive the specified value
Create the initial custom array by calling createMyArray() and passing in 3 as the value of length, and then immediately append the values of the three elements. The length property is 3 until position 3 is assigned to the value “black”, and length is set to 4
[Remove elements by reducing length]
The first array property needs to be simulated only if the array index is greater than or equal to the Length property. The second property does the opposite, removing array elements when the Length property is set to a value smaller than the previous one. This involves not only changing the length attribute, but also removing elements that might otherwise exist. For example, if you have an array of length 4, setting the length attribute to 2 removes elements in positions 2 and 3. This can also be done in the set proxy trap, which does not affect the first feature. The following example updates the createMyArray method on the previous one
function toUint32(value) { return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32); } function isArrayIndex(key) { let numericKey = toUint32(key); return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1); } function createMyArray(length=0) { return new Proxy({ length }, { set(trapTarget, key, value) { let currentLength = Reflect.get(trapTarget, "length"); If (isArrayIndex(key)) {let numericKey = Number(key); if (numericKey >= currentLength) { Reflect.set(trapTarget, "length", numericKey + 1); } } else if (key === "length") { if (value < currentLength) { for (let index = currentLength - 1; index >= value; index--) { Reflect.deleteProperty(trapTarget, index); Return reflect. set(trapTarget, key, value); }}); } let colors = createMyArray(3); console.log(colors.length); // 3 colors[0] = "red"; colors[1] = "green"; colors[2] = "blue"; colors[3] = "black"; console.log(colors.length); // 4 colors.length = 2; console.log(colors.length); // 2 console.log(colors[3]); // undefined console.log(colors[2]); // undefined console.log(colors[1]); // "green" console.log(colors[0]); // "red"Copy the code
The set proxy trap in this code checks if the key is “length” so that the rest of the object can be adjusted correctly. To start the check, reflect.get () first gets the current Length value and then compares it to the new value. If the new value is less than the current Length, all attributes that are no longer available on the target are removed through a for loop. The foP loop removes each attribute backwards from the current Length. Until a new array length (value) is reached
This example adds four colors to colors, then sets its Length property to 2, and the elements in positions 2 and 3 are removed, so an attempt to access them returns undefined. The length attribute is correctly set to 2, and elements in positions 0 and 1 are still accessible
With these two features implemented, you can easily create an object that mimics the features of built-in arrays. But it’s better to create a class to encapsulate these features, so the next step is to implement this functionality with a class
MyArray class
The easiest way to create a class that uses a proxy is to define the class as usual and then return a proxy in the constructor so that when the class is instantiated, the object returned is the proxy instead of the instance (the value of this in the constructor is the instance). The instance becomes the target of the proxy, and the proxy is returned as the original instance. The instance is fully privatized and cannot be accessed directly except indirectly through a proxy
The following is a simple example of returning a proxy from a class constructor
class Thing {
constructor() {
return new Proxy(this, {});
}
}
let myThing = new Thing();
console.log(myThing instanceof Thing); // trueCopy the code
In this example, class Thing returns a proxy from its constructor, and the target of the proxy is this, so myThing is actually a proxy even though it was created by calling the Thing constructor. MyThing is still considered an instance of Thing because agents communicate their properties transparently to the target, so the agent is completely transparent to anyone who uses the Thing class
Once you understand that you can return a proxy from a constructor, it’s relatively easy to create a custom array class using a proxy. The code is mostly the same as the previous “reduce length to remove elements” code. You can use the same proxy code, but this time you need to put it in a class constructor. Here is a complete example
function toUint32(value) { return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32); } function isArrayIndex(key) { let numericKey = toUint32(key); return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1); } class MyArray { constructor(length=0) { this.length = length; return new Proxy(this, { set(trapTarget, key, value) { let currentLength = Reflect.get(trapTarget, "length"); If (isArrayIndex(key)) {let numericKey = Number(key); if (numericKey >= currentLength) { Reflect.set(trapTarget, "length", numericKey + 1); } } else if (key === "length") { if (value < currentLength) { for (let index = currentLength - 1; index >= value; index--) { Reflect.deleteProperty(trapTarget, index); Return reflect. set(trapTarget, key, value); }}); } } let colors = new MyArray(3); console.log(colors instanceof MyArray); // true console.log(colors.length); // 3 colors[0] = "red"; colors[1] = "green"; colors[2] = "blue"; colors[3] = "black"; console.log(colors.length); // 4 colors.length = 2; console.log(colors.length); // 2 console.log(colors[3]); // undefined console.log(colors[2]); // undefined console.log(colors[1]); // "green" console.log(colors[0]); // "red"Copy the code
This code creates a MyArray class that returns a proxy from its constructor. The Length attribute is added to the constructor, initialized to the passed value or the default value of 0, and then the proxy is created and returned. The Colors variable looks like just an instance of MyArray and implements two key features of arrays
Although it is easy to return a proxy from the class constructor, it also means that a new proxy is created for every instance created. However, there is a way to have all instances share a single proxy: use the proxy as a prototype
Use the agent as a prototype
If the proxy is a stereotype, the proxy trap is invoked only if the default action continues on the stereotype, limiting the proxy’s ability to act as a stereotype
let target = {}; Let newTarget = object. create(new Proxy(target, {// will never be called defineProperty(trapTarget, name, Descriptor) {// Return false if called; }})); Object.defineProperty(newTarget, "name", { value: "newTarget" }); console.log(newTarget.name); // "newTarget" console.log(newTarget.hasOwnProperty("name")); // trueCopy the code
Create the newTarget object, which is prototyped as a proxy. Because the agent is transparent, using target as the target of the agent actually makes target the prototype for newTarget. Proxy traps are now invoked only when operations on newTarget are passed transparently to the target
Call the Object.defineProperty() method and pass newTarget to create a own property called name. The operation of defining properties on an object does not need to operate on the object stereotype, so the defineProperty trap in the proxy is never called and name is added to newTarget as its own property
Although the use of proxies as prototypes is extremely limited, several pitfalls are still useful
Using get traps on prototypes
The operation that calls the internal method [[Get]] to read a property looks for its own property first, and if it does not find its own property with the specified name, it continues to look in the stereotype until there are no more stereotypes to look for
If you set a GET proxy trap, the trap on the prototype will often be invoked whenever its own property with the specified name does not exist because of the procedure above. Get traps can be used to prevent unexpected behavior when accessing properties that we cannot guarantee exist. You simply create an object that throws an error when you try to access a property that doesn’t exist
let target = {}; let thing = Object.create(new Proxy(target, { get(trapTarget, key, receiver) { throw new ReferenceError(`${key} doesn't exist`); }})); thing.name = "thing"; console.log(thing.name); // "thing" // let unknown = thing. Unknown;Copy the code
In this code, the Thing object is prototyped with a proxy, and when it is called, the get trap throws an error if the given key does not exist on it. Because the thing. Name property exists, reading it does not invoke the get trap on the prototype, but only when accessing a nonexistent thing. Unknown property
When the last line is executed, since Unknown is not a property of Thing, the operation continues to look up on the prototype, after which the GET trap throws an error. In JS, accessing an unknown property usually silently returns undefined, a feature that throws errors (as it does in other languages) that is very useful
It is important to understand that trapTarget and receiver are different objects in this example. When the proxy is used as a prototype, the trapTarget is the prototype object and the Receiver is the instance object. In this case, the trapTarget is equal to the target and the receiver is equal to the thing, so you have access to the agent’s original target and the target to operate on
Using set traps on prototypes
The inner method [[Set]] also checks to see if the target object has a property of its own, and continues to look for the stereotype if it does not. When assigning a value to an object attribute, assign to it if there is an own attribute of the same name; If no given name exists, the search continues on the stereotype. The trickiest part is that assigning a value to an attribute with the same name, regardless of whether it exists on the stereotype, creates the attribute in the instance (not the stereotype) by default
let target = {}; let thing = Object.create(new Proxy(target, { set(trapTarget, key, value, receiver) { return Reflect.set(trapTarget, key, value, receiver); }})); console.log(thing.hasOwnProperty("name")); // false // trigger 'set' proxy trap thing. Name = "thing"; console.log(thing.name); // "thing" console.log(thing.hasOwnProperty("name")); // true // Does not trigger 'set' proxy trap thing.name = "boo"; console.log(thing.name); // "boo"Copy the code
In this example, Target doesn’t have its own properties to start with, and the prototype for Thing is a proxy that defines a set trap to catch the creation of any new properties. When thing. Name is assigned to “thing”, the set proxy trap is called because name is not a property of thing. In a trap, trapTarget equals target and receiver equals thing. This will eventually create a new property on thing, and fortunately, reflect.set () will implement this default behavior if passed receiver as the fourth argument
Once the name property is created on Thing, the set proxy trap is no longer called when thing.name is set to something else, and name is a property of its own, so the [[set] operation does not continue to look up on the stereotype
[Using has traps on prototypes]
Recall the HAS trap, which intercepts the IN operator in an object. The in operator searches for the object’s own properties based on the given name, and if none exists, searches for subsequent objects’ own properties along the prototype chain until the given name is found or there are no more prototypes
Thus, the HAS trap is called only when a proxy object on the stereotype chain is searched, and when a proxy is used as a stereotype, the HAS trap is called only when the specified name has no corresponding own property
let target = {}; let thing = Object.create(new Proxy(target, { has(trapTarget, key) { return Reflect.has(trapTarget, key); }})); // Trigger the 'has' proxy trap console.log("name" in thing); // false thing.name = "thing"; // Does not trigger the 'has' proxy trap console.log("name" in thing); // trueCopy the code
This code creates a has proxy trap on the prototype of Thing. Since the in operator is automatically searched for the prototype, this HAS trap does not pass a receiver object like get and set traps. It only operates on traptargets equal to Target. In this example, the has trap is called the first time you use the IN operator, because the property name is not the property of thing; Assigning thing. Name will use the IN operator again, and this time the HAS trap will not be called, because name is already a property of Thing, so it will not continue to look up in the prototype
Using a proxy as a prototype for a class
Because the prototype property of the class is not writable, you cannot modify the class directly to use a proxy as a prototype for the class. However, you can trick a class into thinking it can use a proxy as its prototype through inheritance. First, you need to create an ES5-style type definition with the constructor
function NoSuchProperty() { // empty } NoSuchProperty.prototype = new Proxy({}, { get(trapTarget, key, receiver) { throw new ReferenceError(`${key} doesn't exist`); }}); let thing = new NoSuchProperty(); // Let result = thing. Name;Copy the code
NoSuchProperty represents the base class that the class will inherit from, and the function’s Prototype property is unlimited, so it can be overridden with a proxy. The thing object is created as an instance of NoSuchProperty. The property name being accessed does not exist and an error is thrown
The next step is to create a class that inherits from NoSuchProperty
function NoSuchProperty() { // empty } NoSuchProperty.prototype = new Proxy({}, { get(trapTarget, key, receiver) { throw new ReferenceError(`${key} doesn't exist`); }}); class Square extends NoSuchProperty { constructor(length, width) { super(); this.length = length; this.width = width; } } let shape = new Square(2, 6); let area1 = shape.length * shape.width; console.log(area1); Let area2 = shape.length * shape. WDTH;Copy the code
The Square class inherits from NoSuchProperty, so it has a proxy in its prototype chain. Our shape object is a new instance of Square that has two properties of its own, Length and Width. The get proxy trap is not invoked when reading the values of these two properties, but only when accessing a property that does not exist on a Shape object (such as Shape.wdth, which is obviously a misspelling) will trigger the GET proxy trap and throw an error. On the other hand, this shows that the proxy is indeed in the shape’s prototype chain. What is less obvious, however, is that the proxy is not a direct prototype of the Shape. It actually resides in the shape’s prototype chain and takes several steps to get there
Function NoSuchProperty() {// empty} // For the agent to be used as the prototype, Let proxy = new proxy ({}, {get(trapTarget, key, receiver) {throw new ReferenceError(' ${key} doesn't exist '); }}); NoSuchProperty.prototype = proxy; class Square extends NoSuchProperty { constructor(length, width) { super(); this.length = length; this.width = width; } } let shape = new Square(2, 6); let shapeProto = Object.getPrototypeOf(shape); console.log(shapeProto === proxy); // false let secondLevelProto = Object.getPrototypeOf(shapeProto); console.log(secondLevelProto === proxy); // trueCopy the code
In this version of the code, the proxy is stored in the variable proxy for later identification. Prototype of Shape is not a proxy, but the prototype of shape. Prototype is a proxy inherited from NoSuchProperty
Adding another extra step to the stereotype chain through inheritance is important because it takes an extra step to trigger the GET trap in the proxy. If Shape.prototype has a property, it will prevent get proxy traps from being called
function NoSuchProperty() { // empty } NoSuchProperty.prototype = new Proxy({}, { get(trapTarget, key, receiver) { throw new ReferenceError(`${key} doesn't exist`); }}); class Square extends NoSuchProperty { constructor(length, width) { super(); this.length = length; this.width = width; } getArea() { return this.length * this.width; } } let shape = new Square(2, 6); let area1 = shape.length * shape.width; console.log(area1); // 12 let area2 = shape.getArea(); console.log(area2); Let area3 = shape.length * shape. WDTH;Copy the code
In this case, the Square class has a getArea() method that is automatically added to square.prototype, so when shape.getarea () is called, the getArea() method is searched in the Shape instance and then continued in its prototype. Because getArea() was found in the stereotype, the search ends and the proxy is not invoked