JavaScript Proxy and Reflect
Proxies are commonly applied in various libraries and several browser frameworks.
A Proxy object is used for wrapping another object and intercepting operations such as writing and reading properties, dealing with them, or transparently making the object to handle them.
Proxy¶
The first thing to know is the syntax of proxy, which looks as follows:
let proxy = new Proxy(target, handler)
This syntax includes a target and a handler. The target is the object that is going to be wrapped. It can be functions, and so on. The handler is the configuration of the proxy.
In case there is a trap in the handler, then it runs, and the proxy can handle it. In another way, the operation will be implemented on the target.
As a starting point, let’s consider an example of a proxy without traps:
let target = {};
let proxy = new Proxy(target, {}); // empty handler
proxy.test = 7; // writing proxy (1)
console.log(target.test); // 7, property appeared in the target
console.log(proxy.test); // 7, we can read this from a proxy (2)
for (let key in proxy) console.log(key); // test, iteration works (3)
In case there isn’t any trap, the operations are passed to the target. It is necessary to add traps for activating more capabilities.
In JavaScript, there is a so-called “internal method” for most object operations, describing how it works at the lowest level. For example, the [[Get]] method is used for reading a property, [[Set]] - for writing a property. Anyway, such kinds of methods can not be called by name.
Further, we will cover operations with some of the methods and traps. JavaScript imposes invariants: those are conditions that must be carried out by the internal methods and traps.
For example,[[Set]] , [[Delete]] , and others return values. In the case of using [[Delete]], it should return true once the value was deleted, and, otherwise, it will return false.
Other types of variants also exist. For example, when [[GetPrototypeOf]] is inserted to the proxy object, it must return the same value as [[GetPrototypeOf]], attached to the target object of the proxy.
Default Value with “get” Trap¶
The most frequently used traps are for writing and reading properties. For intercepting reading, the handler should include a get method. It occurs once a property is read, along with the target (the target object), property (the property name), and the receiver (if the target property is a getter, then the object is the receiver).
For using get to perform default values for an object, it is necessary to make a numeric array, which returns 0 for nonexistent values.
As a rule, trying to get a non-existing array item leads to undefined. However, you can wrap a regular array into the proxy that returns 0, like here:
let numbers = [11, 22, 33];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0;
}
}
});
console.log(numbers[1]); // 22
console.log(numbers[12]); // 0, no such item
Validation with “set” Trap¶
Imagine that you need to get an array numbers. Adding another value type will cause an error.
The set trap occurs when a property is written. It includes target, property, value, and receiver.
So, the set trap returns true when the setting is done successfully, and false, otherwise.
p>It can be used for validating new values, like this:
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, value) { // intercept property record
if (typeof value == 'number') {
target[prop] = value;
return true;
} else {
return false;
}
}
});
numbers.push(11); // added successfully
numbers.push(22); // added successfully
console.log("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError ,'set' on proxy returns false
console.log("Error in the line above,this line is never reached ");
Also consider that the built-in functionality of arrays still works. With push, it is possible to add values.
Value-adding methods such as push and unshift shouldn’t be overridden.
Protected Properties with “deleteProperty” and Other Traps¶
It is known that properties and methods that are prefixed by an underscore _ are considered internal. It is not possible to access them outside the object.
Let’s see how it is possible technically:
let user = {
name: "Jack",
_userPassword: "secret"
};
console.log(user._userPassword); // secret
So, proxies are used for preventing any access to the properties that begin with _.
The necessary traps are as follows:
- get - for throwing an error while reading a property like that.
- set - for throwing an error while writing.
- deleteProperty - for throwing an error while deleting.
- ownKeys - for excluding properties that begin with _ from for..in and methods such as Object.keys.
The code will look like this:
console.log(1 / 0); // Infinity
let user = {
userName: "Jack",
_userPassword: "***"
};
user = new Proxy(user, {
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error("Access denied");
}
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
},
set(target, prop, value) { // to intercept property writing
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
target[prop] = value;
return true;
}
},
deleteProperty(target, prop) { // to intercept property deletion
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
delete target[prop];
return true;
}
},
ownKeys(target) { // to intercept property list
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "get" doesn't allow to read _userPassword
try {
console.log(user._userPassword); // Error: Access denied
} catch (e) {
console.log(e.message);
}
// "set" doesn't allow to write _userPassword
try {
user._userPassword = "test"; // Error: Access denied
} catch (e) {
console.log(e.message);
}
// "deleteProperty" doesn't allow to delete _userPassword
try {
delete user._userPassword; // Error: Access denied
} catch (e) {
console.log(e.message);
}
// "ownKeys" filters out _userPassword
for (let key in user) console.log(key); // userName
In the line (*), there is an important detail:
get(target, prop) {
// ...
let val = target[prop];
return (typeof val === 'function') ? val.bind(target) : val; // (*)
}
So, a function is necessary to call value.bind(target) because object methods like user.checkPassword() should be able to access _password, like this:
user = {
// ...
checkPassword(val) {
// object method should be able to read _userPassword
return val === this._userPassword;
}
}
Wrapping Functions: "apply"¶
It is possible to wrap a proxy around a function, too.
The oapply(target, thisArg, args) trap can handle calling a proxy as a function. It includes target (the target object), thisArg (the value of this), and args (a list of arguments).
For instance, imagine recalling the delay(fn, ms) decorator. It returns a function, which forwards all the calls to fn after ms milliseconds.
The function-based performance will look like this:
function delay(fn, ms) {
// return wrapper that passes fn call after timeout
return function () { // (*)
setTimeout(() => fn.apply(this, arguments), ms);
};
}
function sayWelcome(siteName) {
console.log(`Welcome to ${siteName}!`);
}
// after this wrapping, sayWelcome calls will be delayed for 2 seconds
sayWelcome = delay(sayWelcome, 2000);
sayWelcome("Web"); // Welcome to Web(after 2 seconds)
It is noticeable that the wrapper function (*) makes the call after a timeout. But the wrapper function isn’t capable of forwarding write and read operations, and so on. And, when the wrapping is implemented, the access to the properties will be lost:
function delay(f, ms) {
return function () {
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayWelcome(siteName) {
console.log(`Welcome to ${siteName}!`);
}
console.log(sayWelcome.length); // 1 (function length is the count of arguments in the declaration)
sayWelcome = delay(sayWelcome, 2000);
console.log(sayWelcome.length); // 0 (wrapper declaration has zero arguments)
Luckily, the proxy can forward anything to the target object. So, it is recommended to use proxy rather than the wrapping function. Here is an example of using the proxy:
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayWelcome(siteName) {
console.log(`Welcome to ${siteName}!`);
}
sayWelcome = delay(sayWelcome, 2000);
console.log(sayWelcome.length); // 1 (*) proxy forwarded "get length" operation to target
sayWelcome("Web"); // Welcome to Web(after 2 seconds)
Reflect¶
Reflect is the term used for specifying a built-in object, which simplifies the proxy creation.
As it was noted above, the internal methods such as [[Set]],[[Get]] , and others can’t be called directly. Reflect can be used for making it possible.
Let’s see an example of using reflect:
let site = {};
Reflect.set(site, 'siteName', 'Web');
console.log(site.siteName); //
For each internal method, that is trapped by proxy, there is a matching method in reflect. It has the same name and arguments as the proxy trap.
So, reflect is used for forwarding operation to the original object.
Let’s check out another example:
let site = {
name: "Web",
};
site = new Proxy(site, {
get(target, prop, receiver) {
console.log(`GET ${prop}`);
return Reflect.get(target, prop, receiver); // (1)
},
set(target, prop, val, receiver) {
console.log(`SET ${prop}=${val}`);
return Reflect.set(target, prop, val, receiver); // (2)
}
});
let name = site.name; // shows "GET name"
site.name = "Web"; // shows "SET name=Web"
In this example, an object property is read by Reflect.get. Also, Reflect.set writes an object property, returning true if successful, and false, otherwise.
Proxy Limitations¶
Of course, proxies are a unique and handy way for altering or tweaking the behavior of the existing objects at the lowest level. But, it can’t handle anything and has limitations. Below, we will look through the main proxy limitations.
Built-in objects: internal slots
Most of the built-in objects such as Set, Map, Date, and so on work with internal slots.
Those are similar to properties but reserved for internal purposes. For instance, Map embraces items in the internal slot [[MapData]] . The built-in methods are capable of accessing them directly and not through the [[Get]]/[[Set]] methods. So, it can’t be intercepted by the proxy.
An essential issue here is that after a built-in object gets proxied, the proxy doesn’t include the slots, so an error occurs, like this:
let map = new Map();
let proxy = new Proxy(map, {});
console.log(proxy.set('testArg', 1)); // Error
Let’s see another case. A Map encloses all the data in the [[MapData]] internal slot. Then, the Map.prototype.set method attempts to access the internal this.[[MapData]]. As a result, it will not be found in the proxy.
But there is a way of fixing it:
let map = new Map();
let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
proxy.set('test', 10);
console.log(proxy.get('test')); // 10
Please, take into consideration a notable exception: the built-in Array never uses internal slots.
Private Fields
Something similar takes place with private fields. For instance, the getName() method can access the #name and break after proxying, like this:
class Site {
#name = "Web";
getName() {
return this.#name;
}
}
let site = new Site();
site = new Proxy(site, {});
console.log(site.getName()); // Error
That happens because private fields are performed by applying internal slots. While accessing them, JavaScript doesn’t use [[Get]]/[[Set]] .
Calling getName() the proxied user is the value of this . It doesn’t have a slot with private fields.
Let’s have a look at the solution, which will make it work:
class Site {
#name = "Web";
getName() {
return this.#name;
}
}
let site = new Site();
site = new Proxy(site, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
console.log(site.getName()); // Web
Proxy != target
The original object and the proxy are considered different objects.
Having the original object as a key, proxying it, then it won’t be found, like here:
let allBooks = new Set();
class Book {
constructor(name) {
this.name = name;
allBooks.add(this);
}
}
let book = new Book("Javascript");
console.log(allBooks.has(book)); // true
book = new Proxy(book, {});
console.log(allBooks.has(book)); // false
It is noticeable that the user can’t be found in the allUsers after proxying. That’s because it’s a different object.
Please, also note that proxies don’t intercept various operators like new, delete, and more. All the operations and built-in classes comparing objects for equality differentiate between the proxy and the object.
Revocable Proxies¶
A proxy that can be disabled is called a revocable proxy.
Imagine having a resource and want to close access to it at any time. It is necessary to wrap it to a revocable proxy without using traps. A proxy like that will forward operations to object. It can be disabled at any moment.
The syntax is the following:
let {
proxy,
revoke
} = Proxy.revocable(target, handler)
Now, let’s see an example of the call returning an object with the proxy and revoke function for disabling it:
let object = {
data: "Valued data"
};
let {
proxy,
revoke
} = Proxy.revocable(object, {});
// pass proxy somewhere instead of an object
console.log(proxy.data); // Valued data
// later
revoke();
// the proxy doesn't work (revoked)
console.log(proxy.data); // Error
Calling revoke() deletes all the internal references to the target object. So, there is not connection between them.
Also, revoke can be stored in WeakMap. That makes it easier to detect it using a proxy object, like this:
let revokes = new WeakMap();
let object = {
data: "Valued data"
};
let {
proxy,
revoke
} = Proxy.revocable(object, {});
revokes.set(proxy, revoke);
// later
revoke = revokes.get(proxy);
revoke();
console.log(proxy.data); // Error (revoked)
The primary benefit here is that it’s not necessary to carry revoke anymore. To sum up, we can state that a Proxy is a wrapper, surrounding an object, which forwards operations on it to the object. Some of them can optionally by trapped. Any type of object can be wrapped, including functions and classes.
The proxy doesn’t include its own methods and properties. It will trap the operation when there is a trap. Otherwise, it will be forwarded to target the object.
To complement proxy, an API called reflect is created. For each proxy trap, you can find a corresponding reflect, using the same arguments.
0 Comments
CAN FEEDBACK
Emoji